diff --git a/.gitignore b/.gitignore index f24532312..908eae39d 100644 --- a/.gitignore +++ b/.gitignore @@ -41,4 +41,6 @@ addon.late.local.gradle.kts layout.json out run -src/main/resources/assets/customnpcs/api \ No newline at end of file +src/main/resources/assets/customnpcs/api +df/ +tmp/ diff --git a/Ability_Update_Review.md b/Ability_Update_Review.md new file mode 100644 index 000000000..2ceb38797 --- /dev/null +++ b/Ability_Update_Review.md @@ -0,0 +1,301 @@ +# Ability System Review - CustomNPC+ + +A comprehensive technical review of the Ability System identifying bugs, logic issues, incomplete features, and inconsistencies. + +--- + +## Critical Issues + +### 1. Cooldown Uses Mixed Time Systems +**Status:** FIXED +**Files:** +- `Ability.java` - Removed cooldown tracking (cooldownEndTime, startCooldown(), isOnCooldown(), canUse()) +- `DataAbilities.java` - Now sole owner of cooldown management using world time + +**Solution:** Removed all cooldown tracking from `Ability.java`. Cooldowns are now managed exclusively by `DataAbilities` using `world.getTotalWorldTime()` for consistency. + +--- + +### 2. AbilityHeal - Heal Over Time Completely Broken +**Status:** FIXED +**File:** `AbilityHeal.java` + +**Solution:** Refactored to separate ally-finding from healing. The `findAlliesInRadius()` method is now called regardless of `instantHeal` flag, populating the list for HoT to use. Added heal particles during HoT ticks. + +--- + +### 3. AbilityGuard - Counter Integration Missing +**Status:** FIXED +**Files:** +- `DataAbilities.java:onDamage()` - Added integration to call `AbilityGuard.onDamageTaken()` +- `DataAbilities.java:getGuardDamageReduction()` - Added helper method for damage reduction + +**Solution:** `DataAbilities.onDamage()` now checks if current ability is an `AbilityGuard` and calls `onDamageTaken()` when the NPC is hit while guarding. + +--- + +## High Priority Issues + +### 4. knockbackUp Stored But Never Applied +**Status:** FIXED +**File:** `Ability.java` + +**Solution:** Updated `applyNpcKnockback()` to accept a `knockbackUp` parameter and use it instead of hardcoded `0.1D`. Both `applyAbilityDamage()` and `applyAbilityDamageWithDirection()` now pass the knockbackUp value from the HitEvent. + +--- + +### 5. Targeting Modes Not Implemented +**Status:** FIXED (Removed) +**File:** `TargetingMode.java` + +**Solution:** Removed the unused targeting modes (`RANDOM_ENEMY`, `LOWEST_HP`, `HIGHEST_THREAT`) from the enum. Only the implemented modes remain: `AGGRO_TARGET`, `SELF`, `AOE_SELF`, `AOE_TARGET`. + +--- + +### 6. Damage Source Inconsistency +**Status:** FIXED +**File:** `Ability.java` + +**Solution:** Standardized both `applyAbilityDamage()` and `applyAbilityDamageWithDirection()` to use `NpcDamageSource("mob", npc)` for consistent damage source handling. + +--- + +## Incomplete Features + +### 7. AbilityProjectile - No Actual Projectile +**Status:** CONFIRMED - INCOMPLETE +**File:** `AbilityProjectile.java:88` + +```java +// TODO: Use custom EntityAbilityProjectile for actual tracking +applyAbilityDamageWithDirection(npc, target, damage, knockback, dx, dz); +``` + +**Current behavior:** +- `speed`, `homing`, `homingStrength` fields exist but are unused +- No projectile entity is spawned +- Damage is instant, just with particle cosmetics + +--- + +### 8. AbilityOrb - No Visual Orb Rendering +**Status:** CONFIRMED - INCOMPLETE +**File:** `AbilityOrb.java:117-179` + +**Problem:** The orb tracks position (`orbX`, `orbY`, `orbZ`) with runtime getters but nothing renders it client-side. Players only see particle effects on hit, not the flying orb itself. + +--- + +### 9. AbilityBeam - No Visual Beam Rendering +**Status:** CONFIRMED - INCOMPLETE +**File:** `AbilityBeam.java:213-220` + +**Problem:** Only spawns flame particles along the path. No actual beam entity or proper beam visual. The `lockOnTarget` telegraph during active phase isn't visually communicated. + +--- + +## Medium Priority Issues + +### 10. Hazard/Trap Telegraph Offset Mismatch +**Status:** CONFIRMED +**Files:** +- `AbilityHazard.java:119-147, 201-248` +- `AbilityTrap.java:109-138, 166-205` + +**Problem:** Telegraph is created at target position, but actual effect applies random offset: + +```java +// createTelegraph() - No offset applied, shows at target position + +// onExecute() - Random offset applied AFTER telegraph locks +double[] pos = calculateOffsetPosition(telegraph.getX(), telegraph.getY(), telegraph.getZ()); +zoneX = pos[0]; // Actual effect position differs from telegraph +``` + +**Impact:** Players see telegraph at one location, damage happens elsewhere. Misleading visual feedback. + +**Fix:** Either apply offset during telegraph creation, or update telegraph position in `onExecute()`. + +--- + +### 11. AbilityCharge Telegraph Direction Timing +**Status:** CONFIRMED - MINOR +**File:** `AbilityCharge.java:91, 254-286` + +**Problem:** Telegraph created at tick 0, direction locked at tick 1: + +```java +// Telegraph created during setup (tick 0) with current target direction +// Line 91: Direction locked at tick 1 +if (tick == 1) { + lockChargeDirection(npc, target); +} +``` + +**Impact:** If target moves between tick 0-1, telegraph direction won't match actual charge. + +--- + +### 12. AbilitySlam - activeTicks May End Before Landing +**Status:** CONFIRMED - MINOR +**File:** `AbilitySlam.java:52-53, 230-250` + +**Problem:** `activeTicks = 60` is fixed, but if NPC takes longer to land, ability transitions to RECOVERY without `onLanding()` being called. + +**Secondary issue:** If NPC lands early (tick 15), active phase continues until tick 60 doing nothing - wasted ticks. + +--- + +### 13. FOLLOW Mode Telegraph Not Updated +**Status:** CONFIRMED +**Files:** `AbilityHazard.java:139-146`, `AbilityVortex.java` + +**Problem:** For `FOLLOW_TARGET` mode, telegraph locks position on execute, but the actual hazard zone follows the target. Visual mismatch between warning and danger. + +--- + +## Redundancies + +### 14. Duplicate findGroundLevel() Methods +**Status:** FIXED +**Files:** +- `Ability.java` - Made `findGroundLevel()` public static +- `TelegraphInstance.java` - Now uses `Ability.findGroundLevel()` + +--- + +### 15. Duplicate calculateOffsetPosition() Methods +**Status:** FIXED +**Files:** +- `Ability.java` - Added shared static `calculateOffsetPosition()` method +- `AbilityTrap.java` - Removed duplicate, uses `Ability.calculateOffsetPosition()` +- `AbilityHazard.java` - Removed duplicate, uses `Ability.calculateOffsetPosition()` + +--- + +### 16. Unused applyKnockback in AbilityOrb +**Status:** FIXED +**File:** `AbilityOrb.java` + +Removed the unused `applyKnockback()` method. + +--- + +### 17. Duplicate Reset/Interrupt Cleanup +**Status:** NOT FIXED (Low Priority) +**Multiple ability files** + +Most abilities override both `reset()` and `onInterrupt()` with duplicate cleanup code. Pattern should have `onInterrupt()` call `reset()` or use shared cleanup method. + +--- + +## Minor Issues & Inconsistencies + +### 18. Field Naming Confusion +- `AbilityHazard`: `damagePerSecond` with `damageInterval` - name suggests per-second but applied every N ticks +- `AbilityCharge`: `hitWidth` stored as `hitRadius` in older NBT (backward compat exists) + +### 19. AbilityVortex Entity Lookup Performance +**File:** `AbilityVortex.java:201-212` + +Iterates through ALL loaded entities every tick to find pulled entities by UUID. Should cache entity references or use entity IDs. + +### 20. Telegraph Yaw Rotation +**File:** `TelegraphRenderer.java:95` + +Uses `GL11.glRotatef(-instance.getYaw(), 0, 1, 0)` - negative yaw. Should verify this matches Minecraft's yaw conventions. + +### 21. Missing Null Checks +Various locations where `target` could be null but is used without defensive checks: +- `AbilityProjectile.java:73` +- `AbilityOrb.java:138-171` + +--- + +## Missing Features (Enhancement Requests) + +These are not bugs but notable missing functionality: + +1. **No Ability Chaining/Combo System** - No way to trigger abilities from other abilities +2. **No Cooldown Reduction on Hit** - Can't reduce cooldowns based on successful hits +3. **No Multi-Target Hit Limit** - AOE abilities hit unlimited targets (unlike Vortex's `maxTargets`) +4. **No Damage Type Configuration** - All abilities use mob/melee damage, no magic/fire/etc options +5. **No Target Validation During Execution** - If target dies/despawns mid-ability, behavior varies + +--- + +## Priority Summary + +| Priority | Issue | Status | +|----------|-------|--------| +| **CRITICAL** | Cooldown time system mismatch | FIXED | +| **CRITICAL** | AbilityHeal HoT broken | FIXED | +| **CRITICAL** | AbilityGuard counter not integrated | FIXED | +| **HIGH** | knockbackUp ignored | FIXED | +| **HIGH** | Targeting modes unimplemented | FIXED (Removed) | +| **HIGH** | Damage source inconsistency | FIXED | +| **MEDIUM** | Projectile/Orb/Beam no visuals | Not Fixed | +| **MEDIUM** | Hazard/Trap telegraph offset | Not Fixed | +| **LOW** | Charge telegraph timing | Not Fixed | +| **LOW** | Slam activeTicks timing | Not Fixed | +| **LOW** | Code redundancies | FIXED | + +--- + +## Remaining Issues + +The following issues were not addressed in this fix round: + +1. **Projectile/Orb/Beam Visual Rendering** - These abilities still only use particles, no actual entity rendering +2. **Hazard/Trap Telegraph Offset Mismatch** - Telegraph shows at target, effect spawns with offset +3. **Charge Telegraph Direction Timing** - Minor visual mismatch between tick 0-1 +4. **Slam activeTicks Timing** - Fixed at 60 ticks regardless of landing time + +--- + +## Changes Made + +### Ability.java +- Removed `cooldownEndTime` transient field +- Removed `startCooldown()`, `isOnCooldown()`, `canUse()` methods (cooldown managed by DataAbilities) +- Updated `tick()` to not call startCooldown (DataAbilities handles it) +- Updated `reset()` to not reset cooldownEndTime +- Made `findGroundLevel()` public static for shared use +- Added static `calculateOffsetPosition()` utility method +- Fixed `applyNpcKnockback()` to accept and use knockbackUp parameter +- Standardized damage source to `NpcDamageSource("mob", npc)` in both damage methods +- Removed deprecated getters/setters (telegraphColor, castSound, animationId) + +### IAbility.java (API) +- Removed `isOnCooldown()` method from interface + +### DataAbilities.java +- Added AbilityGuard import +- Added guard counter integration in `onDamage()` +- Added `getGuardDamageReduction()` helper method +- Removed deprecated `isMovementBlocked()` method +- Updated knockbackUp javadoc (no longer deprecated) + +### CombatHandler.java +- Renamed `isMovementBlocked()` to `isAbilityControllingMovement()` + +### TargetingMode.java +- Removed unused modes: `RANDOM_ENEMY`, `LOWEST_HP`, `HIGHEST_THREAT` + +### AbilityHeal.java +- Fixed HoT by separating `findAlliesInRadius()` from healing logic +- Allies are now found in `onExecute()` regardless of instantHeal flag +- Added heal particles during HoT ticks + +### AbilityHazard.java +- Removed duplicate `calculateOffsetPosition()`, uses `Ability.calculateOffsetPosition()` +- Removed deprecated `getDamagePerTick()`/`setDamagePerTick()` methods + +### AbilityTrap.java +- Removed duplicate `calculateOffsetPosition()`, uses `Ability.calculateOffsetPosition()` + +### AbilityOrb.java +- Removed unused `applyKnockback()` method + +### TelegraphInstance.java +- Removed duplicate `findGroundLevel()`, uses `Ability.findGroundLevel()` diff --git a/dependencies.gradle b/dependencies.gradle index 5f8f40b1e..2e3e5db52 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -38,8 +38,13 @@ dependencies { runtimeOnlyNonPublishable("com.github.GTNewHorizons:NotEnoughItems:2.5.4-GTNH:dev") runtimeOnlyNonPublishable("curse.maven:Nashorn-374413:2924946") + compileOnly(files("./tools/bukkit-api/spigot-api-ONLY.jar")) +// compileOnly(files("./tools/bukkit-api/Vault-1.4.1.jar")) + shadowImplementation(project.files("tools/standalone-JaninoLoader-1.0.1-ALPHA.jar")) annotationProcessor(project.files("tools/standalone-JaninoLoader-1.0.1-ALPHA.jar")) + + testImplementation("junit:junit:4.13.2") if(embedMixin){ // Normal Jar will include built in Mixins diff --git a/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityBeam.java b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityBeam.java new file mode 100644 index 000000000..4912e4b72 --- /dev/null +++ b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityBeam.java @@ -0,0 +1,609 @@ +package kamkeel.npcs.client.renderer; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import kamkeel.npcs.client.renderer.lightning.AttachedLightningRenderer; +import kamkeel.npcs.entity.EntityAbilityBeam; +import net.minecraft.client.renderer.Tessellator; +import net.minecraft.entity.Entity; +import net.minecraft.util.Vec3; +import org.lwjgl.opengl.GL11; + +import java.util.ArrayList; +import java.util.List; + +/** + * Renders the AbilityBeam entity with a head, tail orb, and curving trail. + * The trail shows the path the head has traveled using laser-style rectangular segments. + * + * IMPORTANT: Trail points are stored RELATIVE to the origin position. + * We render by translating to the origin first, then drawing all points relative to that. + * + * Trail segments now share vertices at connection points to prevent gaps during curves. + * + * Design inspired by LouisXIV's energy rendering system. + */ +@SideOnly(Side.CLIENT) +public class RenderAbilityBeam extends RenderAbilityProjectile { + + @Override + public void doRender(Entity entity, double x, double y, double z, float yaw, float partialTicks) { + EntityAbilityBeam beam = (EntityAbilityBeam) entity; + + setupRenderState(); + + // Handle charging state (windup phase) - render only growing orb + if (beam.isCharging()) { + renderChargingOrb(beam, x, y, z, partialTicks); + restoreRenderState(); + return; + } + + // Get trail points (these are RELATIVE to origin) + List trail = beam.getTrailPoints(); + if (trail.isEmpty()) { + restoreRenderState(); + return; + } + + // Get interpolated head offset + double headOffsetX = beam.getInterpolatedHeadOffsetX(partialTicks); + double headOffsetY = beam.getInterpolatedHeadOffsetY(partialTicks); + double headOffsetZ = beam.getInterpolatedHeadOffsetZ(partialTicks); + + // The render system passes x,y,z as the position to render the entity at + // Entity position = origin + headOffset (in world coords) + // So origin render position = x,y,z - headOffset + double renderOriginX = x - headOffsetX; + double renderOriginY = y - headOffsetY; + double renderOriginZ = z - headOffsetZ; + + // Get beam properties + float beamWidth = beam.getBeamWidth(); + float headSize = beam.getHeadSize(); + int innerColor = beam.getInnerColor(); + int outerColor = beam.getOuterColor(); + float outerAlpha = beam.getOuterColorAlpha(); + + // Render everything relative to origin position + GL11.glPushMatrix(); + GL11.glTranslated(renderOriginX, renderOriginY, renderOriginZ); + + // Calculate head distance from origin (for overlap check) + double headDistFromOrigin = Math.sqrt(headOffsetX * headOffsetX + headOffsetY * headOffsetY + headOffsetZ * headOffsetZ); + + // Render tail orb at origin (0,0,0) - only in anchored mode + // Don't render if head is too close to origin (would cause overlap/extra orb appearance) + if (beam.shouldRenderTailOrb() && headDistFromOrigin > headSize * 0.5) { + renderTailOrb(headSize * 0.8f, innerColor, outerColor, beam.isOuterColorEnabled(), beam.getOuterColorWidth(), outerAlpha); + } + + // Render trail segments with smooth connections (with fading for non-anchored) + if (beam.hasFadingTrail()) { + renderFadingTrail(beam, trail, beamWidth, innerColor, outerColor, beam.isOuterColorEnabled(), beam.getOuterColorWidth()); + } else { + renderSmoothTrail(trail, beamWidth, innerColor, outerColor, beam.isOuterColorEnabled(), beam.getOuterColorWidth()); + } + + // Render head at interpolated offset position + renderHead(beam, headOffsetX, headOffsetY, headOffsetZ, headSize, innerColor, outerColor, outerAlpha, partialTicks); + + GL11.glPopMatrix(); + + restoreRenderState(); + } + + /** + * Render the charging orb during windup phase. + * Grows from 0 to headSize based on charge progress. + */ + private void renderChargingOrb(EntityAbilityBeam beam, double x, double y, double z, float partialTicks) { + float headSize = beam.getHeadSize(); + float chargeProgress = beam.getInterpolatedChargeProgress(partialTicks); + float size = headSize * chargeProgress; + + if (size <= 0.01f) return; + + int innerColor = beam.getInnerColor(); + int outerColor = beam.getOuterColor(); + + GL11.glPushMatrix(); + GL11.glTranslated(x, y, z); + + // Pulsing effect + float pulseTime = beam.ticksExisted + partialTicks; + float scaleModifier = (float) Math.sin(pulseTime * 0.2f) * 0.08f; + float scale = size * (1.0f + scaleModifier); + + // Render lightning during charging if enabled + if (beam.hasLightningEffect()) { + renderAttachedLightning(beam, scale); + } + + GL11.glPushMatrix(); + GL11.glScalef(scale, scale, scale); + + // Inner scale defines the core size + float innerScale = 0.6f; + + // Render outer glow only if enabled + if (beam.isOuterColorEnabled()) { + float outerScale = innerScale + beam.getOuterColorWidth(); + float outerAlpha = beam.getOuterColorAlpha(); + GL11.glDepthMask(false); + GL11.glPushMatrix(); + GL11.glScalef(outerScale, outerScale, outerScale); + renderCube(outerColor, outerAlpha, 0.5f); + GL11.glPopMatrix(); + GL11.glDepthMask(true); + } + + // Render inner core + GL11.glScalef(innerScale, innerScale, innerScale); + renderCube(innerColor, 1.0f, 0.5f); + + GL11.glPopMatrix(); + GL11.glPopMatrix(); + } + + /** + * Render the tail orb at the origin (where the beam emanates from). + * This orb does not rotate - it's a stable anchor point. + */ + private void renderTailOrb(float size, int innerColor, int outerColor, boolean outerColorEnabled, float outerColorWidth, float outerAlpha) { + GL11.glPushMatrix(); + // Already at origin (0,0,0) + + // Inner scale defines the core size + float innerScale = 0.6f; + + // Render outer glow only if enabled + // outerColorWidth is an additive offset from inner size + if (outerColorEnabled) { + float outerScale = innerScale + outerColorWidth; + GL11.glDepthMask(false); + GL11.glPushMatrix(); + GL11.glScalef(size * outerScale, size * outerScale, size * outerScale); + renderCube(outerColor, outerAlpha, 0.5f); + GL11.glPopMatrix(); + GL11.glDepthMask(true); + } + + // Render inner core (solid) + GL11.glPushMatrix(); + GL11.glScalef(size * innerScale, size * innerScale, size * innerScale); + renderCube(innerColor, 1.0f, 0.5f); + GL11.glPopMatrix(); + + GL11.glPopMatrix(); + } + + /** + * Render the beam trail with smooth connections between segments. + * Uses averaged perpendicular vectors at connection points to prevent gaps. + */ + private void renderSmoothTrail(List trail, float width, int innerColor, int outerColor, + boolean outerColorEnabled, float outerColorWidth) { + if (trail.size() < 2) return; + + int trailSize = trail.size(); + + // Pre-compute perpendicular frames at each point + // At connection points, average the perpendiculars for smooth transitions + List perpFrames = computePerpendiculars(trail); + + // Inner scale defines the core width + float innerScale = 0.6f; + float innerWidth = width * innerScale; + + // Render outer glow trail (wider, translucent) - only if enabled + // outerColorWidth is an additive offset from inner width + if (outerColorEnabled) { + float outerWidth = innerWidth + outerColorWidth * width; + GL11.glDepthMask(false); + renderTrailTube(trail, perpFrames, outerWidth, outerColor, 0.3f, true); + GL11.glDepthMask(true); + } + + // Render inner core trail + renderTrailTube(trail, perpFrames, innerWidth, innerColor, 0.9f, false); + } + + /** + * Render trail with fading effect (comet style) for non-anchored beams. + * Older trail points fade out based on their age. + */ + private void renderFadingTrail(EntityAbilityBeam beam, List trail, float width, int innerColor, int outerColor, + boolean outerColorEnabled, float outerColorWidth) { + if (trail.size() < 2) return; + + List ages = beam.getTrailPointAges(); + int fadeTime = beam.getTrailFadeTime(); + + // Pre-compute perpendicular frames + List perpFrames = computePerpendiculars(trail); + + // Inner scale defines the core width + float innerScale = 0.6f; + float innerWidth = width * innerScale; + + // Render outer glow trail with fading + if (outerColorEnabled) { + float outerWidth = innerWidth + outerColorWidth * width; + GL11.glDepthMask(false); + renderFadingTrailTube(trail, perpFrames, ages, fadeTime, outerWidth, outerColor, 0.3f); + GL11.glDepthMask(true); + } + + // Render inner core trail with fading + renderFadingTrailTube(trail, perpFrames, ages, fadeTime, innerWidth, innerColor, 0.9f); + } + + /** + * Render trail tube with age-based fading. + */ + private void renderFadingTrailTube(List trail, List perpFrames, List ages, + int fadeTime, float width, int color, float baseAlpha) { + float[] rgb = extractRGB(color); + float r = rgb[0], g = rgb[1], b = rgb[2]; + float halfWidth = width * 0.5f; + int size = trail.size(); + + Tessellator tess = Tessellator.instance; + tess.startDrawingQuads(); + + for (int i = 0; i < size - 1; i++) { + Vec3 p1 = trail.get(i); + Vec3 p2 = trail.get(i + 1); + double[] frame1 = perpFrames.get(i); + double[] frame2 = perpFrames.get(i + 1); + + // Calculate alpha based on age (older = more faded) + float age1 = i < ages.size() ? ages.get(i) : 0; + float age2 = (i + 1) < ages.size() ? ages.get(i + 1) : 0; + float fade1 = 1.0f - Math.min(1.0f, age1 / fadeTime); + float fade2 = 1.0f - Math.min(1.0f, age2 / fadeTime); + float alpha1 = baseAlpha * fade1; + float alpha2 = baseAlpha * fade2; + + // Skip fully faded segments + if (alpha1 <= 0.01f && alpha2 <= 0.01f) continue; + + // Calculate 4 corners at start + double s1x = p1.xCoord - frame1[0] * halfWidth - frame1[3] * halfWidth; + double s1y = p1.yCoord - frame1[1] * halfWidth - frame1[4] * halfWidth; + double s1z = p1.zCoord - frame1[2] * halfWidth - frame1[5] * halfWidth; + + double s2x = p1.xCoord + frame1[0] * halfWidth - frame1[3] * halfWidth; + double s2y = p1.yCoord + frame1[1] * halfWidth - frame1[4] * halfWidth; + double s2z = p1.zCoord + frame1[2] * halfWidth - frame1[5] * halfWidth; + + double s3x = p1.xCoord + frame1[0] * halfWidth + frame1[3] * halfWidth; + double s3y = p1.yCoord + frame1[1] * halfWidth + frame1[4] * halfWidth; + double s3z = p1.zCoord + frame1[2] * halfWidth + frame1[5] * halfWidth; + + double s4x = p1.xCoord - frame1[0] * halfWidth + frame1[3] * halfWidth; + double s4y = p1.yCoord - frame1[1] * halfWidth + frame1[4] * halfWidth; + double s4z = p1.zCoord - frame1[2] * halfWidth + frame1[5] * halfWidth; + + // Calculate 4 corners at end + double e1x = p2.xCoord - frame2[0] * halfWidth - frame2[3] * halfWidth; + double e1y = p2.yCoord - frame2[1] * halfWidth - frame2[4] * halfWidth; + double e1z = p2.zCoord - frame2[2] * halfWidth - frame2[5] * halfWidth; + + double e2x = p2.xCoord + frame2[0] * halfWidth - frame2[3] * halfWidth; + double e2y = p2.yCoord + frame2[1] * halfWidth - frame2[4] * halfWidth; + double e2z = p2.zCoord + frame2[2] * halfWidth - frame2[5] * halfWidth; + + double e3x = p2.xCoord + frame2[0] * halfWidth + frame2[3] * halfWidth; + double e3y = p2.yCoord + frame2[1] * halfWidth + frame2[4] * halfWidth; + double e3z = p2.zCoord + frame2[2] * halfWidth + frame2[5] * halfWidth; + + double e4x = p2.xCoord - frame2[0] * halfWidth + frame2[3] * halfWidth; + double e4y = p2.yCoord - frame2[1] * halfWidth + frame2[4] * halfWidth; + double e4z = p2.zCoord - frame2[2] * halfWidth + frame2[5] * halfWidth; + + // Bottom face + tess.setColorRGBA_F(r, g, b, alpha1); + tess.addVertex(s1x, s1y, s1z); + tess.addVertex(s2x, s2y, s2z); + tess.setColorRGBA_F(r, g, b, alpha2); + tess.addVertex(e2x, e2y, e2z); + tess.addVertex(e1x, e1y, e1z); + + // Top face + tess.setColorRGBA_F(r, g, b, alpha1); + tess.addVertex(s4x, s4y, s4z); + tess.setColorRGBA_F(r, g, b, alpha2); + tess.addVertex(e4x, e4y, e4z); + tess.addVertex(e3x, e3y, e3z); + tess.setColorRGBA_F(r, g, b, alpha1); + tess.addVertex(s3x, s3y, s3z); + + // Left face + tess.setColorRGBA_F(r, g, b, alpha1); + tess.addVertex(s1x, s1y, s1z); + tess.setColorRGBA_F(r, g, b, alpha2); + tess.addVertex(e1x, e1y, e1z); + tess.addVertex(e4x, e4y, e4z); + tess.setColorRGBA_F(r, g, b, alpha1); + tess.addVertex(s4x, s4y, s4z); + + // Right face + tess.setColorRGBA_F(r, g, b, alpha1); + tess.addVertex(s2x, s2y, s2z); + tess.addVertex(s3x, s3y, s3z); + tess.setColorRGBA_F(r, g, b, alpha2); + tess.addVertex(e3x, e3y, e3z); + tess.addVertex(e2x, e2y, e2z); + } + + tess.draw(); + } + + /** + * Compute perpendicular frames for each point in the trail. + * At interior points, average the perpendiculars from both directions for smooth curves. + * Returns list of [horzX, horzY, horzZ, vertX, vertY, vertZ] for each point. + */ + private List computePerpendiculars(List trail) { + List frames = new ArrayList<>(); + int size = trail.size(); + + for (int i = 0; i < size; i++) { + double[] frame = new double[6]; + + // Calculate direction at this point + double dx, dy, dz; + if (i == 0) { + // First point - use direction to next + Vec3 p1 = trail.get(0); + Vec3 p2 = trail.get(1); + dx = p2.xCoord - p1.xCoord; + dy = p2.yCoord - p1.yCoord; + dz = p2.zCoord - p1.zCoord; + } else if (i == size - 1) { + // Last point - use direction from previous + Vec3 p1 = trail.get(size - 2); + Vec3 p2 = trail.get(size - 1); + dx = p2.xCoord - p1.xCoord; + dy = p2.yCoord - p1.yCoord; + dz = p2.zCoord - p1.zCoord; + } else { + // Interior point - average directions + Vec3 p0 = trail.get(i - 1); + Vec3 p1 = trail.get(i); + Vec3 p2 = trail.get(i + 1); + double dx1 = p1.xCoord - p0.xCoord; + double dy1 = p1.yCoord - p0.yCoord; + double dz1 = p1.zCoord - p0.zCoord; + double dx2 = p2.xCoord - p1.xCoord; + double dy2 = p2.yCoord - p1.yCoord; + double dz2 = p2.zCoord - p1.zCoord; + // Average the directions + dx = (dx1 + dx2) * 0.5; + dy = (dy1 + dy2) * 0.5; + dz = (dz1 + dz2) * 0.5; + } + + // Normalize direction + double len = Math.sqrt(dx * dx + dy * dy + dz * dz); + if (len > 0.001) { + dx /= len; + dy /= len; + dz /= len; + } else { + dx = 0; dy = 0; dz = 1; + } + + // Calculate perpendicular vectors using consistent up reference + double upX = 0, upY = 1, upZ = 0; + if (Math.abs(dy) > 0.9) { + upX = 1; upY = 0; upZ = 0; + } + + // Cross product for horizontal perpendicular + double horzX = dy * upZ - dz * upY; + double horzY = dz * upX - dx * upZ; + double horzZ = dx * upY - dy * upX; + double horzLen = Math.sqrt(horzX * horzX + horzY * horzY + horzZ * horzZ); + if (horzLen > 0) { + horzX /= horzLen; + horzY /= horzLen; + horzZ /= horzLen; + } + + // Cross product for vertical perpendicular + double vertX = dy * horzZ - dz * horzY; + double vertY = dz * horzX - dx * horzZ; + double vertZ = dx * horzY - dy * horzX; + + frame[0] = horzX; frame[1] = horzY; frame[2] = horzZ; + frame[3] = vertX; frame[4] = vertY; frame[5] = vertZ; + frames.add(frame); + } + + return frames; + } + + /** + * Render the trail as a continuous tube using pre-computed perpendicular frames. + */ + private void renderTrailTube(List trail, List perpFrames, + float width, int color, float baseAlpha, boolean fadeFromStart) { + float[] rgb = extractRGB(color); + float r = rgb[0], g = rgb[1], b = rgb[2]; + float halfWidth = width * 0.5f; + int size = trail.size(); + + Tessellator tess = Tessellator.instance; + tess.startDrawingQuads(); + + for (int i = 0; i < size - 1; i++) { + Vec3 p1 = trail.get(i); + Vec3 p2 = trail.get(i + 1); + double[] frame1 = perpFrames.get(i); + double[] frame2 = perpFrames.get(i + 1); + + // Calculate alpha based on position (fade from start or constant) + float alpha1, alpha2; + if (fadeFromStart) { + float progress1 = (float) i / size; + float progress2 = (float) (i + 1) / size; + alpha1 = baseAlpha * progress1; + alpha2 = baseAlpha * progress2; + } else { + alpha1 = baseAlpha; + alpha2 = baseAlpha; + } + + // Calculate 4 corners at start + double s1x = p1.xCoord - frame1[0] * halfWidth - frame1[3] * halfWidth; + double s1y = p1.yCoord - frame1[1] * halfWidth - frame1[4] * halfWidth; + double s1z = p1.zCoord - frame1[2] * halfWidth - frame1[5] * halfWidth; + + double s2x = p1.xCoord + frame1[0] * halfWidth - frame1[3] * halfWidth; + double s2y = p1.yCoord + frame1[1] * halfWidth - frame1[4] * halfWidth; + double s2z = p1.zCoord + frame1[2] * halfWidth - frame1[5] * halfWidth; + + double s3x = p1.xCoord + frame1[0] * halfWidth + frame1[3] * halfWidth; + double s3y = p1.yCoord + frame1[1] * halfWidth + frame1[4] * halfWidth; + double s3z = p1.zCoord + frame1[2] * halfWidth + frame1[5] * halfWidth; + + double s4x = p1.xCoord - frame1[0] * halfWidth + frame1[3] * halfWidth; + double s4y = p1.yCoord - frame1[1] * halfWidth + frame1[4] * halfWidth; + double s4z = p1.zCoord - frame1[2] * halfWidth + frame1[5] * halfWidth; + + // Calculate 4 corners at end + double e1x = p2.xCoord - frame2[0] * halfWidth - frame2[3] * halfWidth; + double e1y = p2.yCoord - frame2[1] * halfWidth - frame2[4] * halfWidth; + double e1z = p2.zCoord - frame2[2] * halfWidth - frame2[5] * halfWidth; + + double e2x = p2.xCoord + frame2[0] * halfWidth - frame2[3] * halfWidth; + double e2y = p2.yCoord + frame2[1] * halfWidth - frame2[4] * halfWidth; + double e2z = p2.zCoord + frame2[2] * halfWidth - frame2[5] * halfWidth; + + double e3x = p2.xCoord + frame2[0] * halfWidth + frame2[3] * halfWidth; + double e3y = p2.yCoord + frame2[1] * halfWidth + frame2[4] * halfWidth; + double e3z = p2.zCoord + frame2[2] * halfWidth + frame2[5] * halfWidth; + + double e4x = p2.xCoord - frame2[0] * halfWidth + frame2[3] * halfWidth; + double e4y = p2.yCoord - frame2[1] * halfWidth + frame2[4] * halfWidth; + double e4z = p2.zCoord - frame2[2] * halfWidth + frame2[5] * halfWidth; + + // Bottom face (s1-s2-e2-e1) + tess.setColorRGBA_F(r, g, b, alpha1); + tess.addVertex(s1x, s1y, s1z); + tess.addVertex(s2x, s2y, s2z); + tess.setColorRGBA_F(r, g, b, alpha2); + tess.addVertex(e2x, e2y, e2z); + tess.addVertex(e1x, e1y, e1z); + + // Top face (s4-e4-e3-s3) + tess.setColorRGBA_F(r, g, b, alpha1); + tess.addVertex(s4x, s4y, s4z); + tess.setColorRGBA_F(r, g, b, alpha2); + tess.addVertex(e4x, e4y, e4z); + tess.addVertex(e3x, e3y, e3z); + tess.setColorRGBA_F(r, g, b, alpha1); + tess.addVertex(s3x, s3y, s3z); + + // Left face (s1-e1-e4-s4) + tess.setColorRGBA_F(r, g, b, alpha1); + tess.addVertex(s1x, s1y, s1z); + tess.setColorRGBA_F(r, g, b, alpha2); + tess.addVertex(e1x, e1y, e1z); + tess.addVertex(e4x, e4y, e4z); + tess.setColorRGBA_F(r, g, b, alpha1); + tess.addVertex(s4x, s4y, s4z); + + // Right face (s2-s3-e3-e2) + tess.setColorRGBA_F(r, g, b, alpha1); + tess.addVertex(s2x, s2y, s2z); + tess.addVertex(s3x, s3y, s3z); + tess.setColorRGBA_F(r, g, b, alpha2); + tess.addVertex(e3x, e3y, e3z); + tess.addVertex(e2x, e2y, e2z); + } + + tess.draw(); + } + + /** + * Render the beam head as a glowing cube. + * Coordinates are relative to origin. + */ + private void renderHead(EntityAbilityBeam beam, double x, double y, double z, + float size, int innerColor, int outerColor, float outerAlpha, float partialTicks) { + GL11.glPushMatrix(); + GL11.glTranslated(x, y, z); + + // Pulsing effect + float pulseTime = beam.ticksExisted + partialTicks; + float scaleModifier = (float) Math.sin(pulseTime * 0.2f) * 0.1f; + float scale = size * (1.0f + scaleModifier); + + // Render lightning BEFORE rotation so it crackles in all directions (only on head) + if (beam.hasLightningEffect()) { + renderAttachedLightning(beam, scale); + } + + // Rotation + GL11.glRotatef(beam.getInterpolatedRotationX(partialTicks), 1.0f, 0.0f, 0.0f); + GL11.glRotatef(beam.getInterpolatedRotationY(partialTicks), 0.0f, 1.0f, 0.0f); + GL11.glRotatef(beam.getInterpolatedRotationZ(partialTicks), 0.0f, 0.0f, 1.0f); + + GL11.glPushMatrix(); + GL11.glScalef(scale, scale, scale); + + // Inner scale defines the core size + float innerScale = 0.6f; + + // Render outer glow only if enabled + // outerColorWidth is an additive offset from inner size + if (beam.isOuterColorEnabled()) { + float outerScale = innerScale + beam.getOuterColorWidth(); + GL11.glDepthMask(false); + GL11.glPushMatrix(); + GL11.glScalef(outerScale, outerScale, outerScale); + renderCube(outerColor, outerAlpha, 0.5f); + GL11.glPopMatrix(); + GL11.glDepthMask(true); + } + + // Render inner core + GL11.glScalef(innerScale, innerScale, innerScale); + renderCube(innerColor, 1.0f, 0.5f); + + GL11.glPopMatrix(); + GL11.glPopMatrix(); + } + + /** + * Render fading lightning arcs attached to the beam head (in local space). + */ + private void renderAttachedLightning(EntityAbilityBeam beam, float headScale) { + AttachedLightningRenderer.LightningState state = getLightningState(beam); + + float density = beam.getLightningDensity(); + // Lightning radius extends outward from inner surface (innerScale = 0.6) + float innerRadius = 0.6f * headScale * 0.5f; + float radius = innerRadius + beam.getLightningRadius() * headScale; + int outerColor = beam.getOuterColor(); + int innerColor = beam.getInnerColor(); + int fadeTime = beam.getLightningFadeTime(); + + state.update(density, radius, outerColor, innerColor, fadeTime); + state.render(); + } + + /** + * Get or create the lightning state for an entity. + */ + private AttachedLightningRenderer.LightningState getLightningState(EntityAbilityBeam beam) { + if (beam.lightningState == null) { + beam.lightningState = new AttachedLightningRenderer.LightningState(); + } + return (AttachedLightningRenderer.LightningState) beam.lightningState; + } +} diff --git a/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityDisc.java b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityDisc.java new file mode 100644 index 000000000..20486d70e --- /dev/null +++ b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityDisc.java @@ -0,0 +1,178 @@ +package kamkeel.npcs.client.renderer; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import kamkeel.npcs.client.renderer.lightning.AttachedLightningRenderer; +import kamkeel.npcs.entity.EntityAbilityDisc; +import net.minecraft.entity.Entity; +import org.lwjgl.opengl.GL11; + +/** + * Renders the AbilityDisc entity as a flat spinning square. + * Rotates only on Y axis (flat spin like a saw blade). + * + * Design inspired by LouisXIV's energy rendering system. + */ +@SideOnly(Side.CLIENT) +public class RenderAbilityDisc extends RenderAbilityProjectile { + + @Override + public void doRender(Entity entity, double x, double y, double z, float yaw, float partialTicks) { + EntityAbilityDisc disc = (EntityAbilityDisc) entity; + + setupRenderState(); + + GL11.glPushMatrix(); + GL11.glTranslated(x, y, z); + + // Get interpolated values + float size = disc.getInterpolatedSize(partialTicks); + float radius = disc.getDiscRadius() * size; + float thickness = disc.getDiscThickness() * size; + + // Subtle pulsing effect + float pulseTime = entity.ticksExisted + partialTicks; + float scaleModifier = (float) Math.sin(pulseTime * 0.1f) * 0.03f; + float scale = 1.0f + scaleModifier; + + // Render lightning BEFORE rotation so it crackles in all directions + if (disc.hasLightningEffect()) { + renderAttachedLightning(disc, radius * scale); + } + + GL11.glPushMatrix(); + GL11.glScalef(scale, scale, scale); + + // Apply ONLY Y-axis rotation for flat spin + GL11.glRotatef(disc.getInterpolatedRotationY(partialTicks), 0.0f, 1.0f, 0.0f); + + // Inner scale defines the core size + float innerScale = 0.6f; + + // Render outer square (translucent) - only if enabled + // outerColorWidth is an additive offset from inner size + if (disc.isOuterColorEnabled()) { + float outerScale = innerScale + disc.getOuterColorWidth(); + GL11.glDepthMask(false); + renderFlatSquare(disc.getOuterColor(), disc.getOuterColorAlpha(), radius * outerScale, thickness); + GL11.glDepthMask(true); + } + + // Render inner square (solid) + renderFlatSquare(disc.getInnerColor(), 1.0f, radius * innerScale, thickness * 1.2f); + + GL11.glPopMatrix(); + GL11.glPopMatrix(); + + restoreRenderState(); + } + + /** + * Render fading lightning arcs attached to the disc (in local space). + */ + private void renderAttachedLightning(EntityAbilityDisc disc, float discSize) { + AttachedLightningRenderer.LightningState state = getLightningState(disc); + + float density = disc.getLightningDensity(); + // Lightning radius extends outward from inner surface (innerScale = 0.6) + float innerRadius = 0.6f * discSize * 0.5f; + float radius = innerRadius + disc.getLightningRadius() * discSize; + int outerColor = disc.getOuterColor(); + int innerColor = disc.getInnerColor(); + int fadeTime = disc.getLightningFadeTime(); + + state.update(density, radius, outerColor, innerColor, fadeTime); + state.render(); + } + + /** + * Get or create the lightning state for an entity. + */ + private AttachedLightningRenderer.LightningState getLightningState(EntityAbilityDisc disc) { + if (disc.lightningState == null) { + disc.lightningState = new AttachedLightningRenderer.LightningState(); + } + return (AttachedLightningRenderer.LightningState) disc.lightningState; + } + + /** + * Render a flat square (thin cube) centered at origin. + * The square lies flat on the XZ plane with thin Y height. + */ + private void renderFlatSquare(int color, float alpha, float halfSize, float thickness) { + float halfThick = thickness * 0.5f; + + // Render as a thin cube + renderCubeCustom(color, alpha, halfSize, halfThick, halfSize); + } + + /** + * Render a cube with custom dimensions. + */ + private void renderCubeCustom(int color, float alpha, float halfX, float halfY, float halfZ) { + float[] rgb = extractRGB(color); + float r = rgb[0], g = rgb[1], b = rgb[2]; + + net.minecraft.client.renderer.Tessellator tess = net.minecraft.client.renderer.Tessellator.instance; + + // Top face (y+) + tess.startDrawingQuads(); + tess.setColorRGBA_F(r, g, b, alpha); + tess.setNormal(0, 1, 0); + tess.addVertex(-halfX, halfY, halfZ); + tess.addVertex(halfX, halfY, halfZ); + tess.addVertex(halfX, halfY, -halfZ); + tess.addVertex(-halfX, halfY, -halfZ); + tess.draw(); + + // Bottom face (y-) + tess.startDrawingQuads(); + tess.setColorRGBA_F(r, g, b, alpha); + tess.setNormal(0, -1, 0); + tess.addVertex(-halfX, -halfY, -halfZ); + tess.addVertex(halfX, -halfY, -halfZ); + tess.addVertex(halfX, -halfY, halfZ); + tess.addVertex(-halfX, -halfY, halfZ); + tess.draw(); + + // Front face (z+) + tess.startDrawingQuads(); + tess.setColorRGBA_F(r, g, b, alpha); + tess.setNormal(0, 0, 1); + tess.addVertex(-halfX, -halfY, halfZ); + tess.addVertex(halfX, -halfY, halfZ); + tess.addVertex(halfX, halfY, halfZ); + tess.addVertex(-halfX, halfY, halfZ); + tess.draw(); + + // Back face (z-) + tess.startDrawingQuads(); + tess.setColorRGBA_F(r, g, b, alpha); + tess.setNormal(0, 0, -1); + tess.addVertex(halfX, -halfY, -halfZ); + tess.addVertex(-halfX, -halfY, -halfZ); + tess.addVertex(-halfX, halfY, -halfZ); + tess.addVertex(halfX, halfY, -halfZ); + tess.draw(); + + // Right face (x+) + tess.startDrawingQuads(); + tess.setColorRGBA_F(r, g, b, alpha); + tess.setNormal(1, 0, 0); + tess.addVertex(halfX, -halfY, halfZ); + tess.addVertex(halfX, -halfY, -halfZ); + tess.addVertex(halfX, halfY, -halfZ); + tess.addVertex(halfX, halfY, halfZ); + tess.draw(); + + // Left face (x-) + tess.startDrawingQuads(); + tess.setColorRGBA_F(r, g, b, alpha); + tess.setNormal(-1, 0, 0); + tess.addVertex(-halfX, -halfY, -halfZ); + tess.addVertex(-halfX, -halfY, halfZ); + tess.addVertex(-halfX, halfY, halfZ); + tess.addVertex(-halfX, halfY, -halfZ); + tess.draw(); + } +} diff --git a/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityLaser.java b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityLaser.java new file mode 100644 index 000000000..e70e5c6ba --- /dev/null +++ b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityLaser.java @@ -0,0 +1,344 @@ +package kamkeel.npcs.client.renderer; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import kamkeel.npcs.client.renderer.lightning.AttachedLightningRenderer; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityAbilityLaser; +import net.minecraft.client.renderer.Tessellator; +import net.minecraft.entity.Entity; +import org.lwjgl.opengl.GL11; + +/** + * Renders the AbilityLaser entity as a rectangular beam with clear inner/outer colors. + * + * Design inspired by LouisXIV's energy rendering system. + */ +@SideOnly(Side.CLIENT) +public class RenderAbilityLaser extends RenderAbilityProjectile { + + @Override + public void doRender(Entity entity, double x, double y, double z, float yaw, float partialTicks) { + EntityAbilityLaser laser = (EntityAbilityLaser) entity; + + setupRenderState(); + + if (laser.isCharging()) { + renderChargingOrb(laser, x, y, z, partialTicks); + restoreRenderState(); + return; + } + + // Get laser parameters + double startX = laser.getStartX(); + double startY = laser.getStartY(); + double startZ = laser.getStartZ(); + double endX = laser.getEndX(); + double endY = laser.getEndY(); + double endZ = laser.getEndZ(); + float width = laser.getLaserWidth(); + float alpha = laser.getLingerAlpha(); + + // Calculate render offset (entity position is used as reference) + double offsetX = x - laser.posX; + double offsetY = y - laser.posY; + double offsetZ = z - laser.posZ; + + // Render start relative to camera + double renderStartX = startX + offsetX; + double renderStartY = startY + offsetY; + double renderStartZ = startZ + offsetZ; + double renderEndX = endX + offsetX; + double renderEndY = endY + offsetY; + double renderEndZ = endZ + offsetZ; + + // Calculate perpendicular vectors for the beam rectangle + double dx = renderEndX - renderStartX; + double dy = renderEndY - renderStartY; + double dz = renderEndZ - renderStartZ; + double len = Math.sqrt(dx * dx + dy * dy + dz * dz); + if (len < 0.001) { + restoreRenderState(); + return; + } + + // Normalize direction + dx /= len; + dy /= len; + dz /= len; + + // Calculate perpendicular vectors (one horizontal, one vertical relative to beam) + double upX = 0, upY = 1, upZ = 0; + if (Math.abs(dy) > 0.9) { + upX = 1; upY = 0; upZ = 0; + } + + // Cross product for horizontal perpendicular + double horzX = dy * upZ - dz * upY; + double horzY = dz * upX - dx * upZ; + double horzZ = dx * upY - dy * upX; + double horzLen = Math.sqrt(horzX * horzX + horzY * horzY + horzZ * horzZ); + if (horzLen > 0) { + horzX /= horzLen; + horzY /= horzLen; + horzZ /= horzLen; + } + + // Cross product for vertical perpendicular + double vertX = dy * horzZ - dz * horzY; + double vertY = dz * horzX - dx * horzZ; + double vertZ = dx * horzY - dy * horzX; + + // Inner scale defines the core width + float innerScale = 0.6f; + float innerWidth = width * innerScale; + + // Render outer rectangle (larger, outer color, translucent) - only if enabled + // outerColorWidth is an additive offset from inner width + if (laser.isOuterColorEnabled()) { + float outerWidth = innerWidth + laser.getOuterColorWidth() * width; + GL11.glDepthMask(false); + renderBeamRectangle(renderStartX, renderStartY, renderStartZ, + renderEndX, renderEndY, renderEndZ, + horzX, horzY, horzZ, vertX, vertY, vertZ, + outerWidth, laser.getOuterColor(), alpha * 0.4f); + + // Render middle layer (halfway between inner and outer) + float midWidth = innerWidth + (outerWidth - innerWidth) * 0.5f; + renderBeamRectangle(renderStartX, renderStartY, renderStartZ, + renderEndX, renderEndY, renderEndZ, + horzX, horzY, horzZ, vertX, vertY, vertZ, + midWidth, laser.getOuterColor(), alpha * 0.7f); + GL11.glDepthMask(true); + } + + // Render inner core (solid) + renderBeamRectangle(renderStartX, renderStartY, renderStartZ, + renderEndX, renderEndY, renderEndZ, + horzX, horzY, horzZ, vertX, vertY, vertZ, + innerWidth, laser.getInnerColor(), alpha); + + // Render lightning along the laser beam + if (laser.hasLightningEffect() && laser.getCurrentLength() > 0.1f) { + renderLaserLightning(laser, renderStartX, renderStartY, renderStartZ, + renderEndX, renderEndY, renderEndZ, width); + } + + restoreRenderState(); + } + + private void renderChargingOrb(EntityAbilityLaser laser, double x, double y, double z, float partialTicks) { + float headSize = laser.getLaserWidth() * 1.5f; + float chargeProgress = laser.getInterpolatedChargeProgress(partialTicks); + float size = headSize * chargeProgress; + + if (size <= 0.01f) return; + + int innerColor = laser.getInnerColor(); + int outerColor = laser.getOuterColor(); + + GL11.glPushMatrix(); + GL11.glTranslated(x, y, z); + + // Pulsing effect + float pulseTime = laser.ticksExisted + partialTicks; + float scaleModifier = (float) Math.sin(pulseTime * 0.2f) * 0.08f; + float scale = size * (1.0f + scaleModifier); + + // Render lightning during charging if enabled + if (laser.hasLightningEffect()) { + renderAttachedLightning(laser, scale); + } + + GL11.glPushMatrix(); + GL11.glScalef(scale, scale, scale); + + // Inner scale defines the core size + float innerScale = 0.6f; + + // Render outer glow only if enabled + if (laser.isOuterColorEnabled()) { + float outerScale = innerScale + laser.getOuterColorWidth(); + float outerAlpha = laser.getOuterColorAlpha(); + GL11.glDepthMask(false); + GL11.glPushMatrix(); + GL11.glScalef(outerScale, outerScale, outerScale); + renderCube(outerColor, outerAlpha, 0.5f); + GL11.glPopMatrix(); + GL11.glDepthMask(true); + } + + // Render inner core + GL11.glScalef(innerScale, innerScale, innerScale); + renderCube(innerColor, 1.0f, 0.5f); + + GL11.glPopMatrix(); + GL11.glPopMatrix(); + } + + /** + * Render lightning effects along the laser beam at multiple points. + */ + private void renderLaserLightning(EntityAbilityLaser laser, + double startX, double startY, double startZ, + double endX, double endY, double endZ, float width) { + AttachedLightningRenderer.LightningState state = getLightningState(laser); + + float density = laser.getLightningDensity(); + // Lightning radius extends outward from inner width + float innerRadius = width * 0.6f * 0.5f; + float radius = innerRadius + laser.getLightningRadius() * width; + int outerColor = laser.getOuterColor(); + int innerColor = laser.getInnerColor(); + int fadeTime = laser.getLightningFadeTime(); + + // Render lightning at multiple points along the laser + // Start, middle, and end points + double midX = (startX + endX) * 0.5; + double midY = (startY + endY) * 0.5; + double midZ = (startZ + endZ) * 0.5; + + GL11.glPushMatrix(); + GL11.glTranslated(midX, midY, midZ); + state.update(density, radius, outerColor, innerColor, fadeTime); + state.render(); + GL11.glPopMatrix(); + + // Also render at the tip + GL11.glPushMatrix(); + GL11.glTranslated(endX, endY, endZ); + // Use a secondary state for the tip + AttachedLightningRenderer.LightningState tipState = getTipLightningState(laser); + tipState.update(density * 0.7f, radius * 0.8f, outerColor, innerColor, fadeTime); + tipState.render(); + GL11.glPopMatrix(); + } + + + private void renderAttachedLightning(EntityAbilityLaser laser, float headScale) { + AttachedLightningRenderer.LightningState state = getLightningState(laser); + + float density = laser.getLightningDensity(); + // Lightning radius extends outward from inner surface (innerScale = 0.6) + float innerRadius = 0.6f * headScale * 0.5f; + float radius = innerRadius + laser.getLightningRadius() * headScale; + int outerColor = laser.getOuterColor(); + int innerColor = laser.getInnerColor(); + int fadeTime = laser.getLightningFadeTime(); + + state.update(density, radius, outerColor, innerColor, fadeTime); + state.render(); + } + + /** + * Get or create the main lightning state for the laser. + */ + private AttachedLightningRenderer.LightningState getLightningState(EntityAbilityLaser laser) { + if (laser.lightningState == null) { + laser.lightningState = new AttachedLightningRenderer.LightningState(); + } + return (AttachedLightningRenderer.LightningState) laser.lightningState; + } + + /** + * Get or create the tip lightning state for the laser. + * Stored as a secondary object in lightningState2. + */ + private AttachedLightningRenderer.LightningState getTipLightningState(EntityAbilityLaser laser) { + // Use a simple approach - store in a transient field + // Since lightningState is already used, we'll create a new state each time + // This works because we're updating and rendering immediately + return new AttachedLightningRenderer.LightningState(); + } + + /** + * Render a beam as a 3D rectangular prism (box along a line). + */ + private void renderBeamRectangle(double x1, double y1, double z1, + double x2, double y2, double z2, + double horzX, double horzY, double horzZ, + double vertX, double vertY, double vertZ, + float width, int color, float alpha) { + float[] rgb = extractRGB(color); + float r = rgb[0], g = rgb[1], b = rgb[2]; + + float halfWidth = width * 0.5f; + + // Calculate 8 corners of the box + // Start corners + double s1x = x1 - horzX * halfWidth - vertX * halfWidth; + double s1y = y1 - horzY * halfWidth - vertY * halfWidth; + double s1z = z1 - horzZ * halfWidth - vertZ * halfWidth; + + double s2x = x1 + horzX * halfWidth - vertX * halfWidth; + double s2y = y1 + horzY * halfWidth - vertY * halfWidth; + double s2z = z1 + horzZ * halfWidth - vertZ * halfWidth; + + double s3x = x1 + horzX * halfWidth + vertX * halfWidth; + double s3y = y1 + horzY * halfWidth + vertY * halfWidth; + double s3z = z1 + horzZ * halfWidth + vertZ * halfWidth; + + double s4x = x1 - horzX * halfWidth + vertX * halfWidth; + double s4y = y1 - horzY * halfWidth + vertY * halfWidth; + double s4z = z1 - horzZ * halfWidth + vertZ * halfWidth; + + // End corners + double e1x = x2 - horzX * halfWidth - vertX * halfWidth; + double e1y = y2 - horzY * halfWidth - vertY * halfWidth; + double e1z = z2 - horzZ * halfWidth - vertZ * halfWidth; + + double e2x = x2 + horzX * halfWidth - vertX * halfWidth; + double e2y = y2 + horzY * halfWidth - vertY * halfWidth; + double e2z = z2 + horzZ * halfWidth - vertZ * halfWidth; + + double e3x = x2 + horzX * halfWidth + vertX * halfWidth; + double e3y = y2 + horzY * halfWidth + vertY * halfWidth; + double e3z = z2 + horzZ * halfWidth + vertZ * halfWidth; + + double e4x = x2 - horzX * halfWidth + vertX * halfWidth; + double e4y = y2 - horzY * halfWidth + vertY * halfWidth; + double e4z = z2 - horzZ * halfWidth + vertZ * halfWidth; + + Tessellator tess = Tessellator.instance; + tess.startDrawingQuads(); + tess.setColorRGBA_F(r, g, b, alpha); + + // Bottom face + tess.addVertex(s1x, s1y, s1z); + tess.addVertex(s2x, s2y, s2z); + tess.addVertex(e2x, e2y, e2z); + tess.addVertex(e1x, e1y, e1z); + + // Top face + tess.addVertex(s4x, s4y, s4z); + tess.addVertex(e4x, e4y, e4z); + tess.addVertex(e3x, e3y, e3z); + tess.addVertex(s3x, s3y, s3z); + + // Left face + tess.addVertex(s1x, s1y, s1z); + tess.addVertex(e1x, e1y, e1z); + tess.addVertex(e4x, e4y, e4z); + tess.addVertex(s4x, s4y, s4z); + + // Right face + tess.addVertex(s2x, s2y, s2z); + tess.addVertex(s3x, s3y, s3z); + tess.addVertex(e3x, e3y, e3z); + tess.addVertex(e2x, e2y, e2z); + + // Start cap + tess.addVertex(s1x, s1y, s1z); + tess.addVertex(s4x, s4y, s4z); + tess.addVertex(s3x, s3y, s3z); + tess.addVertex(s2x, s2y, s2z); + + // End cap + tess.addVertex(e1x, e1y, e1z); + tess.addVertex(e2x, e2y, e2z); + tess.addVertex(e3x, e3y, e3z); + tess.addVertex(e4x, e4y, e4z); + + tess.draw(); + } +} diff --git a/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityOrb.java b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityOrb.java new file mode 100644 index 000000000..048b3642f --- /dev/null +++ b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityOrb.java @@ -0,0 +1,106 @@ +package kamkeel.npcs.client.renderer; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import kamkeel.npcs.client.renderer.lightning.AttachedLightningRenderer; +import kamkeel.npcs.entity.EntityAbilityOrb; +import net.minecraft.entity.Entity; +import org.lwjgl.opengl.GL11; + +/** + * Renders the AbilityOrb entity as a cube with inner and outer layers. + * Minecraft-style blocky appearance using Tessellator. + */ +@SideOnly(Side.CLIENT) +public class RenderAbilityOrb extends RenderAbilityProjectile { + + private static final int LIGHTNING_FADE_TICKS = 6; + + @Override + public void doRender(Entity entity, double x, double y, double z, float yaw, float partialTicks) { + EntityAbilityOrb orb = (EntityAbilityOrb) entity; + + setupRenderState(); + + GL11.glPushMatrix(); + GL11.glTranslated(x, y, z); + + // Get interpolated size for smooth rendering + float size = orb.getInterpolatedSize(partialTicks); + + // Pulsing scale effect + float pulseTime = entity.ticksExisted + partialTicks; + float scaleModifier = (float) Math.sin(pulseTime * 0.15f) * 0.05f; + float scale = size * (1.0f + scaleModifier); + + // Render lightning BEFORE rotation so it crackles in all directions + if (orb.hasLightningEffect()) { + renderAttachedLightning(orb, scale); + } + + // Use entity's interpolated rotation for smooth spinning + GL11.glRotatef(orb.getInterpolatedRotationX(partialTicks), 1.0f, 0.0f, 0.0f); + GL11.glRotatef(orb.getInterpolatedRotationY(partialTicks), 0.0f, 1.0f, 0.0f); + GL11.glRotatef(orb.getInterpolatedRotationZ(partialTicks), 0.0f, 0.0f, 1.0f); + + GL11.glPushMatrix(); + GL11.glScalef(scale, scale, scale); + + // Inner scale defines the core size + float innerScale = 0.6f; + + // Render outer cube (translucent) - only if enabled + // outerColorWidth is an additive offset from inner size + if (orb.isOuterColorEnabled()) { + float outerScale = innerScale + orb.getOuterColorWidth(); + GL11.glDepthMask(false); + GL11.glPushMatrix(); + GL11.glScalef(outerScale, outerScale, outerScale); + renderCube(orb.getOuterColor(), orb.getOuterColorAlpha(), 0.5f); + GL11.glPopMatrix(); + GL11.glDepthMask(true); + } + + // Render inner cube (solid) + GL11.glScalef(innerScale, innerScale, innerScale); + renderCube(orb.getInnerColor(), 1.0f, 0.5f); + + GL11.glPopMatrix(); + GL11.glPopMatrix(); + + restoreRenderState(); + } + + /** + * Render fading lightning arcs attached to the orb (in local space). + * Arcs persist for a few ticks and fade out, while staying attached to the orb. + */ + private void renderAttachedLightning(EntityAbilityOrb orb, float orbScale) { + // Get or create the lightning state for this entity + AttachedLightningRenderer.LightningState state = getLightningState(orb); + + float density = orb.getLightningDensity(); + // Lightning radius extends outward from inner surface (innerScale = 0.6) + float innerRadius = 0.6f * orbScale * 0.5f; // Half size of inner cube + float radius = innerRadius + orb.getLightningRadius() * orbScale; + int outerColor = orb.getOuterColor(); + int innerColor = orb.getInnerColor(); + int fadeTime = orb.getLightningFadeTime(); + + // Update: age existing arcs, spawn new ones based on density + state.update(density, radius, outerColor, innerColor, fadeTime); + + // Render all active arcs in local space (they move with the orb) + state.render(); + } + + /** + * Get or create the lightning state for an entity. + */ + private AttachedLightningRenderer.LightningState getLightningState(EntityAbilityOrb orb) { + if (orb.lightningState == null) { + orb.lightningState = new AttachedLightningRenderer.LightningState(); + } + return (AttachedLightningRenderer.LightningState) orb.lightningState; + } +} diff --git a/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityProjectile.java b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityProjectile.java new file mode 100644 index 000000000..2ba840aec --- /dev/null +++ b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityProjectile.java @@ -0,0 +1,259 @@ +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; +import net.minecraft.util.ResourceLocation; +import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GL12; + +/** + * Base renderer for ability projectiles. + * Provides common GL state setup and rendering utilities. + * + * Design inspired by LouisXIV's energy rendering system. + */ +@SideOnly(Side.CLIENT) +public abstract class RenderAbilityProjectile extends Render { + + protected static final ResourceLocation WHITE_TEXTURE = new ResourceLocation("customnpcs", "textures/entity/white.png"); + + public RenderAbilityProjectile() { + this.shadowSize = 0.0f; + } + + /** + * Setup GL state for translucent, self-illuminated rendering. + * Forces full brightness so world lighting does not affect projectiles. + */ + protected void setupRenderState() { + GL11.glPushMatrix(); + GL11.glPushAttrib(GL11.GL_ALL_ATTRIB_BITS); + GL11.glDisable(GL11.GL_CULL_FACE); + GL11.glEnable(GL12.GL_RESCALE_NORMAL); + GL11.glEnable(GL11.GL_ALPHA_TEST); + GL11.glEnable(GL11.GL_BLEND); + GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); + 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); + } + + /** + * Restore GL state after rendering. + */ + protected void restoreRenderState() { + GL11.glPopAttrib(); + GL11.glPopMatrix(); + } + + /** + * Extract RGB components from color int. + */ + protected float[] extractRGB(int color) { + float r = ((color >> 16) & 0xFF) / 255.0f; + float g = ((color >> 8) & 0xFF) / 255.0f; + float b = (color & 0xFF) / 255.0f; + return new float[]{r, g, b}; + } + + /** + * Render a cube centered at origin with given color and alpha. + */ + protected void renderCube(int color, float alpha, float halfSize) { + float[] rgb = extractRGB(color); + float r = rgb[0], g = rgb[1], b = rgb[2]; + + Tessellator tess = Tessellator.instance; + + // Front face (z+) + tess.startDrawingQuads(); + tess.setColorRGBA_F(r, g, b, alpha); + tess.setNormal(0, 0, 1); + tess.addVertex(-halfSize, -halfSize, halfSize); + tess.addVertex(halfSize, -halfSize, halfSize); + tess.addVertex(halfSize, halfSize, halfSize); + tess.addVertex(-halfSize, halfSize, halfSize); + tess.draw(); + + // Back face (z-) + tess.startDrawingQuads(); + tess.setColorRGBA_F(r, g, b, alpha); + tess.setNormal(0, 0, -1); + tess.addVertex(halfSize, -halfSize, -halfSize); + tess.addVertex(-halfSize, -halfSize, -halfSize); + tess.addVertex(-halfSize, halfSize, -halfSize); + tess.addVertex(halfSize, halfSize, -halfSize); + tess.draw(); + + // Top face (y+) + tess.startDrawingQuads(); + tess.setColorRGBA_F(r, g, b, alpha); + tess.setNormal(0, 1, 0); + tess.addVertex(-halfSize, halfSize, halfSize); + tess.addVertex(halfSize, halfSize, halfSize); + tess.addVertex(halfSize, halfSize, -halfSize); + tess.addVertex(-halfSize, halfSize, -halfSize); + tess.draw(); + + // Bottom face (y-) + tess.startDrawingQuads(); + tess.setColorRGBA_F(r, g, b, alpha); + tess.setNormal(0, -1, 0); + tess.addVertex(-halfSize, -halfSize, -halfSize); + tess.addVertex(halfSize, -halfSize, -halfSize); + tess.addVertex(halfSize, -halfSize, halfSize); + tess.addVertex(-halfSize, -halfSize, halfSize); + tess.draw(); + + // Right face (x+) + tess.startDrawingQuads(); + tess.setColorRGBA_F(r, g, b, alpha); + tess.setNormal(1, 0, 0); + tess.addVertex(halfSize, -halfSize, halfSize); + tess.addVertex(halfSize, -halfSize, -halfSize); + tess.addVertex(halfSize, halfSize, -halfSize); + tess.addVertex(halfSize, halfSize, halfSize); + tess.draw(); + + // Left face (x-) + tess.startDrawingQuads(); + tess.setColorRGBA_F(r, g, b, alpha); + tess.setNormal(-1, 0, 0); + tess.addVertex(-halfSize, -halfSize, -halfSize); + tess.addVertex(-halfSize, -halfSize, halfSize); + tess.addVertex(-halfSize, halfSize, halfSize); + tess.addVertex(-halfSize, halfSize, -halfSize); + tess.draw(); + } + + /** + * Render a flat disc (cylinder with very small height) centered at origin. + * The disc lies flat on the XZ plane. + */ + protected void renderDisc(int color, float alpha, float radius, float thickness, int segments) { + float[] rgb = extractRGB(color); + float r = rgb[0], g = rgb[1], b = rgb[2]; + + Tessellator tess = Tessellator.instance; + float halfThick = thickness * 0.5f; + + // Top face + tess.startDrawingQuads(); + tess.setColorRGBA_F(r, g, b, alpha); + tess.setNormal(0, 1, 0); + for (int i = 0; i < segments; i++) { + float angle1 = (float) (2 * Math.PI * i / segments); + float angle2 = (float) (2 * Math.PI * (i + 1) / segments); + float x1 = (float) Math.cos(angle1) * radius; + float z1 = (float) Math.sin(angle1) * radius; + float x2 = (float) Math.cos(angle2) * radius; + float z2 = (float) Math.sin(angle2) * radius; + + tess.addVertex(0, halfThick, 0); + tess.addVertex(x1, halfThick, z1); + tess.addVertex(x2, halfThick, z2); + tess.addVertex(0, halfThick, 0); + } + tess.draw(); + + // Bottom face + tess.startDrawingQuads(); + tess.setColorRGBA_F(r, g, b, alpha); + tess.setNormal(0, -1, 0); + for (int i = 0; i < segments; i++) { + float angle1 = (float) (2 * Math.PI * i / segments); + float angle2 = (float) (2 * Math.PI * (i + 1) / segments); + float x1 = (float) Math.cos(angle1) * radius; + float z1 = (float) Math.sin(angle1) * radius; + float x2 = (float) Math.cos(angle2) * radius; + float z2 = (float) Math.sin(angle2) * radius; + + tess.addVertex(0, -halfThick, 0); + tess.addVertex(x2, -halfThick, z2); + tess.addVertex(x1, -halfThick, z1); + tess.addVertex(0, -halfThick, 0); + } + tess.draw(); + + // Edge + tess.startDrawingQuads(); + tess.setColorRGBA_F(r, g, b, alpha); + for (int i = 0; i < segments; i++) { + float angle1 = (float) (2 * Math.PI * i / segments); + float angle2 = (float) (2 * Math.PI * (i + 1) / segments); + float x1 = (float) Math.cos(angle1) * radius; + float z1 = (float) Math.sin(angle1) * radius; + float x2 = (float) Math.cos(angle2) * radius; + float z2 = (float) Math.sin(angle2) * radius; + + // Normal pointing outward + float nx = (float) Math.cos((angle1 + angle2) * 0.5); + float nz = (float) Math.sin((angle1 + angle2) * 0.5); + tess.setNormal(nx, 0, nz); + + tess.addVertex(x1, -halfThick, z1); + tess.addVertex(x2, -halfThick, z2); + tess.addVertex(x2, halfThick, z2); + tess.addVertex(x1, halfThick, z1); + } + tess.draw(); + } + + /** + * Render a line segment as a quad billboard facing the camera. + */ + protected void renderLineSegment(double x1, double y1, double z1, + double x2, double y2, double z2, + float width, int color, float alpha) { + float[] rgb = extractRGB(color); + float r = rgb[0], g = rgb[1], b = rgb[2]; + + // Calculate direction + double dx = x2 - x1; + double dy = y2 - y1; + double dz = z2 - z1; + double len = Math.sqrt(dx * dx + dy * dy + dz * dz); + if (len < 0.001) return; + + // Perpendicular vectors for billboard + double perpX, perpY, perpZ; + if (Math.abs(dy) < 0.9) { + // Cross with up vector + perpX = -dz; + perpY = 0; + perpZ = dx; + } else { + // Cross with right vector + perpX = 0; + perpY = dz; + perpZ = -dy; + } + double perpLen = Math.sqrt(perpX * perpX + perpY * perpY + perpZ * perpZ); + if (perpLen > 0) { + perpX = perpX / perpLen * width * 0.5; + perpY = perpY / perpLen * width * 0.5; + perpZ = perpZ / perpLen * width * 0.5; + } + + Tessellator tess = Tessellator.instance; + tess.startDrawingQuads(); + tess.setColorRGBA_F(r, g, b, alpha); + tess.addVertex(x1 - perpX, y1 - perpY, z1 - perpZ); + tess.addVertex(x2 - perpX, y2 - perpY, z2 - perpZ); + tess.addVertex(x2 + perpX, y2 + perpY, z2 + perpZ); + tess.addVertex(x1 + perpX, y1 + perpY, z1 + perpZ); + tess.draw(); + } + + @Override + protected ResourceLocation getEntityTexture(Entity entity) { + return WHITE_TEXTURE; + } +} diff --git a/src/main/java/kamkeel/npcs/client/renderer/RenderAbilitySweeper.java b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilitySweeper.java new file mode 100644 index 000000000..10705d762 --- /dev/null +++ b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilitySweeper.java @@ -0,0 +1,219 @@ +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; +import net.minecraft.util.ResourceLocation; +import org.lwjgl.opengl.GL11; + +/** + * Renders the Sweeper beam as a thin rotating laser attached to the NPC. + * Uses similar rendering style to the Beam trail for visual consistency. + */ +@SideOnly(Side.CLIENT) +public class RenderAbilitySweeper extends Render { + + @Override + public void doRender(Entity entity, double x, double y, double z, float yaw, float partialTicks) { + EntityAbilitySweeper sweeper = (EntityAbilitySweeper) entity; + + setupRenderState(); + + float beamLength = sweeper.getBeamLength(); + float beamWidth = sweeper.getBeamWidth(); + int innerColor = sweeper.getInnerColor(); + int outerColor = sweeper.getOuterColor(); + boolean outerColorEnabled = sweeper.isOuterColorEnabled(); + float outerColorWidth = sweeper.getOuterColorWidth(); + float angle = sweeper.getInterpolatedAngle(partialTicks); + + GL11.glPushMatrix(); + GL11.glTranslated(x, y, z); + + // Rotate the beam around Y axis (angle already includes baseYaw) + GL11.glRotatef(-angle, 0, 1, 0); // Negative for correct direction + + // Render the beam extending in the +Z direction (like beam trail segments) + renderBeam(beamLength, beamWidth, innerColor, outerColor, outerColorEnabled, outerColorWidth); + + GL11.glPopMatrix(); + + restoreRenderState(); + } + + private void setupRenderState() { + 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.glShadeModel(GL11.GL_SMOOTH); + + // Force full brightness - prevents world lighting from darkening sweeper + OpenGlHelper.setLightmapTextureCoords(OpenGlHelper.lightmapTexUnit, 240.0f, 240.0f); + } + + private void restoreRenderState() { + GL11.glShadeModel(GL11.GL_FLAT); + GL11.glEnable(GL11.GL_LIGHTING); + GL11.glDisable(GL11.GL_BLEND); + GL11.glEnable(GL11.GL_CULL_FACE); + GL11.glEnable(GL11.GL_TEXTURE_2D); + GL11.glPopAttrib(); + } + + /** + * Render beam using the same style as the Beam entity trail. + * Three layers: outer glow, middle, inner core. + */ + private void renderBeam(float length, float width, int innerColor, int outerColor, + boolean outerColorEnabled, float outerColorWidth) { + // Extract colors + float outerR = ((outerColor >> 16) & 0xFF) / 255.0f; + float outerG = ((outerColor >> 8) & 0xFF) / 255.0f; + float outerB = (outerColor & 0xFF) / 255.0f; + + float innerR = ((innerColor >> 16) & 0xFF) / 255.0f; + float innerG = ((innerColor >> 8) & 0xFF) / 255.0f; + float innerB = (innerColor & 0xFF) / 255.0f; + + // Render from origin (0,0,0) to (0,0,length) in local coords + // After rotation this extends in the correct world direction + + // Outer glow (wider, translucent) - like beam trail - only if enabled + if (outerColorEnabled) { + GL11.glDepthMask(false); + renderBeamSegment(0, 0, 0, 0, 0, length, width * outerColorWidth, outerR, outerG, outerB, 0.3f); + GL11.glDepthMask(true); + + // Middle layer + renderBeamSegment(0, 0, 0, 0, 0, length, width * 1.3f, outerR, outerG, outerB, 0.6f); + } + + // Inner core (solid) - like beam trail inner + renderBeamSegment(0, 0, 0, 0, 0, length, width * 0.6f, innerR, innerG, innerB, 1.0f); + } + + /** + * Render a beam segment as a 3D rectangular prism. + * Uses the same approach as RenderAbilityBeam.renderBeamRectangle. + */ + private void renderBeamSegment(double x1, double y1, double z1, + double x2, double y2, double z2, + float width, float r, float g, float b, float a) { + // Calculate direction + double dx = x2 - x1; + double dy = y2 - y1; + double dz = z2 - z1; + double len = Math.sqrt(dx * dx + dy * dy + dz * dz); + if (len < 0.001) return; + + // Normalize direction + dx /= len; + dy /= len; + dz /= len; + + // Calculate perpendicular vectors + double upX = 0, upY = 1, upZ = 0; + if (Math.abs(dy) > 0.9) { + upX = 1; upY = 0; upZ = 0; + } + + // Cross product for horizontal perpendicular + double horzX = dy * upZ - dz * upY; + double horzY = dz * upX - dx * upZ; + double horzZ = dx * upY - dy * upX; + double horzLen = Math.sqrt(horzX * horzX + horzY * horzY + horzZ * horzZ); + if (horzLen > 0) { + horzX /= horzLen; + horzY /= horzLen; + horzZ /= horzLen; + } + + // Cross product for vertical perpendicular + double vertX = dy * horzZ - dz * horzY; + double vertY = dz * horzX - dx * horzZ; + double vertZ = dx * horzY - dy * horzX; + + float halfWidth = width * 0.5f; + + // Calculate 8 corners of the box + double s1x = x1 - horzX * halfWidth - vertX * halfWidth; + double s1y = y1 - horzY * halfWidth - vertY * halfWidth; + double s1z = z1 - horzZ * halfWidth - vertZ * halfWidth; + + double s2x = x1 + horzX * halfWidth - vertX * halfWidth; + double s2y = y1 + horzY * halfWidth - vertY * halfWidth; + double s2z = z1 + horzZ * halfWidth - vertZ * halfWidth; + + double s3x = x1 + horzX * halfWidth + vertX * halfWidth; + double s3y = y1 + horzY * halfWidth + vertY * halfWidth; + double s3z = z1 + horzZ * halfWidth + vertZ * halfWidth; + + double s4x = x1 - horzX * halfWidth + vertX * halfWidth; + double s4y = y1 - horzY * halfWidth + vertY * halfWidth; + double s4z = z1 - horzZ * halfWidth + vertZ * halfWidth; + + double e1x = x2 - horzX * halfWidth - vertX * halfWidth; + double e1y = y2 - horzY * halfWidth - vertY * halfWidth; + double e1z = z2 - horzZ * halfWidth - vertZ * halfWidth; + + double e2x = x2 + horzX * halfWidth - vertX * halfWidth; + double e2y = y2 + horzY * halfWidth - vertY * halfWidth; + double e2z = z2 + horzZ * halfWidth - vertZ * halfWidth; + + double e3x = x2 + horzX * halfWidth + vertX * halfWidth; + double e3y = y2 + horzY * halfWidth + vertY * halfWidth; + double e3z = z2 + horzZ * halfWidth + vertZ * halfWidth; + + double e4x = x2 - horzX * halfWidth + vertX * halfWidth; + double e4y = y2 - horzY * halfWidth + vertY * halfWidth; + double e4z = z2 - horzZ * halfWidth + vertZ * halfWidth; + + Tessellator tess = Tessellator.instance; + tess.startDrawingQuads(); + tess.setColorRGBA_F(r, g, b, a); + + // Bottom face + tess.addVertex(s1x, s1y, s1z); + tess.addVertex(s2x, s2y, s2z); + tess.addVertex(e2x, e2y, e2z); + tess.addVertex(e1x, e1y, e1z); + + // Top face + tess.addVertex(s4x, s4y, s4z); + tess.addVertex(e4x, e4y, e4z); + tess.addVertex(e3x, e3y, e3z); + tess.addVertex(s3x, s3y, s3z); + + // Left face + tess.addVertex(s1x, s1y, s1z); + tess.addVertex(e1x, e1y, e1z); + tess.addVertex(e4x, e4y, e4z); + tess.addVertex(s4x, s4y, s4z); + + // Right face + tess.addVertex(s2x, s2y, s2z); + tess.addVertex(s3x, s3y, s3z); + tess.addVertex(e3x, e3y, e3z); + tess.addVertex(e2x, e2y, e2z); + + // End cap (tip of beam) + tess.addVertex(e1x, e1y, e1z); + tess.addVertex(e2x, e2y, e2z); + tess.addVertex(e3x, e3y, e3z); + tess.addVertex(e4x, e4y, e4z); + + tess.draw(); + } + + @Override + protected ResourceLocation getEntityTexture(Entity entity) { + return null; + } +} diff --git a/src/main/java/kamkeel/npcs/client/renderer/TelegraphRenderer.java b/src/main/java/kamkeel/npcs/client/renderer/TelegraphRenderer.java index 0cac8805e..3b5e49e2d 100644 --- a/src/main/java/kamkeel/npcs/client/renderer/TelegraphRenderer.java +++ b/src/main/java/kamkeel/npcs/client/renderer/TelegraphRenderer.java @@ -76,10 +76,10 @@ public class TelegraphRenderer { return; } - // Calculate render position relative to camera - double renderX = instance.getX() - playerX; - double renderY = instance.getY() - playerY + telegraph.getHeightOffset(); - double renderZ = instance.getZ() - playerZ; + // Calculate render position relative to camera using interpolated positions for smooth movement + double renderX = instance.getInterpolatedX(partialTicks) - playerX; + double renderY = instance.getInterpolatedY(partialTicks) - playerY + telegraph.getHeightOffset(); + double renderZ = instance.getInterpolatedZ(partialTicks) - playerZ; // Get animated color int color = instance.getAnimatedColor(partialTicks); @@ -91,8 +91,8 @@ public class TelegraphRenderer { GL11.glPushMatrix(); GL11.glTranslated(renderX, renderY, renderZ); - // Rotate for directional shapes - GL11.glRotatef(-instance.getYaw(), 0, 1, 0); + // Rotate for directional shapes using interpolated yaw for smooth rotation + GL11.glRotatef(-instance.getInterpolatedYaw(partialTicks), 0, 1, 0); switch (telegraph.getType()) { case CIRCLE: @@ -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/client/renderer/lightning/AttachedLightningRenderer.java b/src/main/java/kamkeel/npcs/client/renderer/lightning/AttachedLightningRenderer.java new file mode 100644 index 000000000..4fa28cda8 --- /dev/null +++ b/src/main/java/kamkeel/npcs/client/renderer/lightning/AttachedLightningRenderer.java @@ -0,0 +1,281 @@ +package kamkeel.npcs.client.renderer.lightning; + +import net.minecraft.client.renderer.Tessellator; +import org.lwjgl.opengl.GL11; + +import java.awt.Color; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Random; + +/** + * Renders lightning arcs attached to an entity (in local space). + * Supports both instant arcs and fading persistent bolts that move with the entity. + */ +public class AttachedLightningRenderer { + + private static final Random rand = new Random(); + + /** + * Data for a single lightning bolt with fading support. + */ + public static class LightningArc { + public List points; + public int age; + public int maxAge; + public int outerColor; + public int innerColor; + + public LightningArc(List points, int maxAge, int outerColor, int innerColor) { + this.points = points; + this.age = 0; + this.maxAge = maxAge; + this.outerColor = outerColor; + this.innerColor = innerColor; + } + + public float getAlphaMultiplier() { + // Fade out over lifetime + return 1.0f - ((float) age / (float) maxAge); + } + + public boolean isDead() { + return age >= maxAge; + } + + public void tick() { + age++; + } + } + + /** + * Storage for persistent lightning arcs per entity. + * Call updateAndRender() each frame from the entity's renderer. + */ + public static class LightningState { + private final List arcs = new ArrayList<>(); + private float spawnAccumulator = 0; + + /** + * Update arc ages, remove dead arcs, spawn new ones based on density. + * + * @param density Arcs per tick (can be fractional) + * @param radius Max arc distance from center + * @param outerColor Outer glow color + * @param innerColor Inner core color + * @param maxAge How many ticks arcs live (for fading) + */ + public void update(float density, float radius, int outerColor, int innerColor, int maxAge) { + // Age existing arcs and remove dead ones + Iterator iter = arcs.iterator(); + while (iter.hasNext()) { + LightningArc arc = iter.next(); + arc.tick(); + if (arc.isDead()) { + iter.remove(); + } + } + + // Spawn new arcs based on density + spawnAccumulator += density; + while (spawnAccumulator >= 1.0f) { + spawnAccumulator -= 1.0f; + arcs.add(createArc(radius, outerColor, innerColor, maxAge)); + } + } + + /** + * Render all active arcs in local space. + * Call this after glTranslated to entity position. + */ + public void render() { + if (arcs.isEmpty()) return; + + // Setup GL state + GL11.glPushAttrib(GL11.GL_ALL_ATTRIB_BITS); + GL11.glDisable(GL11.GL_TEXTURE_2D); + GL11.glDisable(GL11.GL_LIGHTING); + GL11.glDisable(GL11.GL_CULL_FACE); + GL11.glEnable(GL11.GL_BLEND); + GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); + GL11.glDepthMask(false); + + Tessellator tess = Tessellator.instance; + + for (LightningArc arc : arcs) { + float alphaMult = arc.getAlphaMultiplier(); + + // Render outer glow + renderBoltPath(tess, arc.points, arc.outerColor, 0.025f, 0.35f * alphaMult); + + // Render inner core + renderBoltPath(tess, arc.points, arc.innerColor, 0.012f, 0.7f * alphaMult); + } + + GL11.glDepthMask(true); + GL11.glPopAttrib(); + } + + public int getArcCount() { + return arcs.size(); + } + } + + /** + * Create a new lightning arc from center to a random point. + */ + private static LightningArc createArc(float radius, int outerColor, int innerColor, int maxAge) { + // Random end point on sphere + double theta = rand.nextDouble() * Math.PI * 2; + double phi = Math.acos(2 * rand.nextDouble() - 1); + double r = radius * (0.5 + rand.nextDouble() * 0.5); + + double endX = r * Math.sin(phi) * Math.cos(theta); + double endY = r * Math.sin(phi) * Math.sin(theta); + double endZ = r * Math.cos(phi); + + int segments = 4 + rand.nextInt(3); + float displacement = radius * 0.25f; + + List points = generateLightningPath(0, 0, 0, endX, endY, endZ, segments, displacement); + + return new LightningArc(points, maxAge, outerColor, innerColor); + } + + /** + * Generate a jagged lightning path between two points. + */ + private static List generateLightningPath(double x1, double y1, double z1, + double x2, double y2, double z2, + int segments, float displacement) { + List points = new ArrayList<>(); + points.add(new double[]{x1, y1, z1}); + + double dx = x2 - x1; + double dy = y2 - y1; + double dz = z2 - z1; + + // Create perpendicular vectors for displacement + Vec3d dir = new Vec3d(dx, dy, dz); + Vec3d perp1 = dir.copy().perpendicular().normalize(); + Vec3d perp2 = dir.copy().crossProduct(perp1).normalize(); + + for (int i = 1; i < segments; i++) { + double t = (double) i / segments; + + double px = x1 + dx * t; + double py = y1 + dy * t; + double pz = z1 + dz * t; + + // Displacement strongest in middle + float strength = (float) (1.0 - Math.abs(t - 0.5) * 2.0) * displacement; + double offset1 = (rand.nextDouble() - 0.5) * 2 * strength; + double offset2 = (rand.nextDouble() - 0.5) * 2 * strength; + + px += perp1.x * offset1 + perp2.x * offset2; + py += perp1.y * offset1 + perp2.y * offset2; + pz += perp1.z * offset1 + perp2.z * offset2; + + points.add(new double[]{px, py, pz}); + } + + points.add(new double[]{x2, y2, z2}); + return points; + } + + /** + * Render the lightning path as quads. + */ + private static void renderBoltPath(Tessellator tess, List points, int color, float width, float alpha) { + if (alpha <= 0.01f) return; + + Color c = new Color(color); + int r = c.getRed(); + int g = c.getGreen(); + int b = c.getBlue(); + int a = Math.min(255, Math.max(0, (int) (alpha * 255))); + + for (int i = 0; i < points.size() - 1; i++) { + double[] p1 = points.get(i); + double[] p2 = points.get(i + 1); + + double segDx = p2[0] - p1[0]; + double segDy = p2[1] - p1[1]; + double segDz = p2[2] - p1[2]; + + Vec3d segDir = new Vec3d(segDx, segDy, segDz); + Vec3d perp = segDir.copy().perpendicular().normalize().multiply(width); + + // First quad + tess.startDrawingQuads(); + tess.setBrightness(0xF000F0); + tess.setColorRGBA(r, g, b, a); + + tess.addVertex(p1[0] - perp.x, p1[1] - perp.y, p1[2] - perp.z); + tess.addVertex(p1[0] + perp.x, p1[1] + perp.y, p1[2] + perp.z); + tess.addVertex(p2[0] + perp.x, p2[1] + perp.y, p2[2] + perp.z); + tess.addVertex(p2[0] - perp.x, p2[1] - perp.y, p2[2] - perp.z); + + tess.draw(); + + // Second quad (perpendicular for 3D look) + Vec3d perp2 = segDir.copy().crossProduct(perp).normalize().multiply(width); + + tess.startDrawingQuads(); + tess.setBrightness(0xF000F0); + tess.setColorRGBA(r, g, b, a); + + tess.addVertex(p1[0] - perp2.x, p1[1] - perp2.y, p1[2] - perp2.z); + tess.addVertex(p1[0] + perp2.x, p1[1] + perp2.y, p1[2] + perp2.z); + tess.addVertex(p2[0] + perp2.x, p2[1] + perp2.y, p2[2] + perp2.z); + tess.addVertex(p2[0] - perp2.x, p2[1] - perp2.y, p2[2] - perp2.z); + + tess.draw(); + } + } + + // ==================== INSTANT RENDERING (no persistence) ==================== + + /** + * Render a single instant lightning arc (no fading, drawn this frame only). + */ + public static void renderInstantArc(float radius, int outerColor, int innerColor) { + double theta = rand.nextDouble() * Math.PI * 2; + double phi = Math.acos(2 * rand.nextDouble() - 1); + double r = radius * (0.5 + rand.nextDouble() * 0.5); + + double endX = r * Math.sin(phi) * Math.cos(theta); + double endY = r * Math.sin(phi) * Math.sin(theta); + double endZ = r * Math.cos(phi); + + int segments = 4 + rand.nextInt(3); + float displacement = radius * 0.25f; + + List points = generateLightningPath(0, 0, 0, endX, endY, endZ, segments, displacement); + + GL11.glPushAttrib(GL11.GL_ALL_ATTRIB_BITS); + GL11.glDisable(GL11.GL_TEXTURE_2D); + GL11.glDisable(GL11.GL_LIGHTING); + GL11.glDisable(GL11.GL_CULL_FACE); + GL11.glEnable(GL11.GL_BLEND); + GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); + GL11.glDepthMask(false); + + Tessellator tess = Tessellator.instance; + renderBoltPath(tess, points, outerColor, 0.025f, 0.35f); + renderBoltPath(tess, points, innerColor, 0.012f, 0.7f); + + GL11.glDepthMask(true); + GL11.glPopAttrib(); + } + + /** + * Render multiple instant arcs. + */ + public static void renderCracklingLightning(int count, float radius, int outerColor, int innerColor) { + for (int i = 0; i < count; i++) { + renderInstantArc(radius, outerColor, innerColor); + } + } +} diff --git a/src/main/java/kamkeel/npcs/client/renderer/lightning/LightningBolt.java b/src/main/java/kamkeel/npcs/client/renderer/lightning/LightningBolt.java new file mode 100644 index 000000000..6cf6e7d3a --- /dev/null +++ b/src/main/java/kamkeel/npcs/client/renderer/lightning/LightningBolt.java @@ -0,0 +1,295 @@ +package kamkeel.npcs.client.renderer.lightning; + +import net.minecraft.world.World; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Random; +import java.util.concurrent.ConcurrentLinkedQueue; + +/** + * Lightning bolt data model with fractal segment generation. + * Adapted from Botania's LightningBolt by Vazkii/ChickenBones. + */ +public class LightningBolt { + + public static ConcurrentLinkedQueue boltList = new ConcurrentLinkedQueue<>(); + + public ArrayList segments = new ArrayList<>(); + public Vec3d start; + public Vec3d end; + + public double length; + public int numSegments0; + private int numSplits; + private boolean finalized; + private Random rand; + public long seed; + + public int particleAge; + public int particleMaxAge; + public boolean isDead; + + private World world; + private HashMap splitParents = new HashMap<>(); + + public float speed = 1.5F; + public static final int FADE_TIME = 4; // Short lifespan for projectile-attached lightning + + public int colorOuter; + public int colorInner; + + public LightningBolt(World world, Vec3d start, Vec3d end, float ticksPerMeter, long seed, int colorOuter, int colorInner) { + this.world = world; + this.seed = seed; + this.rand = new Random(seed); + + this.start = start; + this.end = end; + this.speed = ticksPerMeter; + + this.colorOuter = colorOuter; + this.colorInner = colorInner; + + this.numSegments0 = 1; + this.length = end.copy().subtract(start).mag(); + this.particleMaxAge = FADE_TIME + rand.nextInt(FADE_TIME) - FADE_TIME / 2; + this.particleAge = -(int) (length * speed); + + segments.add(new Segment(start, end)); + } + + /** + * Apply default fractal splitting for natural-looking lightning. + */ + public void defaultFractal() { + fractal(2, length / 1.5, 0.7F, 0.7F, 45); + fractal(2, length / 4, 0.5F, 0.8F, 50); + fractal(2, length / 15, 0.5F, 0.9F, 55); + fractal(2, length / 30, 0.5F, 1.0F, 60); + fractal(2, length / 60, 0, 0, 0); + fractal(2, length / 100, 0, 0, 0); + fractal(2, length / 400, 0, 0, 0); + } + + /** + * Simplified fractal for short-lived projectile lightning. + * Less detail but faster and more appropriate for fast-moving objects. + */ + public void simpleFractal() { + fractal(2, length / 2.0, 0.4F, 0.6F, 35); + fractal(2, length / 6, 0.3F, 0.7F, 40); + fractal(2, length / 20, 0, 0, 0); + } + + /** + * Apply fractal splitting to create branching lightning. + * + * @param splits Number of splits per segment + * @param amount Displacement amount + * @param splitChance Probability of branching (0-1) + * @param splitLength Length multiplier for branches + * @param splitAngle Angle for branch splits in degrees + */ + public void fractal(int splits, double amount, double splitChance, double splitLength, double splitAngle) { + if (finalized) return; + + ArrayList oldSegments = segments; + segments = new ArrayList<>(); + + Segment prev = null; + + for (Segment segment : oldSegments) { + prev = segment.prev; + + Vec3d subSegment = segment.diff.copy().multiply(1.0 / splits); + + BoltPoint[] newPoints = new BoltPoint[splits + 1]; + + Vec3d startPoint = segment.startPoint.point; + newPoints[0] = segment.startPoint; + newPoints[splits] = segment.endPoint; + + for (int i = 1; i < splits; i++) { + Vec3d randOff = segment.diff.copy().perpendicular().normalize() + .rotate(rand.nextFloat() * 360, segment.diff); + randOff.multiply((rand.nextFloat() - 0.5F) * amount * 2); + + Vec3d basePoint = startPoint.copy().add(subSegment.copy().multiply(i)); + + newPoints[i] = new BoltPoint(basePoint, randOff); + } + + for (int i = 0; i < splits; i++) { + Segment next = new Segment(newPoints[i], newPoints[i + 1], segment.light, + segment.segmentNo * splits + i, segment.splitNo); + next.prev = prev; + if (prev != null) { + prev.next = next; + } + + if (i != 0 && rand.nextFloat() < splitChance) { + Vec3d splitRot = next.diff.copy().xCrossProduct() + .rotate(rand.nextFloat() * 360, next.diff); + Vec3d diff = next.diff.copy() + .rotate((rand.nextFloat() * 0.66F + 0.33F) * splitAngle, splitRot) + .multiply(splitLength); + + numSplits++; + splitParents.put(numSplits, next.splitNo); + + Segment split = new Segment(newPoints[i], + new BoltPoint(newPoints[i + 1].basePoint, newPoints[i + 1].offsetVec.copy().add(diff)), + segment.light / 2F, next.segmentNo, numSplits); + split.prev = prev; + + segments.add(split); + } + + prev = next; + segments.add(next); + } + + if (segment.next != null) { + segment.next.prev = prev; + } + } + + numSegments0 *= splits; + } + + /** + * Finalize the bolt after all fractal passes. + */ + public void finalizeBolt() { + if (finalized) return; + finalized = true; + + calculateEndDiffs(); + Collections.sort(segments, new SegmentLightSorter()); + } + + private void calculateEndDiffs() { + Collections.sort(segments, new SegmentSorter()); + + for (Segment segment : segments) { + segment.calcEndDiffs(); + } + } + + public void onUpdate() { + particleAge++; + if (particleAge >= particleMaxAge) { + isDead = true; + } + } + + /** + * Update all active lightning bolts. Called from client tick handler. + */ + public static void updateAll() { + for (Iterator iterator = boltList.iterator(); iterator.hasNext(); ) { + LightningBolt bolt = iterator.next(); + bolt.onUpdate(); + if (bolt.isDead) { + iterator.remove(); + } + } + } + + // ==================== INNER CLASSES ==================== + + public class BoltPoint { + public Vec3d point; + public Vec3d basePoint; + public Vec3d offsetVec; + + public BoltPoint(Vec3d basePoint, Vec3d offsetVec) { + this.point = basePoint.copy().add(offsetVec); + this.basePoint = basePoint; + this.offsetVec = offsetVec; + } + } + + public class Segment { + public BoltPoint startPoint; + public BoltPoint endPoint; + + public Vec3d diff; + public Segment prev; + public Segment next; + + public Vec3d nextDiff; + public Vec3d prevDiff; + + public float sinPrev; + public float sinNext; + public float light; + + public int segmentNo; + public int splitNo; + + public Segment(BoltPoint start, BoltPoint end, float light, int segmentNo, int splitNo) { + this.startPoint = start; + this.endPoint = end; + this.light = light; + this.segmentNo = segmentNo; + this.splitNo = splitNo; + calcDiff(); + } + + public Segment(Vec3d start, Vec3d end) { + this(new BoltPoint(start, new Vec3d(0, 0, 0)), + new BoltPoint(end, new Vec3d(0, 0, 0)), 1, 0, 0); + } + + public void calcDiff() { + diff = endPoint.point.copy().subtract(startPoint.point); + } + + public void calcEndDiffs() { + if (prev != null) { + Vec3d prevDiffNorm = prev.diff.copy().normalize(); + Vec3d thisDiffNorm = diff.copy().normalize(); + + prevDiff = thisDiffNorm.copy().add(prevDiffNorm).normalize(); + sinPrev = (float) Math.sin(thisDiffNorm.angle(prevDiffNorm.multiply(-1)) / 2); + } else { + prevDiff = diff.copy().normalize(); + sinPrev = 1; + } + + if (next != null) { + Vec3d nextDiffNorm = next.diff.copy().normalize(); + Vec3d thisDiffNorm = diff.copy().normalize(); + + nextDiff = thisDiffNorm.add(nextDiffNorm).normalize(); + sinNext = (float) Math.sin(thisDiffNorm.angle(nextDiffNorm.multiply(-1)) / 2); + } else { + nextDiff = diff.copy().normalize(); + sinNext = 1; + } + } + } + + public class SegmentSorter implements Comparator { + @Override + public int compare(Segment o1, Segment o2) { + int comp = Integer.compare(o1.splitNo, o2.splitNo); + if (comp == 0) { + return Integer.compare(o1.segmentNo, o2.segmentNo); + } + return comp; + } + } + + public class SegmentLightSorter implements Comparator { + @Override + public int compare(Segment o1, Segment o2) { + return Float.compare(o2.light, o1.light); + } + } +} diff --git a/src/main/java/kamkeel/npcs/client/renderer/lightning/LightningHandler.java b/src/main/java/kamkeel/npcs/client/renderer/lightning/LightningHandler.java new file mode 100644 index 000000000..1951666e4 --- /dev/null +++ b/src/main/java/kamkeel/npcs/client/renderer/lightning/LightningHandler.java @@ -0,0 +1,202 @@ +package kamkeel.npcs.client.renderer.lightning; + +import cpw.mods.fml.common.eventhandler.SubscribeEvent; +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import kamkeel.npcs.client.renderer.lightning.LightningBolt.Segment; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.ActiveRenderInfo; +import net.minecraft.client.renderer.Tessellator; +import net.minecraft.client.renderer.texture.TextureManager; +import net.minecraft.entity.Entity; +import net.minecraft.util.ResourceLocation; +import net.minecraft.world.World; +import net.minecraftforge.client.event.RenderWorldLastEvent; +import org.lwjgl.opengl.GL11; + +import java.awt.Color; + +/** + * Handles rendering of lightning bolts in the world. + * Adapted from Botania's LightningHandler by Vazkii/ChickenBones. + * + * Register this handler with: MinecraftForge.EVENT_BUS.register(new LightningHandler()); + */ +@SideOnly(Side.CLIENT) +public class LightningHandler { + + private static final ResourceLocation TEXTURE_OUTER = new ResourceLocation("customnpcs", "textures/effects/lightning_outer.png"); + private static final ResourceLocation TEXTURE_INNER = new ResourceLocation("customnpcs", "textures/effects/lightning_inner.png"); + + public static int lightningCount = 0; + + private static Vec3d getRelativeViewVector(Vec3d pos) { + Entity renderEntity = Minecraft.getMinecraft().renderViewEntity; + return new Vec3d( + (float) renderEntity.posX - pos.x, + (float) renderEntity.posY + renderEntity.getEyeHeight() - pos.y, + (float) renderEntity.posZ - pos.z + ); + } + + @SubscribeEvent + public void onRenderWorldLast(RenderWorldLastEvent event) { + float partialTicks = event.partialTicks; + Entity entity = Minecraft.getMinecraft().thePlayer; + TextureManager textureManager = Minecraft.getMinecraft().renderEngine; + + double interpPosX = entity.lastTickPosX + (entity.posX - entity.lastTickPosX) * partialTicks; + double interpPosY = entity.lastTickPosY + (entity.posY - entity.lastTickPosY) * partialTicks; + double interpPosZ = entity.lastTickPosZ + (entity.posZ - entity.lastTickPosZ) * partialTicks; + + if (LightningBolt.boltList.isEmpty()) { + return; + } + + GL11.glPushMatrix(); + GL11.glTranslated(-interpPosX, -interpPosY, -interpPosZ); + + Tessellator tessellator = Tessellator.instance; + + GL11.glDepthMask(false); + GL11.glEnable(GL11.GL_BLEND); + GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); + GL11.glDisable(GL11.GL_TEXTURE_2D); + GL11.glDisable(GL11.GL_LIGHTING); + GL11.glDisable(GL11.GL_CULL_FACE); + + lightningCount = 0; + + // Render outer layer (translucent glow) + for (LightningBolt bolt : LightningBolt.boltList) { + renderBolt(bolt, tessellator, partialTicks, false); + } + + // Render inner layer (bright core) + for (LightningBolt bolt : LightningBolt.boltList) { + renderBolt(bolt, tessellator, partialTicks, true); + } + + GL11.glEnable(GL11.GL_CULL_FACE); + GL11.glEnable(GL11.GL_LIGHTING); + GL11.glEnable(GL11.GL_TEXTURE_2D); + GL11.glDisable(GL11.GL_BLEND); + GL11.glDepthMask(true); + + GL11.glPopMatrix(); + } + + private void renderBolt(LightningBolt bolt, Tessellator tessellator, float partialTicks, boolean inner) { + lightningCount++; + + float boltAge = bolt.particleAge < 0 ? 0 : (float) bolt.particleAge / (float) bolt.particleMaxAge; + float mainAlpha; + if (!inner) { + mainAlpha = (1 - boltAge) * 0.4F; + } else { + mainAlpha = 1 - boltAge * 0.5F; + } + + int expandTime = (int) (bolt.length * bolt.speed); + + int renderStart = (int) ((expandTime / 2 - bolt.particleMaxAge + bolt.particleAge) / (float) (expandTime / 2) * bolt.numSegments0); + int renderEnd = (int) ((bolt.particleAge + expandTime) / (float) expandTime * bolt.numSegments0); + + for (Segment segment : bolt.segments) { + if (segment.segmentNo < renderStart || segment.segmentNo > renderEnd) { + continue; + } + + Vec3d playerVec = getRelativeViewVector(segment.startPoint.point).multiply(-1); + + double width = 0.008F * (playerVec.mag() / 8 + 1) * (1 + segment.light) * 0.4F; + + Vec3d diff1 = playerVec.copy().crossProduct(segment.prevDiff).normalize().multiply(width / segment.sinPrev); + Vec3d diff2 = playerVec.copy().crossProduct(segment.nextDiff).normalize().multiply(width / segment.sinNext); + + Vec3d startVec = segment.startPoint.point; + Vec3d endVec = segment.endPoint.point; + + int color = inner ? bolt.colorInner : bolt.colorOuter; + Color c = new Color(color); + int alpha = (int) (mainAlpha * segment.light * c.getAlpha()); + + tessellator.startDrawingQuads(); + tessellator.setBrightness(0xF000F0); + tessellator.setColorRGBA(c.getRed(), c.getGreen(), c.getBlue(), alpha); + + tessellator.addVertex(endVec.x - diff2.x, endVec.y - diff2.y, endVec.z - diff2.z); + tessellator.addVertex(startVec.x - diff1.x, startVec.y - diff1.y, startVec.z - diff1.z); + tessellator.addVertex(startVec.x + diff1.x, startVec.y + diff1.y, startVec.z + diff1.z); + tessellator.addVertex(endVec.x + diff2.x, endVec.y + diff2.y, endVec.z + diff2.z); + + tessellator.draw(); + + // Rounded end caps + if (segment.next == null) { + Vec3d roundEnd = segment.endPoint.point.copy().add(segment.diff.copy().normalize().multiply(width)); + + tessellator.startDrawingQuads(); + tessellator.setBrightness(0xF000F0); + tessellator.setColorRGBA(c.getRed(), c.getGreen(), c.getBlue(), alpha); + + tessellator.addVertex(roundEnd.x - diff2.x, roundEnd.y - diff2.y, roundEnd.z - diff2.z); + tessellator.addVertex(endVec.x - diff2.x, endVec.y - diff2.y, endVec.z - diff2.z); + tessellator.addVertex(endVec.x + diff2.x, endVec.y + diff2.y, endVec.z + diff2.z); + tessellator.addVertex(roundEnd.x + diff2.x, roundEnd.y + diff2.y, roundEnd.z + diff2.z); + + tessellator.draw(); + } + + if (segment.prev == null) { + Vec3d roundEnd = segment.startPoint.point.copy().subtract(segment.diff.copy().normalize().multiply(width)); + + tessellator.startDrawingQuads(); + tessellator.setBrightness(0xF000F0); + tessellator.setColorRGBA(c.getRed(), c.getGreen(), c.getBlue(), alpha); + + tessellator.addVertex(startVec.x - diff1.x, startVec.y - diff1.y, startVec.z - diff1.z); + tessellator.addVertex(roundEnd.x - diff1.x, roundEnd.y - diff1.y, roundEnd.z - diff1.z); + tessellator.addVertex(roundEnd.x + diff1.x, roundEnd.y + diff1.y, roundEnd.z + diff1.z); + tessellator.addVertex(startVec.x + diff1.x, startVec.y + diff1.y, startVec.z + diff1.z); + + tessellator.draw(); + } + } + } + + /** + * Spawn a lightning bolt between two points. + * + * @param world The world + * @param start Start position + * @param end End position + * @param ticksPerMeter Speed of bolt expansion + * @param seed Random seed for consistent appearance + * @param colorOuter Outer glow color (ARGB) + * @param colorInner Inner core color (ARGB) + */ + public static void spawnLightningBolt(World world, Vec3d start, Vec3d end, float ticksPerMeter, long seed, int colorOuter, int colorInner) { + LightningBolt bolt = new LightningBolt(world, start, end, ticksPerMeter, seed, colorOuter, colorInner); + bolt.defaultFractal(); + bolt.finalizeBolt(); + LightningBolt.boltList.add(bolt); + } + + /** + * Spawn a lightning bolt with automatic seed. + */ + public static void spawnLightningBolt(World world, Vec3d start, Vec3d end, float ticksPerMeter, int colorOuter, int colorInner) { + spawnLightningBolt(world, start, end, ticksPerMeter, System.nanoTime(), colorOuter, colorInner); + } + + /** + * Spawn a simple lightning bolt for projectiles - less detail, faster. + */ + public static void spawnSimpleLightningBolt(World world, Vec3d start, Vec3d end, float ticksPerMeter, int colorOuter, int colorInner) { + LightningBolt bolt = new LightningBolt(world, start, end, ticksPerMeter, System.nanoTime(), colorOuter, colorInner); + bolt.simpleFractal(); + bolt.finalizeBolt(); + LightningBolt.boltList.add(bolt); + } +} diff --git a/src/main/java/kamkeel/npcs/client/renderer/lightning/Quaternion.java b/src/main/java/kamkeel/npcs/client/renderer/lightning/Quaternion.java new file mode 100644 index 000000000..cda0307b7 --- /dev/null +++ b/src/main/java/kamkeel/npcs/client/renderer/lightning/Quaternion.java @@ -0,0 +1,85 @@ +package kamkeel.npcs.client.renderer.lightning; + +/** + * Quaternion class for axis-angle rotations in lightning calculations. + * Adapted from Botania's Quat by ChickenBones. + */ +public class Quaternion { + public double x; + public double y; + public double z; + public double s; + + public Quaternion() { + s = 1.0; + x = 0.0; + y = 0.0; + z = 0.0; + } + + public Quaternion(Quaternion quat) { + x = quat.x; + y = quat.y; + z = quat.z; + s = quat.s; + } + + public Quaternion(double s, double x, double y, double z) { + this.x = x; + this.y = y; + this.z = z; + this.s = s; + } + + public static Quaternion aroundAxis(double ax, double ay, double az, double angle) { + angle *= 0.5; + double sinAngle = Math.sin(angle); + return new Quaternion(Math.cos(angle), ax * sinAngle, ay * sinAngle, az * sinAngle); + } + + public static Quaternion aroundAxis(Vec3d axis, double angle) { + return aroundAxis(axis.x, axis.y, axis.z, angle); + } + + public void multiply(Quaternion quat) { + double d = s * quat.s - x * quat.x - y * quat.y - z * quat.z; + double d1 = s * quat.x + x * quat.s - y * quat.z + z * quat.y; + double d2 = s * quat.y + x * quat.z + y * quat.s - z * quat.x; + double d3 = s * quat.z - x * quat.y + y * quat.x + z * quat.s; + s = d; + x = d1; + y = d2; + z = d3; + } + + public double mag() { + return Math.sqrt(x * x + y * y + z * z + s * s); + } + + public void normalize() { + double d = mag(); + if (d == 0.0) { + return; + } + d = 1.0 / d; + x *= d; + y *= d; + z *= d; + s *= d; + } + + public void rotate(Vec3d vec) { + double d = -x * vec.x - y * vec.y - z * vec.z; + double d1 = s * vec.x + y * vec.z - z * vec.y; + double d2 = s * vec.y - x * vec.z + z * vec.x; + double d3 = s * vec.z + x * vec.y - y * vec.x; + vec.x = d1 * s - d * x - d2 * z + d3 * y; + vec.y = d2 * s - d * y + d1 * z - d3 * x; + vec.z = d3 * s - d * z - d1 * y + d2 * x; + } + + @Override + public String toString() { + return "Quaternion(" + s + ", " + x + ", " + y + ", " + z + ")"; + } +} diff --git a/src/main/java/kamkeel/npcs/client/renderer/lightning/Vec3d.java b/src/main/java/kamkeel/npcs/client/renderer/lightning/Vec3d.java new file mode 100644 index 000000000..018e55bdb --- /dev/null +++ b/src/main/java/kamkeel/npcs/client/renderer/lightning/Vec3d.java @@ -0,0 +1,184 @@ +package kamkeel.npcs.client.renderer.lightning; + +import net.minecraft.entity.Entity; +import net.minecraft.util.Vec3; +import org.lwjgl.opengl.GL11; + +/** + * 3D Vector helper class for lightning calculations. + * Adapted from Botania's Vector3 by ChickenBones. + */ +public class Vec3d { + public double x; + public double y; + public double z; + + public Vec3d() { + } + + public Vec3d(double x, double y, double z) { + this.x = x; + this.y = y; + this.z = z; + } + + public Vec3d(Vec3d vec) { + this.x = vec.x; + this.y = vec.y; + this.z = vec.z; + } + + public Vec3d(Vec3 vec) { + this.x = vec.xCoord; + this.y = vec.yCoord; + this.z = vec.zCoord; + } + + public Vec3d copy() { + return new Vec3d(this); + } + + public static Vec3d fromEntity(Entity e) { + return new Vec3d(e.posX, e.posY, e.posZ); + } + + public static Vec3d fromEntityCenter(Entity e) { + return new Vec3d(e.posX, e.posY - e.yOffset + e.height / 2, e.posZ); + } + + public Vec3d set(double x, double y, double z) { + this.x = x; + this.y = y; + this.z = z; + return this; + } + + public Vec3d set(Vec3d vec) { + this.x = vec.x; + this.y = vec.y; + this.z = vec.z; + return this; + } + + public double dotProduct(Vec3d vec) { + double d = vec.x * x + vec.y * y + vec.z * z; + if (d > 1 && d < 1.00001) d = 1; + else if (d < -1 && d > -1.00001) d = -1; + return d; + } + + public Vec3d crossProduct(Vec3d vec) { + double d = y * vec.z - z * vec.y; + double d1 = z * vec.x - x * vec.z; + double d2 = x * vec.y - y * vec.x; + x = d; + y = d1; + z = d2; + return this; + } + + public Vec3d add(double x, double y, double z) { + this.x += x; + this.y += y; + this.z += z; + return this; + } + + public Vec3d add(Vec3d vec) { + this.x += vec.x; + this.y += vec.y; + this.z += vec.z; + return this; + } + + public Vec3d subtract(Vec3d vec) { + this.x -= vec.x; + this.y -= vec.y; + this.z -= vec.z; + return this; + } + + public Vec3d multiply(double d) { + this.x *= d; + this.y *= d; + this.z *= d; + return this; + } + + public double mag() { + return Math.sqrt(x * x + y * y + z * z); + } + + public double magSquared() { + return x * x + y * y + z * z; + } + + public Vec3d normalize() { + double d = mag(); + if (d != 0) { + multiply(1 / d); + } + return this; + } + + public Vec3d perpendicular() { + if (z == 0) { + return zCrossProduct(); + } + return xCrossProduct(); + } + + public Vec3d xCrossProduct() { + double d = z; + double d1 = -y; + x = 0; + y = d; + z = d1; + return this; + } + + public Vec3d zCrossProduct() { + double d = y; + double d1 = -x; + x = d; + y = d1; + z = 0; + return this; + } + + public Vec3 toVec3() { + return Vec3.createVectorHelper(x, y, z); + } + + public double angle(Vec3d vec) { + return Math.acos(copy().normalize().dotProduct(vec.copy().normalize())); + } + + public Vec3d negate() { + x = -x; + y = -y; + z = -z; + return this; + } + + public Vec3d rotate(double angle, Vec3d axis) { + Quaternion.aroundAxis(axis.copy().normalize(), angle).rotate(this); + return this; + } + + public void glVertex() { + GL11.glVertex3d(x, y, z); + } + + @Override + public String toString() { + return "Vec3d(" + x + ", " + y + ", " + z + ")"; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof Vec3d)) return false; + Vec3d v = (Vec3d) o; + return x == v.x && y == v.y && z == v.z; + } +} diff --git a/src/main/java/kamkeel/npcs/command/AbilityCommand.java b/src/main/java/kamkeel/npcs/command/AbilityCommand.java index 4a783f551..d5b9420cc 100644 --- a/src/main/java/kamkeel/npcs/command/AbilityCommand.java +++ b/src/main/java/kamkeel/npcs/command/AbilityCommand.java @@ -25,25 +25,23 @@ public class AbilityCommand extends CommandKamkeelBase { } @SubCommand( - desc = "List all saved abilities" + desc = "List all custom abilities" ) public void list(ICommandSender sender, String[] args) { - Set names = AbilityController.Instance.getSavedAbilityNames(); - if (names.isEmpty()) { - sendResult(sender, "No saved abilities found."); + Set uuids = AbilityController.Instance.getCustomAbilityIds(); + if (uuids.isEmpty()) { + sendResult(sender, "No custom abilities found."); return; } - List sortedNames = new ArrayList<>(names); - Collections.sort(sortedNames); - - sendResult(sender, "Saved Abilities (" + sortedNames.size() + "):"); - for (String name : sortedNames) { - Ability ability = AbilityController.Instance.getSavedAbility(name); + sendResult(sender, "Custom Abilities (" + uuids.size() + "):"); + for (String uuid : uuids) { + Ability ability = AbilityController.Instance.getCustomAbility(uuid); if (ability != null) { - sendResult(sender, " - \u00A7b" + name + "\u00A77 (" + ability.getTypeId() + ")"); + String name = ability.getName() != null ? ability.getName() : uuid; + sendResult(sender, " - \u00A7b" + name + "\u00A77 (" + ability.getTypeId() + ") [" + uuid + "]"); } else { - sendResult(sender, " - \u00A7b" + name); + sendResult(sender, " - \u00A7b" + uuid); } } } @@ -79,39 +77,39 @@ public class AbilityCommand extends CommandKamkeelBase { } @SubCommand( - desc = "Delete a saved ability", - usage = "" + desc = "Delete a custom ability by UUID", + usage = "" ) public void delete(ICommandSender sender, String[] args) { if (args.length < 1) { - sendError(sender, "Usage: /kam ability delete "); + sendError(sender, "Usage: /kam ability delete "); return; } - String name = args[0]; - if (!AbilityController.Instance.getSavedAbilityNames().contains(name)) { - sendError(sender, "No saved ability with name: " + name); + String uuid = args[0]; + if (!AbilityController.Instance.hasCustomAbility(uuid)) { + sendError(sender, "No custom ability with UUID: " + uuid); return; } - AbilityController.Instance.deleteAbility(name); - sendResult(sender, "Deleted ability: \u00A7b" + name); + AbilityController.Instance.deleteCustomAbility(uuid); + sendResult(sender, "Deleted ability: \u00A7b" + uuid); } @SubCommand( - desc = "Get info about a saved ability", - usage = "" + desc = "Get info about an ability", + usage = "" ) public void info(ICommandSender sender, String[] args) { if (args.length < 1) { - sendError(sender, "Usage: /kam ability info "); + sendError(sender, "Usage: /kam ability info "); return; } - String name = args[0]; - Ability ability = AbilityController.Instance.getSavedAbility(name); + String key = args[0]; + Ability ability = AbilityController.Instance.resolveAbility(key); if (ability == null) { - sendError(sender, "No saved ability with name: " + name); + sendError(sender, "No ability found for: " + key); return; } @@ -119,8 +117,9 @@ public class AbilityCommand extends CommandKamkeelBase { sendResult(sender, " Type: \u00A7d" + ability.getTypeId()); sendResult(sender, " Cooldown: \u00A7e" + ability.getCooldownTicks() + " ticks"); sendResult(sender, " Wind Up: \u00A7e" + ability.getWindUpTicks() + " ticks"); - sendResult(sender, " Active: \u00A7e" + ability.getActiveTicks() + " ticks"); - sendResult(sender, " Recovery: \u00A7e" + ability.getRecoveryTicks() + " ticks"); + if (ability.isInterruptible()) { + sendResult(sender, " Dazed: \u00A7e" + ability.getDazedTicks() + " ticks"); + } sendResult(sender, " Range: \u00A7e" + ability.getMinRange() + " - " + ability.getMaxRange()); sendResult(sender, " Telegraph: \u00A7e" + (ability.isShowTelegraph() ? "Yes" : "No")); } diff --git a/src/main/java/kamkeel/npcs/controllers/SyncController.java b/src/main/java/kamkeel/npcs/controllers/SyncController.java index 3247498b5..b6010ad86 100644 --- a/src/main/java/kamkeel/npcs/controllers/SyncController.java +++ b/src/main/java/kamkeel/npcs/controllers/SyncController.java @@ -39,11 +39,14 @@ import noppes.npcs.controllers.data.Quest; import noppes.npcs.controllers.data.QuestCategory; import noppes.npcs.controllers.data.RecipeAnvil; import noppes.npcs.controllers.data.RecipeCarpentry; +import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.AbilityController; import java.io.IOException; import java.util.EnumMap; import java.util.EnumSet; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.Map; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; @@ -72,7 +75,8 @@ public class SyncController { EnumSyncType.ANVIL_RECIPES, EnumSyncType.CUSTOM_EFFECTS, EnumSyncType.MAGIC, - EnumSyncType.MAGIC_CYCLE + EnumSyncType.MAGIC_CYCLE, + EnumSyncType.CUSTOM_ABILITY }; public static void load() { @@ -88,6 +92,7 @@ public class SyncController { registerCache(EnumSyncType.CUSTOM_EFFECTS, SyncController::customEffectsNBT); registerCache(EnumSyncType.MAGIC, SyncController::magicsNBT); registerCache(EnumSyncType.MAGIC_CYCLE, SyncController::magicCyclesNBT); + registerCache(EnumSyncType.CUSTOM_ABILITY, SyncController::customAbilitiesNBT); } public static void syncPlayer(EntityPlayerMP player) { @@ -323,6 +328,27 @@ public class SyncController { return compound; } + public static NBTTagCompound customAbilitiesNBT() { + NBTTagList list = new NBTTagList(); + NBTTagCompound compound = new NBTTagCompound(); + for (Map.Entry entry : AbilityController.Instance.getCustomAbilities().entrySet()) { + NBTTagCompound abilityNBT = entry.getValue().writeNBT(); + abilityNBT.setString("CustomAbilityId", entry.getKey()); + list.appendTag(abilityNBT); + } + compound.setTag("Data", list); + return compound; + } + + public static void syncAllCustomAbilities() { + CachedSyncPayload payload = rebuildNow(EnumSyncType.CUSTOM_ABILITY); + if (payload == null) { + return; + } + PacketHandler.Instance.sendToAll(new SyncPacket(EnumSyncType.CUSTOM_ABILITY, payload)); + updateAllPlayerRevisions(EnumSyncType.CUSTOM_ABILITY, payload.getRevision()); + } + public static void syncPlayerData(EntityPlayerMP player, boolean update) { PlayerData data = PlayerData.get(player); if (data != null) { @@ -595,6 +621,21 @@ public class SyncController { ce.customEffectsSync.clear(); break; } + case CUSTOM_ABILITY: { + NBTTagList list = fullCompound.getTagList("Data", 10); + LinkedHashMap sync = new LinkedHashMap<>(); + for (int i = 0; i < list.tagCount(); i++) { + NBTTagCompound nbt = list.getCompoundTagAt(i); + String uuid = nbt.getString("CustomAbilityId"); + Ability ability = AbilityController.Instance.fromNBT(nbt); + if (ability != null) { + ability.setId(uuid); + sync.put(uuid, ability); + } + } + AbilityController.Instance.setCustomAbilities(sync); + break; + } } ClientCacheHandler.updateClientRevision(enumSyncType, revision); @@ -843,6 +884,8 @@ public class SyncController { return EnumSet.of(EnumSyncType.ANVIL_RECIPES); case CUSTOM_EFFECTS: return EnumSet.of(EnumSyncType.CUSTOM_EFFECTS); + case CUSTOM_ABILITY: + return EnumSet.of(EnumSyncType.CUSTOM_ABILITY); default: return EnumSet.noneOf(EnumSyncType.class); } 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 c3f6d86ed..c75d7d5d6 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java @@ -17,13 +17,28 @@ import noppes.npcs.api.INbt; import noppes.npcs.api.ability.IAbility; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; import noppes.npcs.client.gui.util.IAbilityConfigCallback; +import noppes.npcs.controllers.AnimationController; +import noppes.npcs.controllers.data.Animation; +import net.minecraft.entity.player.EntityPlayer; +import noppes.npcs.controllers.data.Frame; import noppes.npcs.entity.EntityNPCInterface; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.controllers.ScriptController; +import noppes.npcs.controllers.data.PlayerDataScript; import noppes.npcs.scripted.NpcAPI; import noppes.npcs.scripted.event.AbilityEvent; +import noppes.npcs.scripted.event.player.PlayerAbilityEvent; + +import noppes.npcs.client.gui.builder.ColumnHint; +import noppes.npcs.client.gui.builder.FieldDef; import java.util.ArrayList; +import java.util.Collections; import java.util.List; +import net.minecraft.entity.Entity; +import somehussar.gui.annotationHandling.GuiEditable; + public abstract class Ability implements IAbility { // ═══════════════════════════════════════════════════════════════════ @@ -45,17 +60,16 @@ public abstract class Ability implements IAbility { protected float maxRange = 20; // Timing (ticks) - protected int cooldownTicks = 100; + protected int cooldownTicks = 0; // Added to global cooldown after ability completes protected int windUpTicks = 20; - protected int dazedTicks = 0; - protected int activeTicks = 10; - protected int recoveryTicks = 20; + protected boolean syncWindupWithAnimation = true; + protected int dazedTicks = 80; // Only used when interrupted during WINDUP (if interruptible) // Interruption protected boolean interruptible = true; // Feedback - protected boolean lockMovement = true; + protected LockMovementType lockMovement = LockMovementType.WINDUP; protected int windUpColor = 0x80FF4400; // Telegraph color during wind up protected int activeColor = 0xC0FF0000; // Telegraph warning/active color @@ -64,8 +78,12 @@ public abstract class Ability implements IAbility { protected String activeSound = ""; // Sound to play when active phase starts // Animations (global animation IDs, -1 = none) - protected int windUpAnimationId = -1; // Animation to play during wind up - protected int activeAnimationId = -1; // Animation to play during active phase + protected int windUpAnimationId = -1; // Animation to play during wind up (user animations) + protected int activeAnimationId = -1; // Animation to play during active phase (user animations) + + // Built-in animation names (empty = none, takes priority over IDs if set) + protected String windUpAnimationName = ""; // Built-in animation name for wind up + protected String activeAnimationName = ""; // Built-in animation name for active phase // Telegraph configuration protected boolean showTelegraph = true; @@ -75,13 +93,21 @@ public abstract class Ability implements IAbility { // Custom data for external mods protected NBTTagCompound customData = new NBTTagCompound(); + // User type restriction + protected UserType allowedBy = UserType.BOTH; + + // Cooldown override + protected boolean ignoreCooldown = false; + + // Configurable potion effects + protected List effects = new ArrayList<>(); + // ═══════════════════════════════════════════════════════════════════ // EXECUTION STATE (not saved, reset each combat) // ═══════════════════════════════════════════════════════════════════ protected transient AbilityPhase phase = AbilityPhase.IDLE; protected transient int currentTick = 0; - protected transient long cooldownEndTime = 0; protected transient EntityLivingBase currentTarget; protected transient long executionStartTime; protected transient TelegraphInstance telegraphInstance; @@ -93,12 +119,12 @@ public abstract class Ability implements IAbility { /** * Called first tick of ACTIVE phase */ - public abstract void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world); + public abstract void onExecute(EntityLivingBase caster, EntityLivingBase target, World world); /** * Called every tick of ACTIVE phase */ - public abstract void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick); + public abstract void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick); /** * Write type-specific config to NBT @@ -114,111 +140,155 @@ public abstract class Ability implements IAbility { // OPTIONAL OVERRIDES // ═══════════════════════════════════════════════════════════════════ - public void onWindUpTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { + public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { } - public void onInterrupt(EntityNPCInterface npc, DamageSource source, float damage) { + public void onInterrupt(EntityLivingBase caster, DamageSource source, float damage) { } - public void onComplete(EntityNPCInterface npc, EntityLivingBase target) { + public void onComplete(EntityLivingBase caster, EntityLivingBase target) { } /** * Apply damage to an entity with ability hit event support. * Fires the abilityHit script event, allowing scripts to modify or cancel the damage. * - * @param npc The NPC executing the ability + * @param caster The entity executing the ability (NPC or Player) * @param hitEntity The entity being hit * @param damage The damage amount * @param knockback The horizontal knockback * @return true if damage was applied (not cancelled), false if cancelled */ - protected boolean applyAbilityDamage(EntityNPCInterface npc, EntityLivingBase hitEntity, + protected boolean applyAbilityDamage(EntityLivingBase caster, EntityLivingBase hitEntity, float damage, float knockback) { - return applyAbilityDamage(npc, hitEntity, damage, knockback, 0.0f); + double dx = hitEntity.posX - caster.posX; + double dz = hitEntity.posZ - caster.posZ; + return applyAbilityDamageInternal(caster, hitEntity, damage, knockback, 0.0f, dx, dz); } /** - * Backward-compatible signature (vertical knockback is ignored). + * Apply damage to an entity with ability hit event support and vertical knockback. + * Fires the abilityHit script event, allowing scripts to modify or cancel the damage. + * + * @param caster The entity executing the ability (NPC or Player) + * @param hitEntity The entity being hit + * @param damage The damage amount + * @param knockback The horizontal knockback + * @param knockbackUp The vertical knockback + * @return true if damage was applied (not cancelled), false if cancelled */ - protected boolean applyAbilityDamage(EntityNPCInterface npc, EntityLivingBase hitEntity, + protected boolean applyAbilityDamage(EntityLivingBase caster, EntityLivingBase hitEntity, float damage, float knockback, float knockbackUp) { - DataAbilities dataAbilities = npc.abilities; - AbilityEvent.HitEvent event = dataAbilities.fireHitEvent( - this, currentTarget, hitEntity, damage, knockback, knockbackUp); - - if (event == null) { - return false; // Cancelled - } - - // Apply damage with potentially modified values - float finalDamage = event.getDamage(); - float finalKnockback = event.getKnockback(); - - // Apply damage - if (finalDamage > 0) { - hitEntity.attackEntityFrom(new NpcDamageSource("mob", npc), finalDamage); - } - - // Apply knockback if any - if (finalKnockback > 0) { - double dx = hitEntity.posX - npc.posX; - double dz = hitEntity.posZ - npc.posZ; - applyNpcKnockback(hitEntity, dx, dz, finalKnockback); - } - - return true; + double dx = hitEntity.posX - caster.posX; + double dz = hitEntity.posZ - caster.posZ; + return applyAbilityDamageInternal(caster, hitEntity, damage, knockback, knockbackUp, dx, dz); } /** * Apply damage to an entity with ability hit event support and custom knockback direction. * Fires the abilityHit script event, allowing scripts to modify or cancel the damage. * - * @param npc The NPC executing the ability + * @param caster The entity executing the ability (NPC or Player) * @param hitEntity The entity being hit * @param damage The damage amount * @param knockback The horizontal knockback - * @param knockbackDirX The X component of knockback direction (normalized) - * @param knockbackDirZ The Z component of knockback direction (normalized) + * @param knockbackDirX The X component of knockback direction + * @param knockbackDirZ The Z component of knockback direction * @return true if damage was applied (not cancelled), false if cancelled */ - protected boolean applyAbilityDamageWithDirection(EntityNPCInterface npc, EntityLivingBase hitEntity, + protected boolean applyAbilityDamageWithDirection(EntityLivingBase caster, EntityLivingBase hitEntity, float damage, float knockback, double knockbackDirX, double knockbackDirZ) { - DataAbilities dataAbilities = npc.abilities; - AbilityEvent.HitEvent event = dataAbilities.fireHitEvent( - this, currentTarget, hitEntity, damage, knockback, 0.0f); + return applyAbilityDamageInternal(caster, hitEntity, damage, knockback, 0.0f, knockbackDirX, knockbackDirZ); + } - if (event == null) { - return false; // Cancelled + /** + * Internal unified damage application method. + * All public damage methods delegate to this. + * Supports both NPC and Player casters. + */ + private boolean applyAbilityDamageInternal(EntityLivingBase caster, EntityLivingBase hitEntity, + float damage, float knockback, float knockbackUp, + double knockbackDirX, double knockbackDirZ) { + // Fire hit event for NPC casters (NPC event system) + if (caster instanceof EntityNPCInterface) { + EntityNPCInterface npc = (EntityNPCInterface) caster; + DataAbilities dataAbilities = npc.abilities; + AbilityEvent.HitEvent event = dataAbilities.fireHitEvent( + this, currentTarget, hitEntity, damage, knockback, knockbackUp); + + if (event == null) { + return false; // Cancelled + } + + damage = event.getDamage(); + knockback = event.getKnockback(); + knockbackUp = event.getKnockbackUp(); } + // Fire hit event for Player casters (Player event system) + else if (caster instanceof EntityPlayer && ScriptController.Instance != null) { + EntityPlayer player = (EntityPlayer) caster; + PlayerDataScript handler = ScriptController.Instance.getPlayerScripts(player); + if (handler != null) { + IPlayer iPlayer = (IPlayer) NpcAPI.Instance().getIEntity(player); + PlayerAbilityEvent.HitEvent event = new PlayerAbilityEvent.HitEvent( + iPlayer, this, currentTarget, hitEntity, damage, knockback, knockbackUp); - // Apply damage with potentially modified values - float finalDamage = event.getDamage(); - float finalKnockback = event.getKnockback(); + if (noppes.npcs.EventHooks.onPlayerAbilityHit(handler, event)) { + return false; // Cancelled + } + + damage = event.getDamage(); + knockback = event.getKnockback(); + knockbackUp = event.getKnockbackUp(); + } + } // Apply damage - if (finalDamage > 0) { - hitEntity.attackEntityFrom(DamageSource.causeMobDamage(npc), finalDamage); + if (damage > 0) { + if (caster instanceof EntityNPCInterface) { + hitEntity.attackEntityFrom(new NpcDamageSource("mob", (EntityNPCInterface) caster), damage); + } else if (caster instanceof EntityPlayer) { + hitEntity.attackEntityFrom(DamageSource.causePlayerDamage((EntityPlayer) caster), damage); + } else { + hitEntity.attackEntityFrom(DamageSource.causeMobDamage(caster), damage); + } } - // Apply knockback in specified direction - if (finalKnockback > 0) { - applyNpcKnockback(hitEntity, knockbackDirX, knockbackDirZ, finalKnockback); + // Apply knockback if any + if (knockback > 0 || knockbackUp > 0) { + applyKnockback(hitEntity, knockbackDirX, knockbackDirZ, knockback, knockbackUp); } return true; } - private void applyNpcKnockback(EntityLivingBase hitEntity, double dirX, double dirZ, float knockback) { + private void applyKnockback(EntityLivingBase hitEntity, double dirX, double dirZ, float knockback, float knockbackUp) { double len = Math.sqrt(dirX * dirX + dirZ * dirZ); - if (len <= 0) { + double x = 0; + double z = 0; + if (len > 0) { + x = (dirX / len) * knockback * 0.5; + z = (dirZ / len) * knockback * 0.5; + } + double y = knockbackUp > 0 ? knockbackUp : 0.1D; + hitEntity.addVelocity(x, y, z); + hitEntity.velocityChanged = true; + } + + /** + * Applies all configured effects to the given entity. + * Call this after damage is applied to the target. + * + * @param entity The entity to apply effects to + */ + protected void applyEffects(EntityLivingBase entity) { + if (entity == null || effects.isEmpty()) { return; } - double x = (dirX / len) * knockback * 0.5; - double z = (dirZ / len) * knockback * 0.5; - hitEntity.addVelocity(x, 0.1D, z); - hitEntity.velocityChanged = true; + for (AbilityEffect effect : effects) { + effect.apply(entity); + } } public float getTelegraphRadius() { @@ -272,19 +342,117 @@ public abstract class Ability implements IAbility { } /** - * Returns true if this ability type has custom settings that need a Type-specific tab. - * Override to return true if the ability has settings beyond the base Ability fields. + * Returns the declarative field definitions for this ability's GUI. + * Override in subclasses to declare type-specific and visual fields. + * The GUI rendering engine in SubGuiAbilityConfig uses this list to + * automatically build the Type and Visual tabs. */ - public boolean hasTypeSettings() { - return false; + @SideOnly(Side.CLIENT) + public List getFieldDefinitions() { + return Collections.emptyList(); } /** - * Get the number of rows needed for type-specific settings in the GUI. - * Each row is approximately 24 pixels. Used for GUI layout. + * Returns field definitions for all base Ability fields (General, Target, Effects tabs). */ - public int getTypeSettingsRowCount() { - return 0; + @SideOnly(Side.CLIENT) + public List getBaseFieldDefinitions() { + List defs = new ArrayList<>(); + + // General tab + defs.add(FieldDef.stringField("gui.name", this::getName, this::setName) + .tab("General")); + defs.add(FieldDef.intField("ability.weight", this::getWeight, this::setWeight) + .tab("General").range(1, 1000).column(ColumnHint.LEFT)); + defs.add(FieldDef.boolField("gui.enabled", this::isEnabled, this::setEnabled) + .tab("General").column(ColumnHint.RIGHT)); + defs.add(FieldDef.section("ability.section.timing").tab("General")); + defs.add(FieldDef.intField("ability.windUpTicks", this::getRawWindUpTicks, this::setWindUpTicks) + .tab("General").range(0, 1000).column(ColumnHint.LEFT) + .visibleWhen(() -> !hasWindUpAnimation() || !isSyncWindupWithAnimation())); + defs.add(FieldDef.labelField("ability.windUpTicks", () -> getWindUpTicks() + "t") + .tab("General").column(ColumnHint.LEFT) + .visibleWhen(() -> hasWindUpAnimation() && isSyncWindupWithAnimation())); + defs.add(FieldDef.boolField("ability.syncWindup", this::isSyncWindupWithAnimation, this::setSyncWindupWithAnimation) + .tab("General").hover("ability.hover.sync").column(ColumnHint.RIGHT) + .visibleWhen(this::hasWindUpAnimation)); + defs.add(FieldDef.intField("ability.cooldownTicks", this::getCooldownTicks, this::setCooldownTicks) + .tab("General").range(0, 10000)); + defs.add(FieldDef.section("ability.section.movement").tab("General")); + defs.add(FieldDef.stringEnumField("ability.lockMovement", LockMovementType.getDisplayKeys(), + () -> this.getLockMovement().getDisplayKey(), + v -> { + String[] keys = LockMovementType.getDisplayKeys(); + for (int i = 0; i < keys.length; i++) { + if (keys[i].equals(v)) { this.setLockMovement(LockMovementType.fromOrdinal(i)); break; } + } + }) + .tab("General").hover("ability.hover.lockMove")); + defs.add(FieldDef.boolField("ability.interruptible", this::isInterruptible, this::setInterruptible) + .tab("General").hover("ability.hover.interruptible").column(ColumnHint.LEFT)); + defs.add(FieldDef.intField("ability.dazedTicks", this::getDazedTicks, this::setDazedTicks) + .tab("General").range(0, 1000) + .visibleWhen(this::isInterruptible).column(ColumnHint.RIGHT)); + + // Target tab + defs.add(FieldDef.intField("ability.minRange", () -> (int) getMinRange(), v -> setMinRange(v)) + .tab("Target").range(0, 100).column(ColumnHint.LEFT)); + defs.add(FieldDef.intField("ability.maxRange", () -> (int) getMaxRange(), v -> setMaxRange(v)) + .tab("Target").range(1, 100).column(ColumnHint.RIGHT)); + if (!isTargetingModeLocked()) { + defs.add(FieldDef.enumField("ability.targetingMode", TargetingMode.class, + this::getTargetingMode, this::setTargetingMode) + .tab("Target").hover("ability.hover.targeting")); + } + + // Effects tab - Sounds + defs.add(FieldDef.section("ability.section.sounds").tab("Effects")); + defs.add(FieldDef.soundSubGui("ability.windUpSound", this::getWindUpSound, this::setWindUpSound) + .tab("Effects")); + defs.add(FieldDef.soundSubGui("ability.activeSound", this::getActiveSound, this::setActiveSound) + .tab("Effects")); + // Effects tab - Animations + defs.add(FieldDef.section("ability.section.animations").tab("Effects")); + defs.add(FieldDef.animSubGui("ability.windUpAnimation", + this::getWindUpAnimationId, this::setWindUpAnimationId, + this::getWindUpAnimationName, this::setWindUpAnimationName) + .tab("Effects")); + defs.add(FieldDef.animSubGui("ability.activeAnimation", + this::getActiveAnimationId, this::setActiveAnimationId, + this::getActiveAnimationName, this::setActiveAnimationName) + .tab("Effects")); + + // Effects tab - Telegraph + TelegraphType tType = getTelegraphType(); + if (tType != null && tType != TelegraphType.NONE) { + defs.add(FieldDef.section("ability.section.telegraph").tab("Effects") + .tooltip("telegraph." + tType.name().toLowerCase())); + defs.add(FieldDef.boolField("ability.showTelegraph", this::isShowTelegraph, this::setShowTelegraph) + .tab("Effects").hover("ability.hover.showTelegraph")); + defs.add(FieldDef.colorSubGui("ability.windUpColor", this::getWindUpColor, this::setWindUpColor) + .tab("Effects").visibleWhen(this::isShowTelegraph)); + defs.add(FieldDef.colorSubGui("ability.activeColor", this::getActiveColor, this::setActiveColor) + .tab("Effects").visibleWhen(this::isShowTelegraph)); + } + + return defs; + } + + /** + * Returns ALL field definitions: base (General/Target/Effects) + type-specific (Type/Visual). + */ + @SideOnly(Side.CLIENT) + public final List getAllFieldDefinitions() { + List all = new ArrayList<>(getBaseFieldDefinitions()); + List typeDefs = getFieldDefinitions(); + // Default tab for type-specific fields that don't explicitly set one + for (FieldDef def : typeDefs) { + if (def.getTab() == null) { + def.tab("Type"); + } + } + all.addAll(typeDefs); + return all; } /** @@ -306,37 +474,37 @@ public abstract class Ability implements IAbility { * Create a telegraph instance for this ability. * Override for custom telegraph shapes. * - * @param npc The caster + * @param caster The caster * @param target The target (for position calculation) * @return The telegraph instance, or null if no telegraph */ - public TelegraphInstance createTelegraph(EntityNPCInterface npc, EntityLivingBase target) { + public TelegraphInstance createTelegraph(EntityLivingBase caster, EntityLivingBase target) { if (!showTelegraph || telegraphType == TelegraphType.NONE) { return null; } Telegraph telegraph; double x, y, z; - float yaw = npc.rotationYaw; + float yaw = caster.rotationYaw; // Determine position based on targeting mode - boolean positionAtNpc = targetingMode == TargetingMode.AOE_SELF || + boolean positionAtCaster = targetingMode == TargetingMode.AOE_SELF || targetingMode == TargetingMode.SELF || telegraphType == TelegraphType.LINE || telegraphType == TelegraphType.CONE; - if (positionAtNpc) { - x = npc.posX; - y = findGroundLevel(npc.worldObj, npc.posX, npc.posY, npc.posZ); - z = npc.posZ; + if (positionAtCaster) { + x = caster.posX; + y = findGroundLevel(caster.worldObj, caster.posX, caster.posY, caster.posZ); + z = caster.posZ; } else if (target != null) { x = target.posX; - y = findGroundLevel(npc.worldObj, target.posX, target.posY, target.posZ); + y = findGroundLevel(caster.worldObj, target.posX, target.posY, target.posZ); z = target.posZ; } else { - x = npc.posX; - y = findGroundLevel(npc.worldObj, npc.posX, npc.posY, npc.posZ); - z = npc.posZ; + x = caster.posX; + y = findGroundLevel(caster.worldObj, caster.posX, caster.posY, caster.posZ); + z = caster.posZ; } // Create telegraph based on type @@ -367,12 +535,17 @@ public abstract class Ability implements IAbility { telegraph.setHeightOffset(telegraphHeightOffset); TelegraphInstance instance = new TelegraphInstance(telegraph, x, y, z, yaw); - instance.setCasterEntityId(npc.getEntityId()); + instance.setCasterEntityId(caster.getEntityId()); // Set entity to follow based on targeting mode - if (positionAtNpc) { - // AOE_SELF abilities: telegraph follows NPC during windup - instance.setEntityIdToFollow(npc.getEntityId()); + if (positionAtCaster) { + // AOE_SELF abilities: telegraph follows caster during windup + instance.setEntityIdToFollow(caster.getEntityId()); + + // For LINE/CONE telegraphs: track target direction during windup + if ((telegraphType == TelegraphType.LINE || telegraphType == TelegraphType.CONE) && target != null) { + instance.setTargetEntityId(target.getEntityId()); + } } else if (target != null) { // AOE_TARGET abilities: telegraph follows target during windup instance.setEntityIdToFollow(target.getEntityId()); @@ -391,7 +564,7 @@ public abstract class Ability implements IAbility { * @param z Z coordinate * @return The Y coordinate of the ground surface */ - protected double findGroundLevel(World world, double x, double startY, double z) { + public static double findGroundLevel(World world, double x, double startY, double z) { if (world == null) return startY; int blockX = (int) Math.floor(x); @@ -411,6 +584,43 @@ public abstract class Ability implements IAbility { return startY; } + /** + * Calculates offset position near the given coordinates. + * Used to place effects near a target rather than exactly on them. + * + * @param baseX Base X coordinate + * @param baseY Base Y coordinate + * @param baseZ Base Z coordinate + * @param minOffset Minimum offset distance + * @param maxOffset Maximum offset distance + * @param randomOffset Whether to use random offset within range, or fixed at max + * @param random Random instance to use + * @return Array of [x, y, z] with offset applied + */ + public static double[] calculateOffsetPosition(double baseX, double baseY, double baseZ, + float minOffset, float maxOffset, + boolean randomOffset, java.util.Random random) { + if (maxOffset <= 0) { + return new double[]{baseX, baseY, baseZ}; + } + + double offsetDist; + double offsetAngle; + + if (randomOffset) { + offsetDist = minOffset + random.nextDouble() * (maxOffset - minOffset); + offsetAngle = random.nextDouble() * Math.PI * 2; + } else { + offsetDist = maxOffset; + offsetAngle = random.nextDouble() * Math.PI * 2; + } + + double offsetX = Math.cos(offsetAngle) * offsetDist; + double offsetZ = Math.sin(offsetAngle) * offsetDist; + + return new double[]{baseX + offsetX, baseY, baseZ + offsetZ}; + } + /** * Get the current telegraph instance. */ @@ -440,13 +650,17 @@ public abstract class Ability implements IAbility { } /** - * Tick the ability. Returns true if phase changed. + * Tick the ability forward. Called every game tick while executing. + *

+ * Normal flow: WINDUP -> ACTIVE -> IDLE + * Interrupted flow: WINDUP -> DAZED -> IDLE (dazed only on interrupt during WINDUP) + * + * @return true if phase changed this tick */ public boolean tick() { if (phase == AbilityPhase.IDLE) return false; currentTick++; - AbilityPhase oldPhase = phase; switch (phase) { case WINDUP: @@ -457,16 +671,15 @@ public abstract class Ability implements IAbility { } break; case ACTIVE: - if (currentTick >= activeTicks) { - phase = AbilityPhase.RECOVERY; - currentTick = 0; - return true; - } + // Abilities control their own completion by calling signalCompletion() + // No auto-complete - each ability determines when it's done break; - case RECOVERY: - if (currentTick >= recoveryTicks) { + case DAZED: + // Dazed phase is only entered on interrupt during WINDUP + // NPC cannot attack during this time + if (currentTick >= dazedTicks) { phase = AbilityPhase.IDLE; - startCooldown(); + currentTick = 0; return true; } break; @@ -475,29 +688,41 @@ public abstract class Ability implements IAbility { } /** - * Interrupt this ability + * Signal that the ability has completed its active phase. + * Called by abilities when they are done. + * For example, AbilitySlam calls this when the NPC lands. + * + * @return true if phase changed to IDLE + */ + public boolean signalCompletion() { + if (phase == AbilityPhase.ACTIVE) { + cleanup(); + phase = AbilityPhase.IDLE; + currentTick = 0; + return true; + } + return false; + } + + /** + * Interrupt this ability. Transitions to DAZED phase if interruptible during WINDUP. + * Called when NPC takes direct damage during WINDUP. */ public void interrupt() { - phase = AbilityPhase.IDLE; - currentTick = 0; + cleanup(); + if (interruptible && phase == AbilityPhase.WINDUP) { + // Interrupted during windup - go to dazed state + phase = AbilityPhase.DAZED; + currentTick = 0; + } else { + // Force stop - go directly to IDLE + phase = AbilityPhase.IDLE; + currentTick = 0; + } currentTarget = null; telegraphInstance = null; } - /** - * Start cooldown timer - */ - public void startCooldown() { - cooldownEndTime = System.currentTimeMillis() + (cooldownTicks * 50L); - } - - /** - * Check if on cooldown - */ - public boolean isOnCooldown() { - return System.currentTimeMillis() < cooldownEndTime; - } - /** * Check if executing */ @@ -538,41 +763,135 @@ public abstract class Ability implements IAbility { } /** - * Reset all execution state (called on combat end) + * Reset all execution state (called on combat end, NPC death, target lost, etc.) */ public void reset() { + cleanup(); phase = AbilityPhase.IDLE; currentTick = 0; currentTarget = null; - cooldownEndTime = 0; telegraphInstance = null; } + /** + * Cleanup any resources created by this ability. + * Override this to kill spawned entities, cancel telegraphs, etc. + * Called when ability ends for any reason (completion, interrupt, reset, NPC death). + */ + public void cleanup() { + // 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 // ═══════════════════════════════════════════════════════════════════ - public boolean checkConditions(EntityNPCInterface npc, EntityLivingBase target) { + public boolean checkConditions(EntityLivingBase caster, EntityLivingBase target) { for (Condition c : conditions) { - if (!c.check(npc, target)) return false; + if (!c.check(caster, target)) return false; } return true; } /** - * Full eligibility check + * Check conditions for a player caster, skipping target-requiring conditions. */ - public boolean canUse(EntityNPCInterface npc, EntityLivingBase target) { - if (!enabled) return false; - if (isOnCooldown()) return false; - if (isExecuting()) return false; - - float distance = npc.getDistanceToEntity(target); - if (distance < minRange || distance > maxRange) return false; - - return checkConditions(npc, target); + public boolean checkConditionsForPlayer(EntityLivingBase caster) { + for (Condition c : conditions) { + if (c.requiresTarget()) continue; + if (!c.check(caster, null)) return false; + } + return true; } + // ═══════════════════════════════════════════════════════════════════ // NBT (config only, execution state is transient) // ═══════════════════════════════════════════════════════════════════ @@ -589,27 +908,37 @@ public abstract class Ability implements IAbility { nbt.setFloat("maxRange", maxRange); nbt.setInteger("cooldown", cooldownTicks); nbt.setInteger("windUp", windUpTicks); - nbt.setInteger("dazed", dazedTicks); - nbt.setInteger("active", activeTicks); - nbt.setInteger("recovery", recoveryTicks); + nbt.setBoolean("syncWindup", syncWindupWithAnimation); + nbt.setInteger("recovery", dazedTicks); nbt.setBoolean("interruptible", interruptible); - nbt.setBoolean("lockMovement", lockMovement); + nbt.setInteger("lockMovement", lockMovement.ordinal()); nbt.setInteger("windUpColor", windUpColor); nbt.setInteger("activeColor", activeColor); nbt.setString("windUpSound", windUpSound); nbt.setString("activeSound", activeSound); nbt.setInteger("windUpAnimationId", windUpAnimationId); nbt.setInteger("activeAnimationId", activeAnimationId); + nbt.setString("windUpAnimationName", windUpAnimationName); + nbt.setString("activeAnimationName", activeAnimationName); nbt.setBoolean("showTelegraph", showTelegraph); nbt.setString("telegraphType", telegraphType.name()); nbt.setFloat("telegraphHeightOffset", telegraphHeightOffset); nbt.setTag("customData", customData); + nbt.setInteger("allowedBy", allowedBy.ordinal()); + nbt.setBoolean("ignoreCooldown", ignoreCooldown); // Conditions NBTTagList condList = new NBTTagList(); for (Condition c : conditions) condList.appendTag(c.writeNBT()); nbt.setTag("conditions", condList); + // Effects + NBTTagList effectList = new NBTTagList(); + for (AbilityEffect effect : effects) { + effectList.appendTag(effect.writeNBT()); + } + nbt.setTag("effects", effectList); + // Type-specific NBTTagCompound typeNBT = new NBTTagCompound(); writeTypeNBT(typeNBT); @@ -629,41 +958,18 @@ public abstract class Ability implements IAbility { maxRange = nbt.getFloat("maxRange"); cooldownTicks = nbt.getInteger("cooldown"); windUpTicks = nbt.getInteger("windUp"); - dazedTicks = nbt.hasKey("dazed") ? nbt.getInteger("dazed") : 0; - activeTicks = nbt.getInteger("active"); - recoveryTicks = nbt.getInteger("recovery"); + syncWindupWithAnimation = !nbt.hasKey("syncWindup") || nbt.getBoolean("syncWindup"); + dazedTicks = nbt.getInteger("recovery"); interruptible = nbt.getBoolean("interruptible"); - lockMovement = nbt.getBoolean("lockMovement"); - // Support old telegraphColor key for backwards compatibility - if (nbt.hasKey("windUpColor")) { - windUpColor = nbt.getInteger("windUpColor"); - } else if (nbt.hasKey("telegraphColor")) { - windUpColor = nbt.getInteger("telegraphColor"); - } - if (nbt.hasKey("activeColor")) { - activeColor = nbt.getInteger("activeColor"); - } else { - // Default active color to windUpColor with higher alpha - activeColor = (windUpColor & 0x00FFFFFF) | 0xC0000000; - } - // Sound fields (with backwards compatibility for old castSound) - if (nbt.hasKey("windUpSound")) { - windUpSound = nbt.getString("windUpSound"); - } else if (nbt.hasKey("castSound")) { - windUpSound = nbt.getString("castSound"); // Migrate old field - } else { - windUpSound = ""; - } - activeSound = nbt.hasKey("activeSound") ? nbt.getString("activeSound") : ""; - // Animation fields (with backwards compatibility for old animationId) - if (nbt.hasKey("windUpAnimationId")) { - windUpAnimationId = nbt.getInteger("windUpAnimationId"); - } else if (nbt.hasKey("animationId")) { - windUpAnimationId = nbt.getInteger("animationId"); // Migrate old field - } else { - windUpAnimationId = -1; - } + lockMovement = LockMovementType.fromOrdinal(nbt.getInteger("lockMovement")); + windUpColor = nbt.getInteger("windUpColor"); + activeColor = nbt.getInteger("activeColor"); + windUpSound = nbt.getString("windUpSound"); + activeSound = nbt.getString("activeSound"); + windUpAnimationId = nbt.hasKey("windUpAnimationId") ? nbt.getInteger("windUpAnimationId") : -1; activeAnimationId = nbt.hasKey("activeAnimationId") ? nbt.getInteger("activeAnimationId") : -1; + windUpAnimationName = nbt.getString("windUpAnimationName"); + activeAnimationName = nbt.getString("activeAnimationName"); showTelegraph = !nbt.hasKey("showTelegraph") || nbt.getBoolean("showTelegraph"); if (nbt.hasKey("telegraphType")) { try { @@ -674,6 +980,8 @@ public abstract class Ability implements IAbility { } telegraphHeightOffset = nbt.hasKey("telegraphHeightOffset") ? nbt.getFloat("telegraphHeightOffset") : 0.1f; customData = nbt.getCompoundTag("customData"); + allowedBy = nbt.hasKey("allowedBy") ? UserType.fromOrdinal(nbt.getInteger("allowedBy")) : UserType.BOTH; + ignoreCooldown = nbt.hasKey("ignoreCooldown") && nbt.getBoolean("ignoreCooldown"); // Conditions conditions.clear(); @@ -683,6 +991,18 @@ public abstract class Ability implements IAbility { if (c != null) conditions.add(c); } + // Effects + effects.clear(); + if (nbt.hasKey("effects")) { + NBTTagList effectList = nbt.getTagList("effects", 10); + for (int i = 0; i < effectList.tagCount(); i++) { + AbilityEffect effect = AbilityEffect.fromNBT(effectList.getCompoundTagAt(i)); + if (effect != null && effect.isValid()) { + effects.add(effect); + } + } + } + readTypeNBT(nbt.getCompoundTag("typeData")); } @@ -755,41 +1075,39 @@ public abstract class Ability implements IAbility { } public void setCooldownTicks(int cooldownTicks) { - this.cooldownTicks = cooldownTicks; + this.cooldownTicks = Math.max(0, cooldownTicks); } public int getWindUpTicks() { + return calculateWindupFromAnimation(); + } + + public int getRawWindUpTicks() { return windUpTicks; } public void setWindUpTicks(int windUpTicks) { - this.windUpTicks = windUpTicks; + this.windUpTicks = Math.max(0, windUpTicks); + } + + public boolean isSyncWindupWithAnimation() { + return syncWindupWithAnimation; + } + + public boolean hasWindUpAnimation() { + return (windUpAnimationName != null && !windUpAnimationName.isEmpty()) || windUpAnimationId >= 0; + } + + public void setSyncWindupWithAnimation(boolean syncWindupWithAnimation) { + this.syncWindupWithAnimation = syncWindupWithAnimation; } - @Override public int getDazedTicks() { return dazedTicks; } - @Override - public void setDazedTicks(int ticks) { - this.dazedTicks = ticks; - } - - public int getActiveTicks() { - return activeTicks; - } - - public void setActiveTicks(int activeTicks) { - this.activeTicks = activeTicks; - } - - public int getRecoveryTicks() { - return recoveryTicks; - } - - public void setRecoveryTicks(int recoveryTicks) { - this.recoveryTicks = recoveryTicks; + public void setDazedTicks(int dazedTicks) { + this.dazedTicks = Math.max(0, dazedTicks); } public boolean isInterruptible() { @@ -800,14 +1118,62 @@ public abstract class Ability implements IAbility { this.interruptible = interruptible; } - public boolean isLockMovement() { + public LockMovementType getLockMovement() { return lockMovement; } - public void setLockMovement(boolean lockMovement) { + public void setLockMovement(LockMovementType lockMovement) { this.lockMovement = lockMovement; } + /** + * API method: Get lock movement type as integer. + * @return 0=NO, 1=WINDUP, 2=ACTIVE, 3=WINDUP_AND_ACTIVE + */ + @Override + public int getLockMovementType() { + return lockMovement.ordinal(); + } + + /** + * API method: Set lock movement type from integer. + * @param type 0=NO, 1=WINDUP, 2=ACTIVE, 3=WINDUP_AND_ACTIVE + */ + @Override + public void setLockMovementType(int type) { + this.lockMovement = LockMovementType.fromOrdinal(type); + } + + /** + * Check if movement should be locked during WINDUP phase. + */ + @Override + public boolean isMovementLockedDuringWindup() { + return lockMovement.locksWindup(); + } + + /** + * Check if movement should be locked during ACTIVE phase. + */ + @Override + public boolean isMovementLockedDuringActive() { + return lockMovement.locksActive(); + } + + /** + * Check if movement should be locked during the current phase. + */ + public boolean isMovementLockedForCurrentPhase() { + switch (phase) { + case WINDUP: + return lockMovement.locksWindup(); + case ACTIVE: + return lockMovement.locksActive(); + default: + return false; + } + } + public int getWindUpColor() { return windUpColor; } @@ -824,22 +1190,6 @@ public abstract class Ability implements IAbility { this.activeColor = activeColor; } - /** - * @deprecated Use getWindUpColor() instead - */ - @Deprecated - public int getTelegraphColor() { - return windUpColor; - } - - /** - * @deprecated Use setWindUpColor() instead - */ - @Deprecated - public void setTelegraphColor(int telegraphColor) { - this.windUpColor = telegraphColor; - } - public String getWindUpSound() { return windUpSound; } @@ -856,6 +1206,20 @@ public abstract class Ability implements IAbility { this.activeSound = activeSound; } + public Animation getWindUpAnimation() { + if (AnimationController.Instance == null) return null; + + // Built-in animation by name takes priority + if (windUpAnimationName != null && !windUpAnimationName.isEmpty()) { + return (Animation) AnimationController.Instance.get(windUpAnimationName, true); + } + // Fall back to user animation by ID + if (windUpAnimationId >= 0) { + return (Animation) AnimationController.Instance.get(windUpAnimationId); + } + return null; + } + public int getWindUpAnimationId() { return windUpAnimationId; } @@ -864,6 +1228,28 @@ public abstract class Ability implements IAbility { this.windUpAnimationId = windUpAnimationId; } + public String getWindUpAnimationName() { + return windUpAnimationName; + } + + public void setWindUpAnimationName(String windUpAnimationName) { + this.windUpAnimationName = windUpAnimationName != null ? windUpAnimationName : ""; + } + + public Animation getActiveAnimation() { + if (AnimationController.Instance == null) return null; + + // Built-in animation by name takes priority + if (activeAnimationName != null && !activeAnimationName.isEmpty()) { + return (Animation) AnimationController.Instance.get(activeAnimationName, true); + } + // Fall back to user animation by ID + if (activeAnimationId >= 0) { + return (Animation) AnimationController.Instance.get(activeAnimationId); + } + return null; + } + public int getActiveAnimationId() { return activeAnimationId; } @@ -872,36 +1258,12 @@ public abstract class Ability implements IAbility { this.activeAnimationId = activeAnimationId; } - /** - * @deprecated Use getWindUpSound() instead - */ - @Deprecated - public String getCastSound() { - return windUpSound; + public String getActiveAnimationName() { + return activeAnimationName; } - /** - * @deprecated Use setWindUpSound() instead - */ - @Deprecated - public void setCastSound(String castSound) { - this.windUpSound = castSound; - } - - /** - * @deprecated Use getWindUpAnimationId() instead - */ - @Deprecated - public int getAnimationId() { - return windUpAnimationId; - } - - /** - * @deprecated Use setWindUpAnimationId() instead - */ - @Deprecated - public void setAnimationId(int animationId) { - this.windUpAnimationId = animationId; + public void setActiveAnimationName(String activeAnimationName) { + this.activeAnimationName = activeAnimationName != null ? activeAnimationName : ""; } public boolean isShowTelegraph() { @@ -933,7 +1295,7 @@ public abstract class Ability implements IAbility { } /** - * Get the current execution phase as int (0=IDLE, 1=WINDUP, 2=ACTIVE, 3=RECOVERY). + * Get the current execution phase as int (0=IDLE, 1=WINDUP, 2=ACTIVE, 3=DAZED). * Required for IAbility interface. */ @Override @@ -961,10 +1323,91 @@ public abstract class Ability implements IAbility { conditions.add(c); } + public UserType getAllowedBy() { + return allowedBy; + } + + public void setAllowedBy(UserType allowedBy) { + this.allowedBy = allowedBy; + } + + public boolean isIgnoreCooldown() { + return ignoreCooldown; + } + + public void setIgnoreCooldown(boolean ignoreCooldown) { + this.ignoreCooldown = ignoreCooldown; + } + + public List getEffects() { + return effects; + } + + public void setEffects(List effects) { + this.effects = effects != null ? effects : new ArrayList<>(); + } + + public void addEffect(AbilityEffect effect) { + if (effect != null && effect.isValid()) { + effects.add(effect); + } + } + + public void clearEffects() { + effects.clear(); + } + + // ═══════════════════════════════════════════════════════════════════ + // SHIT FROM THE ASS + // ═══════════════════════════════════════════════════════════════════ + + private int calculateWindupFromAnimation() { + if (!syncWindupWithAnimation) { + return windUpTicks; + } + + if (AnimationController.Instance == null) { + return this.windUpTicks; + } + + Animation animation = null; + // Check for built-in animation (by name) first + if (windUpAnimationName != null && !windUpAnimationName.isEmpty()) { + animation = (Animation) AnimationController.Instance.get(windUpAnimationName, true); + } + // Fall back to user animation (by ID) + else if (windUpAnimationId >= 0) { + animation = (Animation) AnimationController.Instance.get(windUpAnimationId); + } + + if (animation == null || animation.frames.isEmpty()) { + return this.windUpTicks; + } + + // Calculate total duration by summing all frame durations + int totalDuration = 0; + for (Frame frame : animation.frames) { + totalDuration += frame.getDuration(); + } + + // Return total duration + return totalDuration; + } + // ═══════════════════════════════════════════════════════════════════ // IAbility API METHODS // ═══════════════════════════════════════════════════════════════════ + @Override + public int getAllowedByType() { + return allowedBy.ordinal(); + } + + @Override + public void setAllowedByType(int type) { + this.allowedBy = UserType.fromOrdinal(type); + } + @Override public INbt getNbt() { return NpcAPI.Instance().getINbt(writeNBT()); diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java index 1a9bbd522..eba8795f7 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java @@ -1,20 +1,7 @@ package kamkeel.npcs.controllers.data.ability; -import kamkeel.npcs.controllers.data.ability.type.AbilityBeam; -import kamkeel.npcs.controllers.data.ability.type.AbilityCharge; -import kamkeel.npcs.controllers.data.ability.type.AbilityCutter; -import kamkeel.npcs.controllers.data.ability.type.AbilityDash; -import kamkeel.npcs.controllers.data.ability.type.AbilityGuard; -import kamkeel.npcs.controllers.data.ability.type.AbilityHazard; -import kamkeel.npcs.controllers.data.ability.type.AbilityHeal; -import kamkeel.npcs.controllers.data.ability.type.AbilityHeavyHit; -import kamkeel.npcs.controllers.data.ability.type.AbilityOrb; -import kamkeel.npcs.controllers.data.ability.type.AbilityProjectile; -import kamkeel.npcs.controllers.data.ability.type.AbilityShockwave; -import kamkeel.npcs.controllers.data.ability.type.AbilitySlam; -import kamkeel.npcs.controllers.data.ability.type.AbilityTeleport; -import kamkeel.npcs.controllers.data.ability.type.AbilityTrap; -import kamkeel.npcs.controllers.data.ability.type.AbilityVortex; +import kamkeel.npcs.controllers.data.ability.type.*; +import kamkeel.npcs.controllers.SyncController; import net.minecraft.nbt.NBTTagCompound; import noppes.npcs.CustomNpcs; import noppes.npcs.LogWriter; @@ -22,38 +9,50 @@ import noppes.npcs.api.handler.IAbilityHandler; import noppes.npcs.util.NBTJsonUtil; import java.io.File; -import java.util.Collection; -import java.util.LinkedHashMap; -import java.util.LinkedHashSet; -import java.util.Map; -import java.util.Set; +import java.util.*; import java.util.function.Supplier; /** - * Controller for ability types. Allows external mods to register custom ability types. - * Initialize during FMLInitializationEvent. + * Central controller for ability types and presets. *

- * Also manages saved ability presets that can be reused across NPCs. + * Three maps: + *

    + *
  • {@code typeFactories} — keyed by typeId (lang key), creates new instances of each ability type
  • + *
  • {@code abilities} — built-in pre-configured abilities, keyed by unique name
  • + *
  • {@code customAbilities} — user-created presets, keyed by UUID
  • + *
*/ public class AbilityController implements IAbilityHandler { - public static AbilityController Instance; + public static AbilityController Instance = new AbilityController(); - private final Map> factories = new LinkedHashMap<>(); - private final Map> factoriesByTypeId = new LinkedHashMap<>(); - private final Map savedAbilities = new LinkedHashMap<>(); + /** Type factories keyed by typeId (lang key, e.g. "ability.cnpc.slam"). */ + private final Map> typeFactories = new LinkedHashMap<>(); + + /** Built-in pre-configured abilities keyed by unique name. */ + private final Map abilities = new LinkedHashMap<>(); + + /** User-created custom ability presets keyed by UUID. */ + private final Map customAbilities = new LinkedHashMap<>(); + + /** Version counter — incremented on any custom ability add/modify/delete. + * Used by AbilitySlot for cache invalidation. */ + private int version = 0; public AbilityController() { - Instance = this; - registerBuiltins(); + registerBuiltinTypes(); } + // ═══════════════════════════════════════════════════════════════════════════ + // LOADING / SAVING CUSTOM ABILITIES + // ═══════════════════════════════════════════════════════════════════════════ + /** - * Load saved abilities from world directory. - * Called when world loads. + * Load custom ability presets from world directory. + * Called when world loads. Files are named {@code .json}. */ public void load() { - savedAbilities.clear(); + customAbilities.clear(); File dir = getDir(); if (!dir.exists()) { return; @@ -65,22 +64,22 @@ public class AbilityController implements IAbilityHandler { for (File file : files) { if (!file.isFile() || !file.getName().endsWith(".json")) continue; try { + String filename = file.getName(); + String uuid = filename.substring(0, filename.length() - 5); // strip .json + NBTTagCompound nbt = NBTJsonUtil.LoadFile(file); Ability ability = fromNBT(nbt); - if (ability != null && ability.getName() != null && !ability.getName().isEmpty()) { - savedAbilities.put(ability.getName(), ability); - LogWriter.info("Loaded ability preset: " + ability.getName()); + if (ability != null) { + ability.setId(uuid); + customAbilities.put(uuid, ability); } } catch (Exception e) { - LogWriter.error("Error loading ability preset: " + file.getAbsolutePath(), e); + LogWriter.error("Error loading custom ability: " + file.getAbsolutePath(), e); } } - LogWriter.info("Loaded " + savedAbilities.size() + " ability presets"); + LogWriter.info("Loaded " + customAbilities.size() + " custom abilities"); } - /** - * Get the ability save directory. - */ private File getDir() { File dir = new File(CustomNpcs.getWorldSaveDirectory(), "abilities"); if (!dir.exists()) { @@ -90,26 +89,26 @@ public class AbilityController implements IAbilityHandler { } /** - * Save an ability preset to disk. - * - * @param ability The ability to save - * @return true if successful + * Save a custom ability preset. If the ability has no id, generates a UUID. + * Stores in customAbilities map and writes to disk. */ - public boolean saveAbility(Ability ability) { - if (ability == null || ability.getName() == null || ability.getName().isEmpty()) { - return false; + public boolean saveCustomAbility(Ability ability) { + if (ability == null) return false; + + String uuid = ability.getId(); + if (uuid == null || uuid.isEmpty()) { + uuid = UUID.randomUUID().toString(); + ability.setId(uuid); } File dir = getDir(); - String safeName = ability.getName().replaceAll("[^a-zA-Z0-9_-]", "_"); - File fileNew = new File(dir, safeName + ".json_new"); - File fileCurrent = new File(dir, safeName + ".json"); + File fileNew = new File(dir, uuid + ".json_new"); + File fileCurrent = new File(dir, uuid + ".json"); try { NBTTagCompound nbt = ability.writeNBT(); NBTJsonUtil.SaveFile(fileNew, nbt); - // Atomic rename if (fileCurrent.exists()) { fileCurrent.delete(); } @@ -118,76 +117,144 @@ public class AbilityController implements IAbilityHandler { fileNew.delete(); } - savedAbilities.put(ability.getName(), ability); - LogWriter.info("Saved ability preset: " + ability.getName()); + customAbilities.put(uuid, ability); + version++; + LogWriter.info("Saved custom ability: " + ability.getName() + " [" + uuid + "]"); + SyncController.syncAllCustomAbilities(); return true; } catch (Exception e) { - LogWriter.error("Error saving ability preset: " + ability.getName(), e); + LogWriter.error("Error saving custom ability: " + uuid, e); return false; } } /** - * Delete a saved ability preset. - * - * @param name The name of the ability to delete - * @return true if successful + * Delete a custom ability preset by UUID. */ - public boolean deleteAbility(String name) { - if (name == null || name.isEmpty()) { - return false; - } + public boolean deleteCustomAbility(String uuid) { + if (uuid == null || uuid.isEmpty()) return false; - Ability removed = savedAbilities.remove(name); - if (removed == null) { - return false; - } + Ability removed = customAbilities.remove(uuid); + if (removed == null) return false; File dir = getDir(); - String safeName = name.replaceAll("[^a-zA-Z0-9_-]", "_"); - File file = new File(dir, safeName + ".json"); + File file = new File(dir, uuid + ".json"); if (file.exists()) { file.delete(); } - LogWriter.info("Deleted ability preset: " + name); + version++; + LogWriter.info("Deleted custom ability: " + uuid); + SyncController.syncAllCustomAbilities(); return true; } + // ═══════════════════════════════════════════════════════════════════════════ + // BUILT-IN ABILITY REGISTRY + // ═══════════════════════════════════════════════════════════════════════════ + /** - * Get a saved ability preset by name. + * Register a pre-configured built-in ability by name. + * These are looked up by name in {@link #resolveAbility(String)}. * - * @param name The name of the ability - * @return A copy of the ability, or null if not found + * @param name Unique name (e.g., "Jump Attack", "Ki Blast") + * @param ability Pre-configured ability instance */ - public Ability getSavedAbility(String name) { - Ability saved = savedAbilities.get(name); - if (saved == null) { - return null; + public void registerAbility(String name, Ability ability) { + if (abilities.containsKey(name)) { + LogWriter.info("AbilityController: Overwriting built-in ability: " + name); } - // Return a copy so modifications don't affect the saved version - return fromNBT(saved.writeNBT()); + ability.setName(name); + abilities.put(name, ability); + LogWriter.info("Registered ability: " + name); + } + + // ═══════════════════════════════════════════════════════════════════════════ + // RESOLUTION (used by AbilitySlot, PlayerAbilityData, etc.) + // ═══════════════════════════════════════════════════════════════════════════ + + /** + * Resolve an ability by key. Checks built-in abilities first (by name), + * then custom abilities (by UUID). Returns a copy safe to modify. + * + * @param key The ability name (built-in) or UUID (custom) + * @return A copy of the resolved ability, or null if not found + */ + public Ability resolveAbility(String key) { + if (key == null || key.isEmpty()) return null; + + // Check built-in abilities first + Ability builtIn = abilities.get(key); + if (builtIn != null) { + return fromNBT(builtIn.writeNBT()); + } + + // Check custom abilities + Ability custom = customAbilities.get(key); + if (custom != null) { + return fromNBT(custom.writeNBT()); + } + + return null; } /** - * Check if a saved ability exists. + * Check if an ability key exists (either built-in or custom). */ - public boolean hasSavedAbility(String name) { - return savedAbilities.containsKey(name); + public boolean hasAbility(String key) { + return abilities.containsKey(key) || customAbilities.containsKey(key); } - /** - * Get all saved ability names. - */ - public Set getSavedAbilityNames() { - return new LinkedHashSet<>(savedAbilities.keySet()); + // ═══════════════════════════════════════════════════════════════════════════ + // ACCESSORS + // ═══════════════════════════════════════════════════════════════════════════ + + /** Get a built-in ability by name. Returns the stored instance (not a copy). */ + public Ability getAbility(String name) { + return abilities.get(name); } - /** - * Get all saved abilities. - */ - public Collection getSavedAbilities() { - return savedAbilities.values(); + /** Get all built-in ability names. */ + public Set getAbilityNames() { + return new LinkedHashSet<>(abilities.keySet()); + } + + /** Get a custom ability by UUID. Returns the stored instance (not a copy). */ + public Ability getCustomAbility(String uuid) { + return customAbilities.get(uuid); + } + + /** Check if a custom ability exists by UUID. */ + public boolean hasCustomAbility(String uuid) { + return customAbilities.containsKey(uuid); + } + + /** Get all custom ability UUIDs. */ + public Set getCustomAbilityIds() { + return new LinkedHashSet<>(customAbilities.keySet()); + } + + /** Get the display name for a custom ability by UUID. */ + public String getCustomAbilityName(String uuid) { + Ability a = customAbilities.get(uuid); + return a != null ? a.getName() : null; + } + + /** Get the custom abilities map (used by SyncController). */ + public Map getCustomAbilities() { + return customAbilities; + } + + /** Replace the custom abilities map (used by client-side sync). */ + public void setCustomAbilities(Map synced) { + customAbilities.clear(); + customAbilities.putAll(synced); + version++; + } + + /** Get the version counter (incremented on custom ability changes). */ + public int getVersion() { + return version; } // ═══════════════════════════════════════════════════════════════════════════ @@ -196,28 +263,37 @@ public class AbilityController implements IAbilityHandler { @Override public String[] getTypes() { - // Return the ability typeIds (lang keys), not the factory keys - return factoriesByTypeId.keySet().toArray(new String[0]); + return typeFactories.keySet().toArray(new String[0]); } @Override public boolean hasType(String typeId) { - return factories.containsKey(typeId) || factoriesByTypeId.containsKey(typeId); + return typeFactories.containsKey(typeId); } @Override - public String[] getSavedNames() { - return savedAbilities.keySet().toArray(new String[0]); + public String[] getAbilityNameArray() { + return abilities.keySet().toArray(new String[0]); } @Override - public boolean has(String name) { - return savedAbilities.containsKey(name); + public boolean hasAbilityName(String name) { + return abilities.containsKey(name); } @Override - public boolean delete(String name) { - return deleteAbility(name); + public String[] getCustomAbilityIdArray() { + return customAbilities.keySet().toArray(new String[0]); + } + + @Override + public boolean hasCustomAbilityId(String uuid) { + return customAbilities.containsKey(uuid); + } + + @Override + public boolean deleteCustomAbilityById(String uuid) { + return deleteCustomAbility(uuid); } // ═══════════════════════════════════════════════════════════════════════════ @@ -226,26 +302,23 @@ public class AbilityController implements IAbilityHandler { /** * Register a new ability type. - * Call during FMLInitializationEvent. + * Extracts the typeId from a temporary instance and stores in typeFactories. * - * @param factoryKey The factory key (e.g., "cnpc:slam", "mymod:custom_ability") + * @param factoryKey Unused legacy key — the typeId from the created instance is used * @param factory Factory lambda that creates a new instance */ public void registerType(String factoryKey, Supplier factory) { - if (factories.containsKey(factoryKey)) { - LogWriter.info("AbilityController: Overwriting existing ability type: " + factoryKey); - } - factories.put(factoryKey, factory); - - // Also register by the ability's actual typeId (lang key) Ability temp = factory.get(); - String abilityTypeId = temp.getTypeId(); - factoriesByTypeId.put(abilityTypeId, factory); + String typeId = temp.getTypeId(); + if (typeFactories.containsKey(typeId)) { + LogWriter.info("AbilityController: Overwriting type: " + typeId); + } + typeFactories.put(typeId, factory); } /** * Create an ability from NBT data. - * Uses the "typeId" field to determine which type to create. + * Uses the "typeId" field to look up in typeFactories. */ public Ability fromNBT(NBTTagCompound nbt) { if (nbt == null || !nbt.hasKey("typeId")) { @@ -253,11 +326,7 @@ public class AbilityController implements IAbilityHandler { } String typeId = nbt.getString("typeId"); - Supplier factory = factories.get(typeId); - if (factory == null) { - // Try looking up by ability typeId (lang key) - factory = factoriesByTypeId.get(typeId); - } + Supplier factory = typeFactories.get(typeId); if (factory == null) { LogWriter.info("AbilityController: Unknown ability type: " + typeId); return null; @@ -271,15 +340,10 @@ public class AbilityController implements IAbilityHandler { /** * Create a new empty ability of the given type. * - * @param factoryKey The factory key (e.g., "cnpc:slam") or the ability typeId (e.g., "ability.cnpc.slam") + * @param typeId The ability typeId (e.g., "ability.cnpc.slam") */ - public Ability create(String factoryKey) { - // Try direct lookup first (factory key) - Supplier factory = factories.get(factoryKey); - if (factory == null) { - // Try looking up by ability typeId - factory = factoriesByTypeId.get(factoryKey); - } + public Ability create(String typeId) { + Supplier factory = typeFactories.get(typeId); if (factory == null) { return null; } @@ -289,16 +353,22 @@ public class AbilityController implements IAbilityHandler { /** * Register built-in ability types. */ - private void registerBuiltins() { + private void registerBuiltinTypes() { // Melee/AOE damage abilities registerType("cnpc:slam", AbilitySlam::new); registerType("cnpc:heavy_hit", AbilityHeavyHit::new); registerType("cnpc:cutter", AbilityCutter::new); // Ranged/Projectile abilities - registerType("cnpc:beam", AbilityBeam::new); + registerType("cnpc:sweeper", AbilitySweeper::new); registerType("cnpc:projectile", AbilityProjectile::new); registerType("cnpc:orb", AbilityOrb::new); + registerType("cnpc:dual_orb", AbilityOrbDual::new); + registerType("cnpc:dual_disc", AbilityDisc::new); + registerType("cnpc:dual_disc", AbilityDiscDual::new); + registerType("cnpc:laser_shot", AbilityLaserShot::new); + registerType("cnpc:beam", AbilityEnergyBeam::new); + registerType("cnpc:dual_beam", AbilityBeamDual::new); // Movement abilities registerType("cnpc:charge", AbilityCharge::new); diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityEffect.java b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityEffect.java new file mode 100644 index 000000000..57f0cbf17 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityEffect.java @@ -0,0 +1,153 @@ +package kamkeel.npcs.controllers.data.ability; + +import net.minecraft.entity.EntityLivingBase; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.potion.Potion; +import net.minecraft.potion.PotionEffect; + +/** + * Represents a configurable potion effect that can be applied by abilities. + * Provides a unified system for managing debuffs like slowness, weakness, poison, etc. + */ +public class AbilityEffect { + + public enum EffectType { + NONE("gui.none", -1), + SLOWNESS("potion.moveSlowdown", Potion.moveSlowdown.id), + WEAKNESS("potion.weakness", Potion.weakness.id), + POISON("potion.poison", Potion.poison.id), + WITHER("potion.wither", Potion.wither.id), + BLINDNESS("potion.blindness", Potion.blindness.id), + NAUSEA("potion.confusion", Potion.confusion.id), + HUNGER("potion.hunger", Potion.hunger.id), + MINING_FATIGUE("potion.digSlowDown", Potion.digSlowdown.id); + + private final String langKey; + private final int potionId; + + EffectType(String langKey, int potionId) { + this.langKey = langKey; + this.potionId = potionId; + } + + public String getLangKey() { + return langKey; + } + + public int getPotionId() { + return potionId; + } + + public static String[] getLangKeys() { + EffectType[] types = values(); + String[] keys = new String[types.length]; + for (int i = 0; i < types.length; i++) keys[i] = types[i].langKey; + return keys; + } + + public static EffectType fromOrdinal(int ordinal) { + EffectType[] values = values(); + if (ordinal >= 0 && ordinal < values.length) { + return values[ordinal]; + } + return NONE; + } + } + + private EffectType type = EffectType.NONE; + private int durationTicks = 60; + private int amplifier = 0; // 0-10 + + public AbilityEffect() { + } + + public AbilityEffect(EffectType type, int durationTicks, int amplifier) { + this.type = type; + this.durationTicks = Math.max(1, durationTicks); + this.amplifier = Math.max(0, Math.min(10, amplifier)); + } + + /** + * Creates a copy of this effect. + */ + public AbilityEffect copy() { + return new AbilityEffect(type, durationTicks, amplifier); + } + + /** + * Applies this effect to the given entity. + * Does nothing if type is NONE. + */ + public void apply(EntityLivingBase entity) { + if (entity == null || type == EffectType.NONE) { + return; + } + + int potionId = type.getPotionId(); + if (potionId >= 0) { + entity.addPotionEffect(new PotionEffect(potionId, durationTicks, amplifier)); + } + } + + /** + * Writes this effect to NBT. + */ + public NBTTagCompound writeNBT() { + NBTTagCompound nbt = new NBTTagCompound(); + nbt.setInteger("type", type.ordinal()); + nbt.setInteger("duration", durationTicks); + nbt.setInteger("amplifier", amplifier); + return nbt; + } + + /** + * Reads this effect from NBT. + */ + public void readNBT(NBTTagCompound nbt) { + this.type = EffectType.fromOrdinal(nbt.getInteger("type")); + this.durationTicks = nbt.hasKey("duration") ? nbt.getInteger("duration") : 60; + this.amplifier = nbt.hasKey("amplifier") ? nbt.getInteger("amplifier") : 0; + } + + /** + * Creates an AbilityEffect from NBT. + */ + public static AbilityEffect fromNBT(NBTTagCompound nbt) { + AbilityEffect effect = new AbilityEffect(); + effect.readNBT(nbt); + return effect; + } + + // Getters & Setters + + public EffectType getType() { + return type; + } + + public void setType(EffectType type) { + this.type = type != null ? type : EffectType.NONE; + } + + public int getDurationTicks() { + return durationTicks; + } + + public void setDurationTicks(int durationTicks) { + this.durationTicks = Math.max(1, durationTicks); + } + + public int getAmplifier() { + return amplifier; + } + + public void setAmplifier(int amplifier) { + this.amplifier = Math.max(0, Math.min(10, amplifier)); + } + + /** + * Returns true if this effect is configured (not NONE). + */ + public boolean isValid() { + return type != EffectType.NONE; + } +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityPhase.java b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityPhase.java index 6338279ba..0611a0274 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityPhase.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityPhase.java @@ -4,5 +4,5 @@ public enum AbilityPhase { IDLE, // Not executing WINDUP, // Charging up (telegraph visible) ACTIVE, // Executing (damage happens) - RECOVERY // Finishing animation + DAZED // Stunned after interrupt during WINDUP (cannot attack) } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilitySlot.java b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilitySlot.java new file mode 100644 index 000000000..ca4e7471e --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilitySlot.java @@ -0,0 +1,124 @@ +package kamkeel.npcs.controllers.data.ability; + +import net.minecraft.nbt.NBTTagCompound; + +/** + * Wrapper that stores either an inline ability (full NBT) or a reference + * to a built-in/custom ability (by name or UUID). + *

+ * References are lazy-resolved via {@link AbilityController#resolveAbility(String)} + * and cached until the controller version changes. + */ +public class AbilitySlot { + + private Ability inlineAbility; + private String referenceId; + + private transient Ability cachedAbility; + private transient int cachedVersion = -1; + + private AbilitySlot() {} + + /** + * Create a slot with an inline (embedded) ability. + */ + public static AbilitySlot inline(Ability ability) { + AbilitySlot slot = new AbilitySlot(); + slot.inlineAbility = ability; + slot.referenceId = null; + return slot; + } + + /** + * Create a slot that references a built-in ability (by name) or custom ability (by UUID). + */ + public static AbilitySlot reference(String referenceId) { + AbilitySlot slot = new AbilitySlot(); + slot.inlineAbility = null; + slot.referenceId = referenceId; + return slot; + } + + public boolean isReference() { + return referenceId != null; + } + + public String getReferenceId() { + return referenceId; + } + + /** + * Get the resolved ability. For inline slots, returns the ability directly. + * For reference slots, lazily resolves from AbilityController and caches. + * Returns null if the reference cannot be resolved (broken ref). + */ + public Ability getAbility() { + if (!isReference()) { + return inlineAbility; + } + + // Lazy resolve with version-based cache invalidation + AbilityController controller = AbilityController.Instance; + if (controller == null) return null; + + int currentVersion = controller.getVersion(); + if (cachedAbility != null && cachedVersion == currentVersion) { + return cachedAbility; + } + + cachedAbility = controller.resolveAbility(referenceId); + cachedVersion = currentVersion; + return cachedAbility; + } + + /** + * Get the inline ability directly (null for reference slots). + */ + public Ability getInlineAbility() { + return inlineAbility; + } + + /** + * Convert this reference slot to an inline slot by copying the resolved ability. + * No-op if already inline. Returns false if resolution fails. + */ + public boolean convertToInline() { + if (!isReference()) return true; + + Ability resolved = getAbility(); + if (resolved == null) return false; + + // Deep copy via NBT round-trip + inlineAbility = AbilityController.Instance.fromNBT(resolved.writeNBT()); + referenceId = null; + cachedAbility = null; + cachedVersion = -1; + return true; + } + + // ═══════════════════════════════════════════════════════════════════ + // NBT + // ═══════════════════════════════════════════════════════════════════ + + public NBTTagCompound writeNBT() { + if (isReference()) { + NBTTagCompound nbt = new NBTTagCompound(); + nbt.setString("Reference", referenceId); + return nbt; + } + return inlineAbility != null ? inlineAbility.writeNBT() : new NBTTagCompound(); + } + + public static AbilitySlot fromNBT(NBTTagCompound nbt) { + if (nbt == null) return null; + + if (nbt.hasKey("Reference")) { + return reference(nbt.getString("Reference")); + } + + // Inline ability + Ability ability = AbilityController.Instance.fromNBT(nbt); + if (ability == null) return null; + return inline(ability); + } +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/AnchorPoint.java b/src/main/java/kamkeel/npcs/controllers/data/ability/AnchorPoint.java new file mode 100644 index 000000000..1c308493c --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/AnchorPoint.java @@ -0,0 +1,61 @@ +package kamkeel.npcs.controllers.data.ability; + +/** + * Defines anchor points for ability charging effects. + * Determines where the charging visual (orb, disc, etc.) appears relative to the entity. + */ +public enum AnchorPoint { + FRONT("Front"), // In front of entity face (default) + CENTER("Center"), // Entity center + RIGHT_HAND("Right Hand"), // Right arm/hand position + LEFT_HAND("Left Hand"), // Left arm/hand position + ABOVE_HEAD("Above Head"), // Above the entity + CHEST("Chest"); // Chest level, centered + + private final String displayName; + + AnchorPoint(String displayName) { + this.displayName = displayName; + } + + public String getDisplayName() { + return displayName; + } + + /** + * Get AnchorPoint from ordinal value. + * Returns FRONT if ordinal is out of bounds. + */ + public static AnchorPoint fromOrdinal(int ordinal) { + AnchorPoint[] vals = values(); + if (ordinal < 0 || ordinal >= vals.length) { + return FRONT; + } + return vals[ordinal]; + } + + /** + * Get all display names for GUI dropdowns. + */ + public static String[] getDisplayNames() { + AnchorPoint[] values = values(); + String[] names = new String[values.length]; + for (int i = 0; i < values.length; i++) { + names[i] = values[i].displayName; + } + return names; + } + + @Override + public String toString() { + switch (this) { + case FRONT: return "ability.anchor.front"; + case CENTER: return "ability.anchor.center"; + case RIGHT_HAND: return "ability.anchor.rightHand"; + case LEFT_HAND: return "ability.anchor.leftHand"; + case ABOVE_HEAD: return "ability.anchor.aboveHead"; + case CHEST: return "ability.anchor.chest"; + default: return name(); + } + } +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/BuiltInAbility.java b/src/main/java/kamkeel/npcs/controllers/data/ability/BuiltInAbility.java new file mode 100644 index 000000000..904fad0b9 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/BuiltInAbility.java @@ -0,0 +1,55 @@ +package kamkeel.npcs.controllers.data.ability; + +import net.minecraft.nbt.NBTTagCompound; + +/** + * Abstract base class for built-in (non-customizable) abilities. + * Built-in abilities are registered by key, have fixed configurations, + * and cannot be modified through the preset GUI system. + *

+ * Examples: simple utility abilities like Jump, Sprint, etc. + * Third-party mods can extend this class to create their own built-in abilities. + */ +public abstract class BuiltInAbility extends Ability { + + /** + * The registry key used to look up this ability (e.g., "cnpc:jump", "dbc:ki_blast"). + */ + private final String registryKey; + + public BuiltInAbility(String registryKey) { + this.registryKey = registryKey; + this.id = registryKey; + this.name = registryKey; + this.typeId = registryKey; + } + + /** + * Get the registry key for this built-in ability. + */ + public String getRegistryKey() { + return registryKey; + } + + /** + * Built-in abilities do not write type-specific NBT (they have fixed configs). + */ + @Override + public void writeTypeNBT(NBTTagCompound nbt) { + // No-op: built-in abilities have fixed configurations + } + + /** + * Built-in abilities do not read type-specific NBT. + */ + @Override + public void readTypeNBT(NBTTagCompound nbt) { + // No-op: built-in abilities have fixed configurations + } + + /** + * Get the duration of the active phase in ticks. + * Built-in abilities must define their own active duration. + */ + public abstract int getActiveDurationTicks(); +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/Condition.java b/src/main/java/kamkeel/npcs/controllers/data/ability/Condition.java index 8d078421a..1183fe858 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/Condition.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/Condition.java @@ -13,11 +13,19 @@ public interface Condition { /** * Check if this condition is met. * - * @param npc The NPC that would use the ability - * @param target The current target (may be null for self-targeting abilities) + * @param caster The entity that would use the ability (NPC or Player) + * @param target The current target (may be null for self-targeting abilities or players) * @return true if the condition is satisfied */ - boolean check(EntityNPCInterface npc, EntityLivingBase target); + boolean check(EntityLivingBase caster, EntityLivingBase target); + + /** + * Whether this condition requires a target entity to evaluate. + * Conditions that return true here will be skipped for player ability checks. + */ + default boolean requiresTarget() { + return false; + } /** * Get the condition type ID for serialization. @@ -75,7 +83,7 @@ public interface Condition { // ═══════════════════════════════════════════════════════════════════ /** - * Condition: NPC HP is above a percentage threshold. + * Condition: Caster HP is above a percentage threshold. */ class ConditionHPAbove implements Condition { private float threshold = 0.5f; // 50% @@ -88,8 +96,8 @@ public interface Condition { } @Override - public boolean check(EntityNPCInterface npc, EntityLivingBase target) { - return npc.getHealth() / npc.getMaxHealth() > threshold; + public boolean check(EntityLivingBase caster, EntityLivingBase target) { + return caster.getHealth() / caster.getMaxHealth() > threshold; } @Override @@ -112,7 +120,7 @@ public interface Condition { } /** - * Condition: NPC HP is below a percentage threshold. + * Condition: Caster HP is below a percentage threshold. */ class ConditionHPBelow implements Condition { private float threshold = 0.5f; @@ -125,8 +133,8 @@ public interface Condition { } @Override - public boolean check(EntityNPCInterface npc, EntityLivingBase target) { - return npc.getHealth() / npc.getMaxHealth() < threshold; + public boolean check(EntityLivingBase caster, EntityLivingBase target) { + return caster.getHealth() / caster.getMaxHealth() < threshold; } @Override @@ -150,6 +158,7 @@ public interface Condition { /** * Condition: Target HP is above a percentage threshold. + * Requires a target - will be skipped for player ability checks. */ class ConditionTargetHPAbove implements Condition { private float threshold = 0.5f; @@ -162,11 +171,16 @@ public interface Condition { } @Override - public boolean check(EntityNPCInterface npc, EntityLivingBase target) { + public boolean check(EntityLivingBase caster, EntityLivingBase target) { if (target == null) return false; return target.getHealth() / target.getMaxHealth() > threshold; } + @Override + public boolean requiresTarget() { + return true; + } + @Override public String getTypeId() { return "target_hp_above"; @@ -188,6 +202,7 @@ public interface Condition { /** * Condition: Target HP is below a percentage threshold. + * Requires a target - will be skipped for player ability checks. */ class ConditionTargetHPBelow implements Condition { private float threshold = 0.5f; @@ -200,11 +215,16 @@ public interface Condition { } @Override - public boolean check(EntityNPCInterface npc, EntityLivingBase target) { + public boolean check(EntityLivingBase caster, EntityLivingBase target) { if (target == null) return false; return target.getHealth() / target.getMaxHealth() < threshold; } + @Override + public boolean requiresTarget() { + return true; + } + @Override public String getTypeId() { return "target_hp_below"; @@ -225,8 +245,9 @@ public interface Condition { } /** - * Condition: NPC has been hit X times within a time window. + * Condition: Caster has been hit X times within a time window. * Useful for reactive abilities that trigger after receiving multiple hits. + * Currently only works for NPCs (requires DataAbilities hit tracking). */ class ConditionHitCount implements Condition { private int requiredHits = 3; @@ -241,11 +262,15 @@ public interface Condition { } @Override - public boolean check(EntityNPCInterface npc, EntityLivingBase target) { + public boolean check(EntityLivingBase caster, EntityLivingBase target) { // Check NPC's recent hit count from DataAbilities - if (npc.abilities != null) { - return npc.abilities.getRecentHitCount(withinTicks) >= requiredHits; + if (caster instanceof EntityNPCInterface) { + EntityNPCInterface npc = (EntityNPCInterface) caster; + if (npc.abilities != null) { + return npc.abilities.getRecentHitCount(withinTicks) >= requiredHits; + } } + // For players, hit count tracking is not yet supported return false; } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/LockMovementType.java b/src/main/java/kamkeel/npcs/controllers/data/ability/LockMovementType.java new file mode 100644 index 000000000..1b81a3bb3 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/LockMovementType.java @@ -0,0 +1,83 @@ +package kamkeel.npcs.controllers.data.ability; + +/** + * Defines when an ability locks NPC movement and rotation. + */ +public enum LockMovementType { + /** + * No movement lock - NPC can move freely during both phases. + */ + NO, + + /** + * Lock movement during WINDUP phase only. + * NPC can move during ACTIVE phase. + */ + WINDUP, + + /** + * Lock movement during ACTIVE phase only. + * NPC can move during WINDUP phase. + */ + ACTIVE, + + /** + * Lock movement during both WINDUP and ACTIVE phases. + */ + WINDUP_AND_ACTIVE; + + /** + * Check if movement should be locked during WINDUP phase. + */ + public boolean locksWindup() { + return this == WINDUP || this == WINDUP_AND_ACTIVE; + } + + /** + * Check if movement should be locked during ACTIVE phase. + */ + public boolean locksActive() { + return this == ACTIVE || this == WINDUP_AND_ACTIVE; + } + + /** + * Get display name for GUI. + */ + public String getDisplayKey() { + switch (this) { + case NO: + return "gui.no"; + case WINDUP: + return "ability.lockMove.windup"; + case ACTIVE: + return "ability.lockMove.active"; + case WINDUP_AND_ACTIVE: + return "ability.lockMove.both"; + default: + return "gui.no"; + } + } + + /** + * Get all display keys for GUI dropdown. + */ + public static String[] getDisplayKeys() { + return new String[]{ + "gui.no", + "ability.lockMove.windup", + "ability.lockMove.active", + "ability.lockMove.both" + }; + } + + /** + * Get LockMovementType from ordinal, with fallback to WINDUP. + */ + public static LockMovementType fromOrdinal(int ordinal) { + LockMovementType[] values = values(); + if (ordinal >= 0 && ordinal < values.length) { + return values[ordinal]; + } + return WINDUP; + } +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/TargetingMode.java b/src/main/java/kamkeel/npcs/controllers/data/ability/TargetingMode.java index 7a2e43b64..9bdbdac6f 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/TargetingMode.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/TargetingMode.java @@ -22,20 +22,16 @@ public enum TargetingMode { /** * AOE centered on target */ - AOE_TARGET, + AOE_TARGET; - /** - * Random nearby enemy - */ - RANDOM_ENEMY, - - /** - * Lowest HP enemy in range - */ - LOWEST_HP, - - /** - * Highest threat enemy - */ - HIGHEST_THREAT + @Override + public String toString() { + switch (this) { + case AGGRO_TARGET: return "ability.target.aggro_target"; + case SELF: return "ability.target.self"; + case AOE_SELF: return "ability.target.aoe_self"; + case AOE_TARGET: return "ability.target.aoe_target"; + default: return name(); + } + } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/UserType.java b/src/main/java/kamkeel/npcs/controllers/data/ability/UserType.java new file mode 100644 index 000000000..d68693f59 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/UserType.java @@ -0,0 +1,26 @@ +package kamkeel.npcs.controllers.data.ability; + +/** + * Defines which entity types can use an ability. + */ +public enum UserType { + NPC_ONLY, + PLAYER_ONLY, + BOTH; + + public boolean allowsNpc() { + return this == NPC_ONLY || this == BOTH; + } + + public boolean allowsPlayer() { + return this == PLAYER_ONLY || this == BOTH; + } + + public static UserType fromOrdinal(int ordinal) { + UserType[] values = values(); + if (ordinal >= 0 && ordinal < values.length) { + return values[ordinal]; + } + return BOTH; + } +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyAnchorData.java b/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyAnchorData.java new file mode 100644 index 000000000..66983f89b --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyAnchorData.java @@ -0,0 +1,54 @@ +package kamkeel.npcs.controllers.data.ability.data; + +import kamkeel.npcs.controllers.data.ability.AnchorPoint; +import net.minecraft.nbt.NBTTagCompound; + +public class EnergyAnchorData { + public AnchorPoint anchorPoint = AnchorPoint.FRONT; + public float anchorOffsetX = 0; + public float anchorOffsetY = 0; + public float anchorOffsetZ = 0; + + public EnergyAnchorData() { + } + + public EnergyAnchorData(AnchorPoint anchorPoint) { + this.anchorPoint = anchorPoint; + } + + public EnergyAnchorData(AnchorPoint anchorPoint, float anchorOffsetX, float anchorOffsetY, float anchorOffsetZ) { + this.anchorPoint = anchorPoint; + this.anchorOffsetX = anchorOffsetX; + this.anchorOffsetY = anchorOffsetY; + this.anchorOffsetZ = anchorOffsetZ; + } + + public EnergyAnchorData(int anchorPoint) { + this.anchorPoint = AnchorPoint.fromOrdinal(anchorPoint); + } + + public EnergyAnchorData(int anchorPoint, float anchorOffsetX, float anchorOffsetY, float anchorOffsetZ) { + this.anchorPoint = AnchorPoint.fromOrdinal(anchorPoint); + this.anchorOffsetX = anchorOffsetX; + this.anchorOffsetY = anchorOffsetY; + this.anchorOffsetZ = anchorOffsetZ; + } + + public void writeNBT(NBTTagCompound nbt) { + nbt.setInteger("anchorPoint", anchorPoint.ordinal()); + nbt.setFloat("anchorOffsetX", anchorOffsetX); + nbt.setFloat("anchorOffsetY", anchorOffsetY); + nbt.setFloat("anchorOffsetZ", anchorOffsetZ); + } + + public void readNBT(NBTTagCompound nbt) { + this.anchorPoint = nbt.hasKey("anchorPoint") ? AnchorPoint.fromOrdinal(nbt.getInteger("anchorPoint")) : AnchorPoint.FRONT; + this.anchorOffsetX = nbt.hasKey("anchorOffsetX") ? nbt.getFloat("anchorOffsetX") : 0; + this.anchorOffsetY = nbt.hasKey("anchorOffsetY") ? nbt.getFloat("anchorOffsetY") : 0; + this.anchorOffsetZ = nbt.hasKey("anchorOffsetZ") ? nbt.getFloat("anchorOffsetZ") : 0; + } + + public EnergyAnchorData copy() { + return new EnergyAnchorData(anchorPoint, anchorOffsetX, anchorOffsetY, anchorOffsetZ); + } +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyColorData.java b/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyColorData.java new file mode 100644 index 000000000..4bd841742 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyColorData.java @@ -0,0 +1,50 @@ +package kamkeel.npcs.controllers.data.ability.data; + +import net.minecraft.nbt.NBTTagCompound; + +/** + * Groups visual color properties shared by all energy abilities. + * Used as a parameter object for entity constructors and ability configuration. + */ +public class EnergyColorData { + public int innerColor = 0xFFFFFF; + public int outerColor = 0x8888FF; + public boolean outerColorEnabled = true; + public float outerColorWidth = 0.4f; + public float outerColorAlpha = 0.5f; + public float rotationSpeed = 4.0f; + + public EnergyColorData() {} + + public EnergyColorData(int innerColor, int outerColor, boolean outerColorEnabled, + float outerColorWidth, float outerColorAlpha, float rotationSpeed) { + this.innerColor = innerColor; + this.outerColor = outerColor; + this.outerColorEnabled = outerColorEnabled; + this.outerColorWidth = outerColorWidth; + this.outerColorAlpha = outerColorAlpha; + this.rotationSpeed = rotationSpeed; + } + + public void writeNBT(NBTTagCompound nbt) { + nbt.setInteger("innerColor", innerColor); + nbt.setInteger("outerColor", outerColor); + nbt.setBoolean("outerColorEnabled", outerColorEnabled); + nbt.setFloat("outerColorWidth", outerColorWidth); + nbt.setFloat("outerColorAlpha", outerColorAlpha); + nbt.setFloat("rotationSpeed", rotationSpeed); + } + + public void readNBT(NBTTagCompound nbt) { + innerColor = nbt.hasKey("innerColor") ? nbt.getInteger("innerColor") : 0xFFFFFF; + outerColor = nbt.hasKey("outerColor") ? nbt.getInteger("outerColor") : 0x8888FF; + outerColorEnabled = !nbt.hasKey("outerColorEnabled") || nbt.getBoolean("outerColorEnabled"); + outerColorWidth = nbt.hasKey("outerColorWidth") ? nbt.getFloat("outerColorWidth") : 0.4f; + outerColorAlpha = nbt.hasKey("outerColorAlpha") ? nbt.getFloat("outerColorAlpha") : 0.5f; + rotationSpeed = nbt.hasKey("rotationSpeed") ? nbt.getFloat("rotationSpeed") : 4.0f; + } + + public EnergyColorData copy() { + return new EnergyColorData(innerColor, outerColor, outerColorEnabled, outerColorWidth, outerColorAlpha, rotationSpeed); + } +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyCombatData.java b/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyCombatData.java new file mode 100644 index 000000000..fa09645a6 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyCombatData.java @@ -0,0 +1,51 @@ +package kamkeel.npcs.controllers.data.ability.data; + +import net.minecraft.nbt.NBTTagCompound; + +/** + * Groups combat properties shared by energy projectile abilities. + * Used as a parameter object for entity constructors and ability configuration. + */ +public class EnergyCombatData { + public float damage = 7.0f; + public float knockback = 1.0f; + public float knockbackUp = 0.1f; + public boolean explosive = false; + public float explosionRadius = 3.0f; + public float explosionDamageFalloff = 0.5f; + + public EnergyCombatData() {} + + public EnergyCombatData(float damage, float knockback, float knockbackUp, + boolean explosive, float explosionRadius, float explosionDamageFalloff) { + this.damage = damage; + this.knockback = knockback; + this.knockbackUp = knockbackUp; + this.explosive = explosive; + this.explosionRadius = explosionRadius; + this.explosionDamageFalloff = explosionDamageFalloff; + } + + public void writeNBT(NBTTagCompound nbt) { + nbt.setFloat("damage", damage); + nbt.setFloat("knockback", knockback); + nbt.setFloat("knockbackUp", knockbackUp); + nbt.setBoolean("explosive", explosive); + nbt.setFloat("explosionRadius", explosionRadius); + nbt.setFloat("explosionDamageFalloff", explosionDamageFalloff); + } + + public void readNBT(NBTTagCompound nbt) { + damage = nbt.hasKey("damage") ? nbt.getFloat("damage") : 7.0f; + knockback = nbt.hasKey("knockback") ? nbt.getFloat("knockback") : 1.0f; + knockbackUp = nbt.hasKey("knockbackUp") ? nbt.getFloat("knockbackUp") : 0.1f; + explosive = nbt.hasKey("explosive") && nbt.getBoolean("explosive"); + explosionRadius = nbt.hasKey("explosionRadius") ? nbt.getFloat("explosionRadius") : 3.0f; + explosionDamageFalloff = nbt.hasKey("explosionDamageFalloff") ? nbt.getFloat("explosionDamageFalloff") : 0.5f; + } + + public EnergyCombatData copy() { + return new EnergyCombatData(damage, knockback, knockbackUp, + explosive, explosionRadius, explosionDamageFalloff); + } +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyHomingData.java b/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyHomingData.java new file mode 100644 index 000000000..5b90bafce --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyHomingData.java @@ -0,0 +1,41 @@ +package kamkeel.npcs.controllers.data.ability.data; + +import net.minecraft.nbt.NBTTagCompound; + +/** + * Groups homing/movement properties for energy projectile abilities. + * Used by Orb, Disc, and Beam (not Laser or Sweeper). + */ +public class EnergyHomingData { + public float speed = 0.5f; + public boolean homing = true; + public float homingStrength = 0.15f; + public float homingRange = 20.0f; + + public EnergyHomingData() {} + + public EnergyHomingData(float speed, boolean homing, float homingStrength, float homingRange) { + this.speed = speed; + this.homing = homing; + this.homingStrength = homingStrength; + this.homingRange = homingRange; + } + + public void writeNBT(NBTTagCompound nbt) { + nbt.setFloat("speed", speed); + nbt.setBoolean("homing", homing); + nbt.setFloat("homingStrength", homingStrength); + nbt.setFloat("homingRange", homingRange); + } + + public void readNBT(NBTTagCompound nbt) { + speed = nbt.hasKey("speed") ? nbt.getFloat("speed") : 0.5f; + homing = !nbt.hasKey("homing") || nbt.getBoolean("homing"); + homingStrength = nbt.hasKey("homingStrength") ? nbt.getFloat("homingStrength") : 0.15f; + homingRange = nbt.hasKey("homingRange") ? nbt.getFloat("homingRange") : 20.0f; + } + + public EnergyHomingData copy() { + return new EnergyHomingData(speed, homing, homingStrength, homingRange); + } +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyLifespanData.java b/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyLifespanData.java new file mode 100644 index 000000000..f8f0122e8 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyLifespanData.java @@ -0,0 +1,33 @@ +package kamkeel.npcs.controllers.data.ability.data; + +import net.minecraft.nbt.NBTTagCompound; + +/** + * Groups lifespan/range properties for energy projectile abilities. + * Used as a parameter object for entity constructors and ability configuration. + */ +public class EnergyLifespanData { + public float maxDistance = 30.0f; + public int maxLifetime = 200; + + public EnergyLifespanData() {} + + public EnergyLifespanData(float maxDistance, int maxLifetime) { + this.maxDistance = maxDistance; + this.maxLifetime = maxLifetime; + } + + public void writeNBT(NBTTagCompound nbt) { + nbt.setFloat("maxDistance", maxDistance); + nbt.setInteger("maxLifetime", maxLifetime); + } + + public void readNBT(NBTTagCompound nbt) { + maxDistance = nbt.hasKey("maxDistance") ? nbt.getFloat("maxDistance") : 30.0f; + maxLifetime = nbt.hasKey("maxLifetime") ? nbt.getInteger("maxLifetime") : 200; + } + + public EnergyLifespanData copy() { + return new EnergyLifespanData(maxDistance, maxLifetime); + } +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyLightningData.java b/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyLightningData.java new file mode 100644 index 000000000..30d4ea6d8 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyLightningData.java @@ -0,0 +1,42 @@ +package kamkeel.npcs.controllers.data.ability.data; + +import net.minecraft.nbt.NBTTagCompound; + +/** + * Groups lightning visual effect properties for energy abilities. + * Used as a parameter object for entity constructors and ability configuration. + */ +public class EnergyLightningData { + public boolean lightningEffect = false; + public float lightningDensity = 0.15f; + public float lightningRadius = 0.5f; + public int lightningFadeTime = 6; + + public EnergyLightningData() {} + + public EnergyLightningData(boolean lightningEffect, float lightningDensity, + float lightningRadius, int lightningFadeTime) { + this.lightningEffect = lightningEffect; + this.lightningDensity = lightningDensity; + this.lightningRadius = lightningRadius; + this.lightningFadeTime = lightningFadeTime; + } + + public void writeNBT(NBTTagCompound nbt) { + nbt.setBoolean("lightningEffect", lightningEffect); + nbt.setFloat("lightningDensity", lightningDensity); + nbt.setFloat("lightningRadius", lightningRadius); + nbt.setInteger("lightningFadeTime", lightningFadeTime); + } + + public void readNBT(NBTTagCompound nbt) { + lightningEffect = nbt.hasKey("lightningEffect") && nbt.getBoolean("lightningEffect"); + lightningDensity = nbt.hasKey("lightningDensity") ? nbt.getFloat("lightningDensity") : 0.15f; + lightningRadius = nbt.hasKey("lightningRadius") ? nbt.getFloat("lightningRadius") : 0.5f; + lightningFadeTime = nbt.hasKey("lightningFadeTime") ? nbt.getInteger("lightningFadeTime") : 6; + } + + public EnergyLightningData copy() { + return new EnergyLightningData(lightningEffect, lightningDensity, lightningRadius, lightningFadeTime); + } +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/gui/AbilityFieldBuilder.java b/src/main/java/kamkeel/npcs/controllers/data/ability/gui/AbilityFieldBuilder.java new file mode 100644 index 000000000..a044e1cf4 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/gui/AbilityFieldBuilder.java @@ -0,0 +1,115 @@ +package kamkeel.npcs.controllers.data.ability.gui; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import kamkeel.npcs.controllers.data.ability.AbilityEffect; +import net.minecraft.client.gui.FontRenderer; +import noppes.npcs.client.gui.builder.FieldDef; +import noppes.npcs.client.gui.builder.FieldType; +import noppes.npcs.client.gui.builder.GuiFieldBuilder; +import noppes.npcs.client.gui.util.GuiNPCInterface; +import noppes.npcs.client.gui.util.GuiNpcButton; +import noppes.npcs.client.gui.util.GuiNpcLabel; +import noppes.npcs.client.gui.util.GuiNpcTextField; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Ability-specific extension of {@link GuiFieldBuilder} that handles + * the EFFECTS_LIST field type. + */ +@SideOnly(Side.CLIENT) +public class AbilityFieldBuilder extends GuiFieldBuilder { + + // Effects list metadata: widgetId -> [effectIndex, action] + // action: 0=type, 1=duration(text), 2=amp, 3=delete, 4=add + private final Map effectWidgetMeta = new HashMap<>(); + + public AbilityFieldBuilder(GuiNPCInterface parent, FontRenderer fontRenderer) { + super(parent, fontRenderer); + } + + @Override + protected int build(List fields) { + effectWidgetMeta.clear(); + return super.build(fields); + } + + @Override + protected int buildField(FieldDef def, int y, List fields, int index) { + if (def.getType() == FieldType.EFFECTS_LIST) { + return renderEffectsList(def, y); + } + return -1; // not handled + } + + @SuppressWarnings("unchecked") + private int renderEffectsList(FieldDef def, int y) { + // Section header + y += 3; + sw.addLabel(new GuiNpcLabel(labelId++, def.getLabel(), colLLabel, y + 2, 0xFFFF55)); + y += 15; + + List effects = (List) def.getValue(); + if (effects == null) effects = new ArrayList<>(); + + String[] typeNames = AbilityEffect.EffectType.getLangKeys(); + String[] ampValues = {"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10"}; + + for (int e = 0; e < effects.size() && e < 5; e++) { + AbilityEffect effect = effects.get(e); + + // Type selector button + int typeIdx = effect.getType().ordinal(); + GuiNpcButton typeBtn = new GuiNpcButton(widgetId, colLLabel, y, 100, 20, typeNames, typeIdx); + sw.addButton(typeBtn); + buttonFieldMap.put(widgetId, def); + effectWidgetMeta.put(widgetId, new int[]{e, 0}); + widgetId++; + + // Duration text field + GuiNpcTextField durField = new GuiNpcTextField(widgetId, parent, fontRenderer, + colLLabel + 104, y, 50, 20, String.valueOf(effect.getDurationTicks())); + durField.setIntegersOnly(); + durField.setMinMaxDefault(1, 12000, 60); + sw.addTextField(durField); + textFieldMap.put(widgetId, def); + effectWidgetMeta.put(widgetId, new int[]{e, 1}); + widgetId++; + + // Amplifier selector (0-10) + GuiNpcButton ampBtn = new GuiNpcButton(widgetId, colLLabel + 158, y, 40, 20, ampValues, effect.getAmplifier()); + sw.addButton(ampBtn); + buttonFieldMap.put(widgetId, def); + effectWidgetMeta.put(widgetId, new int[]{e, 2}); + widgetId++; + + // Delete button + GuiNpcButton delBtn = new GuiNpcButton(clearId, colLLabel + 202, y, 20, 20, "X"); + sw.addButton(delBtn); + clearFieldMap.put(clearId, def); + effectWidgetMeta.put(clearId, new int[]{e, 3}); + clearId++; + + y += rowHeight; + } + + // Add button (if < 5 effects) + if (effects.size() < 5) { + GuiNpcButton addBtn = new GuiNpcButton(widgetId, colLLabel, y, 50, 20, "gui.add"); + sw.addButton(addBtn); + buttonFieldMap.put(widgetId, def); + effectWidgetMeta.put(widgetId, new int[]{effects.size(), 4}); + widgetId++; + clearId++; + y += rowHeight; + } + + return y; + } + + public Map getEffectWidgetMeta() { return effectWidgetMeta; } +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/gui/AbilityFieldDefs.java b/src/main/java/kamkeel/npcs/controllers/data/ability/gui/AbilityFieldDefs.java new file mode 100644 index 000000000..bc1dc9ab8 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/gui/AbilityFieldDefs.java @@ -0,0 +1,26 @@ +package kamkeel.npcs.controllers.data.ability.gui; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import kamkeel.npcs.controllers.data.ability.AbilityEffect; +import noppes.npcs.client.gui.builder.FieldDef; +import noppes.npcs.client.gui.builder.FieldType; + +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Supplier; + +/** + * Ability-specific FieldDef factories. These create FieldDef instances + * with types that require ability-specific rendering (e.g. EFFECTS_LIST). + */ +@SideOnly(Side.CLIENT) +public class AbilityFieldDefs { + + @SuppressWarnings("unchecked") + public static FieldDef effectsListField(String label, Supplier> getter, Consumer> setter) { + return FieldDef.custom(label, FieldType.EFFECTS_LIST, + () -> getter.get(), + v -> setter.accept((List) v)); + } +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeam.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeam.java deleted file mode 100644 index 3c448d0b6..000000000 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeam.java +++ /dev/null @@ -1,335 +0,0 @@ -package kamkeel.npcs.controllers.data.ability.type; - -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.TargetingMode; -import kamkeel.npcs.controllers.data.telegraph.TelegraphType; -import net.minecraft.entity.Entity; -import net.minecraft.entity.EntityLivingBase; -import net.minecraft.nbt.NBTTagCompound; -import net.minecraft.util.AxisAlignedBB; -import net.minecraft.world.World; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityBeam; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; -import noppes.npcs.entity.EntityNPCInterface; - -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -/** - * Beam ability: Continuous line attack that damages all targets in a line. - * Can sweep across an arc or remain fixed. - */ -public class AbilityBeam extends Ability { - - // Type-specific parameters - private float beamLength = 10.0f; - private float beamWidth = 1.0f; - private float damage = 5.0f; - private int damageInterval = 5; - private boolean piercing = true; - private float sweepAngle = 0.0f; - private float sweepSpeed = 2.0f; - private boolean sweepBackAndForth = true; - private boolean lockOnTarget = false; - - // Runtime state (transient) - private transient float currentSweepAngle = 0; - private transient boolean sweepingRight = true; - private transient int ticksSinceDamage = 0; - private transient Set hitThisTick = new HashSet<>(); - - public AbilityBeam() { - this.typeId = "ability.cnpc.beam"; - this.name = "Beam"; - this.targetingMode = TargetingMode.AGGRO_TARGET; - this.maxRange = 15.0f; - this.minRange = 0.0f; - this.lockMovement = true; - this.cooldownTicks = 200; - this.windUpTicks = 60; - this.activeTicks = 80; - this.recoveryTicks = 20; - // No telegraph for beam - it's a continuous line attack - this.telegraphType = TelegraphType.NONE; - this.showTelegraph = false; - } - - @Override - public boolean hasTypeSettings() { - return true; - } - - @Override - @SideOnly(Side.CLIENT) - public SubGuiAbilityConfig createConfigGui( - IAbilityConfigCallback callback) { - return new SubGuiAbilityBeam(this, callback); - } - - @Override - public boolean isTargetingModeLocked() { - return true; - } - - @Override - public TargetingMode[] getAllowedTargetingModes() { - return new TargetingMode[]{TargetingMode.AGGRO_TARGET}; - } - - @Override - public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) { - // Initialize beam - currentSweepAngle = -sweepAngle / 2; - sweepingRight = true; - ticksSinceDamage = damageInterval; // Deal damage immediately - hitThisTick.clear(); - } - - @Override - public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { - if (world.isRemote) return; - - hitThisTick.clear(); - ticksSinceDamage++; - - // Update sweep angle - if (sweepAngle > 0) { - if (sweepBackAndForth) { - if (sweepingRight) { - currentSweepAngle += sweepSpeed; - if (currentSweepAngle >= sweepAngle / 2) { - sweepingRight = false; - } - } else { - currentSweepAngle -= sweepSpeed; - if (currentSweepAngle <= -sweepAngle / 2) { - sweepingRight = true; - } - } - } else { - currentSweepAngle += sweepSpeed; - if (currentSweepAngle >= sweepAngle) { - currentSweepAngle = 0; - } - } - } - - // Calculate beam direction - float baseYaw = npc.rotationYaw; - - // If lock on target, point at target - if (lockOnTarget && target != null) { - double dx = target.posX - npc.posX; - double dz = target.posZ - npc.posZ; - baseYaw = (float) Math.toDegrees(Math.atan2(-dx, dz)); - } - - float beamYaw = baseYaw + currentSweepAngle; - float yawRad = (float) Math.toRadians(beamYaw); - - // Calculate beam start position - double startX = npc.posX; - double startY = npc.posY + npc.getEyeHeight() * 0.8; - double startZ = npc.posZ; - - double dirX = -Math.sin(yawRad); - double dirZ = Math.cos(yawRad); - - // Check for entities along the beam - if (ticksSinceDamage >= damageInterval) { - ticksSinceDamage = 0; - - // Sample points along the beam - int samples = (int) (beamLength / 0.5); - for (int i = 1; i <= samples; i++) { - double progress = (double) i / samples; - double checkX = startX + dirX * beamLength * progress; - double checkY = startY; - double checkZ = startZ + dirZ * beamLength * progress; - - // Find entities at this point - AxisAlignedBB checkBox = AxisAlignedBB.getBoundingBox( - checkX - beamWidth, checkY - 1, checkZ - beamWidth, - checkX + beamWidth, checkY + 2, checkZ + beamWidth - ); - - @SuppressWarnings("unchecked") - List entities = world.getEntitiesWithinAABB(EntityLivingBase.class, checkBox); - - for (Entity entity : entities) { - if (!(entity instanceof EntityLivingBase)) continue; - if (entity == npc) continue; - if (hitThisTick.contains(entity.getEntityId())) continue; - - EntityLivingBase livingEntity = (EntityLivingBase) entity; - - // Check if actually in beam path - if (isInBeamPath(livingEntity, startX, startY, startZ, dirX, dirZ)) { - hitThisTick.add(entity.getEntityId()); - // Apply damage with scripted event support (no knockback for beam) - applyAbilityDamage(npc, livingEntity, damage, 0); - - if (!piercing) { - return; // Stop at first hit - } - } - } - } - } - - // Spawn beam particles - spawnBeamParticles(world, startX, startY, startZ, dirX, dirZ); - } - - private boolean isInBeamPath(EntityLivingBase entity, double startX, double startY, double startZ, - double dirX, double dirZ) { - // Project entity position onto beam line - double entityX = entity.posX - startX; - double entityZ = entity.posZ - startZ; - - // Dot product to find projection distance - double projectionDist = entityX * dirX + entityZ * dirZ; - - // Check if within beam length - if (projectionDist < 0 || projectionDist > beamLength) { - return false; - } - - // Calculate perpendicular distance - double projX = dirX * projectionDist; - double projZ = dirZ * projectionDist; - double perpX = entityX - projX; - double perpZ = entityZ - projZ; - double perpDist = Math.sqrt(perpX * perpX + perpZ * perpZ); - - // Check if within beam width - return perpDist <= beamWidth + entity.width / 2; - } - - private void spawnBeamParticles(World world, double startX, double startY, double startZ, - double dirX, double dirZ) { - // Spawn particles along beam line - for (int i = 0; i < beamLength; i++) { - double px = startX + dirX * i; - double pz = startZ + dirZ * i; - world.spawnParticle("flame", px, startY, pz, 0, 0, 0); - } - } - - @Override - public void reset() { - super.reset(); - currentSweepAngle = 0; - sweepingRight = true; - ticksSinceDamage = 0; - hitThisTick.clear(); - } - - @Override - public float getTelegraphRadius() { - return beamLength; - } - - @Override - public void writeTypeNBT(NBTTagCompound nbt) { - nbt.setFloat("beamLength", beamLength); - nbt.setFloat("beamWidth", beamWidth); - nbt.setFloat("damage", damage); - nbt.setInteger("damageInterval", damageInterval); - nbt.setBoolean("piercing", piercing); - nbt.setFloat("sweepAngle", sweepAngle); - nbt.setFloat("sweepSpeed", sweepSpeed); - nbt.setBoolean("sweepBackAndForth", sweepBackAndForth); - nbt.setBoolean("lockOnTarget", lockOnTarget); - } - - @Override - public void readTypeNBT(NBTTagCompound nbt) { - this.beamLength = nbt.hasKey("beamLength") ? nbt.getFloat("beamLength") : 10.0f; - this.beamWidth = nbt.hasKey("beamWidth") ? nbt.getFloat("beamWidth") : 1.0f; - this.damage = nbt.hasKey("damage") ? nbt.getFloat("damage") : 5.0f; - this.damageInterval = nbt.hasKey("damageInterval") ? nbt.getInteger("damageInterval") : 5; - this.piercing = !nbt.hasKey("piercing") || nbt.getBoolean("piercing"); - this.sweepAngle = nbt.hasKey("sweepAngle") ? nbt.getFloat("sweepAngle") : 0.0f; - this.sweepSpeed = nbt.hasKey("sweepSpeed") ? nbt.getFloat("sweepSpeed") : 2.0f; - this.sweepBackAndForth = !nbt.hasKey("sweepBackAndForth") || nbt.getBoolean("sweepBackAndForth"); - this.lockOnTarget = nbt.hasKey("lockOnTarget") && nbt.getBoolean("lockOnTarget"); - } - - // Getters & Setters - public float getBeamLength() { - return beamLength; - } - - public void setBeamLength(float beamLength) { - this.beamLength = beamLength; - } - - public float getBeamWidth() { - return beamWidth; - } - - public void setBeamWidth(float beamWidth) { - this.beamWidth = beamWidth; - } - - public float getDamage() { - return damage; - } - - public void setDamage(float damage) { - this.damage = damage; - } - - public int getDamageInterval() { - return damageInterval; - } - - public void setDamageInterval(int damageInterval) { - this.damageInterval = damageInterval; - } - - public boolean isPiercing() { - return piercing; - } - - public void setPiercing(boolean piercing) { - this.piercing = piercing; - } - - public float getSweepAngle() { - return sweepAngle; - } - - public void setSweepAngle(float sweepAngle) { - this.sweepAngle = sweepAngle; - } - - public float getSweepSpeed() { - return sweepSpeed; - } - - public void setSweepSpeed(float sweepSpeed) { - this.sweepSpeed = sweepSpeed; - } - - public boolean isSweepBackAndForth() { - return sweepBackAndForth; - } - - public void setSweepBackAndForth(boolean sweepBackAndForth) { - this.sweepBackAndForth = sweepBackAndForth; - } - - public boolean isLockOnTarget() { - return lockOnTarget; - } - - public void setLockOnTarget(boolean lockOnTarget) { - this.lockOnTarget = lockOnTarget; - } -} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeamDual.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeamDual.java new file mode 100644 index 000000000..690930987 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeamDual.java @@ -0,0 +1,490 @@ +package kamkeel.npcs.controllers.data.ability.type; + +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.AnchorPoint; +import kamkeel.npcs.controllers.data.ability.LockMovementType; +import kamkeel.npcs.controllers.data.ability.TargetingMode; +import kamkeel.npcs.controllers.data.ability.data.*; +import noppes.npcs.client.gui.builder.ColumnHint; +import noppes.npcs.client.gui.builder.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.AbilityFieldDefs; +import kamkeel.npcs.controllers.data.telegraph.Telegraph; +import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; +import kamkeel.npcs.controllers.data.telegraph.TelegraphType; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.util.AnchorPointHelper; +import net.minecraft.entity.Entity; +import net.minecraft.entity.EntityLivingBase; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.util.Vec3; +import net.minecraft.world.World; +import noppes.npcs.entity.EntityNPCInterface; +import noppes.npcs.util.ValueUtil; + +import java.util.Arrays; +import java.util.List; + +public class AbilityBeamDual extends Ability { + private float beamWidth = 0.4f; + private float headSize = 0.6f; + private boolean dualFire= true; + private int dualFireDelay = 0; + + // Data classes + private EnergyColorData[] colorData = new EnergyColorData[]{ + new EnergyColorData(0xFFFFFF, 0x00AAFF, true, 0.4f, 0.5f, 6.0f), + new EnergyColorData(0xFFFFFF, 0xFF0000, true, 0.4f, 0.5f, 6.0f), + }; + private EnergyCombatData combatData = new EnergyCombatData(10.0f, 1.5f, 0.2f, false, 4.0f, 0.5f); + private EnergyHomingData homingData = new EnergyHomingData(0.4f, true, 0.1f, 15.0f); + private EnergyLightningData[] lightningData = new EnergyLightningData[]{ + new EnergyLightningData(), new EnergyLightningData() + }; + private EnergyLifespanData lifespanData = new EnergyLifespanData(25.0f, 200); + private EnergyAnchorData[] anchorData = new EnergyAnchorData[]{ + new EnergyAnchorData(AnchorPoint.RIGHT_HAND), new EnergyAnchorData(AnchorPoint.LEFT_HAND) + }; + + // Transient state for beam entity (used during windup charging) + private transient EntityAbilityBeam beamEntity = null; + private transient EntityAbilityBeam beamEntity2 = null; + + public AbilityBeamDual() { + this.typeId = "ability.cnpc.beam_dual"; + this.name = "Beam Dual"; + this.targetingMode = TargetingMode.AGGRO_TARGET; + this.maxRange = 20.0f; + this.minRange = 5.0f; + this.cooldownTicks = 0; + this.windUpTicks = 80; + this.lockMovement = LockMovementType.WINDUP_AND_ACTIVE; + this.telegraphType = TelegraphType.CIRCLE; + this.showTelegraph = true; + // Default built-in animations + this.windUpAnimationName = "Ability_BeamDual_Windup"; + this.activeAnimationName = "Ability_BeamDual_Active"; + } + + @Override + public boolean isTargetingModeLocked() { + return true; + } + + @Override + public TargetingMode[] getAllowedTargetingModes() { + return new TargetingMode[]{TargetingMode.AGGRO_TARGET}; + } + + @Override + public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { + if (world.isRemote) { + signalCompletion(); + return; + } + + // Start firing the beam that was spawned during windup + if (beamEntity != null && !beamEntity.isDead) { + beamEntity.startFiring(target); + } + + if (dualFire) { + if (beamEntity2 != null && !beamEntity2.isDead && dualFireDelay <= 0) { + beamEntity2.startFiring(target); + } + } else { + beamEntity2.setDead(); + } + + // Ability stays active until entity dies (prevents firing another while projectile is alive) + // Movement locking is handled separately by the base class + } + + @Override + public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { + if (world.isRemote) return; + + // Spawn beam in charging mode on first tick of windup + if (tick == 1) { + float offsetDist = 1.0f; + + // Create beam in charging mode - follows caster based on anchor point during windup + Vec3 spawnPos1 = AnchorPointHelper.calculateAnchorPosition(caster, anchorData[0], offsetDist); + beamEntity = new EntityAbilityBeam( + world, caster, target, + spawnPos1.xCoord, spawnPos1.yCoord, spawnPos1.zCoord, + beamWidth, headSize, + colorData[0], combatData, homingData, lightningData[0], lifespanData, + lockMovement.locksActive()); + + Vec3 spawnPos2 = AnchorPointHelper.calculateAnchorPosition(caster, anchorData[1], offsetDist); + beamEntity2 = new EntityAbilityBeam( + world, caster, target, + spawnPos2.xCoord, spawnPos2.yCoord, spawnPos2.zCoord, + beamWidth, headSize, + colorData[1], combatData, homingData, lightningData[1], lifespanData, + lockMovement.locksActive()); + + beamEntity.setSiblingUUID(beamEntity2.getPersistentID()); + beamEntity2.setSiblingUUID(beamEntity.getPersistentID()); + + beamEntity.setupCharging(anchorData[0], windUpTicks, offsetDist); + beamEntity2.setupCharging(anchorData[1], windUpTicks, offsetDist); + + beamEntity.setEffects(this.effects); + if (dualFire) beamEntity2.setEffects(this.effects); + + world.spawnEntityInWorld(beamEntity); + world.spawnEntityInWorld(beamEntity2); + } + } + + @Override + public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { + // Signal completion when entity dies + if (dualFire) { + if ((beamEntity == null || beamEntity.isDead) && (beamEntity2 == null || beamEntity2.isDead)) { + beamEntity = null; + beamEntity2 = null; + signalCompletion(); + } + + if (tick == dualFireDelay && beamEntity2 != null && !beamEntity2.isDead) { + beamEntity2.startFiring(target); + } + } else { + if (beamEntity == null || beamEntity.isDead) { + beamEntity = null; + signalCompletion(); + } + } + } + + @Override + public void onComplete(EntityLivingBase caster, EntityLivingBase target) { + } + + @Override + public void cleanup() { + // Despawn beam entity if still alive + if (beamEntity != null && !beamEntity.isDead) { + beamEntity.setDead(); + } + + if (beamEntity2 != null && !beamEntity2.isDead) { + beamEntity2.setDead(); + } + + beamEntity = null; + beamEntity2 = null; + } + + @Override + public TelegraphInstance createTelegraph(EntityLivingBase caster, EntityLivingBase target) { + if (!showTelegraph || telegraphType == TelegraphType.NONE || target == null) { + return null; + } + + // Create circle telegraph at target position + Telegraph telegraph = Telegraph.circle(headSize * 2.0f); + telegraph.setDurationTicks(windUpTicks); + telegraph.setColor(windUpColor); + telegraph.setWarningColor(activeColor); + telegraph.setWarningStartTick(Math.max(5, windUpTicks / 4)); + telegraph.setHeightOffset(telegraphHeightOffset); + + // Position at target and follow target during windup + TelegraphInstance instance = new TelegraphInstance(telegraph, target.posX, target.posY, target.posZ, caster.rotationYaw); + instance.setCasterEntityId(caster.getEntityId()); + instance.setEntityIdToFollow(target.getEntityId()); + + return instance; + } + + @Override + public float getTelegraphRadius() { + return headSize * 2.0f; + } + + @Override + public void writeTypeNBT(NBTTagCompound nbt) { + nbt.setFloat("beamWidth", beamWidth); + nbt.setFloat("headSize", headSize); + nbt.setBoolean("dualFire", dualFire); + nbt.setInteger("dualFireDelay", dualFireDelay); + combatData.writeNBT(nbt); + homingData.writeNBT(nbt); + lifespanData.writeNBT(nbt); + + NBTTagCompound beam1 = new NBTTagCompound(); + NBTTagCompound beam2 = new NBTTagCompound(); + + anchorData[0].writeNBT(beam1); + anchorData[1].writeNBT(beam2); + colorData[0].writeNBT(beam1); + colorData[1].writeNBT(beam2); + lightningData[0].writeNBT(beam1); + lightningData[1].writeNBT(beam2); + + nbt.setTag("Beam_1", beam1); + nbt.setTag("Beam_2", beam2); + } + + @Override + public void readTypeNBT(NBTTagCompound nbt) { + NBTTagCompound beam1 = nbt.hasKey("Beam_1") ? nbt.getCompoundTag("Beam_1") : new NBTTagCompound(); + NBTTagCompound beam2 = nbt.hasKey("Beam_2") ? nbt.getCompoundTag("Beam_2") : new NBTTagCompound(); + + this.beamWidth = nbt.hasKey("beamWidth") ? nbt.getFloat("beamWidth") : 0.4f; + this.headSize = nbt.hasKey("headSize") ? nbt.getFloat("headSize") : 0.6f; + this.dualFire = !nbt.hasKey("dualFire") || nbt.getBoolean("dualFire"); + this.dualFireDelay = nbt.hasKey("dualFireDelay") ? nbt.getInteger("dualFireDelay") : 0; + combatData.readNBT(nbt); + homingData.readNBT(nbt); + lifespanData.readNBT(nbt); + + anchorData[0].readNBT(beam1); + anchorData[1].readNBT(beam2); + colorData[0].readNBT(beam1); + colorData[1].readNBT(beam2); + lightningData[0].readNBT(beam1); + lightningData[1].readNBT(beam2); + } + + // Getters & Setters - Standalone fields + public float getBeamWidth() { return beamWidth; } + public void setBeamWidth(float beamWidth) { this.beamWidth = beamWidth; } + public float getHeadSize() { return headSize; } + public void setHeadSize(float headSize) { this.headSize = headSize; } + public boolean isDualFire() { return dualFire; } + public void setDualFire(boolean dualFire) { this.dualFire = dualFire; } + public int getDualFireDelay() { return dualFireDelay; } + public void setDualFireDelay(int dualFireDelay) { this.dualFireDelay = dualFireDelay; } + + private EnergyColorData getColorData(int beam) { + beam = ValueUtil.clamp(beam, 0, 1); + return colorData[beam]; + } + private EnergyLightningData getLightningData(int beam) { + beam = ValueUtil.clamp(beam, 0, 1); + return lightningData[beam]; + } + private EnergyAnchorData getAnchorData(int beam) { + beam = ValueUtil.clamp(beam, 0, 1); + return anchorData[beam]; + } + + // Getters & Setters - Color data + public int getInnerColor(int beam) { return getColorData(beam).innerColor; } + public void setInnerColor(int beam, int innerColor) { getColorData(beam).innerColor = innerColor; } + public int getOuterColor(int beam) { return getColorData(beam).outerColor; } + public void setOuterColor(int beam, int outerColor) { getColorData(beam).outerColor = outerColor; } + public boolean isOuterColorEnabled(int beam) { return getColorData(beam).outerColorEnabled; } + public void setOuterColorEnabled(int beam, boolean enabled) { getColorData(beam).outerColorEnabled = enabled; } + public float getOuterColorWidth(int beam) { return getColorData(beam).outerColorWidth; } + public void setOuterColorWidth(int beam, float width) { getColorData(beam).outerColorWidth = width; } + public float getOuterColorAlpha(int beam) { return getColorData(beam).outerColorAlpha; } + public void setOuterColorAlpha(int beam, float alpha) { getColorData(beam).outerColorAlpha = alpha; } + public float getRotationSpeed(int beam) { return getColorData(beam).rotationSpeed; } + public void setRotationSpeed(int beam, float speed) { getColorData(beam).rotationSpeed = speed; } + + + public int getInnerColor() { return getInnerColor(0); } + public void setInnerColor(int innerColor) { setInnerColor(0, innerColor); } + public int getOuterColor() { return getOuterColor(0); } + public void setOuterColor(int outerColor) { setOuterColor(0, outerColor); } + public boolean isOuterColorEnabled() { return isOuterColorEnabled(0); } + public void setOuterColorEnabled(boolean enabled) { setOuterColorEnabled(0, enabled); } + public float getOuterColorWidth() { return getOuterColorWidth(0); } + public void setOuterColorWidth(float width) { setOuterColorWidth(0, width); } + public float getOuterColorAlpha() { return getOuterColorAlpha(0); } + public void setOuterColorAlpha(float alpha) { setOuterColorAlpha(0, alpha); } + public float getRotationSpeed() { return getRotationSpeed(0); } + public void setRotationSpeed(float speed) { setRotationSpeed(0, speed); } + + // Getters & Setters - Combat data + public float getDamage() { return combatData.damage; } + public void setDamage(float damage) { this.combatData.damage = damage; } + public float getKnockback() { return combatData.knockback; } + public void setKnockback(float knockback) { this.combatData.knockback = knockback; } + public float getKnockbackUp() { return combatData.knockbackUp; } + public void setKnockbackUp(float knockbackUp) { this.combatData.knockbackUp = knockbackUp; } + public boolean isExplosive() { return combatData.explosive; } + public void setExplosive(boolean explosive) { this.combatData.explosive = explosive; } + public float getExplosionRadius() { return combatData.explosionRadius; } + public void setExplosionRadius(float explosionRadius) { this.combatData.explosionRadius = explosionRadius; } + public float getExplosionDamageFalloff() { return combatData.explosionDamageFalloff; } + public void setExplosionDamageFalloff(float explosionDamageFalloff) { this.combatData.explosionDamageFalloff = explosionDamageFalloff; } + + // Getters & Setters - Homing data + public float getSpeed() { return homingData.speed; } + public void setSpeed(float speed) { this.homingData.speed = speed; } + public boolean isHoming() { return homingData.homing; } + public void setHoming(boolean homing) { this.homingData.homing = homing; } + public float getHomingStrength() { return homingData.homingStrength; } + public void setHomingStrength(float homingStrength) { this.homingData.homingStrength = homingStrength; } + public float getHomingRange() { return homingData.homingRange; } + public void setHomingRange(float homingRange) { this.homingData.homingRange = homingRange; } + + // Getters & Setters - Lightning data + public boolean hasLightningEffect(int beam) { return getLightningData(beam).lightningEffect; } + public void setLightningEffect(int beam, boolean lightningEffect) { getLightningData(beam).lightningEffect = lightningEffect; } + public float getLightningDensity(int beam) { return getLightningData(beam).lightningDensity; } + public void setLightningDensity(int beam, float lightningDensity) { getLightningData(beam).lightningDensity = lightningDensity; } + public float getLightningRadius(int beam) { return getLightningData(beam).lightningRadius; } + public void setLightningRadius(int beam, float lightningRadius) { getLightningData(beam).lightningRadius = lightningRadius; } + + public boolean hasLightningEffect() { return hasLightningEffect(0); } + public void setLightningEffect(boolean lightningEffect) { setLightningEffect(0, lightningEffect); } + public float getLightningDensity() { return getLightningDensity(0); } + public void setLightningDensity(float lightningDensity) { setLightningDensity(0, lightningDensity); } + public float getLightningRadius() { return getLightningRadius(0); } + public void setLightningRadius(float lightningRadius) { setLightningRadius(0, lightningRadius); } + + + // Getters & Setters - Lifespan data + public float getMaxDistance() { return lifespanData.maxDistance; } + public void setMaxDistance(float maxDistance) { this.lifespanData.maxDistance = maxDistance; } + public int getMaxLifetime() { return lifespanData.maxLifetime; } + public void setMaxLifetime(int maxLifetime) { this.lifespanData.maxLifetime = maxLifetime; } + + // Getters & Setters - Anchor point + public AnchorPoint getAnchorPointEnum(int beam) { return getAnchorData(beam).anchorPoint; } + public void setAnchorPointEnum(int beam, AnchorPoint anchorPoint) { getAnchorData(beam).anchorPoint = anchorPoint; } + public float getAnchorOffsetX(int beam) { return getAnchorData(beam).anchorOffsetX; } + public void setAnchorOffsetX(int beam, float x) { getAnchorData(beam).anchorOffsetX = x; } + public float getAnchorOffsetY(int beam) { return getAnchorData(beam).anchorOffsetY; } + public void setAnchorOffsetY(int beam, float y) { getAnchorData(beam).anchorOffsetY = y; } + public float getAnchorOffsetZ(int beam) { return getAnchorData(beam).anchorOffsetZ; } + public void setAnchorOffsetZ(int beam, float z) { getAnchorData(beam).anchorOffsetZ = z; } + + public AnchorPoint getAnchorPointEnum() { return getAnchorPointEnum(0); } + public void setAnchorPointEnum(AnchorPoint anchorPoint) { setAnchorPointEnum(0, anchorPoint); } + public float getAnchorOffsetX() { return getAnchorOffsetX(0); } + public void setAnchorOffsetX(float x) { setAnchorOffsetX(0, x); } + public float getAnchorOffsetY() { return getAnchorOffsetY(0); } + public void setAnchorOffsetY(float y) { setAnchorOffsetY(0, y); } + public float getAnchorOffsetZ() { return getAnchorOffsetZ(0); } + public void setAnchorOffsetZ(float z) { setAnchorOffsetZ(0, z); } + + //@Override + public int getAnchorPoint(int beam) { return getAnchorData(beam).anchorPoint.ordinal(); } + public int getAnchorPoint() { return getAnchorData(0).anchorPoint.ordinal(); } + + //@Override + public void setAnchorPoint(int beam, int point) { getAnchorData(beam).anchorPoint = AnchorPoint.fromOrdinal(point); } + public void setAnchorPoint(int point) { getAnchorData(0).anchorPoint = AnchorPoint.fromOrdinal(point); } + + @SideOnly(Side.CLIENT) + @Override + public List getFieldDefinitions() { + return Arrays.asList( + // Type tab + FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage).column(ColumnHint.LEFT), + FieldDef.floatField("stats.speed", this::getSpeed, this::setSpeed).column(ColumnHint.RIGHT), + FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback), + FieldDef.section("ability.section.beam"), + FieldDef.floatField("ability.beamWidth", this::getBeamWidth, this::setBeamWidth).column(ColumnHint.LEFT), + FieldDef.floatField("ability.headSize", this::getHeadSize, this::setHeadSize).column(ColumnHint.RIGHT), + FieldDef.floatField("ability.maxDistance", this::getMaxDistance, this::setMaxDistance).column(ColumnHint.LEFT), + FieldDef.intField("ability.lifetime", this::getMaxLifetime, this::setMaxLifetime).column(ColumnHint.RIGHT), + FieldDef.section("ability.section.dual"), + FieldDef.boolField("ability.dualFire", this::isDualFire, this::setDualFire), + FieldDef.intField("ability.dualFireDelay", this::getDualFireDelay, this::setDualFireDelay) + .visibleWhen(this::isDualFire), + FieldDef.section("ability.section.homing"), + FieldDef.boolField("gui.enabled", this::isHoming, this::setHoming) + .hover("ability.hover.homing"), + FieldDef.floatField("gui.strength", this::getHomingStrength, this::setHomingStrength) + .visibleWhen(this::isHoming).column(ColumnHint.LEFT), + FieldDef.floatField("gui.range", this::getHomingRange, this::setHomingRange) + .visibleWhen(this::isHoming).column(ColumnHint.RIGHT), + FieldDef.section("ability.section.explosive"), + FieldDef.boolField("gui.enabled", this::isExplosive, this::setExplosive) + .hover("ability.hover.explosive"), + FieldDef.floatField("gui.radius", this::getExplosionRadius, this::setExplosionRadius) + .visibleWhen(this::isExplosive), + AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects), + // Visual tab - Beam 1 + FieldDef.enumField("ability.anchorPoint", AnchorPoint.class, + () -> getAnchorPointEnum(0), v -> setAnchorPointEnum(0, v)) + .tab("ability.tab.visual"), + FieldDef.section("ability.section.beam1").tab("ability.tab.visual"), + FieldDef.colorSubGui("ability.innerColor", + () -> getInnerColor(0), v -> setInnerColor(0, v)) + .tab("ability.tab.visual"), + FieldDef.boolField("ability.outerEnabled", + () -> isOuterColorEnabled(0), v -> setOuterColorEnabled(0, v)) + .tab("ability.tab.visual"), + FieldDef.colorSubGui("ability.outerColor", + () -> getOuterColor(0), v -> setOuterColor(0, v)) + .tab("ability.tab.visual").visibleWhen(() -> isOuterColorEnabled(0)), + FieldDef.floatField("ability.outerWidth", + () -> getOuterColorWidth(0), v -> setOuterColorWidth(0, v)) + .tab("ability.tab.visual").visibleWhen(() -> isOuterColorEnabled(0)).column(ColumnHint.LEFT), + FieldDef.floatField("ability.outerAlpha", + () -> getOuterColorAlpha(0), v -> setOuterColorAlpha(0, v)) + .tab("ability.tab.visual").range(0, 1).visibleWhen(() -> isOuterColorEnabled(0)).column(ColumnHint.RIGHT), + FieldDef.floatField("ability.rotationSpeed", + () -> getRotationSpeed(0), v -> setRotationSpeed(0, v)) + .tab("ability.tab.visual"), + FieldDef.boolField("ability.lightning", + () -> hasLightningEffect(0), v -> setLightningEffect(0, v)) + .tab("ability.tab.visual"), + FieldDef.floatField("gui.density", + () -> getLightningDensity(0), v -> setLightningDensity(0, v)) + .tab("ability.tab.visual").range(0.01f, 5.0f).visibleWhen(() -> hasLightningEffect(0)).column(ColumnHint.LEFT), + FieldDef.floatField("gui.radius", + () -> getLightningRadius(0), v -> setLightningRadius(0, v)) + .tab("ability.tab.visual").range(0.1f, 10.0f).visibleWhen(() -> hasLightningEffect(0)).column(ColumnHint.RIGHT), + // Visual tab - Beam 2 + FieldDef.enumField("ability.anchorPoint", AnchorPoint.class, + () -> getAnchorPointEnum(1), v -> setAnchorPointEnum(1, v)) + .tab("ability.tab.visual"), + FieldDef.section("ability.section.beam2").tab("ability.tab.visual"), + FieldDef.colorSubGui("ability.innerColor", + () -> getInnerColor(1), v -> setInnerColor(1, v)) + .tab("ability.tab.visual"), + FieldDef.boolField("ability.outerEnabled", + () -> isOuterColorEnabled(1), v -> setOuterColorEnabled(1, v)) + .tab("ability.tab.visual"), + FieldDef.colorSubGui("ability.outerColor", + () -> getOuterColor(1), v -> setOuterColor(1, v)) + .tab("ability.tab.visual").visibleWhen(() -> isOuterColorEnabled(1)), + FieldDef.floatField("ability.outerWidth", + () -> getOuterColorWidth(1), v -> setOuterColorWidth(1, v)) + .tab("ability.tab.visual").visibleWhen(() -> isOuterColorEnabled(1)).column(ColumnHint.LEFT), + FieldDef.floatField("ability.outerAlpha", + () -> getOuterColorAlpha(1), v -> setOuterColorAlpha(1, v)) + .tab("ability.tab.visual").range(0, 1).visibleWhen(() -> isOuterColorEnabled(1)).column(ColumnHint.RIGHT), + FieldDef.floatField("ability.rotationSpeed", + () -> getRotationSpeed(1), v -> setRotationSpeed(1, v)) + .tab("ability.tab.visual"), + FieldDef.boolField("ability.lightning", + () -> hasLightningEffect(1), v -> setLightningEffect(1, v)) + .tab("ability.tab.visual"), + FieldDef.floatField("gui.density", + () -> getLightningDensity(1), v -> setLightningDensity(1, v)) + .tab("ability.tab.visual").range(0.01f, 5.0f).visibleWhen(() -> hasLightningEffect(1)).column(ColumnHint.LEFT), + FieldDef.floatField("gui.radius", + () -> getLightningRadius(1), v -> setLightningRadius(1, v)) + .tab("ability.tab.visual").range(0.1f, 10.0f).visibleWhen(() -> hasLightningEffect(1)).column(ColumnHint.RIGHT) + ); + } + + @Override + @SideOnly(Side.CLIENT) + public Entity createPreviewEntity(EntityNPCInterface npc) { + if (npc == null || npc.worldObj == null) return null; + + EntityAbilityBeam beam = new EntityAbilityBeam(npc.worldObj); + beam.setupPreview(npc, beamWidth, headSize, colorData[0], lightningData[0], anchorData[0], windUpTicks, 1.0f); + return beam; + } + + @Override + public int getPreviewActiveDuration() { + return lifespanData.maxLifetime > 0 ? Math.min(lifespanData.maxLifetime, 100) : 100; + } +} 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 6c43ea1cb..d8cc3408d 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 @@ -3,6 +3,7 @@ package kamkeel.npcs.controllers.data.ability.type; 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.LockMovementType; import kamkeel.npcs.controllers.data.ability.TargetingMode; import kamkeel.npcs.controllers.data.telegraph.Telegraph; import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; @@ -13,11 +14,14 @@ import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.AxisAlignedBB; import net.minecraft.util.Vec3; import net.minecraft.world.World; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityCharge; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.entity.EntityNPCInterface; +import noppes.npcs.client.gui.builder.ColumnHint; +import noppes.npcs.client.gui.builder.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.AbilityFieldDefs; +import noppes.npcs.api.ability.type.IAbilityCharge; + +import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -25,7 +29,7 @@ import java.util.Set; /** * Charge ability: Rush attack where NPC charges in a line, damaging all targets hit. */ -public class AbilityCharge extends Ability { +public class AbilityCharge extends Ability implements IAbilityCharge { // Type-specific parameters private float chargeSpeed = 0.8f; @@ -45,11 +49,9 @@ public class AbilityCharge extends Ability { this.targetingMode = TargetingMode.AGGRO_TARGET; this.maxRange = 20.0f; this.minRange = 4.0f; - this.lockMovement = false; // Movement IS the ability - this.cooldownTicks = 80; + this.lockMovement = LockMovementType.WINDUP; + this.cooldownTicks = 0; this.windUpTicks = 20; - this.activeTicks = 40; - this.recoveryTicks = 20; // LINE telegraph showing charge path this.telegraphType = TelegraphType.LINE; this.showTelegraph = true; @@ -57,17 +59,6 @@ public class AbilityCharge extends Ability { this.activeSound = "mob.zombie.attack"; } - @Override - public boolean hasTypeSettings() { - return true; - } - - @Override - @SideOnly(Side.CLIENT) - public SubGuiAbilityConfig createConfigGui(IAbilityConfigCallback callback) { - return new SubGuiAbilityCharge(this, callback); - } - @Override public boolean isTargetingModeLocked() { return true; @@ -87,104 +78,106 @@ public class AbilityCharge extends Ability { * Called on the first tick of windup - lock direction here so it matches telegraph. */ @Override - public void onWindUpTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { + public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { if (tick == 1) { // Lock direction on first windup tick (same time telegraph is created) - lockChargeDirection(npc, target); + lockChargeDirection(caster, target); } - // Keep NPC facing the locked direction during windup - enforceLockedRotation(npc); + // Keep caster facing the locked direction during windup + enforceLockedRotation(caster); } /** * Locks the charge direction based on current target position. * Called once at windup start - direction won't change even if target moves. */ - private void lockChargeDirection(EntityNPCInterface npc, EntityLivingBase target) { + private void lockChargeDirection(EntityLivingBase caster, EntityLivingBase target) { if (target != null) { - double dx = target.posX - npc.posX; - double dz = target.posZ - npc.posZ; + double dx = target.posX - caster.posX; + double dz = target.posZ - caster.posZ; double len = Math.sqrt(dx * dx + dz * dz); if (len > 0) { chargeDirection = Vec3.createVectorHelper(dx / len, 0, dz / len); } else { - float yaw = (float) Math.toRadians(npc.rotationYaw); + float yaw = (float) Math.toRadians(caster.rotationYaw); chargeDirection = Vec3.createVectorHelper(-Math.sin(yaw), 0, Math.cos(yaw)); } } else { - float yaw = (float) Math.toRadians(npc.rotationYaw); + float yaw = (float) Math.toRadians(caster.rotationYaw); chargeDirection = Vec3.createVectorHelper(-Math.sin(yaw), 0, Math.cos(yaw)); } lockedYaw = (float) Math.toDegrees(Math.atan2(-chargeDirection.xCoord, chargeDirection.zCoord)); } @Override - public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) { + public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { // Initialize charge - direction was already locked during windup - startX = npc.posX; - startY = npc.posY; - startZ = npc.posZ; + startX = caster.posX; + startY = caster.posY; + startZ = caster.posZ; hitEntities.clear(); // If direction wasn't set during windup (shouldn't happen), set it now if (chargeDirection == null) { - lockChargeDirection(npc, target); + lockChargeDirection(caster, target); } - enforceLockedRotation(npc); + enforceLockedRotation(caster); } - private void enforceLockedRotation(EntityNPCInterface npc) { - npc.rotationYaw = lockedYaw; - npc.rotationYawHead = lockedYaw; - npc.prevRotationYaw = lockedYaw; - npc.prevRotationYawHead = lockedYaw; - npc.renderYawOffset = lockedYaw; - npc.prevRenderYawOffset = lockedYaw; + private void enforceLockedRotation(EntityLivingBase caster) { + caster.rotationYaw = lockedYaw; + caster.rotationYawHead = lockedYaw; + caster.prevRotationYaw = lockedYaw; + caster.prevRotationYawHead = lockedYaw; + caster.renderYawOffset = lockedYaw; + caster.prevRenderYawOffset = lockedYaw; } @Override - public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { + public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { if (chargeDirection == null) return; // Enforce rotation every tick - enforceLockedRotation(npc); + enforceLockedRotation(caster); // Calculate distance traveled double distanceTraveled = Math.sqrt( - Math.pow(npc.posX - startX, 2) + - Math.pow(npc.posZ - startZ, 2) + Math.pow(caster.posX - startX, 2) + + Math.pow(caster.posZ - startZ, 2) ); // Check if reached max distance if (distanceTraveled >= maxRange) { - npc.motionX = 0; - npc.motionZ = 0; - npc.velocityChanged = true; + caster.motionX = 0; + caster.motionZ = 0; + caster.velocityChanged = true; + signalCompletion(); return; } - if (isChargeBlocked(npc)) { - stopMomentum(npc); + if (isChargeBlocked(caster)) { + stopMomentum(caster); + signalCompletion(); return; } - // Move NPC - npc.motionX = chargeDirection.xCoord * chargeSpeed; - npc.motionY = 0; - npc.motionZ = chargeDirection.zCoord * chargeSpeed; - npc.velocityChanged = true; + // Move caster + caster.motionX = chargeDirection.xCoord * chargeSpeed; + caster.motionY = 0; + caster.motionZ = chargeDirection.zCoord * chargeSpeed; + caster.velocityChanged = true; // Server-side collision damage if (!world.isRemote) { - AxisAlignedBB hitBox = npc.boundingBox.expand(hitWidth, hitWidth * 0.5, hitWidth); + AxisAlignedBB hitBox = caster.boundingBox.expand(hitWidth, hitWidth * 0.5, hitWidth); @SuppressWarnings("unchecked") List entities = world.getEntitiesWithinAABB(EntityLivingBase.class, hitBox); for (Entity entity : entities) { if (!(entity instanceof EntityLivingBase)) continue; - if (entity == npc) continue; + if (entity == caster) continue; if (hitEntities.contains(entity.getEntityId())) continue; EntityLivingBase livingEntity = (EntityLivingBase) entity; @@ -193,7 +186,7 @@ public class AbilityCharge extends Ability { hitEntities.add(entity.getEntityId()); // Apply damage with scripted event support - boolean wasHit = applyAbilityDamageWithDirection(npc, livingEntity, damage, knockback, + boolean wasHit = applyAbilityDamageWithDirection(caster, livingEntity, damage, knockback, chargeDirection.xCoord, chargeDirection.zCoord); // Play impact sound if hit wasn't cancelled @@ -205,28 +198,28 @@ public class AbilityCharge extends Ability { } @Override - public void onComplete(EntityNPCInterface npc, EntityLivingBase target) { - stopMomentum(npc); - super.onComplete(npc, target); + public void onComplete(EntityLivingBase caster, EntityLivingBase target) { + stopMomentum(caster); + super.onComplete(caster, target); } @Override - public void onInterrupt(EntityNPCInterface npc, net.minecraft.util.DamageSource source, float damage) { - stopMomentum(npc); - super.onInterrupt(npc, source, damage); + public void onInterrupt(EntityLivingBase caster, net.minecraft.util.DamageSource source, float damage) { + stopMomentum(caster); + super.onInterrupt(caster, source, damage); } - private void stopMomentum(EntityNPCInterface npc) { - npc.motionX = 0; - npc.motionZ = 0; - npc.velocityChanged = true; + private void stopMomentum(EntityLivingBase caster) { + caster.motionX = 0; + caster.motionZ = 0; + caster.velocityChanged = true; } - private boolean isChargeBlocked(EntityNPCInterface npc) { + private boolean isChargeBlocked(EntityLivingBase caster) { double nextX = chargeDirection.xCoord * chargeSpeed; double nextZ = chargeDirection.zCoord * chargeSpeed; - AxisAlignedBB nextBox = npc.boundingBox.copy().offset(nextX, 0, nextZ); - return !npc.worldObj.getCollidingBoundingBoxes(npc, nextBox).isEmpty(); + AxisAlignedBB nextBox = caster.boundingBox.copy().offset(nextX, 0, nextZ); + return !caster.worldObj.getCollidingBoundingBoxes(caster, nextBox).isEmpty(); } @Override @@ -247,12 +240,12 @@ public class AbilityCharge extends Ability { } /** - * Creates a LINE telegraph from NPC position towards target. - * Telegraph follows NPC during windup (so NPC can reposition before charging). + * Creates a LINE telegraph from caster position towards target. + * Telegraph follows caster during windup (so caster can reposition before charging). * Direction is locked at creation based on target position. */ @Override - public TelegraphInstance createTelegraph(EntityNPCInterface npc, EntityLivingBase target) { + public TelegraphInstance createTelegraph(EntityLivingBase caster, EntityLivingBase target) { if (!isShowTelegraph() || getTelegraphType() == TelegraphType.NONE) { return null; } @@ -260,11 +253,11 @@ public class AbilityCharge extends Ability { // Calculate direction to target at this moment (locked) float yaw; if (target != null) { - double dx = target.posX - npc.posX; - double dz = target.posZ - npc.posZ; + double dx = target.posX - caster.posX; + double dz = target.posZ - caster.posZ; yaw = (float) Math.toDegrees(Math.atan2(-dx, dz)); } else { - yaw = npc.rotationYaw; + yaw = caster.rotationYaw; } // Create LINE telegraph @@ -275,16 +268,79 @@ public class AbilityCharge extends Ability { telegraph.setWarningStartTick(Math.max(5, windUpTicks / 4)); telegraph.setHeightOffset(telegraphHeightOffset); - // Position at NPC ground level, direction towards target - double groundY = findGroundLevel(npc.worldObj, npc.posX, npc.posY, npc.posZ); - TelegraphInstance instance = new TelegraphInstance(telegraph, npc.posX, groundY, npc.posZ, yaw); - instance.setCasterEntityId(npc.getEntityId()); - // Telegraph follows NPC during windup - allows NPC to reposition - instance.setEntityIdToFollow(npc.getEntityId()); + // Position at caster ground level, direction towards target + double groundY = findGroundLevel(caster.worldObj, caster.posX, caster.posY, caster.posZ); + TelegraphInstance instance = new TelegraphInstance(telegraph, caster.posX, groundY, caster.posZ, yaw); + instance.setCasterEntityId(caster.getEntityId()); + // Telegraph follows caster during windup - allows caster to reposition + instance.setEntityIdToFollow(caster.getEntityId()); 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); @@ -339,4 +395,16 @@ public class AbilityCharge extends Ability { public void setHitWidth(float hitWidth) { this.hitWidth = hitWidth; } + + @SideOnly(Side.CLIENT) + @Override + public List getFieldDefinitions() { + return Arrays.asList( + FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage).column(ColumnHint.LEFT), + FieldDef.floatField("ability.chargeSpeed", this::getChargeSpeed, this::setChargeSpeed).column(ColumnHint.RIGHT), + FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback).column(ColumnHint.LEFT), + FieldDef.floatField("ability.hitWidth", this::getHitWidth, this::setHitWidth).column(ColumnHint.RIGHT), + AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects) + ); + } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCutter.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCutter.java index 611d0a4a6..f8419a1b8 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCutter.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCutter.java @@ -1,22 +1,26 @@ package kamkeel.npcs.controllers.data.ability.type; -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.LockMovementType; import kamkeel.npcs.controllers.data.ability.TargetingMode; import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; -import net.minecraft.potion.Potion; -import net.minecraft.potion.PotionEffect; import net.minecraft.util.AxisAlignedBB; import net.minecraft.util.DamageSource; import net.minecraft.world.World; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityCutter; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.entity.EntityNPCInterface; +import noppes.npcs.api.ability.type.IAbilityCutter; + +import noppes.npcs.client.gui.builder.ColumnHint; +import noppes.npcs.client.gui.builder.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.AbilityFieldDefs; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; + +import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -26,11 +30,20 @@ import java.util.Set; * Deals damage to all entities in a cone/arc in front of the caster. * Can sweep outward over time or hit instantly. */ -public class AbilityCutter extends Ability { +public class AbilityCutter extends Ability implements IAbilityCutter { public enum SweepMode { SWIPE, - SPIN + SPIN; + + @Override + public String toString() { + switch (this) { + case SWIPE: return "ability.sweep.swipe"; + case SPIN: return "ability.sweep.spin"; + default: return name(); + } + } } private float arcAngle = 90.0f; @@ -40,10 +53,8 @@ public class AbilityCutter extends Ability { private SweepMode sweepMode = SweepMode.SWIPE; private float sweepSpeed = 6.0f; + private int spinDurationTicks = 60; // Duration for SPIN mode - private int stunDuration = 0; - private int poisonDurationSeconds = 0; - private int poisonLevel = 0; private boolean piercing = true; private float innerRadius = 0.0f; @@ -56,25 +67,14 @@ public class AbilityCutter extends Ability { this.name = "Cutter"; this.targetingMode = TargetingMode.AOE_SELF; this.maxRange = 8.0f; - this.lockMovement = true; - this.cooldownTicks = 80; + this.lockMovement = LockMovementType.NO; + this.cooldownTicks = 0; this.windUpTicks = 20; - this.activeTicks = 15; - this.recoveryTicks = 15; this.telegraphType = TelegraphType.CONE; this.windUpSound = "random.bow"; this.activeSound = "random.break"; - } - - @Override - public boolean hasTypeSettings() { - return true; - } - - @Override - @SideOnly(Side.CLIENT) - public SubGuiAbilityConfig createConfigGui(IAbilityConfigCallback callback) { - return new SubGuiAbilityCutter(this, callback); + this.windUpAnimationName = "Ability_Cutter_Windup"; + this.activeAnimationName = "Ability_Cutter_Active"; } @Override @@ -103,51 +103,56 @@ public class AbilityCutter extends Ability { } @Override - public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) { + public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { hitEntities.clear(); currentRotation = -arcAngle / 2.0f; } @Override - public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { + public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { if (world.isRemote) return; switch (sweepMode) { case SWIPE: if (currentRotation > arcAngle / 2.0f) { + signalCompletion(); // Swipe arc complete return; } - performSweepDamage(npc, world, innerRadius, range, currentRotation); + performSweepDamage(caster, world, innerRadius, range, currentRotation); currentRotation += sweepSpeed; break; case SPIN: + if (tick >= spinDurationTicks) { + signalCompletion(); // Spin duration complete + return; + } currentRotation = (currentRotation + sweepSpeed) % 360.0f; hitEntities.clear(); - performSweepDamage(npc, world, innerRadius, range, currentRotation); + performSweepDamage(caster, world, innerRadius, range, currentRotation); break; } } - private void performSweepDamage(EntityNPCInterface npc, World world, float minDist, float maxDist, float angleOffset) { - float casterYaw = npc.rotationYaw + angleOffset; + private void performSweepDamage(EntityLivingBase caster, World world, float minDist, float maxDist, float angleOffset) { + float casterYaw = caster.rotationYaw + angleOffset; AxisAlignedBB searchBox = AxisAlignedBB.getBoundingBox( - npc.posX - maxDist, npc.posY - 1, npc.posZ - maxDist, - npc.posX + maxDist, npc.posY + 3, npc.posZ + maxDist + caster.posX - maxDist, caster.posY - 1, caster.posZ - maxDist, + caster.posX + maxDist, caster.posY + 3, caster.posZ + maxDist ); @SuppressWarnings("unchecked") List entities = world.getEntitiesWithinAABB(EntityLivingBase.class, searchBox); for (EntityLivingBase entity : entities) { - if (entity == npc) continue; + if (entity == caster) continue; if (hitEntities.contains(entity.getEntityId())) continue; // If not piercing, stop after hitting one entity this sweep if (!piercing && !hitEntities.isEmpty()) break; - double dx = entity.posX - npc.posX; - double dz = entity.posZ - npc.posZ; + double dx = entity.posX - caster.posX; + double dz = entity.posZ - caster.posZ; double dist = Math.sqrt(dx * dx + dz * dz); if (dist < minDist || dist > maxDist) continue; @@ -159,17 +164,11 @@ public class AbilityCutter extends Ability { float actualDamage = damage * distFactor; // Apply damage with scripted event support - boolean wasHit = applyAbilityDamage(npc, entity, actualDamage, knockback); + boolean wasHit = applyAbilityDamage(caster, entity, actualDamage, knockback); - // Only apply effects if hit wasn't cancelled + // Apply effects if hit wasn't cancelled if (wasHit) { - if (stunDuration > 0) { - entity.addPotionEffect(new PotionEffect(Potion.moveSlowdown.id, stunDuration, 10)); - entity.addPotionEffect(new PotionEffect(Potion.weakness.id, stunDuration, 2)); - } - if (poisonDurationSeconds > 0) { - entity.addPotionEffect(new PotionEffect(Potion.poison.id, poisonDurationSeconds * 20, poisonLevel)); - } + applyEffects(entity); } } } @@ -187,13 +186,13 @@ public class AbilityCutter extends Ability { } @Override - public void onComplete(EntityNPCInterface npc, EntityLivingBase target) { + public void onComplete(EntityLivingBase caster, EntityLivingBase target) { hitEntities.clear(); currentRotation = 0.0f; } @Override - public void onInterrupt(EntityNPCInterface npc, DamageSource source, float damage) { + public void onInterrupt(EntityLivingBase caster, DamageSource source, float damage) { hitEntities.clear(); currentRotation = 0.0f; } @@ -206,9 +205,7 @@ public class AbilityCutter extends Ability { nbt.setFloat("knockback", knockback); nbt.setString("sweepMode", sweepMode.name()); nbt.setFloat("sweepSpeed", sweepSpeed); - nbt.setInteger("stunDuration", stunDuration); - nbt.setInteger("poisonDurationSeconds", poisonDurationSeconds); - nbt.setInteger("poisonLevel", poisonLevel); + nbt.setInteger("spinDurationTicks", spinDurationTicks); nbt.setBoolean("piercing", piercing); nbt.setFloat("innerRadius", innerRadius); } @@ -225,9 +222,7 @@ public class AbilityCutter extends Ability { this.sweepMode = SweepMode.SWIPE; } this.sweepSpeed = nbt.hasKey("sweepSpeed") ? nbt.getFloat("sweepSpeed") : 6.0f; - this.stunDuration = nbt.hasKey("stunDuration") ? nbt.getInteger("stunDuration") : 0; - this.poisonDurationSeconds = nbt.hasKey("poisonDurationSeconds") ? nbt.getInteger("poisonDurationSeconds") : 0; - this.poisonLevel = nbt.hasKey("poisonLevel") ? nbt.getInteger("poisonLevel") : 0; + this.spinDurationTicks = nbt.hasKey("spinDurationTicks") ? nbt.getInteger("spinDurationTicks") : 60; this.piercing = !nbt.hasKey("piercing") || nbt.getBoolean("piercing"); this.innerRadius = nbt.hasKey("innerRadius") ? nbt.getFloat("innerRadius") : 0.0f; } @@ -265,14 +260,25 @@ public class AbilityCutter extends Ability { this.knockback = knockback; } - public SweepMode getSweepMode() { + public SweepMode getSweepModeEnum() { return sweepMode; } - public void setSweepMode(SweepMode sweepMode) { + public void setSweepModeEnum(SweepMode sweepMode) { this.sweepMode = sweepMode; } + @Override + public int getSweepMode() { + return sweepMode.ordinal(); + } + + @Override + public void setSweepMode(int mode) { + SweepMode[] values = SweepMode.values(); + this.sweepMode = mode >= 0 && mode < values.length ? values[mode] : SweepMode.SWIPE; + } + public float getSweepSpeed() { return sweepSpeed; } @@ -281,28 +287,12 @@ public class AbilityCutter extends Ability { this.sweepSpeed = sweepSpeed; } - public int getStunDuration() { - return stunDuration; + public int getSpinDurationTicks() { + return spinDurationTicks; } - public void setStunDuration(int stunDuration) { - this.stunDuration = stunDuration; - } - - public int getPoisonDurationSeconds() { - return poisonDurationSeconds; - } - - public void setPoisonDurationSeconds(int poisonDurationSeconds) { - this.poisonDurationSeconds = poisonDurationSeconds; - } - - public int getPoisonLevel() { - return poisonLevel; - } - - public void setPoisonLevel(int poisonLevel) { - this.poisonLevel = poisonLevel; + public void setSpinDurationTicks(int spinDurationTicks) { + this.spinDurationTicks = Math.max(1, spinDurationTicks); } public boolean isPiercing() { @@ -324,4 +314,26 @@ public class AbilityCutter extends Ability { public float getCurrentRotation() { return currentRotation; } + + @SideOnly(Side.CLIENT) + @Override + public List getFieldDefinitions() { + return Arrays.asList( + FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage).column(ColumnHint.LEFT), + FieldDef.floatField("gui.range", this::getRange, this::setRange).column(ColumnHint.RIGHT), + FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback), + FieldDef.section("ability.section.sweep"), + FieldDef.enumField("ability.sweepMode", SweepMode.class, this::getSweepModeEnum, this::setSweepModeEnum) + .hover("ability.hover.sweepMode"), + FieldDef.floatField("ability.arcAngle", this::getArcAngle, this::setArcAngle).column(ColumnHint.LEFT), + FieldDef.floatField("ability.innerRadius", this::getInnerRadius, this::setInnerRadius).column(ColumnHint.RIGHT), + FieldDef.floatField("ability.sweepSpeed", this::getSweepSpeed, this::setSweepSpeed), + FieldDef.intField("ability.spinDuration", this::getSpinDurationTicks, this::setSpinDurationTicks) + .range(1, 1000) + .visibleWhen(() -> this.getSweepModeEnum() == SweepMode.SPIN), + FieldDef.boolField("ability.piercing", this::isPiercing, this::setPiercing) + .hover("ability.hover.piercing"), + AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects) + ); + } } 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 4edc18f82..b045620a4 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 @@ -3,31 +3,46 @@ package kamkeel.npcs.controllers.data.ability.type; 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.LockMovementType; import kamkeel.npcs.controllers.data.ability.TargetingMode; import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.util.AxisAlignedBB; +import net.minecraft.util.DamageSource; import net.minecraft.util.Vec3; import net.minecraft.world.World; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityDash; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.entity.EntityNPCInterface; +import noppes.npcs.client.gui.builder.ColumnHint; +import noppes.npcs.client.gui.builder.FieldDef; +import noppes.npcs.api.ability.type.IAbilityDash; + +import java.util.Arrays; +import java.util.List; import java.util.Random; /** * Dash ability: Quick evasive sidestep with NO telegraph. * Defensive repositioning move to evade attacks. */ -public class AbilityDash extends Ability { +public class AbilityDash extends Ability implements IAbilityDash { /** * Dash behavior mode. */ public enum DashMode { AGGRESSIVE, - DEFENSIVE + DEFENSIVE; + + @Override + public String toString() { + switch (this) { + case AGGRESSIVE: return "ability.dash.aggressive"; + case DEFENSIVE: return "ability.dash.defensive"; + default: return name(); + } + } } /** @@ -86,11 +101,9 @@ public class AbilityDash extends Ability { this.targetingMode = TargetingMode.AGGRO_TARGET; this.maxRange = 20.0f; this.minRange = 0.0f; - this.lockMovement = false; - this.cooldownTicks = 40; + this.lockMovement = LockMovementType.NO; + this.cooldownTicks = 0; this.windUpTicks = 5; - this.activeTicks = 8; - this.recoveryTicks = 10; // No telegraph for dash - it's a quick evasive move this.telegraphType = TelegraphType.NONE; this.showTelegraph = false; @@ -98,18 +111,6 @@ public class AbilityDash extends Ability { this.activeSound = "mob.endermen.portal"; } - @Override - public boolean hasTypeSettings() { - return true; - } - - @Override - @SideOnly(Side.CLIENT) - public SubGuiAbilityConfig createConfigGui( - IAbilityConfigCallback callback) { - return new SubGuiAbilityDash(this, callback); - } - @Override public boolean isTargetingModeLocked() { return true; @@ -126,10 +127,10 @@ public class AbilityDash extends Ability { } @Override - public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) { - startX = npc.posX; - startY = npc.posY; - startZ = npc.posZ; + public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { + startX = caster.posX; + startY = caster.posY; + startZ = caster.posZ; // Choose random direction based on mode DashDirection[] directions = dashMode == DashMode.AGGRESSIVE @@ -140,11 +141,11 @@ public class AbilityDash extends Ability { // Calculate dash direction relative to target float baseYaw; if (target != null) { - double dx = target.posX - npc.posX; - double dz = target.posZ - npc.posZ; + double dx = target.posX - caster.posX; + double dz = target.posZ - caster.posZ; baseYaw = (float) Math.toDegrees(Math.atan2(-dx, dz)); } else { - baseYaw = npc.rotationYaw; + baseYaw = caster.rotationYaw; } // Apply direction offset @@ -160,69 +161,130 @@ public class AbilityDash extends Ability { } @Override - public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { + public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { if (dashDirection == null) return; // Calculate distance traveled double distanceTraveled = Math.sqrt( - Math.pow(npc.posX - startX, 2) + - Math.pow(npc.posZ - startZ, 2) + Math.pow(caster.posX - startX, 2) + + Math.pow(caster.posZ - startZ, 2) ); // Check if reached max distance if (distanceTraveled >= dashDistance) { - npc.motionX = 0; - npc.motionZ = 0; - npc.velocityChanged = true; + caster.motionX = 0; + caster.motionZ = 0; + caster.velocityChanged = true; + signalCompletion(); return; } - if (isDashBlocked(npc)) { - npc.motionX = 0; - npc.motionZ = 0; - npc.velocityChanged = true; + if (isDashBlocked(caster)) { + caster.motionX = 0; + caster.motionZ = 0; + caster.velocityChanged = true; + signalCompletion(); return; } - // Move NPC - npc.motionX = dashDirection.xCoord * dashSpeed; - npc.motionY = 0; - npc.motionZ = dashDirection.zCoord * dashSpeed; - npc.velocityChanged = true; + // Move caster + caster.motionX = dashDirection.xCoord * dashSpeed; + caster.motionY = 0; + caster.motionZ = dashDirection.zCoord * dashSpeed; + caster.velocityChanged = true; // Trail particles - world.spawnParticle("smoke", npc.posX, npc.posY + 0.5, npc.posZ, 0, 0, 0); + world.spawnParticle("smoke", caster.posX, caster.posY + 0.5, caster.posZ, 0, 0, 0); } @Override - public void onComplete(EntityNPCInterface npc, EntityLivingBase target) { - npc.motionX = 0; - npc.motionZ = 0; - npc.velocityChanged = true; + public void onComplete(EntityLivingBase caster, EntityLivingBase target) { + caster.motionX = 0; + caster.motionZ = 0; + caster.velocityChanged = true; + } + + @Override + public void onInterrupt(EntityLivingBase caster, DamageSource source, float damage) { + caster.motionX = 0; + caster.motionZ = 0; + caster.velocityChanged = true; + } + + @Override + public void cleanup() { dashDirection = null; chosenDirection = null; } - @Override - public void onInterrupt(EntityNPCInterface npc, net.minecraft.util.DamageSource source, float damage) { - npc.motionX = 0; - npc.motionZ = 0; - npc.velocityChanged = true; - dashDirection = null; - chosenDirection = null; - } - - private boolean isDashBlocked(EntityNPCInterface npc) { + private boolean isDashBlocked(EntityLivingBase caster) { + if (dashDirection == null) return true; double nextX = dashDirection.xCoord * dashSpeed; double nextZ = dashDirection.zCoord * dashSpeed; - return !npc.worldObj.getCollidingBoundingBoxes(npc, npc.boundingBox.copy().offset(nextX, 0, nextZ)).isEmpty(); + return !caster.worldObj.getCollidingBoundingBoxes(caster, caster.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 - public void reset() { - super.reset(); - dashDirection = null; - chosenDirection = null; + @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 @@ -249,14 +311,25 @@ public class AbilityDash extends Ability { } // Getters & Setters - public DashMode getDashMode() { + public DashMode getDashModeEnum() { return dashMode; } - public void setDashMode(DashMode dashMode) { + public void setDashModeEnum(DashMode dashMode) { this.dashMode = dashMode; } + @Override + public int getDashMode() { + return dashMode.ordinal(); + } + + @Override + public void setDashMode(int mode) { + DashMode[] values = DashMode.values(); + this.dashMode = mode >= 0 && mode < values.length ? values[mode] : DashMode.AGGRESSIVE; + } + public float getDashDistance() { return dashDistance; } @@ -276,4 +349,15 @@ public class AbilityDash extends Ability { public DashDirection getChosenDirection() { return chosenDirection; } + + @SideOnly(Side.CLIENT) + @Override + public List getFieldDefinitions() { + return Arrays.asList( + FieldDef.enumField("ability.dashMode", DashMode.class, this::getDashModeEnum, this::setDashModeEnum) + .hover("ability.hover.dashMode"), + FieldDef.floatField("ability.dashDistance", this::getDashDistance, this::setDashDistance).column(ColumnHint.LEFT), + FieldDef.floatField("ability.dashSpeed", this::getDashSpeed, this::setDashSpeed).column(ColumnHint.RIGHT) + ); + } } 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 new file mode 100644 index 000000000..a7d00eb90 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java @@ -0,0 +1,329 @@ +package kamkeel.npcs.controllers.data.ability.type; + +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.AnchorPoint; +import kamkeel.npcs.controllers.data.ability.LockMovementType; +import kamkeel.npcs.controllers.data.ability.TargetingMode; +import kamkeel.npcs.controllers.data.ability.data.*; +import kamkeel.npcs.controllers.data.telegraph.Telegraph; +import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; +import kamkeel.npcs.controllers.data.telegraph.TelegraphType; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.util.AnchorPointHelper; +import net.minecraft.entity.Entity; +import net.minecraft.entity.EntityLivingBase; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.util.Vec3; +import net.minecraft.world.World; +import noppes.npcs.client.gui.builder.ColumnHint; +import noppes.npcs.client.gui.builder.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.AbilityFieldDefs; +import noppes.npcs.api.ability.type.IAbilityDisc; + +import java.util.Arrays; +import java.util.List; +import noppes.npcs.entity.EntityNPCInterface; + +/** + * Disc ability: Spawns a flat spinning disc projectile. + * Has optional boomerang behavior to return to owner. + */ +public class AbilityDisc extends Ability implements IAbilityDisc { + + // Disc geometry + private float discRadius = 1.0f; + private float discThickness = 0.2f; + + // Boomerang properties + private boolean boomerang = false; + private int boomerangDelay = 40; + + // Energy data classes + private EnergyColorData colorData = new EnergyColorData(0xFFFFFF, 0xFF8800, true, 0.4f, 0.5f, 5.0f); + private EnergyCombatData combatData = new EnergyCombatData(8.0f, 1.2f, 0.15f, false, 3.0f, 0.5f); + private EnergyHomingData homingData = new EnergyHomingData(0.6f, true, 0.12f, 18.0f); + private EnergyLightningData lightningData = new EnergyLightningData(); + private EnergyLifespanData lifespanData = new EnergyLifespanData(35.0f, 200); + private EnergyAnchorData anchorData = new EnergyAnchorData(AnchorPoint.RIGHT_HAND); + + // Transient state for disc entity (used during windup charging) + private transient EntityAbilityDisc discEntity = null; + + public AbilityDisc() { + this.typeId = "ability.cnpc.disc"; + this.name = "Disc"; + this.targetingMode = TargetingMode.AGGRO_TARGET; + this.maxRange = 30.0f; + this.minRange = 5.0f; + this.cooldownTicks = 0; + this.windUpTicks = 60; + this.lockMovement = LockMovementType.WINDUP; + this.telegraphType = TelegraphType.CIRCLE; + this.showTelegraph = true; + // Default built-in animations + this.windUpAnimationName = "Ability_Disc_Windup"; + this.activeAnimationName = "Ability_Disc_Active"; + } + + @Override + public boolean isTargetingModeLocked() { + return true; + } + + @Override + public TargetingMode[] getAllowedTargetingModes() { + return new TargetingMode[]{TargetingMode.AGGRO_TARGET}; + } + + @Override + public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { + if (world.isRemote) { + signalCompletion(); + return; + } + + // Start moving the disc that was spawned during windup + if (discEntity != null && !discEntity.isDead) { + discEntity.startMoving(target); + } + + // Ability stays active until entity dies (prevents firing another while projectile is alive) + // Movement locking is handled separately by the base class + } + + @Override + public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { + if (world.isRemote) return; + + // Spawn disc in charging mode on first tick of windup + if (tick == 1) { + // Create disc in charging mode - follows caster based on anchor point during windup + Vec3 spawnPos = AnchorPointHelper.calculateAnchorPosition(caster, anchorData); + discEntity = new EntityAbilityDisc( + world, caster, target, + spawnPos.xCoord, spawnPos.yCoord, spawnPos.zCoord, + discRadius, discThickness, + colorData, combatData, homingData, lightningData, lifespanData, + boomerang, boomerangDelay); + discEntity.setupCharging(anchorData, windUpTicks); + + discEntity.setEffects(this.effects); + world.spawnEntityInWorld(discEntity); + } + } + + @Override + public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { + // Signal completion when entity dies + if (discEntity == null || discEntity.isDead) { + discEntity = null; + signalCompletion(); + } + } + + @Override + public void onComplete(EntityLivingBase caster, EntityLivingBase target) { + } + + @Override + public void cleanup() { + // Despawn disc entity if still alive + if (discEntity != null && !discEntity.isDead) { + discEntity.setDead(); + } + discEntity = null; + } + + @Override + public TelegraphInstance createTelegraph(EntityLivingBase caster, EntityLivingBase target) { + if (!showTelegraph || telegraphType == TelegraphType.NONE || target == null) { + return null; + } + + // Create circle telegraph at target position + Telegraph telegraph = Telegraph.circle(discRadius * 1.5f); + telegraph.setDurationTicks(windUpTicks); + telegraph.setColor(windUpColor); + telegraph.setWarningColor(activeColor); + telegraph.setWarningStartTick(Math.max(5, windUpTicks / 4)); + telegraph.setHeightOffset(telegraphHeightOffset); + + // Position at target and follow target during windup + TelegraphInstance instance = new TelegraphInstance(telegraph, target.posX, target.posY, target.posZ, caster.rotationYaw); + instance.setCasterEntityId(caster.getEntityId()); + instance.setEntityIdToFollow(target.getEntityId()); + + return instance; + } + + @Override + public float getTelegraphRadius() { + return discRadius * 1.5f; + } + + @Override + public void writeTypeNBT(NBTTagCompound nbt) { + nbt.setFloat("discRadius", discRadius); + nbt.setFloat("discThickness", discThickness); + nbt.setBoolean("boomerang", boomerang); + nbt.setInteger("boomerangDelay", boomerangDelay); + anchorData.writeNBT(nbt); + colorData.writeNBT(nbt); + combatData.writeNBT(nbt); + homingData.writeNBT(nbt); + lightningData.writeNBT(nbt); + lifespanData.writeNBT(nbt); + } + + @Override + public void readTypeNBT(NBTTagCompound nbt) { + this.discRadius = nbt.hasKey("discRadius") ? nbt.getFloat("discRadius") : 1.0f; + this.discThickness = nbt.hasKey("discThickness") ? nbt.getFloat("discThickness") : 0.2f; + this.boomerang = nbt.hasKey("boomerang") && nbt.getBoolean("boomerang"); + this.boomerangDelay = nbt.hasKey("boomerangDelay") ? nbt.getInteger("boomerangDelay") : 40; + anchorData.readNBT(nbt); + colorData.readNBT(nbt); + combatData.readNBT(nbt); + homingData.readNBT(nbt); + lightningData.readNBT(nbt); + lifespanData.readNBT(nbt); + } + + // Getters & Setters + public float getSpeed() { return homingData.speed; } + public void setSpeed(float speed) { homingData.speed = speed; } + public float getDiscRadius() { return discRadius; } + public void setDiscRadius(float discRadius) { this.discRadius = discRadius; } + public float getDiscThickness() { return discThickness; } + public void setDiscThickness(float discThickness) { this.discThickness = discThickness; } + public float getMaxDistance() { return lifespanData.maxDistance; } + public void setMaxDistance(float maxDistance) { lifespanData.maxDistance = maxDistance; } + public int getMaxLifetime() { return lifespanData.maxLifetime; } + public void setMaxLifetime(int maxLifetime) { lifespanData.maxLifetime = maxLifetime; } + public float getDamage() { return combatData.damage; } + public void setDamage(float damage) { combatData.damage = damage; } + public float getKnockback() { return combatData.knockback; } + public void setKnockback(float knockback) { combatData.knockback = knockback; } + public float getKnockbackUp() { return combatData.knockbackUp; } + public void setKnockbackUp(float knockbackUp) { combatData.knockbackUp = knockbackUp; } + public boolean isHoming() { return homingData.homing; } + public void setHoming(boolean homing) { homingData.homing = homing; } + public float getHomingStrength() { return homingData.homingStrength; } + public void setHomingStrength(float homingStrength) { homingData.homingStrength = homingStrength; } + public float getHomingRange() { return homingData.homingRange; } + public void setHomingRange(float homingRange) { homingData.homingRange = homingRange; } + public boolean isBoomerang() { return boomerang; } + public void setBoomerang(boolean boomerang) { this.boomerang = boomerang; } + public int getBoomerangDelay() { return boomerangDelay; } + public void setBoomerangDelay(int boomerangDelay) { this.boomerangDelay = boomerangDelay; } + public boolean isExplosive() { return combatData.explosive; } + public void setExplosive(boolean explosive) { combatData.explosive = explosive; } + public float getExplosionRadius() { return combatData.explosionRadius; } + public void setExplosionRadius(float explosionRadius) { combatData.explosionRadius = explosionRadius; } + public float getExplosionDamageFalloff() { return combatData.explosionDamageFalloff; } + public void setExplosionDamageFalloff(float explosionDamageFalloff) { combatData.explosionDamageFalloff = explosionDamageFalloff; } + public int getInnerColor() { return colorData.innerColor; } + public void setInnerColor(int innerColor) { colorData.innerColor = innerColor; } + public int getOuterColor() { return colorData.outerColor; } + public void setOuterColor(int outerColor) { colorData.outerColor = outerColor; } + public float getOuterColorWidth() { return colorData.outerColorWidth; } + public void setOuterColorWidth(float outerColorWidth) { colorData.outerColorWidth = outerColorWidth; } + public float getOuterColorAlpha() { return colorData.outerColorAlpha; } + public void setOuterColorAlpha(float outerColorAlpha) { colorData.outerColorAlpha = outerColorAlpha; } + public boolean isOuterColorEnabled() { return colorData.outerColorEnabled; } + public void setOuterColorEnabled(boolean outerColorEnabled) { colorData.outerColorEnabled = outerColorEnabled; } + public float getRotationSpeed() { return colorData.rotationSpeed; } + public void setRotationSpeed(float rotationSpeed) { colorData.rotationSpeed = rotationSpeed; } + public boolean hasLightningEffect() { return lightningData.lightningEffect; } + public void setLightningEffect(boolean lightningEffect) { lightningData.lightningEffect = lightningEffect; } + public float getLightningDensity() { return lightningData.lightningDensity; } + public void setLightningDensity(float lightningDensity) { lightningData.lightningDensity = lightningDensity; } + public float getLightningRadius() { return lightningData.lightningRadius; } + public void setLightningRadius(float lightningRadius) { lightningData.lightningRadius = lightningRadius; } + public AnchorPoint getAnchorPointEnum() { return anchorData.anchorPoint; } + public float getAnchorOffsetX() { return anchorData.anchorOffsetX; } + public float getAnchorOffsetY() { return anchorData.anchorOffsetY; } + public float getAnchorOffsetZ() { return anchorData.anchorOffsetZ; } + public void setAnchorPointEnum(AnchorPoint anchorPoint) { this.anchorData.anchorPoint = anchorPoint; } + public void setAnchorOffsetX(float x) { this.anchorData.anchorOffsetX = x; } + public void setAnchorOffsetY(float y) { this.anchorData.anchorOffsetY = y; } + public void setAnchorOffsetZ(float z) { this.anchorData.anchorOffsetZ = z; } + + @Override + public int getAnchorPoint() { return anchorData.anchorPoint.ordinal(); } + + @Override + public void setAnchorPoint(int point) { this.anchorData.anchorPoint = AnchorPoint.fromOrdinal(point); } + + @Override + @SideOnly(Side.CLIENT) + public Entity createPreviewEntity(EntityNPCInterface npc) { + if (npc == null || npc.worldObj == null) return null; + + EntityAbilityDisc disc = new EntityAbilityDisc(npc.worldObj); + disc.setupPreview(npc, discRadius, discThickness, colorData, lightningData, anchorData, windUpTicks); + return disc; + } + + @Override + public int getPreviewActiveDuration() { + return lifespanData.maxLifetime > 0 ? Math.min(lifespanData.maxLifetime, 100) : 100; + } + + @SideOnly(Side.CLIENT) + @Override + public List getFieldDefinitions() { + return Arrays.asList( + // Type tab + FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage).column(ColumnHint.LEFT), + FieldDef.floatField("stats.speed", this::getSpeed, this::setSpeed).column(ColumnHint.RIGHT), + FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback), + FieldDef.section("ability.section.disc"), + FieldDef.floatField("gui.radius", this::getDiscRadius, this::setDiscRadius).column(ColumnHint.LEFT), + FieldDef.floatField("gui.thickness", this::getDiscThickness, this::setDiscThickness).column(ColumnHint.RIGHT), + FieldDef.floatField("ability.maxDistance", this::getMaxDistance, this::setMaxDistance).column(ColumnHint.LEFT), + FieldDef.intField("ability.lifetime", this::getMaxLifetime, this::setMaxLifetime).column(ColumnHint.RIGHT), + FieldDef.section("ability.section.homing"), + FieldDef.boolField("gui.enabled", this::isHoming, this::setHoming) + .hover("ability.hover.homing"), + FieldDef.floatField("gui.strength", this::getHomingStrength, this::setHomingStrength) + .visibleWhen(this::isHoming), + FieldDef.section("ability.section.boomerang"), + FieldDef.boolField("gui.enabled", this::isBoomerang, this::setBoomerang) + .hover("ability.hover.boomerang"), + FieldDef.intField("gui.delay", this::getBoomerangDelay, this::setBoomerangDelay) + .visibleWhen(this::isBoomerang), + FieldDef.section("ability.section.explosive"), + FieldDef.boolField("gui.enabled", this::isExplosive, this::setExplosive) + .hover("ability.hover.explosive"), + FieldDef.floatField("gui.radius", this::getExplosionRadius, this::setExplosionRadius) + .visibleWhen(this::isExplosive), + AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects), + // Visual tab + FieldDef.enumField("ability.anchorPoint", AnchorPoint.class, this::getAnchorPointEnum, this::setAnchorPointEnum) + .tab("ability.tab.visual"), + FieldDef.section("ability.section.colors").tab("ability.tab.visual"), + FieldDef.colorSubGui("ability.innerColor", this::getInnerColor, this::setInnerColor) + .tab("ability.tab.visual"), + FieldDef.boolField("ability.outerEnabled", this::isOuterColorEnabled, this::setOuterColorEnabled) + .tab("ability.tab.visual"), + FieldDef.colorSubGui("ability.outerColor", this::getOuterColor, this::setOuterColor) + .tab("ability.tab.visual").visibleWhen(this::isOuterColorEnabled), + FieldDef.floatField("ability.outerWidth", this::getOuterColorWidth, this::setOuterColorWidth) + .tab("ability.tab.visual").visibleWhen(this::isOuterColorEnabled).column(ColumnHint.LEFT), + FieldDef.floatField("ability.outerAlpha", this::getOuterColorAlpha, this::setOuterColorAlpha) + .tab("ability.tab.visual").range(0, 1).visibleWhen(this::isOuterColorEnabled).column(ColumnHint.RIGHT), + FieldDef.section("ability.section.effects").tab("ability.tab.visual"), + FieldDef.floatField("ability.rotationSpeed", this::getRotationSpeed, this::setRotationSpeed) + .tab("ability.tab.visual"), + FieldDef.boolField("ability.lightning", this::hasLightningEffect, this::setLightningEffect) + .tab("ability.tab.visual"), + FieldDef.floatField("gui.density", this::getLightningDensity, this::setLightningDensity) + .tab("ability.tab.visual").range(0.01f, 5.0f).visibleWhen(this::hasLightningEffect).column(ColumnHint.LEFT), + FieldDef.floatField("gui.radius", this::getLightningRadius, this::setLightningRadius) + .tab("ability.tab.visual").range(0.1f, 10.0f).visibleWhen(this::hasLightningEffect).column(ColumnHint.RIGHT) + ); + } +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDiscDual.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDiscDual.java new file mode 100644 index 000000000..b7b3383c0 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDiscDual.java @@ -0,0 +1,502 @@ +package kamkeel.npcs.controllers.data.ability.type; + +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.AnchorPoint; +import kamkeel.npcs.controllers.data.ability.LockMovementType; +import kamkeel.npcs.controllers.data.ability.TargetingMode; +import kamkeel.npcs.controllers.data.ability.data.*; +import noppes.npcs.client.gui.builder.ColumnHint; +import noppes.npcs.client.gui.builder.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.AbilityFieldDefs; +import kamkeel.npcs.controllers.data.telegraph.Telegraph; +import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; +import kamkeel.npcs.controllers.data.telegraph.TelegraphType; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.util.AnchorPointHelper; +import net.minecraft.entity.Entity; +import net.minecraft.entity.EntityLivingBase; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.util.Vec3; +import net.minecraft.world.World; +import noppes.npcs.entity.EntityNPCInterface; +import noppes.npcs.util.ValueUtil; + +import java.util.Arrays; +import java.util.List; + +public class AbilityDiscDual extends Ability { + + // Disc geometry + private float discRadius = 1.0f; + private float discThickness = 0.2f; + + // Boomerang properties + private boolean boomerang = false; + private int boomerangDelay = 40; + + // Dual properties + private boolean dualFire = true; + private int dualFireDelay = 10; + + // Energy data classes + private EnergyColorData[] colorData = new EnergyColorData[]{ + new EnergyColorData(0xFFFFFF, 0xFF8800, true, 0.4f, 0.5f, 5.0f), + new EnergyColorData(0xFFFFFF, 0x8800FF, true, 0.4f, 0.5f, 5.0f) + }; + private EnergyCombatData combatData = new EnergyCombatData(8.0f, 1.2f, 0.15f, false, 3.0f, 0.5f); + private EnergyHomingData homingData = new EnergyHomingData(0.6f, true, 0.12f, 18.0f); + public final EnergyLightningData[] lightningData = new EnergyLightningData[]{ + new EnergyLightningData(), new EnergyLightningData() + }; + private EnergyLifespanData lifespanData = new EnergyLifespanData(35.0f, 200); + private EnergyAnchorData[] anchorData = new EnergyAnchorData[]{ + new EnergyAnchorData(AnchorPoint.LEFT_HAND), new EnergyAnchorData(AnchorPoint.RIGHT_HAND) + }; + + // Transient state for disc entity (used during windup charging) + private transient EntityAbilityDisc discEntity = null; + private transient EntityAbilityDisc discEntity2 = null; + + public AbilityDiscDual() { + this.typeId = "ability.cnpc.disc_dual"; + this.name = "Disc Dual"; + this.targetingMode = TargetingMode.AGGRO_TARGET; + this.maxRange = 30.0f; + this.minRange = 5.0f; + this.cooldownTicks = 0; + this.windUpTicks = 60; + this.lockMovement = LockMovementType.WINDUP; + this.telegraphType = TelegraphType.CIRCLE; + this.showTelegraph = true; + // Default built-in animations + this.windUpAnimationName = "Ability_DiscDual_Windup"; + this.activeAnimationName = "Ability_DiscDual_Active"; + } + + @Override + public boolean isTargetingModeLocked() { + return true; + } + + @Override + public TargetingMode[] getAllowedTargetingModes() { + return new TargetingMode[]{TargetingMode.AGGRO_TARGET}; + } + + @Override + public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { + if (world.isRemote) { + signalCompletion(); + return; + } + + // Start moving the orb that was spawned during windup + if (discEntity != null && !discEntity.isDead) { + discEntity.startMoving(target); + } + + if (dualFire) { + if (discEntity2 != null && !discEntity2.isDead && dualFireDelay <= 0) { + discEntity2.startMoving(target); + } + } else { + discEntity2.setDead(); + } + + // Ability stays active until entity dies (prevents firing another while projectile is alive) + // Movement locking is handled separately by the base class + } + + @Override + public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { + if (world.isRemote) return; + + // Spawn disc in charging mode on first tick of windup + if (tick == 1) { + // Create disc in charging mode - follows caster based on anchor point during windup + Vec3 spawnPos1 = AnchorPointHelper.calculateAnchorPosition(caster, anchorData[0]); + discEntity = new EntityAbilityDisc( + world, caster, target, + spawnPos1.xCoord, spawnPos1.yCoord, spawnPos1.zCoord, + discRadius, discThickness, + colorData[0], combatData, homingData, lightningData[0], lifespanData, + boomerang, boomerangDelay); + + Vec3 spawnPos2 = AnchorPointHelper.calculateAnchorPosition(caster, anchorData[1]); + discEntity2 = new EntityAbilityDisc( + world, caster, target, + spawnPos2.xCoord, spawnPos2.yCoord, spawnPos2.zCoord, + discRadius, discThickness, + colorData[1], combatData, homingData, lightningData[1], lifespanData, + boomerang, boomerangDelay); + + discEntity.setSiblingUUID(discEntity2.getPersistentID()); + discEntity2.setSiblingUUID(discEntity.getPersistentID()); + + discEntity.setupCharging(anchorData[0], windUpTicks); + discEntity2.setupCharging(anchorData[1], windUpTicks); + + discEntity.setEffects(this.effects); + + if (dualFire) { + discEntity2.setEffects(this.effects); + } + + world.spawnEntityInWorld(discEntity); + world.spawnEntityInWorld(discEntity2); + } + } + + @Override + public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { + // Signal completion when entity dies + if (dualFire) { + if ((discEntity == null || discEntity.isDead) && (discEntity2 == null || discEntity2.isDead)) { + discEntity = null; + discEntity2 = null; + signalCompletion(); + } + + if (tick == dualFireDelay && discEntity2 != null && !discEntity2.isDead) { + discEntity2.startMoving(target); + } + } else { + if (discEntity == null || discEntity.isDead) { + discEntity = null; + signalCompletion(); + } + } + } + + @Override + public void onComplete(EntityLivingBase caster, EntityLivingBase target) { + } + + @Override + public void cleanup() { + // Despawn disc entity if still alive + if (discEntity != null && !discEntity.isDead) { + discEntity.setDead(); + } + + if (discEntity2 != null && !discEntity2.isDead) { + discEntity2.setDead(); + } + + discEntity = null; + discEntity2 = null; + } + + @Override + public TelegraphInstance createTelegraph(EntityLivingBase caster, EntityLivingBase target) { + if (!showTelegraph || telegraphType == TelegraphType.NONE || target == null) { + return null; + } + + // Create circle telegraph at target position + Telegraph telegraph = Telegraph.circle(discRadius * 1.5f); + telegraph.setDurationTicks(windUpTicks); + telegraph.setColor(windUpColor); + telegraph.setWarningColor(activeColor); + telegraph.setWarningStartTick(Math.max(5, windUpTicks / 4)); + telegraph.setHeightOffset(telegraphHeightOffset); + + // Position at target and follow target during windup + TelegraphInstance instance = new TelegraphInstance(telegraph, target.posX, target.posY, target.posZ, caster.rotationYaw); + instance.setCasterEntityId(caster.getEntityId()); + instance.setEntityIdToFollow(target.getEntityId()); + + return instance; + } + + @Override + public float getTelegraphRadius() { + return discRadius * 1.5f; + } + + @Override + public void writeTypeNBT(NBTTagCompound nbt) { + nbt.setFloat("discRadius", discRadius); + nbt.setFloat("discThickness", discThickness); + nbt.setBoolean("boomerang", boomerang); + nbt.setInteger("boomerangDelay", boomerangDelay); + nbt.setBoolean("dualFire", dualFire); + nbt.setInteger("dualFireDelay", dualFireDelay); + combatData.writeNBT(nbt); + homingData.writeNBT(nbt); + lifespanData.writeNBT(nbt); + + NBTTagCompound disc1 = new NBTTagCompound(); + NBTTagCompound disc2 = new NBTTagCompound(); + + anchorData[0].writeNBT(disc1); + anchorData[1].writeNBT(disc2); + colorData[0].writeNBT(disc1); + colorData[1].writeNBT(disc2); + lightningData[0].writeNBT(disc1); + lightningData[1].writeNBT(disc2); + + nbt.setTag("Disc_1", disc1); + nbt.setTag("Disc_2", disc2); + } + + @Override + public void readTypeNBT(NBTTagCompound nbt) { + NBTTagCompound disc1 = nbt.hasKey("Disc_1") ? nbt.getCompoundTag("Disc_1") : new NBTTagCompound(); + NBTTagCompound disc2 = nbt.hasKey("Disc_2") ? nbt.getCompoundTag("Disc_2") : new NBTTagCompound(); + + this.discRadius = nbt.hasKey("discRadius") ? nbt.getFloat("discRadius") : 1.0f; + this.discThickness = nbt.hasKey("discThickness") ? nbt.getFloat("discThickness") : 0.2f; + this.boomerang = nbt.hasKey("boomerang") && nbt.getBoolean("boomerang"); + this.boomerangDelay = nbt.hasKey("boomerangDelay") ? nbt.getInteger("boomerangDelay") : 40; + this.dualFire = !nbt.hasKey("dualFire") || nbt.getBoolean("dualFire"); + this.dualFireDelay = nbt.hasKey("dualFireDelay") ? nbt.getInteger("dualFireDelay") : 0; + combatData.readNBT(nbt); + homingData.readNBT(nbt); + lifespanData.readNBT(nbt); + + anchorData[0].readNBT(disc1); + anchorData[1].readNBT(disc2); + colorData[0].readNBT(disc1); + colorData[1].readNBT(disc2); + lightningData[0].readNBT(disc1); + lightningData[1].readNBT(disc2); + } + + // Getters & Setters + private EnergyColorData getColorData(int orb) { + orb = ValueUtil.clamp(orb, 0, 1); + return colorData[orb]; + } + private EnergyLightningData getLightningData(int orb) { + orb = ValueUtil.clamp(orb, 0, 1); + return lightningData[orb]; + } + private EnergyAnchorData getAnchorData(int orb) { + orb = ValueUtil.clamp(orb, 0, 1); + return anchorData[orb]; + } + + public boolean isDualFire() { return dualFire; } + public void setDualFire(boolean dualFire) { this.dualFire = dualFire; } + public int getDualFireDelay() { return dualFireDelay; } + public void setDualFireDelay(int dualFireDelay) { this.dualFireDelay = dualFireDelay; } + public float getSpeed() { return homingData.speed; } + public void setSpeed(float speed) { homingData.speed = speed; } + public float getDiscRadius() { return discRadius; } + public void setDiscRadius(float discRadius) { this.discRadius = discRadius; } + public float getDiscThickness() { return discThickness; } + public void setDiscThickness(float discThickness) { this.discThickness = discThickness; } + public float getMaxDistance() { return lifespanData.maxDistance; } + public void setMaxDistance(float maxDistance) { lifespanData.maxDistance = maxDistance; } + public int getMaxLifetime() { return lifespanData.maxLifetime; } + public void setMaxLifetime(int maxLifetime) { lifespanData.maxLifetime = maxLifetime; } + public float getDamage() { return combatData.damage; } + public void setDamage(float damage) { combatData.damage = damage; } + public float getKnockback() { return combatData.knockback; } + public void setKnockback(float knockback) { combatData.knockback = knockback; } + public float getKnockbackUp() { return combatData.knockbackUp; } + public void setKnockbackUp(float knockbackUp) { combatData.knockbackUp = knockbackUp; } + public boolean isHoming() { return homingData.homing; } + public void setHoming(boolean homing) { homingData.homing = homing; } + public float getHomingStrength() { return homingData.homingStrength; } + public void setHomingStrength(float homingStrength) { homingData.homingStrength = homingStrength; } + public float getHomingRange() { return homingData.homingRange; } + public void setHomingRange(float homingRange) { homingData.homingRange = homingRange; } + public boolean isBoomerang() { return boomerang; } + public void setBoomerang(boolean boomerang) { this.boomerang = boomerang; } + public int getBoomerangDelay() { return boomerangDelay; } + public void setBoomerangDelay(int boomerangDelay) { this.boomerangDelay = boomerangDelay; } + public boolean isExplosive() { return combatData.explosive; } + public void setExplosive(boolean explosive) { combatData.explosive = explosive; } + public float getExplosionRadius() { return combatData.explosionRadius; } + public void setExplosionRadius(float explosionRadius) { combatData.explosionRadius = explosionRadius; } + public float getExplosionDamageFalloff() { return combatData.explosionDamageFalloff; } + public void setExplosionDamageFalloff(float explosionDamageFalloff) { combatData.explosionDamageFalloff = explosionDamageFalloff; } + + + public int getInnerColor(int orb) { return getColorData(orb).innerColor; } + public void setInnerColor(int orb, int innerColor) { getColorData(orb).innerColor = innerColor; } + public int getOuterColor(int orb) { return getColorData(orb).outerColor; } + public void setOuterColor(int orb, int outerColor) { getColorData(orb).outerColor = outerColor; } + public boolean isOuterColorEnabled(int orb) { return getColorData(orb).outerColorEnabled; } + public void setOuterColorEnabled(int orb, boolean enabled) { getColorData(orb).outerColorEnabled = enabled; } + public float getOuterColorWidth(int orb) { return getColorData(orb).outerColorWidth; } + public void setOuterColorWidth(int orb, float width) { getColorData(orb).outerColorWidth = width; } + public float getOuterColorAlpha(int orb) { return getColorData(orb).outerColorAlpha; } + public void setOuterColorAlpha(int orb, float alpha) { getColorData(orb).outerColorAlpha = alpha; } + public float getRotationSpeed(int orb) { return getColorData(orb).rotationSpeed; } + public void setRotationSpeed(int orb, float speed) { getColorData(orb).rotationSpeed = speed; } + + public int getInnerColor() { return getInnerColor(0); } + public void setInnerColor(int innerColor) { setInnerColor(0, innerColor); } + public int getOuterColor() { return getOuterColor(0); } + public void setOuterColor(int outerColor) { setOuterColor(0, outerColor); } + public boolean isOuterColorEnabled() { return isOuterColorEnabled(0); } + public void setOuterColorEnabled(boolean enabled) { setOuterColorEnabled(0, enabled); } + public float getOuterColorWidth() { return getOuterColorWidth(0); } + public void setOuterColorWidth(float width) { setOuterColorWidth(0, width); } + public float getOuterColorAlpha() { return getOuterColorAlpha(0); } + public void setOuterColorAlpha(float alpha) { setOuterColorAlpha(0, alpha); } + public float getRotationSpeed() { return getRotationSpeed(0); } + public void setRotationSpeed(float speed) { setRotationSpeed(0, speed); } + + + public boolean hasLightningEffect(int orb) { return getLightningData(orb).lightningEffect; } + public void setLightningEffect(int orb, boolean lightningEffect) { getLightningData(orb).lightningEffect = lightningEffect; } + public float getLightningDensity(int orb) { return getLightningData(orb).lightningDensity; } + public void setLightningDensity(int orb, float lightningDensity) { getLightningData(orb).lightningDensity = lightningDensity; } + public float getLightningRadius(int orb) { return getLightningData(orb).lightningRadius; } + public void setLightningRadius(int orb, float lightningRadius) { getLightningData(orb).lightningRadius = lightningRadius; } + + public boolean hasLightningEffect() { return hasLightningEffect(0); } + public void setLightningEffect(boolean lightningEffect) { setLightningEffect(0, lightningEffect); } + public float getLightningDensity() { return getLightningDensity(0); } + public void setLightningDensity(float lightningDensity) { setLightningDensity(0, lightningDensity); } + public float getLightningRadius() { return getLightningRadius(0); } + public void setLightningRadius(float lightningRadius) { setLightningRadius(0, lightningRadius); } + + + public AnchorPoint getAnchorPointEnum(int orb) { return getAnchorData(orb).anchorPoint; } + public void setAnchorPointEnum(int orb, AnchorPoint anchorPoint) { getAnchorData(orb).anchorPoint = anchorPoint; } + public float getAnchorOffsetX(int orb) { return getAnchorData(orb).anchorOffsetX; } + public void setAnchorOffsetX(int orb, float x) { getAnchorData(orb).anchorOffsetX = x; } + public float getAnchorOffsetY(int orb) { return getAnchorData(orb).anchorOffsetY; } + public void setAnchorOffsetY(int orb, float y) { getAnchorData(orb).anchorOffsetY = y; } + public float getAnchorOffsetZ(int orb) { return getAnchorData(orb).anchorOffsetZ; } + public void setAnchorOffsetZ(int orb, float z) { getAnchorData(orb).anchorOffsetZ = z; } + + public AnchorPoint getAnchorPointEnum() { return getAnchorPointEnum(0); } + public void setAnchorPointEnum(AnchorPoint anchorPoint) { setAnchorPointEnum(0, anchorPoint); } + public float getAnchorOffsetX() { return getAnchorOffsetX(0); } + public void setAnchorOffsetX(float x) { setAnchorOffsetX(0, x); } + public float getAnchorOffsetY() { return getAnchorOffsetY(0); } + public void setAnchorOffsetY(float y) { setAnchorOffsetY(0, y); } + public float getAnchorOffsetZ() { return getAnchorOffsetZ(0); } + public void setAnchorOffsetZ(float z) { setAnchorOffsetZ(0, z); } + + //@Override + public int getAnchorPoint(int orb) { return getAnchorData(orb).anchorPoint.ordinal(); } + public int getAnchorPoint() { return getAnchorData(0).anchorPoint.ordinal(); } + + //@Override + public void setAnchorPoint(int orb, int point) { getAnchorData(orb).anchorPoint = AnchorPoint.fromOrdinal(point); } + public void setAnchorPoint(int point) { getAnchorData(0).anchorPoint = AnchorPoint.fromOrdinal(point); } + + @SideOnly(Side.CLIENT) + @Override + public List getFieldDefinitions() { + return Arrays.asList( + // Type tab + FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage).column(ColumnHint.LEFT), + FieldDef.floatField("stats.speed", this::getSpeed, this::setSpeed).column(ColumnHint.RIGHT), + FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback), + FieldDef.section("ability.section.disc"), + FieldDef.floatField("gui.radius", this::getDiscRadius, this::setDiscRadius).column(ColumnHint.LEFT), + FieldDef.floatField("gui.thickness", this::getDiscThickness, this::setDiscThickness).column(ColumnHint.RIGHT), + FieldDef.floatField("ability.maxDistance", this::getMaxDistance, this::setMaxDistance).column(ColumnHint.LEFT), + FieldDef.intField("ability.lifetime", this::getMaxLifetime, this::setMaxLifetime).column(ColumnHint.RIGHT), + FieldDef.section("ability.section.dual"), + FieldDef.boolField("ability.dualFire", this::isDualFire, this::setDualFire), + FieldDef.intField("ability.dualFireDelay", this::getDualFireDelay, this::setDualFireDelay) + .visibleWhen(this::isDualFire), + FieldDef.section("ability.section.homing"), + FieldDef.boolField("gui.enabled", this::isHoming, this::setHoming) + .hover("ability.hover.homing"), + FieldDef.floatField("gui.strength", this::getHomingStrength, this::setHomingStrength) + .visibleWhen(this::isHoming), + FieldDef.section("ability.section.boomerang"), + FieldDef.boolField("gui.enabled", this::isBoomerang, this::setBoomerang) + .hover("ability.hover.boomerang"), + FieldDef.intField("gui.delay", this::getBoomerangDelay, this::setBoomerangDelay) + .visibleWhen(this::isBoomerang), + FieldDef.section("ability.section.explosive"), + FieldDef.boolField("gui.enabled", this::isExplosive, this::setExplosive) + .hover("ability.hover.explosive"), + FieldDef.floatField("gui.radius", this::getExplosionRadius, this::setExplosionRadius) + .visibleWhen(this::isExplosive), + AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects), + // Visual tab - Disc 1 + FieldDef.enumField("ability.anchorPoint", AnchorPoint.class, + () -> getAnchorPointEnum(0), v -> setAnchorPointEnum(0, v)) + .tab("ability.tab.visual"), + FieldDef.section("ability.section.disc1").tab("ability.tab.visual"), + FieldDef.colorSubGui("ability.innerColor", + () -> getInnerColor(0), v -> setInnerColor(0, v)) + .tab("ability.tab.visual"), + FieldDef.boolField("ability.outerEnabled", + () -> isOuterColorEnabled(0), v -> setOuterColorEnabled(0, v)) + .tab("ability.tab.visual"), + FieldDef.colorSubGui("ability.outerColor", + () -> getOuterColor(0), v -> setOuterColor(0, v)) + .tab("ability.tab.visual").visibleWhen(() -> isOuterColorEnabled(0)), + FieldDef.floatField("ability.outerWidth", + () -> getOuterColorWidth(0), v -> setOuterColorWidth(0, v)) + .tab("ability.tab.visual").visibleWhen(() -> isOuterColorEnabled(0)).column(ColumnHint.LEFT), + FieldDef.floatField("ability.outerAlpha", + () -> getOuterColorAlpha(0), v -> setOuterColorAlpha(0, v)) + .tab("ability.tab.visual").range(0, 1).visibleWhen(() -> isOuterColorEnabled(0)).column(ColumnHint.RIGHT), + FieldDef.floatField("ability.rotationSpeed", + () -> getRotationSpeed(0), v -> setRotationSpeed(0, v)) + .tab("ability.tab.visual"), + FieldDef.boolField("ability.lightning", + () -> hasLightningEffect(0), v -> setLightningEffect(0, v)) + .tab("ability.tab.visual"), + FieldDef.floatField("gui.density", + () -> getLightningDensity(0), v -> setLightningDensity(0, v)) + .tab("ability.tab.visual").range(0.01f, 5.0f).visibleWhen(() -> hasLightningEffect(0)).column(ColumnHint.LEFT), + FieldDef.floatField("gui.radius", + () -> getLightningRadius(0), v -> setLightningRadius(0, v)) + .tab("ability.tab.visual").range(0.1f, 10.0f).visibleWhen(() -> hasLightningEffect(0)).column(ColumnHint.RIGHT), + // Visual tab - Disc 2 + FieldDef.enumField("ability.anchorPoint", AnchorPoint.class, + () -> getAnchorPointEnum(1), v -> setAnchorPointEnum(1, v)) + .tab("ability.tab.visual"), + FieldDef.section("ability.section.disc2").tab("ability.tab.visual"), + FieldDef.colorSubGui("ability.innerColor", + () -> getInnerColor(1), v -> setInnerColor(1, v)) + .tab("ability.tab.visual"), + FieldDef.boolField("ability.outerEnabled", + () -> isOuterColorEnabled(1), v -> setOuterColorEnabled(1, v)) + .tab("ability.tab.visual"), + FieldDef.colorSubGui("ability.outerColor", + () -> getOuterColor(1), v -> setOuterColor(1, v)) + .tab("ability.tab.visual").visibleWhen(() -> isOuterColorEnabled(1)), + FieldDef.floatField("ability.outerWidth", + () -> getOuterColorWidth(1), v -> setOuterColorWidth(1, v)) + .tab("ability.tab.visual").visibleWhen(() -> isOuterColorEnabled(1)).column(ColumnHint.LEFT), + FieldDef.floatField("ability.outerAlpha", + () -> getOuterColorAlpha(1), v -> setOuterColorAlpha(1, v)) + .tab("ability.tab.visual").range(0, 1).visibleWhen(() -> isOuterColorEnabled(1)).column(ColumnHint.RIGHT), + FieldDef.floatField("ability.rotationSpeed", + () -> getRotationSpeed(1), v -> setRotationSpeed(1, v)) + .tab("ability.tab.visual"), + FieldDef.boolField("ability.lightning", + () -> hasLightningEffect(1), v -> setLightningEffect(1, v)) + .tab("ability.tab.visual"), + FieldDef.floatField("gui.density", + () -> getLightningDensity(1), v -> setLightningDensity(1, v)) + .tab("ability.tab.visual").range(0.01f, 5.0f).visibleWhen(() -> hasLightningEffect(1)).column(ColumnHint.LEFT), + FieldDef.floatField("gui.radius", + () -> getLightningRadius(1), v -> setLightningRadius(1, v)) + .tab("ability.tab.visual").range(0.1f, 10.0f).visibleWhen(() -> hasLightningEffect(1)).column(ColumnHint.RIGHT) + ); + } + + @Override + @SideOnly(Side.CLIENT) + public Entity createPreviewEntity(EntityNPCInterface npc) { + if (npc == null || npc.worldObj == null) return null; + + EntityAbilityDisc disc = new EntityAbilityDisc(npc.worldObj); + disc.setupPreview(npc, discRadius, discThickness, colorData[0], lightningData[0], anchorData[0], windUpTicks); + return disc; + } + + @Override + public int getPreviewActiveDuration() { + return lifespanData.maxLifetime > 0 ? Math.min(lifespanData.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 new file mode 100644 index 000000000..2918a4b08 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergyBeam.java @@ -0,0 +1,329 @@ +package kamkeel.npcs.controllers.data.ability.type; + +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.AnchorPoint; +import kamkeel.npcs.controllers.data.ability.LockMovementType; +import kamkeel.npcs.controllers.data.ability.TargetingMode; +import kamkeel.npcs.controllers.data.ability.data.*; +import kamkeel.npcs.controllers.data.telegraph.Telegraph; +import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; +import kamkeel.npcs.controllers.data.telegraph.TelegraphType; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.util.AnchorPointHelper; +import net.minecraft.entity.Entity; +import net.minecraft.entity.EntityLivingBase; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.util.Vec3; +import net.minecraft.world.World; +import noppes.npcs.client.gui.builder.ColumnHint; +import noppes.npcs.client.gui.builder.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.AbilityFieldDefs; +import noppes.npcs.api.ability.type.IAbilityEnergyBeam; + +import java.util.Arrays; +import java.util.List; +import noppes.npcs.entity.EntityNPCInterface; + +/** + * Energy Beam ability: A homing head with a trailing path. + * The beam stays attached to the origin and curves as it homes in on target. + * Cannot cross over itself (self-intersection prevention). + */ +public class AbilityEnergyBeam extends Ability implements IAbilityEnergyBeam { + + // Shape properties (standalone) + private float beamWidth = 0.4f; + private float headSize = 0.6f; + + // Data classes + private EnergyColorData colorData = new EnergyColorData(0xFFFFFF, 0x00AAFF, true, 0.4f, 0.5f, 6.0f); + private EnergyCombatData combatData = new EnergyCombatData(10.0f, 1.5f, 0.2f, false, 4.0f, 0.5f); + private EnergyHomingData homingData = new EnergyHomingData(0.4f, true, 0.1f, 15.0f); + private EnergyLightningData lightningData = new EnergyLightningData(); + private EnergyLifespanData lifespanData = new EnergyLifespanData(25.0f, 200); + private EnergyAnchorData anchorData = new EnergyAnchorData(AnchorPoint.RIGHT_HAND); + + // Transient state for beam entity (used during windup charging) + private transient EntityAbilityBeam beamEntity = null; + + public AbilityEnergyBeam() { + this.typeId = "ability.cnpc.beam"; + this.name = "Beam"; + this.targetingMode = TargetingMode.AGGRO_TARGET; + this.maxRange = 20.0f; + this.minRange = 5.0f; + this.cooldownTicks = 0; + this.windUpTicks = 40; + this.lockMovement = LockMovementType.WINDUP_AND_ACTIVE; + this.telegraphType = TelegraphType.CIRCLE; + this.showTelegraph = true; + // Default built-in animations + this.windUpAnimationName = "Ability_Beam_Windup"; + this.activeAnimationName = "Ability_Beam_Active"; + } + + @Override + public boolean isTargetingModeLocked() { + return true; + } + + @Override + public TargetingMode[] getAllowedTargetingModes() { + return new TargetingMode[]{TargetingMode.AGGRO_TARGET}; + } + + @Override + public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { + if (world.isRemote) { + signalCompletion(); + return; + } + + // Start firing the beam that was spawned during windup + if (beamEntity != null && !beamEntity.isDead) { + beamEntity.startFiring(target); + } + + // Ability stays active until entity dies (prevents firing another while projectile is alive) + // Movement locking is handled separately by the base class + } + + @Override + public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { + if (world.isRemote) return; + + // Spawn beam in charging mode on first tick of windup + if (tick == 1) { + float offsetDist = 1.0f; + + // Create beam in charging mode - follows caster based on anchor point during windup + Vec3 spawnPos = AnchorPointHelper.calculateAnchorPosition(caster, anchorData, offsetDist); + beamEntity = new EntityAbilityBeam( + world, caster, target, + spawnPos.xCoord, spawnPos.yCoord, spawnPos.zCoord, + beamWidth, headSize, + colorData, combatData, homingData, lightningData, lifespanData, + lockMovement.locksActive()); + beamEntity.setupCharging(anchorData, windUpTicks, offsetDist); + + beamEntity.setEffects(this.effects); + world.spawnEntityInWorld(beamEntity); + } + } + + @Override + public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { + // Signal completion when entity dies + if (beamEntity == null || beamEntity.isDead) { + beamEntity = null; + signalCompletion(); + } + } + + @Override + public void onComplete(EntityLivingBase caster, EntityLivingBase target) { + } + + @Override + public void cleanup() { + // Despawn beam entity if still alive + if (beamEntity != null && !beamEntity.isDead) { + beamEntity.setDead(); + } + beamEntity = null; + } + + @Override + public TelegraphInstance createTelegraph(EntityLivingBase caster, EntityLivingBase target) { + if (!showTelegraph || telegraphType == TelegraphType.NONE || target == null) { + return null; + } + + // Create circle telegraph at target position + Telegraph telegraph = Telegraph.circle(headSize * 2.0f); + telegraph.setDurationTicks(windUpTicks); + telegraph.setColor(windUpColor); + telegraph.setWarningColor(activeColor); + telegraph.setWarningStartTick(Math.max(5, windUpTicks / 4)); + telegraph.setHeightOffset(telegraphHeightOffset); + + // Position at target and follow target during windup + TelegraphInstance instance = new TelegraphInstance(telegraph, target.posX, target.posY, target.posZ, caster.rotationYaw); + instance.setCasterEntityId(caster.getEntityId()); + instance.setEntityIdToFollow(target.getEntityId()); + + return instance; + } + + @Override + public float getTelegraphRadius() { + return headSize * 2.0f; + } + + @Override + public void writeTypeNBT(NBTTagCompound nbt) { + nbt.setFloat("beamWidth", beamWidth); + nbt.setFloat("headSize", headSize); + anchorData.writeNBT(nbt); + colorData.writeNBT(nbt); + combatData.writeNBT(nbt); + homingData.writeNBT(nbt); + lightningData.writeNBT(nbt); + lifespanData.writeNBT(nbt); + } + + @Override + public void readTypeNBT(NBTTagCompound nbt) { + this.beamWidth = nbt.hasKey("beamWidth") ? nbt.getFloat("beamWidth") : 0.4f; + this.headSize = nbt.hasKey("headSize") ? nbt.getFloat("headSize") : 0.6f; + anchorData.readNBT(nbt); + colorData.readNBT(nbt); + combatData.readNBT(nbt); + homingData.readNBT(nbt); + lightningData.readNBT(nbt); + lifespanData.readNBT(nbt); + } + + // Getters & Setters - Standalone fields + public float getBeamWidth() { return beamWidth; } + public void setBeamWidth(float beamWidth) { this.beamWidth = beamWidth; } + public float getHeadSize() { return headSize; } + public void setHeadSize(float headSize) { this.headSize = headSize; } + + // Getters & Setters - Color data + public int getInnerColor() { return colorData.innerColor; } + public void setInnerColor(int innerColor) { this.colorData.innerColor = innerColor; } + public int getOuterColor() { return colorData.outerColor; } + public void setOuterColor(int outerColor) { this.colorData.outerColor = outerColor; } + public boolean isOuterColorEnabled() { return colorData.outerColorEnabled; } + public void setOuterColorEnabled(boolean outerColorEnabled) { this.colorData.outerColorEnabled = outerColorEnabled; } + public float getOuterColorWidth() { return colorData.outerColorWidth; } + public void setOuterColorWidth(float outerColorWidth) { this.colorData.outerColorWidth = outerColorWidth; } + public float getOuterColorAlpha() { return colorData.outerColorAlpha; } + public void setOuterColorAlpha(float outerColorAlpha) { this.colorData.outerColorAlpha = outerColorAlpha; } + public float getRotationSpeed() { return colorData.rotationSpeed; } + public void setRotationSpeed(float rotationSpeed) { this.colorData.rotationSpeed = rotationSpeed; } + + // Getters & Setters - Combat data + public float getDamage() { return combatData.damage; } + public void setDamage(float damage) { this.combatData.damage = damage; } + public float getKnockback() { return combatData.knockback; } + public void setKnockback(float knockback) { this.combatData.knockback = knockback; } + public float getKnockbackUp() { return combatData.knockbackUp; } + public void setKnockbackUp(float knockbackUp) { this.combatData.knockbackUp = knockbackUp; } + public boolean isExplosive() { return combatData.explosive; } + public void setExplosive(boolean explosive) { this.combatData.explosive = explosive; } + public float getExplosionRadius() { return combatData.explosionRadius; } + public void setExplosionRadius(float explosionRadius) { this.combatData.explosionRadius = explosionRadius; } + public float getExplosionDamageFalloff() { return combatData.explosionDamageFalloff; } + public void setExplosionDamageFalloff(float explosionDamageFalloff) { this.combatData.explosionDamageFalloff = explosionDamageFalloff; } + + // Getters & Setters - Homing data + public float getSpeed() { return homingData.speed; } + public void setSpeed(float speed) { this.homingData.speed = speed; } + public boolean isHoming() { return homingData.homing; } + public void setHoming(boolean homing) { this.homingData.homing = homing; } + public float getHomingStrength() { return homingData.homingStrength; } + public void setHomingStrength(float homingStrength) { this.homingData.homingStrength = homingStrength; } + public float getHomingRange() { return homingData.homingRange; } + public void setHomingRange(float homingRange) { this.homingData.homingRange = homingRange; } + + // Getters & Setters - Lightning data + public boolean hasLightningEffect() { return lightningData.lightningEffect; } + public void setLightningEffect(boolean lightningEffect) { this.lightningData.lightningEffect = lightningEffect; } + public float getLightningDensity() { return lightningData.lightningDensity; } + public void setLightningDensity(float lightningDensity) { this.lightningData.lightningDensity = lightningDensity; } + public float getLightningRadius() { return lightningData.lightningRadius; } + public void setLightningRadius(float lightningRadius) { this.lightningData.lightningRadius = lightningRadius; } + + // Getters & Setters - Lifespan data + public float getMaxDistance() { return lifespanData.maxDistance; } + public void setMaxDistance(float maxDistance) { this.lifespanData.maxDistance = maxDistance; } + public int getMaxLifetime() { return lifespanData.maxLifetime; } + public void setMaxLifetime(int maxLifetime) { this.lifespanData.maxLifetime = maxLifetime; } + + // Getters & Setters - Anchor point + public AnchorPoint getAnchorPointEnum() { return anchorData.anchorPoint; } + public float getAnchorOffsetX() { return anchorData.anchorOffsetX; } + public float getAnchorOffsetY() { return anchorData.anchorOffsetY; } + public float getAnchorOffsetZ() { return anchorData.anchorOffsetZ; } + public void setAnchorPointEnum(AnchorPoint anchorPoint) { this.anchorData.anchorPoint = anchorPoint; } + public void setAnchorOffsetX(float x) { this.anchorData.anchorOffsetX = x; } + public void setAnchorOffsetY(float y) { this.anchorData.anchorOffsetY = y; } + public void setAnchorOffsetZ(float z) { this.anchorData.anchorOffsetZ = z; } + + @Override + public int getAnchorPoint() { return anchorData.anchorPoint.ordinal(); } + + @Override + public void setAnchorPoint(int point) { this.anchorData.anchorPoint = AnchorPoint.fromOrdinal(point); } + + @Override + @SideOnly(Side.CLIENT) + public Entity createPreviewEntity(EntityNPCInterface npc) { + if (npc == null || npc.worldObj == null) return null; + + EntityAbilityBeam beam = new EntityAbilityBeam(npc.worldObj); + beam.setupPreview(npc, beamWidth, headSize, colorData, lightningData, anchorData, windUpTicks, 1.0f); + return beam; + } + + @Override + public int getPreviewActiveDuration() { + return lifespanData.maxLifetime > 0 ? Math.min(lifespanData.maxLifetime, 100) : 100; + } + + @SideOnly(Side.CLIENT) + @Override + public List getFieldDefinitions() { + return Arrays.asList( + // Type tab + FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage).column(ColumnHint.LEFT), + FieldDef.floatField("stats.speed", this::getSpeed, this::setSpeed).column(ColumnHint.RIGHT), + FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback), + FieldDef.section("ability.section.beam"), + FieldDef.floatField("ability.beamWidth", this::getBeamWidth, this::setBeamWidth).column(ColumnHint.LEFT), + FieldDef.floatField("ability.headSize", this::getHeadSize, this::setHeadSize).column(ColumnHint.RIGHT), + FieldDef.floatField("ability.maxDistance", this::getMaxDistance, this::setMaxDistance).column(ColumnHint.LEFT), + FieldDef.intField("ability.lifetime", this::getMaxLifetime, this::setMaxLifetime).column(ColumnHint.RIGHT), + FieldDef.section("ability.section.homing"), + FieldDef.boolField("gui.enabled", this::isHoming, this::setHoming) + .hover("ability.hover.homing"), + FieldDef.floatField("gui.strength", this::getHomingStrength, this::setHomingStrength) + .visibleWhen(this::isHoming).column(ColumnHint.LEFT), + FieldDef.floatField("gui.range", this::getHomingRange, this::setHomingRange) + .visibleWhen(this::isHoming).column(ColumnHint.RIGHT), + FieldDef.section("ability.section.explosive"), + FieldDef.boolField("gui.enabled", this::isExplosive, this::setExplosive) + .hover("ability.hover.explosive"), + FieldDef.floatField("gui.radius", this::getExplosionRadius, this::setExplosionRadius) + .visibleWhen(this::isExplosive), + AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects), + // Visual tab + FieldDef.enumField("ability.anchorPoint", AnchorPoint.class, this::getAnchorPointEnum, this::setAnchorPointEnum) + .tab("ability.tab.visual"), + FieldDef.section("ability.section.colors").tab("ability.tab.visual"), + FieldDef.colorSubGui("ability.innerColor", this::getInnerColor, this::setInnerColor) + .tab("ability.tab.visual"), + FieldDef.boolField("ability.outerEnabled", this::isOuterColorEnabled, this::setOuterColorEnabled) + .tab("ability.tab.visual"), + FieldDef.colorSubGui("ability.outerColor", this::getOuterColor, this::setOuterColor) + .tab("ability.tab.visual").visibleWhen(this::isOuterColorEnabled), + FieldDef.floatField("ability.outerWidth", this::getOuterColorWidth, this::setOuterColorWidth) + .tab("ability.tab.visual").visibleWhen(this::isOuterColorEnabled).column(ColumnHint.LEFT), + FieldDef.floatField("ability.outerAlpha", this::getOuterColorAlpha, this::setOuterColorAlpha) + .tab("ability.tab.visual").range(0, 1).visibleWhen(this::isOuterColorEnabled).column(ColumnHint.RIGHT), + FieldDef.section("ability.section.effects").tab("ability.tab.visual"), + FieldDef.floatField("ability.rotationSpeed", this::getRotationSpeed, this::setRotationSpeed) + .tab("ability.tab.visual"), + FieldDef.boolField("ability.lightning", this::hasLightningEffect, this::setLightningEffect) + .tab("ability.tab.visual"), + FieldDef.floatField("gui.density", this::getLightningDensity, this::setLightningDensity) + .tab("ability.tab.visual").range(0.01f, 5.0f).visibleWhen(this::hasLightningEffect).column(ColumnHint.LEFT), + FieldDef.floatField("gui.radius", this::getLightningRadius, this::setLightningRadius) + .tab("ability.tab.visual").range(0.1f, 10.0f).visibleWhen(this::hasLightningEffect).column(ColumnHint.RIGHT) + ); + } +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java index fa46c2341..b545d32d9 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java @@ -1,30 +1,39 @@ package kamkeel.npcs.controllers.data.ability.type; -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.LockMovementType; import kamkeel.npcs.controllers.data.ability.TargetingMode; +import kamkeel.npcs.controllers.data.ability.UserType; import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.DamageSource; import net.minecraft.world.World; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityGuard; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.entity.EntityNPCInterface; +import noppes.npcs.api.ability.type.IAbilityGuard; + +import noppes.npcs.client.gui.builder.ColumnHint; +import noppes.npcs.client.gui.builder.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.AbilityFieldDefs; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; + +import java.util.Arrays; +import java.util.List; import java.util.Random; /** * Guard ability: Defensive stance that reduces incoming damage. * Can optionally counter-attack when hit. */ -public class AbilityGuard extends Ability { +public class AbilityGuard extends Ability implements IAbilityGuard { private static final Random RANDOM = new Random(); // Type-specific parameters + private int durationTicks = 60; private float damageReduction = 0.5f; private boolean canCounter = false; private CounterType counterType = CounterType.FLAT; @@ -40,36 +49,32 @@ public class AbilityGuard extends Ability { public enum CounterType { FLAT, - PERCENT + PERCENT; + + @Override + public String toString() { + switch (this) { + case FLAT: return "ability.counter.flat"; + case PERCENT: return "ability.counter.percent"; + default: return name(); + } + } } public AbilityGuard() { this.typeId = "ability.cnpc.guard"; this.name = "Guard"; this.targetingMode = TargetingMode.SELF; - this.lockMovement = true; - this.cooldownTicks = 120; + this.lockMovement = LockMovementType.ACTIVE; + this.cooldownTicks = 0; this.windUpTicks = 10; - this.activeTicks = 60; - this.recoveryTicks = 20; this.interruptible = false; // Hard to interrupt while guarding // No telegraph for guard - it's a defensive stance this.telegraphType = TelegraphType.NONE; this.showTelegraph = false; this.windUpSound = "random.anvil_use"; this.activeSound = "random.anvil_land"; - } - - @Override - public boolean hasTypeSettings() { - return true; - } - - @Override - @SideOnly(Side.CLIENT) - public SubGuiAbilityConfig createConfigGui( - IAbilityConfigCallback callback) { - return new SubGuiAbilityGuard(this, callback); + this.allowedBy = UserType.NPC_ONLY; } @Override @@ -83,28 +88,33 @@ public class AbilityGuard extends Ability { } @Override - public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) { + public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { lastAttacker = null; counterTriggered = false; lastDamageTaken = 0.0f; } @Override - public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { + public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { // Counter attack logic if triggered if (canCounter && counterTriggered && lastAttacker != null && !world.isRemote) { - performCounter(npc, world); + performCounter(caster, world); + } + + // Check if guard duration has ended + if (tick >= durationTicks) { + signalCompletion(); } } /** - * Called externally when the NPC takes damage while guarding. + * Called externally when the caster takes damage while guarding. * This should be called from the damage handling code. * * @param attacker The entity that attacked * @param damage The damage amount (after reduction) */ - public void onDamageTaken(EntityNPCInterface npc, EntityLivingBase attacker, DamageSource source, float damage) { + public void onDamageTaken(EntityLivingBase caster, EntityLivingBase attacker, DamageSource source, float damage) { if (!canCounter || attacker == null) return; if (!isDirectHit(source)) return; if (RANDOM.nextFloat() >= counterChance) return; @@ -112,32 +122,21 @@ public class AbilityGuard extends Ability { lastAttacker = attacker; lastDamageTaken = damage; counterTriggered = true; - - if (interruptible) { - performCounter(npc, npc.worldObj); - npc.abilities.interruptCurrentAbility(source, damage); - } + // Counter will be performed in onActiveTick - don't self-interrupt } - private void performCounter(EntityNPCInterface npc, World world) { + private void performCounter(EntityLivingBase caster, World world) { float counterDamage = counterType == CounterType.PERCENT ? lastDamageTaken * (counterValue / 100.0f) : counterValue; - boolean wasHit = applyAbilityDamage(npc, lastAttacker, counterDamage, 0.5f); + boolean wasHit = applyAbilityDamage(caster, lastAttacker, counterDamage, 0.5f); if (wasHit && counterSound != null && !counterSound.isEmpty()) { - world.playSoundAtEntity(npc, counterSound, 1.0f, 1.2f); + world.playSoundAtEntity(caster, counterSound, 1.0f, 1.2f); } - if (counterAnimationId >= 0) { - if (noppes.npcs.controllers.AnimationController.Instance != null) { - noppes.npcs.controllers.data.Animation animation = - noppes.npcs.controllers.AnimationController.Instance.animations.get(counterAnimationId); - if (animation != null) { - npc.display.animationData.setEnabled(true); - npc.display.animationData.setAnimation(animation); - npc.display.animationData.updateClient(); - } - } + // Use shared animation utility - only available for NPCs + if (caster instanceof EntityNPCInterface) { + ((EntityNPCInterface) caster).abilities.playAbilityAnimation(counterAnimationId); } counterTriggered = false; @@ -163,7 +162,7 @@ public class AbilityGuard extends Ability { } /** - * Check if the NPC is currently in guard stance. + * Check if the caster is currently in guard stance. */ public boolean isGuarding() { return isExecuting() && getPhase() == kamkeel.npcs.controllers.data.ability.AbilityPhase.ACTIVE; @@ -184,6 +183,7 @@ public class AbilityGuard extends Ability { @Override public void writeTypeNBT(NBTTagCompound nbt) { + nbt.setInteger("durationTicks", durationTicks); nbt.setFloat("damageReduction", damageReduction); nbt.setBoolean("canCounter", canCounter); nbt.setString("counterType", counterType.name()); @@ -195,6 +195,7 @@ public class AbilityGuard extends Ability { @Override public void readTypeNBT(NBTTagCompound nbt) { + this.durationTicks = nbt.hasKey("durationTicks") ? nbt.getInteger("durationTicks") : 60; this.damageReduction = nbt.hasKey("damageReduction") ? nbt.getFloat("damageReduction") : 0.5f; this.canCounter = nbt.hasKey("canCounter") && nbt.getBoolean("canCounter"); try { @@ -215,6 +216,14 @@ public class AbilityGuard extends Ability { } // Getters & Setters + public int getDurationTicks() { + return durationTicks; + } + + public void setDurationTicks(int durationTicks) { + this.durationTicks = Math.max(1, durationTicks); + } + public float getDamageReduction() { return damageReduction; } @@ -227,18 +236,34 @@ public class AbilityGuard extends Ability { return canCounter; } + @Override + public boolean canCounter() { + return canCounter; + } + public void setCanCounter(boolean canCounter) { this.canCounter = canCounter; } - public CounterType getCounterType() { + public CounterType getCounterTypeEnum() { return counterType; } - public void setCounterType(CounterType counterType) { + public void setCounterTypeEnum(CounterType counterType) { this.counterType = counterType; } + @Override + public int getCounterType() { + return counterType.ordinal(); + } + + @Override + public void setCounterType(int type) { + CounterType[] values = CounterType.values(); + this.counterType = type >= 0 && type < values.length ? values[type] : CounterType.FLAT; + } + public float getCounterValue() { return counterValue; } @@ -270,4 +295,26 @@ public class AbilityGuard extends Ability { public void setCounterAnimationId(int counterAnimationId) { this.counterAnimationId = counterAnimationId; } + + @SideOnly(Side.CLIENT) + @Override + public List getFieldDefinitions() { + return Arrays.asList( + FieldDef.intField("ability.duration", this::getDurationTicks, this::setDurationTicks).range(1, 1000), + FieldDef.floatField("ability.damageReduction", this::getDamageReduction, this::setDamageReduction), + FieldDef.section("ability.section.counter"), + FieldDef.boolField("gui.enabled", this::isCanCounter, this::setCanCounter).hover("ability.hover.canCounter"), + FieldDef.enumField("gui.type", CounterType.class, this::getCounterTypeEnum, this::setCounterTypeEnum) + .hover("ability.hover.counterType").visibleWhen(this::isCanCounter), + FieldDef.floatField("gui.value", this::getCounterValue, this::setCounterValue) + .visibleWhen(this::isCanCounter).column(ColumnHint.LEFT), + FieldDef.floatField("gui.chance", this::getCounterChance, this::setCounterChance) + .visibleWhen(this::isCanCounter).column(ColumnHint.RIGHT), + FieldDef.stringField("gui.sound", this::getCounterSound, this::setCounterSound) + .visibleWhen(this::isCanCounter), + FieldDef.intField("gui.animation", this::getCounterAnimationId, this::setCounterAnimationId) + .visibleWhen(this::isCanCounter), + AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects) + ); + } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java index 028259826..a5996d487 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java @@ -1,8 +1,7 @@ package kamkeel.npcs.controllers.data.ability.type; -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.LockMovementType; import kamkeel.npcs.controllers.data.ability.TargetingMode; import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; import kamkeel.npcs.controllers.data.telegraph.TelegraphType; @@ -13,11 +12,18 @@ import net.minecraft.potion.PotionEffect; import net.minecraft.util.AxisAlignedBB; import net.minecraft.util.DamageSource; import net.minecraft.world.World; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityHazard; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.entity.EntityNPCInterface; +import noppes.npcs.api.ability.type.IAbilityHazard; + +import noppes.npcs.client.gui.builder.ColumnHint; +import noppes.npcs.client.gui.builder.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.AbilityFieldDefs; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; + +import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Random; @@ -27,21 +33,43 @@ import java.util.Set; * Hazard ability: Creates persistent ground effect zones. * Deals damage and/or applies debuffs to entities in the zone over time. */ -public class AbilityHazard extends Ability { +public class AbilityHazard extends Ability implements IAbilityHazard { public enum HazardShape { CIRCLE, RING, - CONE + CONE; + + @Override + public String toString() { + switch (this) { + case CIRCLE: return "ability.shape.circle"; + case RING: return "ability.shape.ring"; + case CONE: return "ability.shape.cone"; + default: return name(); + } + } } public enum PlacementMode { AT_CASTER, AT_TARGET, FOLLOW_CASTER, - FOLLOW_TARGET + FOLLOW_TARGET; + + @Override + public String toString() { + switch (this) { + case AT_CASTER: return "ability.placement.atCaster"; + case AT_TARGET: return "ability.placement.atTarget"; + case FOLLOW_CASTER: return "ability.placement.followCaster"; + case FOLLOW_TARGET: return "ability.placement.followTarget"; + default: return name(); + } + } } + private int durationTicks = 100; private float radius = 4.0f; private float innerRadius = 0.0f; private float coneAngle = 45.0f; @@ -80,26 +108,12 @@ public class AbilityHazard extends Ability { this.name = "Hazard"; this.targetingMode = TargetingMode.AGGRO_TARGET; this.maxRange = 15.0f; - this.lockMovement = true; - this.cooldownTicks = 200; + this.lockMovement = LockMovementType.WINDUP; + this.cooldownTicks = 0; this.windUpTicks = 30; - this.activeTicks = 100; - this.recoveryTicks = 10; this.telegraphType = TelegraphType.CIRCLE; } - @Override - public boolean hasTypeSettings() { - return true; - } - - @Override - @SideOnly(Side.CLIENT) - public SubGuiAbilityConfig createConfigGui( - IAbilityConfigCallback callback) { - return new SubGuiAbilityHazard(this, callback); - } - @Override public boolean isTargetingModeLocked() { return true; @@ -116,8 +130,8 @@ public class AbilityHazard extends Ability { } @Override - public TelegraphInstance createTelegraph(EntityNPCInterface npc, EntityLivingBase target) { - TelegraphInstance instance = super.createTelegraph(npc, target); + public TelegraphInstance createTelegraph(EntityLivingBase caster, EntityLivingBase target) { + TelegraphInstance instance = super.createTelegraph(caster, target); if (instance == null) return null; // Control telegraph following based on placement mode @@ -126,9 +140,9 @@ public class AbilityHazard extends Ability { case FOLLOW_CASTER: // Telegraph stays at caster position, no following instance.setEntityIdToFollow(-1); - instance.setX(npc.posX); - instance.setY(npc.posY); - instance.setZ(npc.posZ); + instance.setX(caster.posX); + instance.setY(caster.posY); + instance.setZ(caster.posZ); break; case AT_TARGET: // Telegraph follows target during windup, locks on execute @@ -146,43 +160,15 @@ public class AbilityHazard extends Ability { return instance; } - /** - * Calculates offset position near the given coordinates. - * Used to place hazard near target rather than exactly on them. - */ - private double[] calculateOffsetPosition(double baseX, double baseY, double baseZ) { - if (maxOffset <= 0) { - return new double[]{baseX, baseY, baseZ}; - } - - double offsetDist; - double offsetAngle; - - if (randomOffset) { - // Random offset within min-max range - offsetDist = minOffset + RANDOM.nextDouble() * (maxOffset - minOffset); - offsetAngle = RANDOM.nextDouble() * Math.PI * 2; - } else { - // Fixed offset (use max, random angle) - offsetDist = maxOffset; - offsetAngle = RANDOM.nextDouble() * Math.PI * 2; - } - - double offsetX = Math.cos(offsetAngle) * offsetDist; - double offsetZ = Math.sin(offsetAngle) * offsetDist; - - return new double[]{baseX + offsetX, baseY, baseZ + offsetZ}; - } - @Override - public void onWindUpTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { + public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { // AT_CASTER locks immediately, AT_TARGET follows during windup and locks on execute if (tick == 0) { switch (placement) { case AT_CASTER: - zoneX = npc.posX; - zoneY = npc.posY; - zoneZ = npc.posZ; + zoneX = caster.posX; + zoneY = caster.posY; + zoneZ = caster.posZ; positionLocked = true; break; case AT_TARGET: @@ -198,7 +184,7 @@ public class AbilityHazard extends Ability { } @Override - public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) { + public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { // Lock position from telegraph (which was following target) or calculate it now TelegraphInstance telegraph = getTelegraphInstance(); @@ -206,33 +192,39 @@ public class AbilityHazard extends Ability { case AT_CASTER: // Already locked during windup if (!positionLocked) { - zoneX = npc.posX; - zoneY = npc.posY; - zoneZ = npc.posZ; + zoneX = caster.posX; + zoneY = caster.posY; + zoneZ = caster.posZ; } break; case AT_TARGET: // Use telegraph position if available, apply offset if (telegraph != null) { - double[] pos = calculateOffsetPosition(telegraph.getX(), telegraph.getY(), telegraph.getZ()); + double[] pos = Ability.calculateOffsetPosition(telegraph.getX(), telegraph.getY(), telegraph.getZ(), + minOffset, maxOffset, randomOffset, RANDOM); zoneX = pos[0]; zoneY = pos[1]; zoneZ = pos[2]; + // Update telegraph to show actual hazard position + telegraph.setX(zoneX); + telegraph.setY(zoneY); + telegraph.setZ(zoneZ); } else if (target != null) { - double[] pos = calculateOffsetPosition(target.posX, target.posY, target.posZ); + double[] pos = Ability.calculateOffsetPosition(target.posX, target.posY, target.posZ, + minOffset, maxOffset, randomOffset, RANDOM); zoneX = pos[0]; zoneY = pos[1]; zoneZ = pos[2]; } else { - zoneX = npc.posX; - zoneY = npc.posY; - zoneZ = npc.posZ; + zoneX = caster.posX; + zoneY = caster.posY; + zoneZ = caster.posZ; } break; case FOLLOW_CASTER: - zoneX = npc.posX; - zoneY = npc.posY; - zoneZ = npc.posZ; + zoneX = caster.posX; + zoneY = caster.posY; + zoneZ = caster.posZ; break; case FOLLOW_TARGET: if (target != null) { @@ -240,9 +232,9 @@ public class AbilityHazard extends Ability { zoneY = target.posY; zoneZ = target.posZ; } else { - zoneX = npc.posX; - zoneY = npc.posY; - zoneZ = npc.posZ; + zoneX = caster.posX; + zoneY = caster.posY; + zoneZ = caster.posZ; } break; } @@ -251,17 +243,23 @@ public class AbilityHazard extends Ability { } @Override - public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { + public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { if (world.isRemote) return; + // Check if hazard duration has ended + if (tick >= durationTicks) { + signalCompletion(); + return; + } + damagedThisTick.clear(); ticksSinceDamage++; switch (placement) { case FOLLOW_CASTER: - zoneX = npc.posX; - zoneY = npc.posY; - zoneZ = npc.posZ; + zoneX = caster.posX; + zoneY = caster.posY; + zoneZ = caster.posZ; break; case FOLLOW_TARGET: if (target != null) { @@ -286,16 +284,16 @@ public class AbilityHazard extends Ability { List entities = world.getEntitiesWithinAABB(EntityLivingBase.class, searchBox); for (EntityLivingBase entity : entities) { - if (entity == npc && !affectsCaster) continue; + if (entity == caster && !affectsCaster) continue; if (damagedThisTick.contains(entity.getEntityId())) continue; - if (!isInZone(entity, npc)) continue; + if (!isInZone(entity, caster)) continue; if (damagePerSecond > 0) { if (ignoreInvulnFrames) { entity.hurtResistantTime = 0; } // Apply damage with scripted event support (no knockback for hazard) - boolean wasHit = applyAbilityDamage(npc, entity, damagePerSecond, 0); + boolean wasHit = applyAbilityDamage(caster, entity, damagePerSecond, 0); if (!wasHit) continue; // Skip debuffs if hit was cancelled } @@ -305,7 +303,7 @@ public class AbilityHazard extends Ability { } } - private boolean isInZone(EntityLivingBase entity, EntityNPCInterface npc) { + private boolean isInZone(EntityLivingBase entity, EntityLivingBase caster) { double dx = entity.posX - zoneX; double dz = entity.posZ - zoneZ; double dist = Math.sqrt(dx * dx + dz * dz); @@ -317,7 +315,7 @@ public class AbilityHazard extends Ability { return dist >= innerRadius && dist <= radius; case CONE: if (dist > radius) return false; - float casterYaw = npc.rotationYaw; + float casterYaw = caster.rotationYaw; double angleToEntity = Math.toDegrees(Math.atan2(-dx, dz)); double angleDiff = Math.abs(normalizeAngle(angleToEntity - casterYaw)); return angleDiff <= coneAngle / 2; @@ -351,19 +349,20 @@ public class AbilityHazard extends Ability { } @Override - public void onComplete(EntityNPCInterface npc, EntityLivingBase target) { + public void onComplete(EntityLivingBase caster, EntityLivingBase target) { damagedThisTick.clear(); positionLocked = false; } @Override - public void onInterrupt(EntityNPCInterface npc, DamageSource source, float damage) { + public void onInterrupt(EntityLivingBase caster, DamageSource source, float damage) { damagedThisTick.clear(); positionLocked = false; } @Override public void writeTypeNBT(NBTTagCompound nbt) { + nbt.setInteger("durationTicks", durationTicks); nbt.setFloat("radius", radius); nbt.setFloat("innerRadius", innerRadius); nbt.setFloat("coneAngle", coneAngle); @@ -388,6 +387,7 @@ public class AbilityHazard extends Ability { @Override public void readTypeNBT(NBTTagCompound nbt) { + this.durationTicks = nbt.hasKey("durationTicks") ? nbt.getInteger("durationTicks") : 100; this.radius = nbt.hasKey("radius") ? nbt.getFloat("radius") : 4.0f; this.innerRadius = nbt.hasKey("innerRadius") ? nbt.getFloat("innerRadius") : 0.0f; this.coneAngle = nbt.hasKey("coneAngle") ? nbt.getFloat("coneAngle") : 45.0f; @@ -426,6 +426,14 @@ public class AbilityHazard extends Ability { } // Getters & Setters + public int getDurationTicks() { + return durationTicks; + } + + public void setDurationTicks(int durationTicks) { + this.durationTicks = Math.max(1, durationTicks); + } + public float getRadius() { return radius; } @@ -450,22 +458,44 @@ public class AbilityHazard extends Ability { this.coneAngle = coneAngle; } - public HazardShape getShape() { + public HazardShape getShapeEnum() { return shape; } - public void setShape(HazardShape shape) { + public void setShapeEnum(HazardShape shape) { this.shape = shape; } - public PlacementMode getPlacement() { + @Override + public int getShape() { + return shape.ordinal(); + } + + @Override + public void setShape(int shape) { + HazardShape[] values = HazardShape.values(); + this.shape = shape >= 0 && shape < values.length ? values[shape] : HazardShape.CIRCLE; + } + + public PlacementMode getPlacementEnum() { return placement; } - public void setPlacement(PlacementMode placement) { + public void setPlacementEnum(PlacementMode placement) { this.placement = placement; } + @Override + public int getPlacement() { + return placement.ordinal(); + } + + @Override + public void setPlacement(int placement) { + PlacementMode[] values = PlacementMode.values(); + this.placement = placement >= 0 && placement < values.length ? values[placement] : PlacementMode.AT_CASTER; + } + public float getDamagePerSecond() { return damagePerSecond; } @@ -474,18 +504,6 @@ public class AbilityHazard extends Ability { this.damagePerSecond = damagePerSecond; } - /** @deprecated Use {@link #getDamagePerSecond()} instead */ - @Deprecated - public float getDamagePerTick() { - return damagePerSecond; - } - - /** @deprecated Use {@link #setDamagePerSecond(float)} instead */ - @Deprecated - public void setDamagePerTick(float damage) { - this.damagePerSecond = damage; - } - public int getDamageInterval() { return damageInterval; } @@ -610,4 +628,29 @@ public class AbilityHazard extends Ability { public double getZoneZ() { return zoneZ; } + + @SideOnly(Side.CLIENT) + @Override + public List getFieldDefinitions() { + return Arrays.asList( + FieldDef.intField("ability.duration", this::getDurationTicks, this::setDurationTicks).range(1, 2000), + FieldDef.enumField("ability.shape", HazardShape.class, this::getShapeEnum, this::setShapeEnum) + .hover("ability.hover.shape"), + FieldDef.enumField("ability.placement", PlacementMode.class, this::getPlacementEnum, this::setPlacementEnum) + .hover("ability.hover.placement"), + FieldDef.section("ability.section.area"), + FieldDef.floatField("gui.radius", this::getRadius, this::setRadius).column(ColumnHint.LEFT), + FieldDef.floatField("ability.innerRadius", this::getInnerRadius, this::setInnerRadius).column(ColumnHint.RIGHT), + FieldDef.section("ability.section.damage"), + FieldDef.floatField("ability.damagePerSecond", this::getDamagePerSecond, this::setDamagePerSecond).column(ColumnHint.LEFT), + FieldDef.intField("ability.damageInterval", this::getDamageInterval, this::setDamageInterval).column(ColumnHint.RIGHT), + FieldDef.section("ability.section.debuffs"), + FieldDef.intField("ability.slownessLevel", this::getSlownessLevel, this::setSlownessLevel).column(ColumnHint.LEFT), + FieldDef.intField("ability.debuffDuration", this::getDebuffDuration, this::setDebuffDuration).column(ColumnHint.RIGHT), + FieldDef.intField("ability.poisonLevel", this::getPoisonLevel, this::setPoisonLevel), + FieldDef.boolField("ability.affectsCaster", this::isAffectsCaster, this::setAffectsCaster) + .hover("ability.hover.affectsCaster"), + AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects) + ); + } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeal.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeal.java index 834b9c055..72f3813c3 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeal.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeal.java @@ -1,8 +1,7 @@ package kamkeel.npcs.controllers.data.ability.type; -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.LockMovementType; import kamkeel.npcs.controllers.data.ability.TargetingMode; import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import net.minecraft.entity.Entity; @@ -10,21 +9,27 @@ import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.AxisAlignedBB; import net.minecraft.world.World; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityHeal; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.entity.EntityNPCInterface; +import noppes.npcs.client.gui.builder.ColumnHint; +import noppes.npcs.client.gui.builder.FieldDef; +import noppes.npcs.api.ability.type.IAbilityHeal; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; + import java.util.ArrayList; +import java.util.Arrays; import java.util.List; /** * Heal ability: Restore health to self or allies. * Can heal a fixed amount or percentage of max health. */ -public class AbilityHeal extends Ability { +public class AbilityHeal extends Ability implements IAbilityHeal { // Type-specific parameters + private int durationTicks = 60; private float healAmount = 10.0f; private float healPercent = 0.0f; private boolean healSelf = true; @@ -33,33 +38,27 @@ public class AbilityHeal extends Ability { private boolean instantHeal = true; // Runtime state - private transient List healedAllies = new ArrayList<>(); + private transient List healedAllies; + + private List getHealedAllies() { + if (healedAllies == null) { + healedAllies = new ArrayList<>(); + } + return healedAllies; + } public AbilityHeal() { this.typeId = "ability.cnpc.heal"; this.name = "Heal"; this.targetingMode = TargetingMode.SELF; - this.lockMovement = true; - this.cooldownTicks = 200; + this.lockMovement = LockMovementType.WINDUP; + this.cooldownTicks = 0; this.windUpTicks = 30; - this.activeTicks = 10; - this.recoveryTicks = 10; // No telegraph for heal - it's a self/ally buff this.telegraphType = TelegraphType.NONE; this.showTelegraph = false; } - @Override - public boolean hasTypeSettings() { - return true; - } - - @Override - @SideOnly(Side.CLIENT) - public SubGuiAbilityConfig createConfigGui(IAbilityConfigCallback callback) { - return new SubGuiAbilityHeal(this, callback); - } - @Override public boolean isTargetingModeLocked() { return true; @@ -71,54 +70,104 @@ public class AbilityHeal extends Ability { } @Override - public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) { + public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { if (world.isRemote) return; - healedAllies.clear(); + getHealedAllies().clear(); + + // Always find allies if we're healing them (needed for both instant and HoT) + if (healAllies && healRadius > 0) { + findAlliesInRadius(caster, world); + } if (instantHeal) { + // Instant heal - apply all healing now if (healSelf) { - healEntity(npc); - spawnHealParticles(world, npc); + healEntity(caster); + spawnHealParticles(world, caster); } - if (healAllies && healRadius > 0) { - healAlliesInRadius(npc, world); + for (EntityLivingBase ally : getHealedAllies()) { + healEntity(ally); + spawnHealParticles(world, ally); } + + // Instant heal completes immediately + signalCompletion(); } + // If not instant, HoT healing is applied in onActiveTick } @Override - public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { + public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { if (world.isRemote || instantHeal) return; - // Heal over time - distribute heal across active ticks + // Heal over time - distribute heal across duration ticks if (tick % 10 == 0) { // Calculate tick-based heal for fixed amount - float tickHeal = (healAmount / (float) activeTicks) * 10; + float tickHeal = (healAmount / (float) durationTicks) * 10; if (healSelf) { float selfTickHeal = tickHeal; // Add percentage-based heal portion if (healPercent > 0) { - selfTickHeal += (npc.getMaxHealth() * healPercent / (float) activeTicks) * 10; + selfTickHeal += (caster.getMaxHealth() * healPercent / (float) durationTicks) * 10; + } + caster.heal(selfTickHeal); + if (tick % 20 == 0) { + spawnHealParticles(world, caster); } - npc.heal(selfTickHeal); } if (healAllies && healRadius > 0) { - for (EntityLivingBase ally : healedAllies) { + for (EntityLivingBase ally : getHealedAllies()) { if (!ally.isDead) { float allyTickHeal = tickHeal; // Add percentage-based heal portion for each ally if (healPercent > 0) { - allyTickHeal += (ally.getMaxHealth() * healPercent / (float) activeTicks) * 10; + allyTickHeal += (ally.getMaxHealth() * healPercent / (float) durationTicks) * 10; } ally.heal(allyTickHeal); + if (tick % 20 == 0) { + spawnHealParticles(world, ally); + } } } } } + + // Check if heal duration has ended + if (tick >= durationTicks) { + signalCompletion(); + } + } + + /** + * Find all allies in radius and add them to healedAllies list. + */ + private void findAlliesInRadius(EntityLivingBase caster, World world) { + AxisAlignedBB aabb = AxisAlignedBB.getBoundingBox( + caster.posX - healRadius, caster.posY - 2, caster.posZ - healRadius, + caster.posX + healRadius, caster.posY + 3, caster.posZ + healRadius + ); + + @SuppressWarnings("unchecked") + List entities = world.getEntitiesWithinAABB(EntityLivingBase.class, aabb); + + for (Entity entity : entities) { + if (!(entity instanceof EntityLivingBase)) continue; + if (entity == caster) continue; + + EntityLivingBase living = (EntityLivingBase) entity; + + // Only heal other NPCs (allies) + if (living instanceof EntityNPCInterface) { + float dist = caster.getDistanceToEntity(living); + if (dist <= healRadius) { + getHealedAllies().add(living); + } + } + } } private void healEntity(EntityLivingBase entity) { @@ -129,33 +178,6 @@ public class AbilityHeal extends Ability { entity.heal(totalHeal); } - private void healAlliesInRadius(EntityNPCInterface npc, World world) { - AxisAlignedBB aabb = AxisAlignedBB.getBoundingBox( - npc.posX - healRadius, npc.posY - 2, npc.posZ - healRadius, - npc.posX + healRadius, npc.posY + 3, npc.posZ + healRadius - ); - - @SuppressWarnings("unchecked") - List entities = world.getEntitiesWithinAABB(EntityLivingBase.class, aabb); - - for (Entity entity : entities) { - if (!(entity instanceof EntityLivingBase)) continue; - if (entity == npc) continue; - - EntityLivingBase living = (EntityLivingBase) entity; - - // Only heal other NPCs (allies) - if (living instanceof EntityNPCInterface) { - float dist = npc.getDistanceToEntity(living); - if (dist <= healRadius) { - healEntity(living); - healedAllies.add(living); - spawnHealParticles(world, living); - } - } - } - } - private void spawnHealParticles(World world, EntityLivingBase entity) { for (int i = 0; i < 10; i++) { double offsetX = (world.rand.nextDouble() - 0.5) * entity.width; @@ -170,9 +192,8 @@ public class AbilityHeal extends Ability { } @Override - public void reset() { - super.reset(); - healedAllies.clear(); + public void cleanup() { + getHealedAllies().clear(); } @Override @@ -182,6 +203,7 @@ public class AbilityHeal extends Ability { @Override public void writeTypeNBT(NBTTagCompound nbt) { + nbt.setInteger("durationTicks", durationTicks); nbt.setFloat("healAmount", healAmount); nbt.setFloat("healPercent", healPercent); nbt.setBoolean("healSelf", healSelf); @@ -192,6 +214,7 @@ public class AbilityHeal extends Ability { @Override public void readTypeNBT(NBTTagCompound nbt) { + this.durationTicks = nbt.hasKey("durationTicks") ? nbt.getInteger("durationTicks") : 60; this.healAmount = nbt.hasKey("healAmount") ? nbt.getFloat("healAmount") : 10.0f; this.healPercent = nbt.hasKey("healPercent") ? nbt.getFloat("healPercent") : 0.0f; this.healSelf = !nbt.hasKey("healSelf") || nbt.getBoolean("healSelf"); @@ -201,6 +224,14 @@ public class AbilityHeal extends Ability { } // Getters & Setters + public int getDurationTicks() { + return durationTicks; + } + + public void setDurationTicks(int durationTicks) { + this.durationTicks = Math.max(1, durationTicks); + } + public float getHealAmount() { return healAmount; } @@ -248,4 +279,23 @@ public class AbilityHeal extends Ability { public void setInstantHeal(boolean instantHeal) { this.instantHeal = instantHeal; } + + @SideOnly(Side.CLIENT) + @Override + public List getFieldDefinitions() { + return Arrays.asList( + FieldDef.boolField("ability.instantHeal", this::isInstantHeal, this::setInstantHeal) + .hover("ability.hover.instant"), + FieldDef.intField("ability.duration", this::getDurationTicks, this::setDurationTicks) + .range(1, 1000).visibleWhen(() -> !this.isInstantHeal()), + FieldDef.section("ability.section.healing"), + FieldDef.floatField("ability.healAmount", this::getHealAmount, this::setHealAmount).column(ColumnHint.LEFT), + FieldDef.floatField("ability.healPercent", this::getHealPercent, this::setHealPercent).column(ColumnHint.RIGHT), + FieldDef.floatField("ability.healRadius", this::getHealRadius, this::setHealRadius), + FieldDef.boolField("ability.healSelf", this::isHealSelf, this::setHealSelf) + .hover("ability.hover.healSelf").column(ColumnHint.LEFT), + FieldDef.boolField("ability.healAllies", this::isHealAllies, this::setHealAllies) + .hover("ability.hover.healAllies").column(ColumnHint.RIGHT) + ); + } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeavyHit.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeavyHit.java index 408120694..8b6597b11 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeavyHit.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeavyHit.java @@ -3,29 +3,29 @@ package kamkeel.npcs.controllers.data.ability.type; 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.LockMovementType; import kamkeel.npcs.controllers.data.ability.TargetingMode; import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; -import net.minecraft.potion.Potion; -import net.minecraft.potion.PotionEffect; import net.minecraft.world.World; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityHeavyHit; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; +import noppes.npcs.client.gui.builder.ColumnHint; +import noppes.npcs.client.gui.builder.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.AbilityFieldDefs; +import noppes.npcs.api.ability.type.IAbilityHeavyHit; import noppes.npcs.entity.EntityNPCInterface; +import java.util.Arrays; +import java.util.List; + /** * Heavy Hit ability: Single-target melee attack with optional stun. * Deals high damage to one target and can stun them. */ -public class AbilityHeavyHit extends Ability { +public class AbilityHeavyHit extends Ability implements IAbilityHeavyHit { private float damage = 8.0f; private float knockback = 2.0f; - private int slownessLevel = 1; - private int weaknessLevel = 0; - private int potionDurationSeconds = 2; public AbilityHeavyHit() { this.typeId = "ability.cnpc.heavy_hit"; @@ -33,27 +33,15 @@ public class AbilityHeavyHit extends Ability { this.targetingMode = TargetingMode.AGGRO_TARGET; this.maxRange = 3.0f; this.minRange = 0.0f; - this.lockMovement = true; - this.cooldownTicks = 80; + this.lockMovement = LockMovementType.NO; + this.cooldownTicks = 0; this.windUpTicks = 30; - this.activeTicks = 5; - this.recoveryTicks = 20; this.telegraphType = TelegraphType.POINT; this.showTelegraph = false; this.windUpSound = "random.anvil_use"; this.activeSound = "random.anvil_land"; - } - - @Override - public boolean hasTypeSettings() { - return true; - } - - @Override - @SideOnly(Side.CLIENT) - public SubGuiAbilityConfig createConfigGui( - IAbilityConfigCallback callback) { - return new SubGuiAbilityHeavyHit(this, callback); + this.windUpAnimationName = "Ability_HeavyHit_Windup"; + this.activeAnimationName = "Ability_HeavyHit_Active"; } @Override @@ -67,44 +55,38 @@ public class AbilityHeavyHit extends Ability { } @Override - public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) { - if (world.isRemote || target == null) return; + public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { + if (world.isRemote || target == null) { + signalCompletion(); + return; + } // Apply damage with scripted event support - boolean wasHit = applyAbilityDamage(npc, target, damage, knockback); + boolean wasHit = applyAbilityDamage(caster, target, damage, knockback); - if (wasHit && potionDurationSeconds > 0) { - int durationTicks = potionDurationSeconds * 20; - if (slownessLevel >= 0) { - target.addPotionEffect(new PotionEffect(Potion.moveSlowdown.id, durationTicks, slownessLevel)); - } - if (weaknessLevel >= 0) { - target.addPotionEffect(new PotionEffect(Potion.weakness.id, durationTicks, weaknessLevel)); - } + // Apply effects if hit wasn't cancelled + if (wasHit) { + applyEffects(target); } } @Override - public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { - // Heavy Hit is instant, no active tick needed + public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { + // Just enough delay to see the active animation + if (tick == 10) + signalCompletion(); } @Override public void writeTypeNBT(NBTTagCompound nbt) { nbt.setFloat("damage", damage); nbt.setFloat("knockback", knockback); - nbt.setInteger("slownessLevel", slownessLevel); - nbt.setInteger("weaknessLevel", weaknessLevel); - nbt.setInteger("potionDurationSeconds", potionDurationSeconds); } @Override public void readTypeNBT(NBTTagCompound nbt) { this.damage = nbt.hasKey("damage") ? nbt.getFloat("damage") : 8.0f; this.knockback = nbt.hasKey("knockback") ? nbt.getFloat("knockback") : 2.0f; - this.slownessLevel = nbt.hasKey("slownessLevel") ? nbt.getInteger("slownessLevel") : 1; - this.weaknessLevel = nbt.hasKey("weaknessLevel") ? nbt.getInteger("weaknessLevel") : 0; - this.potionDurationSeconds = nbt.hasKey("potionDurationSeconds") ? nbt.getInteger("potionDurationSeconds") : 2; } // Getters & Setters @@ -124,27 +106,13 @@ public class AbilityHeavyHit extends Ability { this.knockback = knockback; } - public int getSlownessLevel() { - return slownessLevel; - } - - public void setSlownessLevel(int slownessLevel) { - this.slownessLevel = slownessLevel; - } - - public int getWeaknessLevel() { - return weaknessLevel; - } - - public void setWeaknessLevel(int weaknessLevel) { - this.weaknessLevel = weaknessLevel; - } - - public int getPotionDurationSeconds() { - return potionDurationSeconds; - } - - public void setPotionDurationSeconds(int potionDurationSeconds) { - this.potionDurationSeconds = potionDurationSeconds; + @SideOnly(Side.CLIENT) + @Override + public List getFieldDefinitions() { + return Arrays.asList( + FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage).column(ColumnHint.LEFT), + FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback).column(ColumnHint.RIGHT), + AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects) + ); } } 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 new file mode 100644 index 000000000..39377a19d --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityLaserShot.java @@ -0,0 +1,285 @@ +package kamkeel.npcs.controllers.data.ability.type; + +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.AnchorPoint; +import kamkeel.npcs.controllers.data.ability.LockMovementType; +import kamkeel.npcs.controllers.data.ability.TargetingMode; +import kamkeel.npcs.controllers.data.ability.data.*; +import noppes.npcs.client.gui.builder.ColumnHint; +import noppes.npcs.client.gui.builder.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.AbilityFieldDefs; +import kamkeel.npcs.controllers.data.telegraph.TelegraphType; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.util.AnchorPointHelper; +import net.minecraft.entity.Entity; +import net.minecraft.entity.EntityLivingBase; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.util.DamageSource; +import net.minecraft.util.Vec3; +import net.minecraft.world.World; +import noppes.npcs.api.ability.type.IAbilityLaserShot; + +import java.util.Arrays; +import java.util.List; +import noppes.npcs.entity.EntityNPCInterface; + +/** + * Laser Shot ability: Fast expanding thin line that pierces through targets. + * Travels in a straight line from origin, damaging all entities it passes through. + */ +public class AbilityLaserShot extends Ability implements IAbilityLaserShot { + + // Laser properties + private float laserWidth = 0.3f; + private float expansionSpeed = 3.0f; + private int lingerTicks = 8; + + // Data classes + private final EnergyColorData colorData = new EnergyColorData(0xFFFFFF, 0xFF0000, true, 0.4f, 0.5f, 0.0f); + private final EnergyCombatData combatData = new EnergyCombatData(6.0f, 0.5f, 0.05f, false, 2.0f, 0.5f); + private final EnergyLightningData lightningData = new EnergyLightningData(); + private final EnergyLifespanData lifespanData = new EnergyLifespanData(40.0f, 100); + private final EnergyAnchorData anchorData = new EnergyAnchorData(AnchorPoint.RIGHT_HAND); + + // Transient state for laser entity (used for movement locking) + private transient EntityAbilityLaser laserEntity = null; + + public AbilityLaserShot() { + this.typeId = "ability.cnpc.laser_shot"; + this.name = "Laser Shot"; + this.targetingMode = TargetingMode.AGGRO_TARGET; + this.maxRange = 35.0f; + this.minRange = 3.0f; + this.cooldownTicks = 0; + this.windUpTicks = 15; + this.lockMovement = LockMovementType.WINDUP; + this.telegraphType = TelegraphType.LINE; + this.showTelegraph = true; + // Default built-in animations + this.windUpAnimationName = "Ability_Laser_Windup"; + this.activeAnimationName = "Ability_Laser_Active"; + } + + @Override + public boolean isTargetingModeLocked() { + return false; + } + + @Override + public TargetingMode[] getAllowedTargetingModes() { + return new TargetingMode[]{TargetingMode.AGGRO_TARGET}; + } + + @Override + public float getTelegraphLength() { + return lifespanData.maxDistance; + } + + @Override + public float getTelegraphWidth() { + return laserWidth * 2.0f; // Make telegraph slightly wider for visibility + } + + @Override + public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { + if (world.isRemote) return; + + if (tick == 1) { + Vec3 spawnPos = AnchorPointHelper.calculateAnchorPosition(caster, anchorData); + laserEntity = new EntityAbilityLaser( + world, caster, target, + spawnPos.xCoord, spawnPos.yCoord, spawnPos.zCoord, + laserWidth, + colorData, combatData, lightningData, lifespanData, + expansionSpeed, lingerTicks + ); + + laserEntity.setupCharging(anchorData, windUpTicks); + + laserEntity.setEffects(this.effects); + world.spawnEntityInWorld(laserEntity); + } + } + + @Override + public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { + if (world.isRemote) { + signalCompletion(); + } + + laserEntity.startMoving(target); + + // Ability stays active until entity dies (prevents firing another while projectile is alive) + // Movement locking is handled separately by the base class + } + + @Override + public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { + // Signal completion when entity dies + if (laserEntity == null || laserEntity.isDead) { + laserEntity = null; + signalCompletion(); + } + } + + @Override + public void onComplete(EntityLivingBase caster, EntityLivingBase target) { + } + + @Override + public void onInterrupt(EntityLivingBase caster, DamageSource source, float damage) { + } + + @Override + public void cleanup() { + // Despawn laser entity if still alive + if (laserEntity != null && !laserEntity.isDead) { + laserEntity.setDead(); + } + laserEntity = null; + } + + @Override + public void writeTypeNBT(NBTTagCompound nbt) { + nbt.setFloat("laserWidth", laserWidth); + nbt.setFloat("expansionSpeed", expansionSpeed); + nbt.setInteger("lingerTicks", lingerTicks); + anchorData.writeNBT(nbt); + colorData.writeNBT(nbt); + combatData.writeNBT(nbt); + lightningData.writeNBT(nbt); + lifespanData.writeNBT(nbt); + } + + @Override + public void readTypeNBT(NBTTagCompound nbt) { + this.laserWidth = nbt.hasKey("laserWidth") ? nbt.getFloat("laserWidth") : 0.3f; + this.expansionSpeed = nbt.hasKey("expansionSpeed") ? nbt.getFloat("expansionSpeed") : 3.0f; + this.lingerTicks = nbt.hasKey("lingerTicks") ? nbt.getInteger("lingerTicks") : 8; + anchorData.readNBT(nbt); + colorData.readNBT(nbt); + combatData.readNBT(nbt); + lightningData.readNBT(nbt); + lifespanData.readNBT(nbt); + } + + // Getters & Setters - Standalone fields + public float getLaserWidth() { return laserWidth; } + public void setLaserWidth(float laserWidth) { this.laserWidth = laserWidth; } + public float getExpansionSpeed() { return expansionSpeed; } + public void setExpansionSpeed(float expansionSpeed) { this.expansionSpeed = expansionSpeed; } + public int getLingerTicks() { return lingerTicks; } + public void setLingerTicks(int lingerTicks) { this.lingerTicks = lingerTicks; } + + // Getters & Setters - Lifespan data + public float getMaxDistance() { return lifespanData.maxDistance; } + public void setMaxDistance(float maxDistance) { this.lifespanData.maxDistance = maxDistance; } + public int getMaxLifetime() { return lifespanData.maxLifetime; } + public void setMaxLifetime(int maxLifetime) { this.lifespanData.maxLifetime = maxLifetime; } + + // Getters & Setters - Combat data + public float getDamage() { return combatData.damage; } + public void setDamage(float damage) { this.combatData.damage = damage; } + public float getKnockback() { return combatData.knockback; } + public void setKnockback(float knockback) { this.combatData.knockback = knockback; } + public float getKnockbackUp() { return combatData.knockbackUp; } + public void setKnockbackUp(float knockbackUp) { this.combatData.knockbackUp = knockbackUp; } + public boolean isExplosive() { return combatData.explosive; } + public void setExplosive(boolean explosive) { this.combatData.explosive = explosive; } + public float getExplosionRadius() { return combatData.explosionRadius; } + public void setExplosionRadius(float explosionRadius) { this.combatData.explosionRadius = explosionRadius; } + public float getExplosionDamageFalloff() { return combatData.explosionDamageFalloff; } + public void setExplosionDamageFalloff(float explosionDamageFalloff) { this.combatData.explosionDamageFalloff = explosionDamageFalloff; } + // Getters & Setters - Color data + public int getInnerColor() { return colorData.innerColor; } + public void setInnerColor(int innerColor) { this.colorData.innerColor = innerColor; } + public int getOuterColor() { return colorData.outerColor; } + public void setOuterColor(int outerColor) { this.colorData.outerColor = outerColor; } + public float getOuterColorWidth() { return colorData.outerColorWidth; } + public void setOuterColorWidth(float outerColorWidth) { this.colorData.outerColorWidth = outerColorWidth; } + public float getOuterColorAlpha() { return colorData.outerColorAlpha; } + public void setOuterColorAlpha(float outerColorAlpha) { this.colorData.outerColorAlpha = outerColorAlpha; } + public boolean isOuterColorEnabled() { return colorData.outerColorEnabled; } + public void setOuterColorEnabled(boolean outerColorEnabled) { this.colorData.outerColorEnabled = outerColorEnabled; } + + // Getters & Setters - Lightning data + public boolean hasLightningEffect() { return lightningData.lightningEffect; } + public void setLightningEffect(boolean lightningEffect) { this.lightningData.lightningEffect = lightningEffect; } + public float getLightningDensity() { return lightningData.lightningDensity; } + public void setLightningDensity(float lightningDensity) { this.lightningData.lightningDensity = lightningDensity; } + public float getLightningRadius() { return lightningData.lightningRadius; } + public void setLightningRadius(float lightningRadius) { this.lightningData.lightningRadius = lightningRadius; } + + public AnchorPoint getAnchorPointEnum() { return anchorData.anchorPoint; } + + public float getAnchorOffsetX() { return anchorData.anchorOffsetX; } + public float getAnchorOffsetY() { return anchorData.anchorOffsetY; } + public float getAnchorOffsetZ() { return anchorData.anchorOffsetZ; } + public void setAnchorPointEnum(AnchorPoint anchorPoint) { this.anchorData.anchorPoint = anchorPoint; } + public void setAnchorOffsetX(float x) { this.anchorData.anchorOffsetX = x; } + public void setAnchorOffsetY(float y) { this.anchorData.anchorOffsetY = y; } + public void setAnchorOffsetZ(float z) { this.anchorData.anchorOffsetZ = z; } + public int getAnchorPoint() { + return anchorData.anchorPoint.ordinal(); + } + public void setAnchorPoint(int point) { this.anchorData.anchorPoint = AnchorPoint.fromOrdinal(point); } + + @Override + @SideOnly(Side.CLIENT) + public Entity createPreviewEntity(EntityNPCInterface npc) { + if (npc == null || npc.worldObj == null) return null; + + EntityAbilityLaser laser = new EntityAbilityLaser(npc.worldObj); + laser.setupPreview(npc, laserWidth, colorData, lightningData, expansionSpeed, lifespanData.maxDistance); + return laser; + } + + @Override + public int getPreviewActiveDuration() { + return lifespanData.maxLifetime > 0 ? Math.min(lifespanData.maxLifetime, 60) : 60; + } + + @Override + public boolean spawnPreviewDuringWindup() { + return false; // Laser has no charging, spawns at active phase + } + + @SideOnly(Side.CLIENT) + @Override + public List getFieldDefinitions() { + return Arrays.asList( + // Type tab + FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage), + FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback).column(ColumnHint.LEFT), + FieldDef.floatField("ability.knockbackUp", this::getKnockbackUp, this::setKnockbackUp).column(ColumnHint.RIGHT), + FieldDef.section("ability.section.beam"), + FieldDef.floatField("ability.laserWidth", this::getLaserWidth, this::setLaserWidth).column(ColumnHint.LEFT), + FieldDef.floatField("ability.expansionSpeed", this::getExpansionSpeed, this::setExpansionSpeed).column(ColumnHint.RIGHT), + FieldDef.intField("ability.lingerTicks", this::getLingerTicks, this::setLingerTicks).column(ColumnHint.LEFT), + FieldDef.intField("ability.lifetime", this::getMaxLifetime, this::setMaxLifetime).column(ColumnHint.RIGHT), + FieldDef.floatField("ability.maxDistance", this::getMaxDistance, this::setMaxDistance), + FieldDef.section("ability.section.explosive"), + FieldDef.boolField("gui.enabled", this::isExplosive, this::setExplosive).hover("ability.hover.explosive"), + FieldDef.floatField("gui.radius", this::getExplosionRadius, this::setExplosionRadius).visibleWhen(this::isExplosive), + AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects), + // Visual tab + FieldDef.section("ability.section.colors").tab("ability.tab.visual"), + FieldDef.colorSubGui("ability.innerColor", this::getInnerColor, this::setInnerColor).tab("ability.tab.visual"), + FieldDef.boolField("ability.outerEnabled", this::isOuterColorEnabled, this::setOuterColorEnabled).tab("ability.tab.visual"), + FieldDef.colorSubGui("ability.outerColor", this::getOuterColor, this::setOuterColor) + .tab("ability.tab.visual").visibleWhen(this::isOuterColorEnabled), + FieldDef.floatField("ability.outerWidth", this::getOuterColorWidth, this::setOuterColorWidth) + .tab("ability.tab.visual").visibleWhen(this::isOuterColorEnabled).column(ColumnHint.LEFT), + FieldDef.floatField("ability.outerAlpha", this::getOuterColorAlpha, this::setOuterColorAlpha) + .tab("ability.tab.visual").range(0, 1).visibleWhen(this::isOuterColorEnabled).column(ColumnHint.RIGHT), + FieldDef.section("ability.section.effects").tab("ability.tab.visual"), + FieldDef.boolField("ability.lightning", this::hasLightningEffect, this::setLightningEffect).tab("ability.tab.visual"), + FieldDef.floatField("gui.density", this::getLightningDensity, this::setLightningDensity) + .tab("ability.tab.visual").range(0.01f, 5.0f).visibleWhen(this::hasLightningEffect).column(ColumnHint.LEFT), + FieldDef.floatField("gui.radius", this::getLightningRadius, this::setLightningRadius) + .tab("ability.tab.visual").range(0.1f, 10.0f).visibleWhen(this::hasLightningEffect).column(ColumnHint.RIGHT) + ); + } +} 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 99583a5d0..e8e48a732 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 @@ -3,53 +3,49 @@ package kamkeel.npcs.controllers.data.ability.type; 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.AnchorPoint; +import kamkeel.npcs.controllers.data.ability.LockMovementType; import kamkeel.npcs.controllers.data.ability.TargetingMode; +import kamkeel.npcs.controllers.data.ability.data.*; +import kamkeel.npcs.controllers.data.telegraph.Telegraph; +import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; import kamkeel.npcs.controllers.data.telegraph.TelegraphType; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.util.AnchorPointHelper; +import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; -import net.minecraft.potion.Potion; -import net.minecraft.potion.PotionEffect; -import net.minecraft.util.AxisAlignedBB; -import net.minecraft.util.DamageSource; +import net.minecraft.util.Vec3; import net.minecraft.world.World; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityOrb; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; +import noppes.npcs.client.gui.builder.ColumnHint; +import noppes.npcs.client.gui.builder.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.AbilityFieldDefs; +import noppes.npcs.api.ability.type.IAbilityOrb; + +import java.util.Arrays; +import java.util.List; import noppes.npcs.entity.EntityNPCInterface; -import java.util.List; - /** - * Orb ability: Homing projectile sphere that tracks target. - * Can be explosive on impact and/or apply debuffs. + * Orb ability: Spawns a homing projectile sphere that tracks target. + * The EntityAbilityOrb handles all movement, collision, and damage logic. + * This ability just configures and spawns the orb entity. */ -public class AbilityOrb extends Ability { +public class AbilityOrb extends Ability implements IAbilityOrb { - private float orbSpeed = 0.5f; + // Ability-specific properties private float orbSize = 1.0f; - private float damage = 7.0f; - private float knockback = 1.0f; - private float maxDistance = 30.0f; - private int maxLifetime = 200; - private boolean homing = true; - private float homingStrength = 0.15f; - private float homingRange = 20.0f; + // Data classes for energy properties + private final EnergyColorData colorData = new EnergyColorData(); + private final EnergyCombatData combatData = new EnergyCombatData(); + private final EnergyHomingData homingData = new EnergyHomingData(); + private final EnergyLightningData lightningData = new EnergyLightningData(); + private final EnergyLifespanData lifespanData = new EnergyLifespanData(); + private final EnergyAnchorData anchorData = new EnergyAnchorData(AnchorPoint.RIGHT_HAND); - private boolean explosive = false; - private float explosionRadius = 3.0f; - private float explosionDamageFalloff = 0.5f; - - private int stunDuration = 0; - private int slowDuration = 0; - private int slowLevel = 0; - - // Runtime state - private transient double orbX, orbY, orbZ; - private transient double velocityX, velocityY, velocityZ; - private transient double startX, startY, startZ; - private transient boolean hasHit = false; - private transient int ticksAlive = 0; + // Transient state for orb entity (used during windup charging) + private transient EntityAbilityOrb orbEntity = null; public AbilityOrb() { this.typeId = "ability.cnpc.orb"; @@ -57,24 +53,14 @@ public class AbilityOrb extends Ability { this.targetingMode = TargetingMode.AGGRO_TARGET; this.maxRange = 25.0f; this.minRange = 5.0f; - this.cooldownTicks = 100; + this.cooldownTicks = 0; this.windUpTicks = 30; - this.activeTicks = 200; - this.recoveryTicks = 10; - this.telegraphType = TelegraphType.POINT; - this.showTelegraph = false; - } - - @Override - public boolean hasTypeSettings() { - return true; - } - - @Override - @SideOnly(Side.CLIENT) - public SubGuiAbilityConfig createConfigGui( - IAbilityConfigCallback callback) { - return new SubGuiAbilityOrb(this, callback); + this.lockMovement = LockMovementType.WINDUP; + this.telegraphType = TelegraphType.CIRCLE; + this.showTelegraph = true; + // Default built-in animation + this.windUpAnimationName = "Ability_Orb_Windup"; + this.activeAnimationName = "Ability_Orb_Active"; } @Override @@ -88,241 +74,125 @@ public class AbilityOrb extends Ability { } @Override - public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) { - startX = orbX = npc.posX; - startY = orbY = npc.posY + npc.getEyeHeight() * 0.8; - startZ = orbZ = npc.posZ; - hasHit = false; - ticksAlive = 0; - - if (target != null) { - double dx = target.posX - orbX; - double dy = (target.posY + target.height * 0.5) - orbY; - double dz = target.posZ - orbZ; - double len = Math.sqrt(dx * dx + dy * dy + dz * dz); - if (len > 0) { - velocityX = (dx / len) * orbSpeed; - velocityY = (dy / len) * orbSpeed; - velocityZ = (dz / len) * orbSpeed; - } - } else { - float yaw = (float) Math.toRadians(npc.rotationYaw); - float pitch = (float) Math.toRadians(npc.rotationPitch); - velocityX = -Math.sin(yaw) * Math.cos(pitch) * orbSpeed; - velocityY = -Math.sin(pitch) * orbSpeed; - velocityZ = Math.cos(yaw) * Math.cos(pitch) * orbSpeed; - } - } - - @Override - public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { - if (hasHit) return; - - ticksAlive++; - - if (ticksAlive >= maxLifetime) { - hasHit = true; + public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { + if (world.isRemote) { + signalCompletion(); return; } - double distTraveled = Math.sqrt( - Math.pow(orbX - startX, 2) + - Math.pow(orbY - startY, 2) + - Math.pow(orbZ - startZ, 2) - ); - if (distTraveled >= maxDistance) { - hasHit = true; - return; + // Start moving the orb that was spawned during windup + if (orbEntity != null && !orbEntity.isDead) { + orbEntity.startMoving(target); } - if (homing && target != null && target.isEntityAlive()) { - double targetX = target.posX; - double targetY = target.posY + target.height * 0.5; - double targetZ = target.posZ; - - double distToTarget = Math.sqrt( - Math.pow(targetX - orbX, 2) + - Math.pow(targetY - orbY, 2) + - Math.pow(targetZ - orbZ, 2) - ); - - if (distToTarget <= homingRange) { - double dx = targetX - orbX; - double dy = targetY - orbY; - double dz = targetZ - orbZ; - double len = Math.sqrt(dx * dx + dy * dy + dz * dz); - if (len > 0) { - double desiredVX = (dx / len) * orbSpeed; - double desiredVY = (dy / len) * orbSpeed; - double desiredVZ = (dz / len) * orbSpeed; - - velocityX += (desiredVX - velocityX) * homingStrength; - velocityY += (desiredVY - velocityY) * homingStrength; - velocityZ += (desiredVZ - velocityZ) * homingStrength; - - double vLen = Math.sqrt(velocityX * velocityX + velocityY * velocityY + velocityZ * velocityZ); - if (vLen > 0) { - velocityX = (velocityX / vLen) * orbSpeed; - velocityY = (velocityY / vLen) * orbSpeed; - velocityZ = (velocityZ / vLen) * orbSpeed; - } - } - } - } - - orbX += velocityX; - orbY += velocityY; - orbZ += velocityZ; - - if (!world.isRemote) { - checkCollision(npc, world); - } + // Ability stays active until entity dies (prevents firing another while projectile is alive) + // Movement locking is handled separately by the base class } - private void checkCollision(EntityNPCInterface npc, World world) { - AxisAlignedBB hitBox = AxisAlignedBB.getBoundingBox( - orbX - orbSize, orbY - orbSize, orbZ - orbSize, - orbX + orbSize, orbY + orbSize, orbZ + orbSize - ); + @Override + public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { + if (world.isRemote) return; - @SuppressWarnings("unchecked") - List entities = world.getEntitiesWithinAABB(EntityLivingBase.class, hitBox); + // Spawn orb in charging mode on first tick of windup + if (tick == 1) { + // Create orb in charging mode - follows caster based on anchor point during windup + Vec3 spawnPos = AnchorPointHelper.calculateAnchorPosition(caster, anchorData); + orbEntity = new EntityAbilityOrb( + world, caster, target, + spawnPos.xCoord, spawnPos.yCoord, spawnPos.zCoord, orbSize, + colorData, combatData, homingData, lightningData, lifespanData); + orbEntity.setupCharging(anchorData, windUpTicks); - for (EntityLivingBase entity : entities) { - if (entity == npc) continue; - - hasHit = true; - - if (explosive) { - doExplosion(npc, world); - } else { - // Apply damage with scripted event support - boolean wasHit = applyAbilityDamage(npc, entity, damage, knockback); - if (wasHit) { - applyEffects(entity); - } - } - break; - } - - if (!hasHit && world.getBlock((int) orbX, (int) orbY, (int) orbZ).getMaterial().isSolid()) { - hasHit = true; - if (explosive) { - doExplosion(npc, world); - } - } - } - - private void doExplosion(EntityNPCInterface npc, World world) { - AxisAlignedBB explosionBox = AxisAlignedBB.getBoundingBox( - orbX - explosionRadius, orbY - explosionRadius, orbZ - explosionRadius, - orbX + explosionRadius, orbY + explosionRadius, orbZ + explosionRadius - ); - - @SuppressWarnings("unchecked") - List blastTargets = world.getEntitiesWithinAABB(EntityLivingBase.class, explosionBox); - - for (EntityLivingBase blastTarget : blastTargets) { - if (blastTarget == npc) continue; - - double dist = Math.sqrt( - Math.pow(blastTarget.posX - orbX, 2) + - Math.pow(blastTarget.posY - orbY, 2) + - Math.pow(blastTarget.posZ - orbZ, 2) - ); - - if (dist <= explosionRadius) { - float falloff = 1.0f - (float) (dist / explosionRadius) * explosionDamageFalloff; - // Apply damage with scripted event support - boolean wasHit = applyAbilityDamage(npc, blastTarget, damage * falloff, knockback * falloff); - if (wasHit) { - applyEffects(blastTarget); - } - } - } - - world.playSoundEffect(orbX, orbY, orbZ, "random.explode", 1.0f, 1.0f); - } - - private void applyKnockback(EntityLivingBase target, float strength, float upward) { - double dx = target.posX - orbX; - double dz = target.posZ - orbZ; - double dist = Math.sqrt(dx * dx + dz * dz); - if (dist > 0) { - target.motionX += (dx / dist) * strength; - target.motionZ += (dz / dist) * strength; - } - target.motionY += upward; - target.velocityChanged = true; - } - - private void applyEffects(EntityLivingBase target) { - if (stunDuration > 0) { - target.addPotionEffect(new PotionEffect(Potion.moveSlowdown.id, stunDuration, 10)); - target.addPotionEffect(new PotionEffect(Potion.weakness.id, stunDuration, 2)); - } - if (slowDuration > 0 && slowLevel > 0) { - target.addPotionEffect(new PotionEffect(Potion.moveSlowdown.id, slowDuration, slowLevel)); + orbEntity.setEffects(this.effects); + world.spawnEntityInWorld(orbEntity); } } @Override - public void onComplete(EntityNPCInterface npc, EntityLivingBase target) { - hasHit = false; - ticksAlive = 0; + public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { + // Signal completion when entity dies + if (orbEntity == null || orbEntity.isDead) { + orbEntity = null; + signalCompletion(); + } } @Override - public void onInterrupt(EntityNPCInterface npc, DamageSource source, float damage) { - hasHit = false; - ticksAlive = 0; + public void onComplete(EntityLivingBase caster, EntityLivingBase target) { + // Nothing to clean up - entity manages itself + } + + @Override + public void cleanup() { + // Despawn orb entity if still alive + if (orbEntity != null && !orbEntity.isDead) { + orbEntity.setDead(); + } + orbEntity = null; + } + + @Override + public TelegraphInstance createTelegraph(EntityLivingBase caster, EntityLivingBase target) { + if (!showTelegraph || telegraphType == TelegraphType.NONE || target == null) { + return null; + } + + // Create small circle telegraph at target position + Telegraph telegraph = Telegraph.circle(orbSize * 1.5f); + telegraph.setDurationTicks(windUpTicks); + telegraph.setColor(windUpColor); + telegraph.setWarningColor(activeColor); + telegraph.setWarningStartTick(Math.max(5, windUpTicks / 4)); + telegraph.setHeightOffset(telegraphHeightOffset); + + // Position at target and follow target during windup + TelegraphInstance instance = new TelegraphInstance(telegraph, target.posX, target.posY, target.posZ, caster.rotationYaw); + instance.setCasterEntityId(caster.getEntityId()); + instance.setEntityIdToFollow(target.getEntityId()); + + return instance; + } + + @Override + public float getTelegraphRadius() { + return orbSize * 1.5f; } @Override public void writeTypeNBT(NBTTagCompound nbt) { - nbt.setFloat("orbSpeed", orbSpeed); nbt.setFloat("orbSize", orbSize); - nbt.setFloat("damage", damage); - nbt.setFloat("knockback", knockback); - nbt.setFloat("maxDistance", maxDistance); - nbt.setInteger("maxLifetime", maxLifetime); - nbt.setBoolean("homing", homing); - nbt.setFloat("homingStrength", homingStrength); - nbt.setFloat("homingRange", homingRange); - nbt.setBoolean("explosive", explosive); - nbt.setFloat("explosionRadius", explosionRadius); - nbt.setFloat("explosionDamageFalloff", explosionDamageFalloff); - nbt.setInteger("stunDuration", stunDuration); - nbt.setInteger("slowDuration", slowDuration); - nbt.setInteger("slowLevel", slowLevel); + anchorData.writeNBT(nbt); + colorData.writeNBT(nbt); + combatData.writeNBT(nbt); + homingData.writeNBT(nbt); + lightningData.writeNBT(nbt); + lifespanData.writeNBT(nbt); + // Backward compat: old key was "orbSpeed" + nbt.setFloat("orbSpeed", homingData.speed); } @Override public void readTypeNBT(NBTTagCompound nbt) { - this.orbSpeed = nbt.hasKey("orbSpeed") ? nbt.getFloat("orbSpeed") : 0.5f; this.orbSize = nbt.hasKey("orbSize") ? nbt.getFloat("orbSize") : 1.0f; - this.damage = nbt.hasKey("damage") ? nbt.getFloat("damage") : 7.0f; - this.knockback = nbt.hasKey("knockback") ? nbt.getFloat("knockback") : 1.0f; - this.maxDistance = nbt.hasKey("maxDistance") ? nbt.getFloat("maxDistance") : 30.0f; - this.maxLifetime = nbt.hasKey("maxLifetime") ? nbt.getInteger("maxLifetime") : 200; - this.homing = !nbt.hasKey("homing") || nbt.getBoolean("homing"); - this.homingStrength = nbt.hasKey("homingStrength") ? nbt.getFloat("homingStrength") : 0.15f; - this.homingRange = nbt.hasKey("homingRange") ? nbt.getFloat("homingRange") : 20.0f; - this.explosive = nbt.hasKey("explosive") && nbt.getBoolean("explosive"); - this.explosionRadius = nbt.hasKey("explosionRadius") ? nbt.getFloat("explosionRadius") : 3.0f; - this.explosionDamageFalloff = nbt.hasKey("explosionDamageFalloff") ? nbt.getFloat("explosionDamageFalloff") : 0.5f; - this.stunDuration = nbt.hasKey("stunDuration") ? nbt.getInteger("stunDuration") : 0; - this.slowDuration = nbt.hasKey("slowDuration") ? nbt.getInteger("slowDuration") : 0; - this.slowLevel = nbt.hasKey("slowLevel") ? nbt.getInteger("slowLevel") : 0; + anchorData.readNBT(nbt); + colorData.readNBT(nbt); + combatData.readNBT(nbt); + homingData.readNBT(nbt); + lightningData.readNBT(nbt); + lifespanData.readNBT(nbt); + // Backward compat: old key was "orbSpeed" + if (nbt.hasKey("orbSpeed")) { + homingData.speed = nbt.getFloat("orbSpeed"); + } } // Getters & Setters public float getOrbSpeed() { - return orbSpeed; + return homingData.speed; } public void setOrbSpeed(float orbSpeed) { - this.orbSpeed = orbSpeed; + homingData.speed = orbSpeed; } public float getOrbSize() { @@ -333,124 +203,253 @@ public class AbilityOrb extends Ability { this.orbSize = orbSize; } - public float getDamage() { - return damage; - } - - public void setDamage(float damage) { - this.damage = damage; - } - - public float getKnockback() { - return knockback; - } - - public void setKnockback(float knockback) { - this.knockback = knockback; - } - public float getMaxDistance() { - return maxDistance; + return lifespanData.maxDistance; } public void setMaxDistance(float maxDistance) { - this.maxDistance = maxDistance; + lifespanData.maxDistance = maxDistance; } public int getMaxLifetime() { - return maxLifetime; + return lifespanData.maxLifetime; } public void setMaxLifetime(int maxLifetime) { - this.maxLifetime = maxLifetime; + lifespanData.maxLifetime = maxLifetime; + } + + public float getDamage() { + return combatData.damage; + } + + public void setDamage(float damage) { + combatData.damage = damage; + } + + public float getKnockback() { + return combatData.knockback; + } + + public void setKnockback(float knockback) { + combatData.knockback = knockback; + } + + public float getKnockbackUp() { + return combatData.knockbackUp; + } + + public void setKnockbackUp(float knockbackUp) { + combatData.knockbackUp = knockbackUp; } public boolean isHoming() { - return homing; + return homingData.homing; } public void setHoming(boolean homing) { - this.homing = homing; + homingData.homing = homing; } public float getHomingStrength() { - return homingStrength; + return homingData.homingStrength; } public void setHomingStrength(float homingStrength) { - this.homingStrength = homingStrength; + homingData.homingStrength = homingStrength; } public float getHomingRange() { - return homingRange; + return homingData.homingRange; } public void setHomingRange(float homingRange) { - this.homingRange = homingRange; + homingData.homingRange = homingRange; } public boolean isExplosive() { - return explosive; + return combatData.explosive; } public void setExplosive(boolean explosive) { - this.explosive = explosive; + combatData.explosive = explosive; } public float getExplosionRadius() { - return explosionRadius; + return combatData.explosionRadius; } public void setExplosionRadius(float explosionRadius) { - this.explosionRadius = explosionRadius; + combatData.explosionRadius = explosionRadius; } public float getExplosionDamageFalloff() { - return explosionDamageFalloff; + return combatData.explosionDamageFalloff; } public void setExplosionDamageFalloff(float explosionDamageFalloff) { - this.explosionDamageFalloff = explosionDamageFalloff; + combatData.explosionDamageFalloff = explosionDamageFalloff; } - public int getStunDuration() { - return stunDuration; + public int getInnerColor() { + return colorData.innerColor; } - public void setStunDuration(int stunDuration) { - this.stunDuration = stunDuration; + public void setInnerColor(int innerColor) { + colorData.innerColor = innerColor; } - public int getSlowDuration() { - return slowDuration; + public int getOuterColor() { + return colorData.outerColor; } - public void setSlowDuration(int slowDuration) { - this.slowDuration = slowDuration; + public void setOuterColor(int outerColor) { + colorData.outerColor = outerColor; } - public int getSlowLevel() { - return slowLevel; + public float getOuterColorWidth() { + return colorData.outerColorWidth; } - public void setSlowLevel(int slowLevel) { - this.slowLevel = slowLevel; + public void setOuterColorWidth(float outerColorWidth) { + colorData.outerColorWidth = outerColorWidth; } - // Runtime getters for rendering - public double getOrbX() { - return orbX; + public float getOuterColorAlpha() { + return colorData.outerColorAlpha; } - public double getOrbY() { - return orbY; + public void setOuterColorAlpha(float outerColorAlpha) { + colorData.outerColorAlpha = outerColorAlpha; } - public double getOrbZ() { - return orbZ; + public boolean isOuterColorEnabled() { + return colorData.outerColorEnabled; } - public boolean hasHit() { - return hasHit; + public void setOuterColorEnabled(boolean outerColorEnabled) { + colorData.outerColorEnabled = outerColorEnabled; + } + + public float getRotationSpeed() { + return colorData.rotationSpeed; + } + + public void setRotationSpeed(float rotationSpeed) { + colorData.rotationSpeed = rotationSpeed; + } + + public boolean hasLightningEffect() { + return lightningData.lightningEffect; + } + + public void setLightningEffect(boolean lightningEffect) { + lightningData.lightningEffect = lightningEffect; + } + + public float getLightningDensity() { + return lightningData.lightningDensity; + } + + public void setLightningDensity(float lightningDensity) { + lightningData.lightningDensity = lightningDensity; + } + + public float getLightningRadius() { + return lightningData.lightningRadius; + } + + public void setLightningRadius(float lightningRadius) { + lightningData.lightningRadius = lightningRadius; + } + + public int getLightningFadeTime() { + return lightningData.lightningFadeTime; + } + + public void setLightningFadeTime(int lightningFadeTime) { + lightningData.lightningFadeTime = lightningFadeTime; + } + + public AnchorPoint getAnchorPointEnum() { return anchorData.anchorPoint; } + + public float getAnchorOffsetX() { return anchorData.anchorOffsetX; } + + public float getAnchorOffsetY() { return anchorData.anchorOffsetY; } + + public float getAnchorOffsetZ() { return anchorData.anchorOffsetZ; } + + public void setAnchorPointEnum(AnchorPoint anchorPoint) { this.anchorData.anchorPoint = anchorPoint; } + + public void setAnchorOffsetX(float x) { this.anchorData.anchorOffsetX = x; } + + public void setAnchorOffsetY(float y) { this.anchorData.anchorOffsetY = y; } + + public void setAnchorOffsetZ(float z) { this.anchorData.anchorOffsetZ = z; } + + public int getAnchorPoint() { + return anchorData.anchorPoint.ordinal(); + } + + @Override + public void setAnchorPoint(int point) { + this.anchorData.anchorPoint = AnchorPoint.fromOrdinal(point); + } + + @Override + @SideOnly(Side.CLIENT) + public Entity createPreviewEntity(EntityNPCInterface npc) { + if (npc == null || npc.worldObj == null) return null; + + EntityAbilityOrb orb = new EntityAbilityOrb(npc.worldObj); + orb.setupPreview(npc, orbSize, colorData, lightningData, anchorData, windUpTicks); + return orb; + } + + @Override + public int getPreviewActiveDuration() { + return lifespanData.maxLifetime > 0 ? Math.min(lifespanData.maxLifetime, 100) : 100; + } + + @SideOnly(Side.CLIENT) + @Override + public List getFieldDefinitions() { + return Arrays.asList( + // Type tab + FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage).column(ColumnHint.LEFT), + FieldDef.floatField("stats.speed", this::getOrbSpeed, this::setOrbSpeed).column(ColumnHint.RIGHT), + FieldDef.floatField("stats.size", this::getOrbSize, this::setOrbSize).column(ColumnHint.LEFT), + FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback).column(ColumnHint.RIGHT), + FieldDef.floatField("ability.maxDistance", this::getMaxDistance, this::setMaxDistance).column(ColumnHint.LEFT), + FieldDef.intField("ability.lifetime", this::getMaxLifetime, this::setMaxLifetime).column(ColumnHint.RIGHT), + FieldDef.section("ability.section.homing"), + FieldDef.boolField("gui.enabled", this::isHoming, this::setHoming).hover("ability.hover.homing"), + FieldDef.floatField("gui.strength", this::getHomingStrength, this::setHomingStrength).visibleWhen(this::isHoming), + FieldDef.section("ability.section.explosive"), + FieldDef.boolField("gui.enabled", this::isExplosive, this::setExplosive).hover("ability.hover.explosive"), + FieldDef.floatField("gui.radius", this::getExplosionRadius, this::setExplosionRadius).visibleWhen(this::isExplosive), + AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects), + + + // Visual tab + FieldDef.enumField("ability.anchorPoint", AnchorPoint.class, this::getAnchorPointEnum, this::setAnchorPointEnum) + .tab("ability.tab.visual"), + FieldDef.section("ability.section.colors").tab("ability.tab.visual"), + FieldDef.colorSubGui("ability.innerColor", this::getInnerColor, this::setInnerColor).tab("ability.tab.visual"), + FieldDef.boolField("ability.outerEnabled", this::isOuterColorEnabled, this::setOuterColorEnabled).tab("ability.tab.visual"), + FieldDef.colorSubGui("ability.outerColor", this::getOuterColor, this::setOuterColor) + .tab("ability.tab.visual").visibleWhen(this::isOuterColorEnabled), + FieldDef.floatField("ability.outerWidth", this::getOuterColorWidth, this::setOuterColorWidth) + .tab("ability.tab.visual").visibleWhen(this::isOuterColorEnabled).column(ColumnHint.LEFT), + FieldDef.floatField("ability.outerAlpha", this::getOuterColorAlpha, this::setOuterColorAlpha) + .tab("ability.tab.visual").range(0, 1).visibleWhen(this::isOuterColorEnabled).column(ColumnHint.RIGHT), + FieldDef.section("ability.section.effects").tab("ability.tab.visual"), + FieldDef.floatField("ability.rotationSpeed", this::getRotationSpeed, this::setRotationSpeed).tab("ability.tab.visual"), + FieldDef.boolField("ability.lightning", this::hasLightningEffect, this::setLightningEffect).tab("ability.tab.visual"), + FieldDef.floatField("gui.density", this::getLightningDensity, this::setLightningDensity) + .tab("ability.tab.visual").range(0.01f, 5.0f).visibleWhen(this::hasLightningEffect).column(ColumnHint.LEFT), + FieldDef.floatField("gui.radius", this::getLightningRadius, this::setLightningRadius) + .tab("ability.tab.visual").range(0.1f, 10.0f).visibleWhen(this::hasLightningEffect).column(ColumnHint.RIGHT) + ); } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrbDual.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrbDual.java new file mode 100644 index 000000000..658d4092f --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrbDual.java @@ -0,0 +1,562 @@ +package kamkeel.npcs.controllers.data.ability.type; + +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.AnchorPoint; +import kamkeel.npcs.controllers.data.ability.LockMovementType; +import kamkeel.npcs.controllers.data.ability.TargetingMode; +import kamkeel.npcs.controllers.data.ability.data.*; +import noppes.npcs.client.gui.builder.ColumnHint; +import noppes.npcs.client.gui.builder.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.AbilityFieldDefs; +import kamkeel.npcs.controllers.data.telegraph.Telegraph; +import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; +import kamkeel.npcs.controllers.data.telegraph.TelegraphType; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.util.AnchorPointHelper; +import net.minecraft.entity.Entity; +import net.minecraft.entity.EntityLivingBase; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.util.Vec3; +import net.minecraft.world.World; +import noppes.npcs.entity.EntityNPCInterface; +import noppes.npcs.util.ValueUtil; + +import java.util.Arrays; +import java.util.List; + +public class AbilityOrbDual extends Ability { + // Ability-specific properties + private float orbSize = 1.0f; + private boolean dualFire= true; + private int dualFireDelay = 20; + + // Data classes for energy properties + public final EnergyColorData[] colorData = new EnergyColorData[]{ + new EnergyColorData(), new EnergyColorData() + }; + public final EnergyCombatData combatData = new EnergyCombatData(); + public final EnergyHomingData homingData = new EnergyHomingData(); + public final EnergyLightningData[] lightningData = new EnergyLightningData[]{ + new EnergyLightningData(), new EnergyLightningData() + }; + public final EnergyLifespanData lifespanData = new EnergyLifespanData(); + private EnergyAnchorData[] anchorData = new EnergyAnchorData[]{ + new EnergyAnchorData(AnchorPoint.RIGHT_HAND), new EnergyAnchorData(AnchorPoint.LEFT_HAND) + }; + + // Transient state for orb entity (used during windup charging) + private transient EntityAbilityOrb orbEntity = null; + private transient EntityAbilityOrb orbEntity2 = null; + + public AbilityOrbDual() { + this.typeId = "ability.cnpc.orb_dual"; + this.name = "Orb Dual"; + this.targetingMode = TargetingMode.AGGRO_TARGET; + this.maxRange = 25.0f; + this.minRange = 5.0f; + this.cooldownTicks = 0; + this.windUpTicks = 30; + this.lockMovement = LockMovementType.WINDUP; + this.telegraphType = TelegraphType.CIRCLE; + this.showTelegraph = true; + // Default built-in animation + this.windUpAnimationName = "Ability_BeamDual_Windup"; + this.activeAnimationName = "Ability_BeamDual_Active"; + } + + @Override + public boolean isTargetingModeLocked() { + return true; + } + + @Override + public TargetingMode[] getAllowedTargetingModes() { + return new TargetingMode[]{TargetingMode.AGGRO_TARGET}; + } + + @Override + public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { + if (world.isRemote) { + signalCompletion(); + return; + } + + // Start moving the orb that was spawned during windup + if (orbEntity != null && !orbEntity.isDead) { + orbEntity.startMoving(target); + } + + if (dualFire) { + if (orbEntity2 != null && !orbEntity2.isDead && dualFireDelay <= 0) { + orbEntity2.startMoving(target); + } + } else { + orbEntity2.setDead(); + } + + // Ability stays active until entity dies (prevents firing another while projectile is alive) + // Movement locking is handled separately by the base class + } + + @Override + public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { + if (world.isRemote) return; + + // Spawn orb in charging mode on first tick of windup + if (tick == 1) { + // Create orb in charging mode - follows caster based on anchor point during windup + Vec3 spawnPos1 = AnchorPointHelper.calculateAnchorPosition(caster, anchorData[0]); + orbEntity = new EntityAbilityOrb( + world, caster, target, + spawnPos1.xCoord, spawnPos1.yCoord, spawnPos1.zCoord, orbSize, + colorData[0], combatData, homingData, lightningData[0], lifespanData); + + Vec3 spawnPos2 = AnchorPointHelper.calculateAnchorPosition(caster, anchorData[1]); + orbEntity2 = new EntityAbilityOrb( + world, caster, target, + spawnPos2.xCoord, spawnPos2.yCoord, spawnPos2.zCoord, orbSize, + colorData[1], combatData, homingData, lightningData[1], lifespanData); + + orbEntity.setSiblingUUID(orbEntity2.getPersistentID()); + orbEntity2.setSiblingUUID(orbEntity.getPersistentID()); + + orbEntity.setupCharging(anchorData[0], windUpTicks); + orbEntity2.setupCharging(anchorData[1], windUpTicks); + + orbEntity.setEffects(this.effects); + + if (dualFire) { + orbEntity2.setEffects(this.effects); + } + + world.spawnEntityInWorld(orbEntity); + world.spawnEntityInWorld(orbEntity2); + } + } + + @Override + public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { + // Signal completion when entity dies + if (dualFire) { + if ((orbEntity == null || orbEntity.isDead) && (orbEntity2 == null || orbEntity2.isDead)) { + orbEntity = null; + orbEntity2 = null; + signalCompletion(); + } + + if (tick == dualFireDelay && orbEntity2 != null && !orbEntity2.isDead) { + orbEntity2.startMoving(target); + } + } else { + if (orbEntity == null || orbEntity.isDead) { + orbEntity = null; + signalCompletion(); + } + } + } + + @Override + public void onComplete(EntityLivingBase caster, EntityLivingBase target) { + // Nothing to clean up - entity manages itself + } + + @Override + public void cleanup() { + // Despawn orb entity if still alive + if (orbEntity != null && !orbEntity.isDead) { + orbEntity.setDead(); + } + + if (orbEntity2 != null && !orbEntity2.isDead) { + orbEntity2.setDead(); + } + + orbEntity = null; + orbEntity2 = null; + } + + @Override + public TelegraphInstance createTelegraph(EntityLivingBase caster, EntityLivingBase target) { + if (!showTelegraph || telegraphType == TelegraphType.NONE || target == null) { + return null; + } + + // Create small circle telegraph at target position + Telegraph telegraph = Telegraph.circle(orbSize * 1.5f); + telegraph.setDurationTicks(windUpTicks); + telegraph.setColor(windUpColor); + telegraph.setWarningColor(activeColor); + telegraph.setWarningStartTick(Math.max(5, windUpTicks / 4)); + telegraph.setHeightOffset(telegraphHeightOffset); + + // Position at target and follow target during windup + TelegraphInstance instance = new TelegraphInstance(telegraph, target.posX, target.posY, target.posZ, caster.rotationYaw); + instance.setCasterEntityId(caster.getEntityId()); + instance.setEntityIdToFollow(target.getEntityId()); + + return instance; + } + + @Override + public float getTelegraphRadius() { + return orbSize * 1.5f; + } + + @Override + public void writeTypeNBT(NBTTagCompound nbt) { + nbt.setFloat("orbSize", orbSize); + nbt.setBoolean("dualFire", dualFire); + nbt.setInteger("dualFireDelay", dualFireDelay); + combatData.writeNBT(nbt); + homingData.writeNBT(nbt); + lifespanData.writeNBT(nbt); + // Backward compat: old key was "orbSpeed" + nbt.setFloat("orbSpeed", homingData.speed); + + NBTTagCompound orb1 = new NBTTagCompound(); + NBTTagCompound orb2 = new NBTTagCompound(); + + anchorData[0].writeNBT(orb1); + anchorData[1].writeNBT(orb2); + colorData[0].writeNBT(orb1); + colorData[1].writeNBT(orb2); + lightningData[0].writeNBT(orb1); + lightningData[1].writeNBT(orb2); + + nbt.setTag("Orb_1", orb1); + nbt.setTag("Orb_2", orb2); + } + + @Override + public void readTypeNBT(NBTTagCompound nbt) { + NBTTagCompound orb1 = nbt.hasKey("Orb_1") ? nbt.getCompoundTag("Orb_1") : new NBTTagCompound(); + NBTTagCompound orb2 = nbt.hasKey("Orb_2") ? nbt.getCompoundTag("Orb_2") : new NBTTagCompound(); + + this.orbSize = nbt.hasKey("orbSize") ? nbt.getFloat("orbSize") : 1.0f; + this.dualFire = !nbt.hasKey("dualFire") || nbt.getBoolean("dualFire"); + this.dualFireDelay = nbt.hasKey("dualFireDelay") ? nbt.getInteger("dualFireDelay") : 0; + combatData.readNBT(nbt); + homingData.readNBT(nbt); + lifespanData.readNBT(nbt); + // Backward compat: old key was "orbSpeed" + if (nbt.hasKey("orbSpeed")) { + homingData.speed = nbt.getFloat("orbSpeed"); + } + + anchorData[0].readNBT(orb1); + anchorData[1].readNBT(orb2); + colorData[0].readNBT(orb1); + colorData[1].readNBT(orb2); + lightningData[0].readNBT(orb1); + lightningData[1].readNBT(orb2); + } + + // Getters & Setters + private EnergyColorData getColorData(int orb) { + orb = ValueUtil.clamp(orb, 0, 1); + return colorData[orb]; + } + private EnergyLightningData getLightningData(int orb) { + orb = ValueUtil.clamp(orb, 0, 1); + return lightningData[orb]; + } + private EnergyAnchorData getAnchorData(int orb) { + orb = ValueUtil.clamp(orb, 0, 1); + return anchorData[orb]; + } + + public float getOrbSpeed() { + return homingData.speed; + } + + public void setOrbSpeed(float orbSpeed) { + homingData.speed = orbSpeed; + } + + public float getOrbSize() { + return orbSize; + } + + public void setOrbSize(float orbSize) { + this.orbSize = orbSize; + } + + public boolean isDualFire() { + return dualFire; + } + + public void setDualFire(boolean dualFire) { + this.dualFire = dualFire; + } + + public int getDualFireDelay() { + return dualFireDelay; + } + + public void setDualFireDelay(int dualFireDelay) { + this.dualFireDelay = dualFireDelay; + } + + public float getMaxDistance() { + return lifespanData.maxDistance; + } + + public void setMaxDistance(float maxDistance) { + lifespanData.maxDistance = maxDistance; + } + + public int getMaxLifetime() { + return lifespanData.maxLifetime; + } + + public void setMaxLifetime(int maxLifetime) { + lifespanData.maxLifetime = maxLifetime; + } + + public float getDamage() { + return combatData.damage; + } + + public void setDamage(float damage) { + combatData.damage = damage; + } + + public float getKnockback() { + return combatData.knockback; + } + + public void setKnockback(float knockback) { + combatData.knockback = knockback; + } + + public float getKnockbackUp() { + return combatData.knockbackUp; + } + + public void setKnockbackUp(float knockbackUp) { + combatData.knockbackUp = knockbackUp; + } + + public boolean isHoming() { + return homingData.homing; + } + + public void setHoming(boolean homing) { + homingData.homing = homing; + } + + public float getHomingStrength() { + return homingData.homingStrength; + } + + public void setHomingStrength(float homingStrength) { + homingData.homingStrength = homingStrength; + } + + public float getHomingRange() { + return homingData.homingRange; + } + + public void setHomingRange(float homingRange) { + homingData.homingRange = homingRange; + } + + public boolean isExplosive() { + return combatData.explosive; + } + + public void setExplosive(boolean explosive) { + combatData.explosive = explosive; + } + + public float getExplosionRadius() { + return combatData.explosionRadius; + } + + public void setExplosionRadius(float explosionRadius) { + combatData.explosionRadius = explosionRadius; + } + + public float getExplosionDamageFalloff() { + return combatData.explosionDamageFalloff; + } + + public void setExplosionDamageFalloff(float explosionDamageFalloff) { + combatData.explosionDamageFalloff = explosionDamageFalloff; + } + + + public int getInnerColor(int orb) { return getColorData(orb).innerColor; } + public void setInnerColor(int orb, int innerColor) { getColorData(orb).innerColor = innerColor; } + public int getOuterColor(int orb) { return getColorData(orb).outerColor; } + public void setOuterColor(int orb, int outerColor) { getColorData(orb).outerColor = outerColor; } + public boolean isOuterColorEnabled(int orb) { return getColorData(orb).outerColorEnabled; } + public void setOuterColorEnabled(int orb, boolean enabled) { getColorData(orb).outerColorEnabled = enabled; } + public float getOuterColorWidth(int orb) { return getColorData(orb).outerColorWidth; } + public void setOuterColorWidth(int orb, float width) { getColorData(orb).outerColorWidth = width; } + public float getOuterColorAlpha(int orb) { return getColorData(orb).outerColorAlpha; } + public void setOuterColorAlpha(int orb, float alpha) { getColorData(orb).outerColorAlpha = alpha; } + public float getRotationSpeed(int orb) { return getColorData(orb).rotationSpeed; } + public void setRotationSpeed(int orb, float speed) { getColorData(orb).rotationSpeed = speed; } + + public int getInnerColor() { return getInnerColor(0); } + public void setInnerColor(int innerColor) { setInnerColor(0, innerColor); } + public int getOuterColor() { return getOuterColor(0); } + public void setOuterColor(int outerColor) { setOuterColor(0, outerColor); } + public boolean isOuterColorEnabled() { return isOuterColorEnabled(0); } + public void setOuterColorEnabled(boolean enabled) { setOuterColorEnabled(0, enabled); } + public float getOuterColorWidth() { return getOuterColorWidth(0); } + public void setOuterColorWidth(float width) { setOuterColorWidth(0, width); } + public float getOuterColorAlpha() { return getOuterColorAlpha(0); } + public void setOuterColorAlpha(float alpha) { setOuterColorAlpha(0, alpha); } + public float getRotationSpeed() { return getRotationSpeed(0); } + public void setRotationSpeed(float speed) { setRotationSpeed(0, speed); } + + + public boolean hasLightningEffect(int orb) { return getLightningData(orb).lightningEffect; } + public void setLightningEffect(int orb, boolean lightningEffect) { getLightningData(orb).lightningEffect = lightningEffect; } + public float getLightningDensity(int orb) { return getLightningData(orb).lightningDensity; } + public void setLightningDensity(int orb, float lightningDensity) { getLightningData(orb).lightningDensity = lightningDensity; } + public float getLightningRadius(int orb) { return getLightningData(orb).lightningRadius; } + public void setLightningRadius(int orb, float lightningRadius) { getLightningData(orb).lightningRadius = lightningRadius; } + + public boolean hasLightningEffect() { return hasLightningEffect(0); } + public void setLightningEffect(boolean lightningEffect) { setLightningEffect(0, lightningEffect); } + public float getLightningDensity() { return getLightningDensity(0); } + public void setLightningDensity(float lightningDensity) { setLightningDensity(0, lightningDensity); } + public float getLightningRadius() { return getLightningRadius(0); } + public void setLightningRadius(float lightningRadius) { setLightningRadius(0, lightningRadius); } + + + public AnchorPoint getAnchorPointEnum(int orb) { return getAnchorData(orb).anchorPoint; } + public void setAnchorPointEnum(int orb, AnchorPoint anchorPoint) { getAnchorData(orb).anchorPoint = anchorPoint; } + public float getAnchorOffsetX(int orb) { return getAnchorData(orb).anchorOffsetX; } + public void setAnchorOffsetX(int orb, float x) { getAnchorData(orb).anchorOffsetX = x; } + public float getAnchorOffsetY(int orb) { return getAnchorData(orb).anchorOffsetY; } + public void setAnchorOffsetY(int orb, float y) { getAnchorData(orb).anchorOffsetY = y; } + public float getAnchorOffsetZ(int orb) { return getAnchorData(orb).anchorOffsetZ; } + public void setAnchorOffsetZ(int orb, float z) { getAnchorData(orb).anchorOffsetZ = z; } + + public AnchorPoint getAnchorPointEnum() { return getAnchorPointEnum(0); } + public void setAnchorPointEnum(AnchorPoint anchorPoint) { setAnchorPointEnum(0, anchorPoint); } + public float getAnchorOffsetX() { return getAnchorOffsetX(0); } + public void setAnchorOffsetX(float x) { setAnchorOffsetX(0, x); } + public float getAnchorOffsetY() { return getAnchorOffsetY(0); } + public void setAnchorOffsetY(float y) { setAnchorOffsetY(0, y); } + public float getAnchorOffsetZ() { return getAnchorOffsetZ(0); } + public void setAnchorOffsetZ(float z) { setAnchorOffsetZ(0, z); } + + //@Override + public int getAnchorPoint(int orb) { return getAnchorData(orb).anchorPoint.ordinal(); } + public int getAnchorPoint() { return getAnchorData(0).anchorPoint.ordinal(); } + + //@Override + public void setAnchorPoint(int orb, int point) { getAnchorData(orb).anchorPoint = AnchorPoint.fromOrdinal(point); } + public void setAnchorPoint(int point) { getAnchorData(0).anchorPoint = AnchorPoint.fromOrdinal(point); } + + @SideOnly(Side.CLIENT) + @Override + public List getFieldDefinitions() { + return Arrays.asList( + // Type tab + FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage).column(ColumnHint.LEFT), + FieldDef.floatField("stats.speed", this::getOrbSpeed, this::setOrbSpeed).column(ColumnHint.RIGHT), + FieldDef.floatField("stats.size", this::getOrbSize, this::setOrbSize).column(ColumnHint.LEFT), + FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback).column(ColumnHint.RIGHT), + FieldDef.floatField("ability.maxDistance", this::getMaxDistance, this::setMaxDistance).column(ColumnHint.LEFT), + FieldDef.intField("ability.lifetime", this::getMaxLifetime, this::setMaxLifetime).column(ColumnHint.RIGHT), + FieldDef.section("ability.section.dual"), + FieldDef.boolField("ability.dualFire", this::isDualFire, this::setDualFire), + FieldDef.intField("ability.dualFireDelay", this::getDualFireDelay, this::setDualFireDelay) + .visibleWhen(this::isDualFire), + FieldDef.section("ability.section.homing"), + FieldDef.boolField("gui.enabled", this::isHoming, this::setHoming).hover("ability.hover.homing"), + FieldDef.floatField("gui.strength", this::getHomingStrength, this::setHomingStrength).visibleWhen(this::isHoming), + FieldDef.section("ability.section.explosive"), + FieldDef.boolField("gui.enabled", this::isExplosive, this::setExplosive).hover("ability.hover.explosive"), + FieldDef.floatField("gui.radius", this::getExplosionRadius, this::setExplosionRadius).visibleWhen(this::isExplosive), + AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects), + // Visual tab - Orb 1 + FieldDef.enumField("ability.anchorPoint", AnchorPoint.class, + () -> getAnchorPointEnum(0), v -> setAnchorPointEnum(0, v)) + .tab("ability.tab.visual"), + FieldDef.section("ability.section.orb1").tab("ability.tab.visual"), + FieldDef.colorSubGui("ability.innerColor", + () -> getInnerColor(0), v -> setInnerColor(0, v)) + .tab("ability.tab.visual"), + FieldDef.boolField("ability.outerEnabled", + () -> isOuterColorEnabled(0), v -> setOuterColorEnabled(0, v)) + .tab("ability.tab.visual"), + FieldDef.colorSubGui("ability.outerColor", + () -> getOuterColor(0), v -> setOuterColor(0, v)) + .tab("ability.tab.visual").visibleWhen(() -> isOuterColorEnabled(0)), + FieldDef.floatField("ability.outerWidth", + () -> getOuterColorWidth(0), v -> setOuterColorWidth(0, v)) + .tab("ability.tab.visual").visibleWhen(() -> isOuterColorEnabled(0)).column(ColumnHint.LEFT), + FieldDef.floatField("ability.outerAlpha", + () -> getOuterColorAlpha(0), v -> setOuterColorAlpha(0, v)) + .tab("ability.tab.visual").range(0, 1).visibleWhen(() -> isOuterColorEnabled(0)).column(ColumnHint.RIGHT), + FieldDef.floatField("ability.rotationSpeed", + () -> getRotationSpeed(0), v -> setRotationSpeed(0, v)) + .tab("ability.tab.visual"), + FieldDef.boolField("ability.lightning", + () -> hasLightningEffect(0), v -> setLightningEffect(0, v)) + .tab("ability.tab.visual"), + FieldDef.floatField("gui.density", + () -> getLightningDensity(0), v -> setLightningDensity(0, v)) + .tab("ability.tab.visual").range(0.01f, 5.0f).visibleWhen(() -> hasLightningEffect(0)).column(ColumnHint.LEFT), + FieldDef.floatField("gui.radius", + () -> getLightningRadius(0), v -> setLightningRadius(0, v)) + .tab("ability.tab.visual").range(0.1f, 10.0f).visibleWhen(() -> hasLightningEffect(0)).column(ColumnHint.RIGHT), + // Visual tab - Orb 2 + FieldDef.enumField("ability.anchorPoint", AnchorPoint.class, + () -> getAnchorPointEnum(1), v -> setAnchorPointEnum(1, v)) + .tab("ability.tab.visual"), + FieldDef.section("ability.section.orb2").tab("ability.tab.visual"), + FieldDef.colorSubGui("ability.innerColor", + () -> getInnerColor(1), v -> setInnerColor(1, v)) + .tab("ability.tab.visual"), + FieldDef.boolField("ability.outerEnabled", + () -> isOuterColorEnabled(1), v -> setOuterColorEnabled(1, v)) + .tab("ability.tab.visual"), + FieldDef.colorSubGui("ability.outerColor", + () -> getOuterColor(1), v -> setOuterColor(1, v)) + .tab("ability.tab.visual").visibleWhen(() -> isOuterColorEnabled(1)), + FieldDef.floatField("ability.outerWidth", + () -> getOuterColorWidth(1), v -> setOuterColorWidth(1, v)) + .tab("ability.tab.visual").visibleWhen(() -> isOuterColorEnabled(1)).column(ColumnHint.LEFT), + FieldDef.floatField("ability.outerAlpha", + () -> getOuterColorAlpha(1), v -> setOuterColorAlpha(1, v)) + .tab("ability.tab.visual").range(0, 1).visibleWhen(() -> isOuterColorEnabled(1)).column(ColumnHint.RIGHT), + FieldDef.floatField("ability.rotationSpeed", + () -> getRotationSpeed(1), v -> setRotationSpeed(1, v)) + .tab("ability.tab.visual"), + FieldDef.boolField("ability.lightning", + () -> hasLightningEffect(1), v -> setLightningEffect(1, v)) + .tab("ability.tab.visual"), + FieldDef.floatField("gui.density", + () -> getLightningDensity(1), v -> setLightningDensity(1, v)) + .tab("ability.tab.visual").range(0.01f, 5.0f).visibleWhen(() -> hasLightningEffect(1)).column(ColumnHint.LEFT), + FieldDef.floatField("gui.radius", + () -> getLightningRadius(1), v -> setLightningRadius(1, v)) + .tab("ability.tab.visual").range(0.1f, 10.0f).visibleWhen(() -> hasLightningEffect(1)).column(ColumnHint.RIGHT) + ); + } + + @Override + @SideOnly(Side.CLIENT) + public Entity createPreviewEntity(EntityNPCInterface npc) { + if (npc == null || npc.worldObj == null) return null; + + EntityAbilityOrb orb = new EntityAbilityOrb(npc.worldObj); + orb.setupPreview(npc, orbSize, colorData[0], lightningData[0], anchorData[0], windUpTicks); + return orb; + } + + @Override + public int getPreviewActiveDuration() { + return lifespanData.maxLifetime > 0 ? Math.min(lifespanData.maxLifetime, 100) : 100; + } +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityProjectile.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityProjectile.java index 8ff9f5410..579887826 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityProjectile.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityProjectile.java @@ -3,24 +3,29 @@ package kamkeel.npcs.controllers.data.ability.type; 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.LockMovementType; import kamkeel.npcs.controllers.data.ability.TargetingMode; import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.world.World; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityProjectile; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.entity.EntityNPCInterface; +import noppes.npcs.api.ability.type.IAbilityProjectile; + +import noppes.npcs.client.gui.builder.ColumnHint; +import noppes.npcs.client.gui.builder.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.AbilityFieldDefs; + +import java.util.Arrays; import java.util.List; /** * Projectile ability: Ranged attack that deals damage to target. * Currently instant damage - can be enhanced with custom projectile entities. */ -public class AbilityProjectile extends Ability { +public class AbilityProjectile extends Ability implements IAbilityProjectile { // Type-specific parameters private float damage = 6.0f; @@ -38,26 +43,14 @@ public class AbilityProjectile extends Ability { this.targetingMode = TargetingMode.AGGRO_TARGET; this.minRange = 5.0f; this.maxRange = 20.0f; - this.cooldownTicks = 60; + this.cooldownTicks = 0; this.windUpTicks = 15; - this.activeTicks = 5; - this.recoveryTicks = 10; + this.lockMovement = LockMovementType.NO; // No telegraph for projectile - it's a ranged attack this.telegraphType = TelegraphType.NONE; this.showTelegraph = false; } - @Override - public boolean hasTypeSettings() { - return true; - } - - @Override - @SideOnly(Side.CLIENT) - public SubGuiAbilityConfig createConfigGui(IAbilityConfigCallback callback) { - return new SubGuiAbilityProjectile(this, callback); - } - @Override public boolean isTargetingModeLocked() { return true; @@ -69,13 +62,13 @@ public class AbilityProjectile extends Ability { } @Override - public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) { + public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { if (world.isRemote || target == null) return; // Calculate direction to target - double dx = target.posX - npc.posX; - double dy = (target.posY + target.height / 2) - (npc.posY + npc.getEyeHeight()); - double dz = target.posZ - npc.posZ; + double dx = target.posX - caster.posX; + double dy = (target.posY + target.height / 2) - (caster.posY + caster.getEyeHeight()); + double dz = target.posZ - caster.posZ; double len = Math.sqrt(dx * dx + dy * dy + dz * dz); if (len > 0) { @@ -86,10 +79,10 @@ public class AbilityProjectile extends Ability { // Deal instant damage with scripted event support // TODO: Use custom EntityAbilityProjectile for actual tracking - applyAbilityDamageWithDirection(npc, target, damage, knockback, dx, dz); + applyAbilityDamageWithDirection(caster, target, damage, knockback, dx, dz); // Play sound - world.playSoundAtEntity(npc, "random.bow", 1.0f, 0.8f); + world.playSoundAtEntity(caster, "random.bow", 1.0f, 0.8f); // Handle splash damage for explosive projectiles if (explosive && explosionRadius > 0) { @@ -98,13 +91,13 @@ public class AbilityProjectile extends Ability { target.boundingBox.expand(explosionRadius, explosionRadius, explosionRadius)); for (Entity entity : entities) { - if (entity instanceof EntityLivingBase && entity != npc) { + if (entity instanceof EntityLivingBase && entity != caster) { EntityLivingBase living = (EntityLivingBase) entity; float dist = target.getDistanceToEntity(living); if (dist < explosionRadius) { float falloff = 1.0f - (dist / explosionRadius); // Apply splash damage with scripted event support (no knockback) - applyAbilityDamage(npc, living, damage * falloff * 0.5f, 0); + applyAbilityDamage(caster, living, damage * falloff * 0.5f, 0); } } } @@ -121,13 +114,16 @@ public class AbilityProjectile extends Ability { } // Spawn projectile particles (visual trail) - spawnProjectileParticles(world, npc, target); + spawnProjectileParticles(world, caster, target); + + // Projectile is instant + signalCompletion(); } - private void spawnProjectileParticles(World world, EntityNPCInterface npc, EntityLivingBase target) { - double startX = npc.posX; - double startY = npc.posY + npc.getEyeHeight(); - double startZ = npc.posZ; + private void spawnProjectileParticles(World world, EntityLivingBase caster, EntityLivingBase target) { + double startX = caster.posX; + double startY = caster.posY + caster.getEyeHeight(); + double startZ = caster.posZ; double endX = target.posX; double endY = target.posY + target.height / 2; double endZ = target.posZ; @@ -156,7 +152,7 @@ public class AbilityProjectile extends Ability { } @Override - public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { + public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { // Projectile is instant, nothing to do per-tick } @@ -253,4 +249,22 @@ public class AbilityProjectile extends Ability { public void setHomingStrength(float homingStrength) { this.homingStrength = homingStrength; } + + @SideOnly(Side.CLIENT) + @Override + public List getFieldDefinitions() { + return Arrays.asList( + FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage).column(ColumnHint.LEFT), + FieldDef.floatField("stats.speed", this::getSpeed, this::setSpeed).column(ColumnHint.RIGHT), + FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback), + FieldDef.stringEnumField("ability.projectileType", new String[]{"fireball", "arrow", "magic"}, this::getProjectileType, this::setProjectileType), + FieldDef.section("ability.section.homing"), + FieldDef.boolField("gui.enabled", this::isHoming, this::setHoming).hover("ability.hover.homing"), + FieldDef.floatField("gui.strength", this::getHomingStrength, this::setHomingStrength).visibleWhen(this::isHoming), + FieldDef.section("ability.section.explosive"), + FieldDef.boolField("gui.enabled", this::isExplosive, this::setExplosive).hover("ability.hover.explosive"), + FieldDef.floatField("gui.radius", this::getExplosionRadius, this::setExplosionRadius).visibleWhen(this::isExplosive), + AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects) + ); + } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityShockwave.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityShockwave.java index af3489214..626cc6fc5 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityShockwave.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityShockwave.java @@ -3,63 +3,48 @@ package kamkeel.npcs.controllers.data.ability.type; 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.LockMovementType; import kamkeel.npcs.controllers.data.ability.TargetingMode; import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; -import net.minecraft.potion.Potion; -import net.minecraft.potion.PotionEffect; import net.minecraft.util.AxisAlignedBB; import net.minecraft.util.DamageSource; import net.minecraft.world.World; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityShockwave; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.entity.EntityNPCInterface; +import noppes.npcs.client.gui.builder.ColumnHint; +import noppes.npcs.client.gui.builder.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.AbilityFieldDefs; +import noppes.npcs.api.ability.type.IAbilityShockwave; + +import java.util.Arrays; import java.util.List; /** * Shockwave ability: Pushes targets away from the caster with damage. * Opposite of Vortex - instant knockback rather than gradual pull. */ -public class AbilityShockwave extends Ability { +public class AbilityShockwave extends Ability implements IAbilityShockwave { private float pushRadius = 8.0f; private float pushStrength = 1.5f; private float damage = 8.0f; - private int stunDuration = 0; private int maxTargets = 10; - // Runtime state - private transient boolean executed = false; - public AbilityShockwave() { this.typeId = "ability.cnpc.shockwave"; this.name = "Shockwave"; this.targetingMode = TargetingMode.AOE_SELF; this.maxRange = 15.0f; - this.lockMovement = true; - this.cooldownTicks = 100; + this.lockMovement = LockMovementType.WINDUP_AND_ACTIVE; + this.cooldownTicks = 0; this.windUpTicks = 25; - this.activeTicks = 5; - this.recoveryTicks = 20; this.telegraphType = TelegraphType.CIRCLE; this.windUpSound = "random.explode"; this.activeSound = "random.explode"; } - @Override - public boolean hasTypeSettings() { - return true; - } - - @Override - @SideOnly(Side.CLIENT) - public SubGuiAbilityConfig createConfigGui(IAbilityConfigCallback callback) { - return new SubGuiAbilityShockwave(this, callback); - } - @Override public boolean isTargetingModeLocked() { return true; @@ -76,34 +61,28 @@ public class AbilityShockwave extends Ability { } @Override - public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) { - executed = false; - } - - @Override - public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { - if (executed) return; - executed = true; + public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { + // Shockwave is instant - apply effect immediately after windup // Get all entities in radius - AxisAlignedBB box = npc.boundingBox.expand(pushRadius, pushRadius / 2, pushRadius); + AxisAlignedBB box = caster.boundingBox.expand(pushRadius, pushRadius / 2, pushRadius); @SuppressWarnings("unchecked") List entities = world.getEntitiesWithinAABB(EntityLivingBase.class, box); int count = 0; for (EntityLivingBase entity : entities) { - if (entity == npc) continue; + if (entity == caster) continue; if (entity.isDead) continue; - double dist = npc.getDistanceToEntity(entity); + double dist = caster.getDistanceToEntity(entity); if (dist > pushRadius) continue; count++; if (count > maxTargets) break; // Calculate push direction (away from caster) - double dx = entity.posX - npc.posX; - double dz = entity.posZ - npc.posZ; + double dx = entity.posX - caster.posX; + double dz = entity.posZ - caster.posZ; double len = Math.sqrt(dx * dx + dz * dz); if (len > 0) { @@ -120,24 +99,21 @@ public class AbilityShockwave extends Ability { float distFactor = 1.0f - (float) (dist / pushRadius) * 0.5f; float finalPush = pushStrength * distFactor; // Apply damage with custom knockback direction - boolean wasHit = applyAbilityDamageWithDirection(npc, entity, damage * distFactor, finalPush, dx, dz); + boolean wasHit = applyAbilityDamageWithDirection(caster, entity, damage * distFactor, finalPush, dx, dz); - // Apply stun if hit connected - if (wasHit && stunDuration > 0) { - entity.addPotionEffect(new PotionEffect(Potion.moveSlowdown.id, stunDuration, 10)); - entity.addPotionEffect(new PotionEffect(Potion.weakness.id, stunDuration, 2)); + // Apply effects if hit connected + if (wasHit) { + applyEffects(entity); } } + + // Shockwave completes instantly + signalCompletion(); } @Override - public void onComplete(EntityNPCInterface npc, EntityLivingBase target) { - executed = false; - } - - @Override - public void onInterrupt(EntityNPCInterface npc, DamageSource source, float damage) { - executed = false; + public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { + // Shockwave is instant - nothing to do per-tick } @Override @@ -145,7 +121,6 @@ public class AbilityShockwave extends Ability { nbt.setFloat("pushRadius", pushRadius); nbt.setFloat("pushStrength", pushStrength); nbt.setFloat("damage", damage); - nbt.setInteger("stunDuration", stunDuration); nbt.setInteger("maxTargets", maxTargets); } @@ -154,7 +129,6 @@ public class AbilityShockwave extends Ability { this.pushRadius = nbt.hasKey("pushRadius") ? nbt.getFloat("pushRadius") : 8.0f; this.pushStrength = nbt.hasKey("pushStrength") ? nbt.getFloat("pushStrength") : 1.5f; this.damage = nbt.hasKey("damage") ? nbt.getFloat("damage") : 8.0f; - this.stunDuration = nbt.hasKey("stunDuration") ? nbt.getInteger("stunDuration") : 0; this.maxTargets = nbt.hasKey("maxTargets") ? nbt.getInteger("maxTargets") : 10; } @@ -183,14 +157,6 @@ public class AbilityShockwave extends Ability { this.damage = damage; } - public int getStunDuration() { - return stunDuration; - } - - public void setStunDuration(int stunDuration) { - this.stunDuration = stunDuration; - } - public int getMaxTargets() { return maxTargets; } @@ -199,4 +165,16 @@ public class AbilityShockwave extends Ability { this.maxTargets = maxTargets; } + @SideOnly(Side.CLIENT) + @Override + public List getFieldDefinitions() { + return Arrays.asList( + FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage), + FieldDef.section("ability.section.push"), + FieldDef.floatField("gui.radius", this::getPushRadius, this::setPushRadius).column(ColumnHint.LEFT), + FieldDef.floatField("gui.strength", this::getPushStrength, this::setPushStrength).column(ColumnHint.RIGHT), + FieldDef.intField("ability.maxTargets", this::getMaxTargets, this::setMaxTargets), + AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects) + ); + } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java index 00da63e7f..7b4310030 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 @@ -3,6 +3,7 @@ package kamkeel.npcs.controllers.data.ability.type; 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.LockMovementType; import kamkeel.npcs.controllers.data.ability.TargetingMode; import kamkeel.npcs.controllers.data.telegraph.Telegraph; import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; @@ -12,11 +13,15 @@ import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.DamageSource; import net.minecraft.world.World; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -import noppes.npcs.client.gui.advanced.ability.SubGuiAbilitySlam; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.entity.EntityNPCInterface; +import noppes.npcs.api.ability.type.IAbilitySlam; +import somehussar.gui.annotationHandling.GuiEditable; + +import noppes.npcs.client.gui.builder.ColumnHint; +import noppes.npcs.client.gui.builder.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.AbilityFieldDefs; +import java.util.Arrays; import java.util.List; /** @@ -28,7 +33,7 @@ import java.util.List; * - Landing: When NPC hits ground, deal AOE damage * - RECOVERY: Landing recovery animation */ -public class AbilitySlam extends Ability { +public class AbilitySlam extends Ability implements IAbilitySlam { // Type-specific config private float damage = 10.0f; @@ -49,26 +54,16 @@ public class AbilitySlam extends Ability { this.name = "Slam"; this.targetingMode = TargetingMode.AOE_SELF; // Can also be AOE_TARGET to leap to target this.windUpTicks = 30; - this.activeTicks = 60; // Max time in air + landing - this.recoveryTicks = 20; - this.cooldownTicks = 100; - this.lockMovement = true; + this.cooldownTicks = 0; + this.lockMovement = LockMovementType.WINDUP; this.minRange = 2.0f; this.maxRange = 15.0f; this.telegraphType = TelegraphType.CIRCLE; this.windUpSound = "mob.irongolem.throw"; - this.activeSound = "random.explode"; - } - - @Override - public boolean hasTypeSettings() { - return true; - } - - @Override - @SideOnly(Side.CLIENT) - public SubGuiAbilityConfig createConfigGui(IAbilityConfigCallback callback) { - return new SubGuiAbilitySlam(this, callback); + this.activeSound = ""; + // Default built-in animations + this.windUpAnimationName = "Ability_Slam_Windup"; + this.activeAnimationName = "Ability_Slam_Active"; } /** @@ -90,13 +85,13 @@ public class AbilitySlam extends Ability { } @Override - public void onWindUpTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { + public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { // Update target position during windup if (targetingMode == TargetingMode.AOE_SELF) { - // AOE_SELF: slam at NPC's current position - targetX = npc.posX; - targetY = npc.posY; - targetZ = npc.posZ; + // AOE_SELF: slam at caster's current position + targetX = caster.posX; + targetY = caster.posY; + targetZ = caster.posZ; } else if (targetingMode == TargetingMode.AOE_TARGET && target != null && !target.isDead) { // AOE_TARGET: telegraph follows target via setEntityIdToFollow targetX = target.posX; @@ -106,13 +101,13 @@ public class AbilitySlam extends Ability { } @Override - public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) { + public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { // Lock in the destination at moment of launch if (targetingMode == TargetingMode.AOE_SELF) { // AOE_SELF: slam in place (just jump up) - targetX = npc.posX; - targetY = npc.posY; - targetZ = npc.posZ; + targetX = caster.posX; + targetY = caster.posY; + targetZ = caster.posZ; } else if (targetingMode == TargetingMode.AOE_TARGET && target != null && !target.isDead) { // AOE_TARGET: leap to target's current position targetX = target.posX; @@ -120,9 +115,9 @@ public class AbilitySlam extends Ability { targetZ = target.posZ; } else { // Fallback: slam in place - targetX = npc.posX; - targetY = npc.posY; - targetZ = npc.posZ; + targetX = caster.posX; + targetY = caster.posY; + targetZ = caster.posZ; } hasLaunched = false; @@ -130,31 +125,33 @@ public class AbilitySlam extends Ability { airTicks = 0; // Reset fall distance to prevent fall damage during slam - npc.fallDistance = 0; + caster.fallDistance = 0; // Calculate and apply leap velocity - launchTowardTarget(npc); + launchTowardTarget(caster); } /** - * Calculate ballistic arc and launch NPC toward target. + * Calculate ballistic arc and launch caster toward target. * Accounts for Minecraft's drag physics to actually reach the destination. */ - private void launchTowardTarget(EntityNPCInterface npc) { - double dx = targetX - npc.posX; - double dy = targetY - npc.posY; - double dz = targetZ - npc.posZ; + private void launchTowardTarget(EntityLivingBase caster) { + double dx = targetX - caster.posX; + double dy = targetY - caster.posY; + double dz = targetZ - caster.posZ; double horizontalDist = Math.sqrt(dx * dx + dz * dz); // For AOE_SELF or very close targets, just hop in place if (horizontalDist < 0.5) { - npc.motionX = 0; - npc.motionZ = 0; - npc.motionY = 0.8 * leapSpeed; + caster.motionX = 0; + caster.motionZ = 0; + caster.motionY = 0.8 * leapSpeed; hasLaunched = true; - npc.setNpcJumpingState(true); - npc.velocityChanged = true; - npc.worldObj.playSoundAtEntity(npc, "mob.irongolem.throw", 0.8f, 0.8f); + if (caster instanceof EntityNPCInterface) { + ((EntityNPCInterface) caster).setNpcJumpingState(true); + } + caster.velocityChanged = true; + caster.worldObj.playSoundAtEntity(caster, "mob.irongolem.throw", 0.8f, 0.8f); return; } @@ -164,8 +161,8 @@ public class AbilitySlam extends Ability { dx *= scale; dz *= scale; horizontalDist = maxRange; - targetX = npc.posX + dx; - targetZ = npc.posZ + dz; + targetX = caster.posX + dx; + targetZ = caster.posZ + dz; } // Choose flight time based on distance - shorter distances = faster @@ -211,90 +208,100 @@ public class AbilitySlam extends Ability { double dirX = dx / horizontalDist; double dirZ = dz / horizontalDist; - npc.motionX = dirX * vHorizontal; - npc.motionZ = dirZ * vHorizontal; - npc.motionY = vy; + caster.motionX = dirX * vHorizontal; + caster.motionZ = dirZ * vHorizontal; + caster.motionY = vy; hasLaunched = true; - npc.setNpcJumpingState(true); - npc.velocityChanged = true; + if (caster instanceof EntityNPCInterface) { + ((EntityNPCInterface) caster).setNpcJumpingState(true); + } + caster.velocityChanged = true; // Face the target float targetYaw = (float) (Math.atan2(-dx, dz) * 180.0D / Math.PI); - npc.rotationYaw = targetYaw; - npc.rotationYawHead = targetYaw; + caster.rotationYaw = targetYaw; + caster.rotationYawHead = targetYaw; } @Override - public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { + public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { if (!hasLaunched) return; if (hasLanded) return; airTicks++; // Continuously reset fall distance during slam to prevent fall damage - npc.fallDistance = 0; + caster.fallDistance = 0; // Check for landing - if (npc.onGround && airTicks > 3) { + if (caster.onGround && airTicks > 3) { // Landed! - onLanding(npc, world); + onLanding(caster, world); return; } // Timeout protection - force landing after max air time if (airTicks >= maxAirTicks) { - onLanding(npc, world); + onLanding(caster, world); return; } // While in air, face toward target - double dx = targetX - npc.posX; - double dz = targetZ - npc.posZ; + double dx = targetX - caster.posX; + double dz = targetZ - caster.posZ; float targetYaw = (float) (Math.atan2(-dx, dz) * 180.0D / Math.PI); - npc.rotationYaw = targetYaw; - npc.rotationYawHead = targetYaw; + caster.rotationYaw = targetYaw; + caster.rotationYawHead = targetYaw; } /** - * Called when NPC lands - deal AOE damage. + * Called when caster lands - deal AOE damage and signal completion. */ - private void onLanding(EntityNPCInterface npc, World world) { + private void onLanding(EntityLivingBase caster, World world) { hasLanded = true; - npc.setNpcJumpingState(false); + if (caster instanceof EntityNPCInterface) { + ((EntityNPCInterface) caster).setNpcJumpingState(false); + } + + // Signal that the ability has completed its active phase + signalCompletion(); // Reset fall distance to prevent fall damage on landing - npc.fallDistance = 0; + caster.fallDistance = 0; // Stop horizontal momentum - npc.motionX = 0; - npc.motionZ = 0; - npc.velocityChanged = true; + caster.motionX = 0; + caster.motionZ = 0; + caster.velocityChanged = true; if (world.isRemote) return; + // Play slam impact sound on landing + world.playSoundAtEntity(caster, "random.explode", 1.0f, 1.0f); + // Find all entities in radius @SuppressWarnings("unchecked") List entities = world.getEntitiesWithinAABBExcludingEntity( - npc, npc.boundingBox.expand(radius, radius / 2, radius)); + caster, caster.boundingBox.expand(radius, radius / 2, radius)); for (Entity entity : entities) { - if (entity instanceof EntityLivingBase && entity != npc) { + if (entity instanceof EntityLivingBase && entity != caster) { EntityLivingBase livingTarget = (EntityLivingBase) entity; // Check if actually within radius (bounding box is a cube, we want a circle) - double dx = livingTarget.posX - npc.posX; - double dz = livingTarget.posZ - npc.posZ; + double dx = livingTarget.posX - caster.posX; + double dz = livingTarget.posZ - caster.posZ; if (dx * dx + dz * dz <= radius * radius) { // Apply damage with scripted event support - applyAbilityDamage(npc, livingTarget, damage, knockbackStrength); + applyAbilityDamage(caster, livingTarget, damage, knockbackStrength); } } } // Spawn particles - spawnSlamParticles(world, npc.posX, npc.posY, npc.posZ); + spawnSlamParticles(world, caster.posX, caster.posY, caster.posZ); } /** @@ -334,16 +341,20 @@ public class AbilitySlam extends Ability { } @Override - public void onComplete(EntityNPCInterface npc, EntityLivingBase target) { - npc.setNpcJumpingState(false); + public void onComplete(EntityLivingBase caster, EntityLivingBase target) { + if (caster instanceof EntityNPCInterface) { + ((EntityNPCInterface) caster).setNpcJumpingState(false); + } hasLaunched = false; hasLanded = false; airTicks = 0; } @Override - public void onInterrupt(EntityNPCInterface npc, DamageSource source, float damage) { - npc.setNpcJumpingState(false); + public void onInterrupt(EntityLivingBase caster, DamageSource source, float damage) { + if (caster instanceof EntityNPCInterface) { + ((EntityNPCInterface) caster).setNpcJumpingState(false); + } hasLaunched = false; hasLanded = false; airTicks = 0; @@ -357,8 +368,102 @@ public class AbilitySlam extends Ability { airTicks = 0; } + // ==================== PREVIEW MODE ==================== + + private transient double previewVelX, previewVelY, previewVelZ; + private transient boolean previewLaunched = false; + private transient double previewGroundY; + @Override - public TelegraphInstance createTelegraph(EntityNPCInterface npc, EntityLivingBase target) { + @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(EntityLivingBase caster, EntityLivingBase target) { // Check if telegraph should be shown if (!isShowTelegraph() || getTelegraphType() == TelegraphType.NONE) { return null; @@ -366,24 +471,24 @@ public class AbilitySlam extends Ability { // Telegraph shows at landing zone if (targetingMode == TargetingMode.AOE_SELF) { - // AOE_SELF: telegraph at NPC position, does NOT follow - targetX = npc.posX; - targetY = npc.posY; - targetZ = npc.posZ; + // AOE_SELF: telegraph at caster position, does NOT follow + targetX = caster.posX; + targetY = caster.posY; + targetZ = caster.posZ; } else if (targetingMode == TargetingMode.AOE_TARGET && target != null) { // AOE_TARGET: telegraph at target position, follows target during windup targetX = target.posX; targetY = target.posY; targetZ = target.posZ; } else { - // Fallback to NPC position - targetX = npc.posX; - targetY = npc.posY; - targetZ = npc.posZ; + // Fallback to caster position + targetX = caster.posX; + targetY = caster.posY; + targetZ = caster.posZ; } // Create telegraph at the appropriate position - double groundY = findGroundLevel(npc.worldObj, targetX, targetY, targetZ); + double groundY = findGroundLevel(caster.worldObj, targetX, targetY, targetZ); Telegraph telegraph = Telegraph.circle(radius); telegraph.setDurationTicks(windUpTicks); @@ -392,14 +497,14 @@ public class AbilitySlam extends Ability { telegraph.setWarningStartTick(Math.max(5, windUpTicks / 4)); telegraph.setHeightOffset(telegraphHeightOffset); - TelegraphInstance instance = new TelegraphInstance(telegraph, targetX, groundY, targetZ, npc.rotationYaw); - instance.setCasterEntityId(npc.getEntityId()); + TelegraphInstance instance = new TelegraphInstance(telegraph, targetX, groundY, targetZ, caster.rotationYaw); + instance.setCasterEntityId(caster.getEntityId()); // AOE_TARGET: telegraph follows target during windup if (targetingMode == TargetingMode.AOE_TARGET && target != null) { instance.setEntityIdToFollow(target.getEntityId()); } - // AOE_SELF: telegraph stays at NPC position, no follow + // AOE_SELF: telegraph stays at caster position, no follow return instance; } @@ -467,4 +572,18 @@ public class AbilitySlam extends Ability { public void setLeapHeight(float leapHeight) { this.leapHeight = leapHeight; } + + @SideOnly(Side.CLIENT) + @Override + public List getFieldDefinitions() { + return Arrays.asList( + FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage), + FieldDef.floatField("gui.radius", this::getRadius, this::setRadius).column(ColumnHint.LEFT), + FieldDef.floatField("ability.knockback", this::getKnockbackStrength, this::setKnockbackStrength).column(ColumnHint.RIGHT), + FieldDef.section("ability.section.leap"), + FieldDef.floatField("gui.speed", this::getLeapSpeed, this::setLeapSpeed).column(ColumnHint.LEFT), + FieldDef.floatField("gui.height", this::getLeapHeight, this::setLeapHeight).column(ColumnHint.RIGHT), + AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects) + ); + } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySweeper.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySweeper.java new file mode 100644 index 000000000..aad2c47c2 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySweeper.java @@ -0,0 +1,232 @@ +package kamkeel.npcs.controllers.data.ability.type; + +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.LockMovementType; +import kamkeel.npcs.controllers.data.ability.TargetingMode; +import kamkeel.npcs.controllers.data.ability.data.EnergyColorData; +import noppes.npcs.client.gui.builder.ColumnHint; +import noppes.npcs.client.gui.builder.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.AbilityFieldDefs; +import kamkeel.npcs.controllers.data.telegraph.Telegraph; +import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; +import kamkeel.npcs.controllers.data.telegraph.TelegraphType; +import kamkeel.npcs.entity.EntityAbilitySweeper; +import net.minecraft.entity.EntityLivingBase; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.world.World; +import noppes.npcs.LogWriter; +import noppes.npcs.api.ability.type.IAbilitySweeper; + +import java.util.Arrays; +import java.util.List; +import noppes.npcs.entity.EntityNPCInterface; + +/** + * Sweeper ability: A low sweeping beam that rotates around the NPC. + * Players can jump over it. The beam completes a set number of rotations before ending. + * Telegraph shows a large circle on the ground indicating range. + * + * All damage logic is handled by the EntityAbilitySweeper entity to ensure + * visual and damage hitbox alignment. + */ +public class AbilitySweeper extends Ability implements IAbilitySweeper { + + // Type-specific parameters + private float beamLength = 10.0f; + private float beamWidth = 0.3f; // Thin like beam trail + private float beamHeight = 0.5f; // Height above ground (low enough to jump over) + private float damage = 5.0f; + private int damageInterval = 5; + private boolean piercing = true; + private float sweepSpeed = 3.0f; + private int numberOfRotations = 2; // How many full rotations before ability ends + private boolean lockOnTarget = false; + + // Visual properties + private EnergyColorData colorData = new EnergyColorData(0xFF6600, 0xFF0000, true, 1.8f, 0.5f, 0.0f); + + // Runtime state (transient) + private transient EntityAbilitySweeper activeEntity = null; + + public AbilitySweeper() { + this.typeId = "ability.cnpc.sweeper"; + this.name = "Sweeper"; + this.targetingMode = TargetingMode.AGGRO_TARGET; + this.maxRange = 15.0f; + this.minRange = 0.0f; + this.lockMovement = LockMovementType.WINDUP_AND_ACTIVE; + this.cooldownTicks = 0; + this.windUpTicks = 60; + // Circle telegraph to show range + this.telegraphType = TelegraphType.CIRCLE; + this.showTelegraph = true; + } + + @Override + public boolean isTargetingModeLocked() { + return true; + } + + @Override + public TargetingMode[] getAllowedTargetingModes() { + return new TargetingMode[]{TargetingMode.AGGRO_TARGET}; + } + + @Override + public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { + if (world.isRemote) { + signalCompletion(); + return; + } + + // Spawn the entity that handles BOTH visuals AND damage + activeEntity = new EntityAbilitySweeper(world, caster, target, + beamLength, beamWidth, beamHeight, + colorData, + sweepSpeed, numberOfRotations, + damage, damageInterval, piercing, + lockOnTarget); + world.spawnEntityInWorld(activeEntity); + + // Ability stays active until entity dies (prevents firing another while projectile is alive) + // Movement locking is handled separately by the base class + } + + @Override + public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { + // Signal completion when entity dies + if (activeEntity == null || activeEntity.isDead) { + activeEntity = null; + signalCompletion(); + } + } + + @Override + public void cleanup() { + // Clean up entity if still alive + if (activeEntity != null && !activeEntity.isDead) { + activeEntity.setDead(); + } + activeEntity = null; + } + + @Override + public float getTelegraphRadius() { + return beamLength; + } + + /** + * Override to position telegraph at NPC (not target) since the sweep is centered on NPC. + */ + @Override + public TelegraphInstance createTelegraph(EntityLivingBase caster, EntityLivingBase target) { + if (!showTelegraph || telegraphType == TelegraphType.NONE) { + return null; + } + + // Create circle telegraph centered on caster + Telegraph telegraph = Telegraph.circle(beamLength); + telegraph.setDurationTicks(windUpTicks); + telegraph.setColor(windUpColor); + telegraph.setWarningColor(activeColor); + telegraph.setWarningStartTick(Math.max(5, windUpTicks / 4)); + telegraph.setHeightOffset(telegraphHeightOffset); + + // Position at caster, not target + TelegraphInstance instance = new TelegraphInstance(telegraph, caster.posX, caster.posY, caster.posZ, caster.rotationYaw); + instance.setCasterEntityId(caster.getEntityId()); + instance.setEntityIdToFollow(caster.getEntityId()); // Follow caster + + return instance; + } + + @Override + public void writeTypeNBT(NBTTagCompound nbt) { + nbt.setFloat("beamLength", beamLength); + nbt.setFloat("beamWidth", beamWidth); + nbt.setFloat("beamHeight", beamHeight); + nbt.setFloat("damage", damage); + nbt.setInteger("damageInterval", damageInterval); + nbt.setBoolean("piercing", piercing); + nbt.setFloat("sweepSpeed", sweepSpeed); + nbt.setInteger("numberOfRotations", numberOfRotations); + nbt.setBoolean("lockOnTarget", lockOnTarget); + colorData.writeNBT(nbt); + } + + @Override + public void readTypeNBT(NBTTagCompound nbt) { + this.beamLength = nbt.hasKey("beamLength") ? nbt.getFloat("beamLength") : 10.0f; + this.beamWidth = nbt.hasKey("beamWidth") ? nbt.getFloat("beamWidth") : 0.3f; + this.beamHeight = nbt.hasKey("beamHeight") ? nbt.getFloat("beamHeight") : 0.5f; + this.damage = nbt.hasKey("damage") ? nbt.getFloat("damage") : 5.0f; + this.damageInterval = nbt.hasKey("damageInterval") ? nbt.getInteger("damageInterval") : 5; + this.piercing = !nbt.hasKey("piercing") || nbt.getBoolean("piercing"); + this.sweepSpeed = nbt.hasKey("sweepSpeed") ? nbt.getFloat("sweepSpeed") : 3.0f; + this.numberOfRotations = nbt.hasKey("numberOfRotations") ? nbt.getInteger("numberOfRotations") : 2; + this.lockOnTarget = nbt.hasKey("lockOnTarget") && nbt.getBoolean("lockOnTarget"); + colorData.readNBT(nbt); + } + + // Getters & Setters + public float getBeamLength() { return beamLength; } + public void setBeamLength(float beamLength) { this.beamLength = beamLength; } + public float getBeamWidth() { return beamWidth; } + public void setBeamWidth(float beamWidth) { this.beamWidth = beamWidth; } + public float getBeamHeight() { return beamHeight; } + public void setBeamHeight(float beamHeight) { this.beamHeight = beamHeight; } + public float getDamage() { return damage; } + public void setDamage(float damage) { this.damage = damage; } + public int getDamageInterval() { return damageInterval; } + public void setDamageInterval(int damageInterval) { this.damageInterval = damageInterval; } + public boolean isPiercing() { return piercing; } + public void setPiercing(boolean piercing) { this.piercing = piercing; } + public float getSweepSpeed() { return sweepSpeed; } + public void setSweepSpeed(float sweepSpeed) { this.sweepSpeed = sweepSpeed; } + public int getNumberOfRotations() { return numberOfRotations; } + public void setNumberOfRotations(int numberOfRotations) { this.numberOfRotations = numberOfRotations; } + public boolean isLockOnTarget() { return lockOnTarget; } + public void setLockOnTarget(boolean lockOnTarget) { this.lockOnTarget = lockOnTarget; } + public int getInnerColor() { return colorData.innerColor; } + public void setInnerColor(int innerColor) { colorData.innerColor = innerColor; } + public int getOuterColor() { return colorData.outerColor; } + public void setOuterColor(int outerColor) { colorData.outerColor = outerColor; } + public float getOuterColorWidth() { return colorData.outerColorWidth; } + public void setOuterColorWidth(float outerColorWidth) { colorData.outerColorWidth = outerColorWidth; } + public boolean isOuterColorEnabled() { return colorData.outerColorEnabled; } + public void setOuterColorEnabled(boolean outerColorEnabled) { colorData.outerColorEnabled = outerColorEnabled; } + + @SideOnly(Side.CLIENT) + @Override + public List getFieldDefinitions() { + return Arrays.asList( + + FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage).column(ColumnHint.LEFT), + FieldDef.intField("ability.damageInterval", this::getDamageInterval, this::setDamageInterval).column(ColumnHint.RIGHT), + FieldDef.section("ability.section.beam"), + FieldDef.floatField("gui.length", this::getBeamLength, this::setBeamLength).column(ColumnHint.LEFT), + FieldDef.floatField("gui.width", this::getBeamWidth, this::setBeamWidth).column(ColumnHint.RIGHT), + FieldDef.floatField("gui.height", this::getBeamHeight, this::setBeamHeight).column(ColumnHint.LEFT), + FieldDef.floatField("ability.sweepSpeed", this::getSweepSpeed, this::setSweepSpeed).column(ColumnHint.RIGHT), + FieldDef.intField("ability.rotations", this::getNumberOfRotations, this::setNumberOfRotations), + FieldDef.boolField("ability.piercing", this::isPiercing, this::setPiercing) + .hover("ability.hover.piercing").column(ColumnHint.LEFT), + FieldDef.boolField("ability.lockTarget", this::isLockOnTarget, this::setLockOnTarget) + .hover("ability.hover.lockTarget").column(ColumnHint.RIGHT), + AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects), + + // Visual tab + FieldDef.section("ability.section.colors").tab("ability.tab.visual"), + FieldDef.colorSubGui("ability.innerColor", this::getInnerColor, this::setInnerColor) + .tab("ability.tab.visual"), + FieldDef.boolField("ability.outerEnabled", this::isOuterColorEnabled, this::setOuterColorEnabled) + .tab("ability.tab.visual"), + FieldDef.colorSubGui("ability.outerColor", this::getOuterColor, this::setOuterColor) + .tab("ability.tab.visual").visibleWhen(this::isOuterColorEnabled), + FieldDef.floatField("ability.outerWidth", this::getOuterColorWidth, this::setOuterColorWidth) + .tab("ability.tab.visual").visibleWhen(this::isOuterColorEnabled) + ); + } +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTeleport.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTeleport.java index ad5989f25..414586b80 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTeleport.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTeleport.java @@ -1,8 +1,7 @@ package kamkeel.npcs.controllers.data.ability.type; -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.LockMovementType; import kamkeel.npcs.controllers.data.ability.TargetingMode; import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import net.minecraft.block.Block; @@ -13,18 +12,25 @@ import net.minecraft.util.AxisAlignedBB; import net.minecraft.util.MathHelper; import net.minecraft.util.Vec3; import net.minecraft.world.World; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityTeleport; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.entity.EntityNPCInterface; +import noppes.npcs.api.ability.type.IAbilityTeleport; + +import noppes.npcs.client.gui.builder.ColumnHint; +import noppes.npcs.client.gui.builder.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.AbilityFieldDefs; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; + +import java.util.Arrays; import java.util.List; import java.util.Random; /** * Teleport ability: Instant reposition with wall/line-of-sight checks. */ -public class AbilityTeleport extends Ability { +public class AbilityTeleport extends Ability implements IAbilityTeleport { /** * Teleport behavior mode. @@ -32,7 +38,17 @@ public class AbilityTeleport extends Ability { public enum TeleportMode { BLINK, BEHIND, - SINGLE + SINGLE; + + @Override + public String toString() { + switch (this) { + case BLINK: return "ability.teleport.blink"; + case BEHIND: return "ability.teleport.behind"; + case SINGLE: return "ability.teleport.single"; + default: return name(); + } + } } private static final Random RANDOM = new Random(); @@ -59,10 +75,9 @@ public class AbilityTeleport extends Ability { this.targetingMode = TargetingMode.AGGRO_TARGET; this.maxRange = 30.0f; this.minRange = 5.0f; - this.cooldownTicks = 100; + this.cooldownTicks = 0; this.windUpTicks = 10; - this.activeTicks = 40; - this.recoveryTicks = 10; + this.lockMovement = LockMovementType.NO; // No telegraph for teleport - it's instant repositioning this.telegraphType = TelegraphType.NONE; this.showTelegraph = false; @@ -70,18 +85,6 @@ public class AbilityTeleport extends Ability { this.activeSound = "mob.endermen.portal"; } - @Override - public boolean hasTypeSettings() { - return true; - } - - @Override - @SideOnly(Side.CLIENT) - public SubGuiAbilityConfig createConfigGui( - IAbilityConfigCallback callback) { - return new SubGuiAbilityTeleport(this, callback); - } - @Override public boolean isTargetingModeLocked() { return true; @@ -93,39 +96,47 @@ public class AbilityTeleport extends Ability { } @Override - public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) { + public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { currentBlink = 0; ticksSinceLastBlink = blinkDelayTicks; // Trigger first blink immediately } @Override - public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { + public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { int blinkLimit = mode == TeleportMode.BLINK ? blinkCount : 1; - if (currentBlink >= blinkLimit) return; + if (currentBlink >= blinkLimit) { + signalCompletion(); + return; + } ticksSinceLastBlink++; if (ticksSinceLastBlink >= blinkDelayTicks) { - performBlink(npc, target, world); + performBlink(caster, target, world); currentBlink++; ticksSinceLastBlink = 0; + + // Check if this was the last blink + if (currentBlink >= blinkLimit) { + signalCompletion(); + } } } - private void performBlink(EntityNPCInterface npc, EntityLivingBase target, World world) { + private void performBlink(EntityLivingBase caster, EntityLivingBase target, World world) { if (world.isRemote) return; - double oldX = npc.posX; - double oldY = npc.posY; - double oldZ = npc.posZ; + double oldX = caster.posX; + double oldY = caster.posY; + double oldZ = caster.posZ; - Vec3 destination = calculateDestination(npc, target, world); + Vec3 destination = calculateDestination(caster, target, world); if (destination == null) return; boolean mustHaveLOS = mode == TeleportMode.BEHIND || requireLineOfSight; - if (mustHaveLOS && !hasLineOfSight(world, oldX, oldY + npc.getEyeHeight(), oldZ, - destination.xCoord, destination.yCoord + npc.getEyeHeight(), destination.zCoord)) { - destination = findValidPositionAlongLine(world, npc, oldX, oldY, oldZ, + if (mustHaveLOS && !hasLineOfSight(world, oldX, oldY + caster.getEyeHeight(), oldZ, + destination.xCoord, destination.yCoord + caster.getEyeHeight(), destination.zCoord)) { + destination = findValidPositionAlongLine(world, caster, oldX, oldY, oldZ, destination.xCoord, destination.yCoord, destination.zCoord); if (destination == null) return; } @@ -136,22 +147,22 @@ public class AbilityTeleport extends Ability { // Damage at origin if (damageAtStart) { - dealDamageAt(npc, world, oldX, oldY, oldZ); + dealDamageAt(caster, world, oldX, oldY, oldZ); } // Spawn particles at origin spawnTeleportParticles(world, oldX, oldY, oldZ); // Teleport - npc.setPositionAndUpdate(destination.xCoord, destination.yCoord, destination.zCoord); - npc.fallDistance = 0; + caster.setPositionAndUpdate(destination.xCoord, destination.yCoord, destination.zCoord); + caster.fallDistance = 0; // Spawn particles at destination spawnTeleportParticles(world, destination.xCoord, destination.yCoord, destination.zCoord); // Damage at destination if (damageAtEnd) { - dealDamageAt(npc, world, destination.xCoord, destination.yCoord, destination.zCoord); + dealDamageAt(caster, world, destination.xCoord, destination.yCoord, destination.zCoord); } // Face target after teleport @@ -159,8 +170,8 @@ public class AbilityTeleport extends Ability { double dx = target.posX - destination.xCoord; double dz = target.posZ - destination.zCoord; float newYaw = (float) Math.toDegrees(Math.atan2(-dx, dz)); - npc.rotationYaw = newYaw; - npc.rotationYawHead = newYaw; + caster.rotationYaw = newYaw; + caster.rotationYawHead = newYaw; } } @@ -176,29 +187,50 @@ public class AbilityTeleport extends Ability { } } - private Vec3 calculateDestination(EntityNPCInterface npc, EntityLivingBase target, World world) { - if (target == null) return null; - + private Vec3 calculateDestination(EntityLivingBase caster, EntityLivingBase target, World world) { double newX; double newY; double newZ; + if (target != null) { + switch (mode) { + case BEHIND: + double yaw = Math.toRadians(target.rotationYaw); + newX = target.posX + Math.sin(yaw) * behindDistance; + newZ = target.posZ - Math.cos(yaw) * behindDistance; + newY = target.posY; + return Vec3.createVectorHelper(newX, newY, newZ); + case SINGLE: + case BLINK: + default: + double angle = RANDOM.nextDouble() * Math.PI * 2; + double dist = Math.min(blinkRadius, maxRange); + double offset = RANDOM.nextDouble() * dist; + newX = target.posX + Math.cos(angle) * offset; + newZ = target.posZ + Math.sin(angle) * offset; + newY = target.posY; + return Vec3.createVectorHelper(newX, newY, newZ); + } + } + + // No target (player use) - teleport in caster's look direction + Vec3 look = caster.getLookVec(); switch (mode) { case BEHIND: - double yaw = Math.toRadians(target.rotationYaw); - newX = target.posX + Math.sin(yaw) * behindDistance; - newZ = target.posZ - Math.cos(yaw) * behindDistance; - newY = target.posY; + // Teleport forward in look direction by behindDistance + newX = caster.posX + look.xCoord * behindDistance; + newZ = caster.posZ + look.zCoord * behindDistance; + newY = caster.posY; return Vec3.createVectorHelper(newX, newY, newZ); case SINGLE: case BLINK: default: - double angle = RANDOM.nextDouble() * Math.PI * 2; + // Teleport forward in look direction within blinkRadius double dist = Math.min(blinkRadius, maxRange); - double offset = RANDOM.nextDouble() * dist; - newX = target.posX + Math.cos(angle) * offset; - newZ = target.posZ + Math.sin(angle) * offset; - newY = target.posY; + double offset = (RANDOM.nextDouble() * 0.5 + 0.5) * dist; // 50-100% of max distance + newX = caster.posX + look.xCoord * offset; + newZ = caster.posZ + look.zCoord * offset; + newY = caster.posY; return Vec3.createVectorHelper(newX, newY, newZ); } } @@ -261,8 +293,13 @@ public class AbilityTeleport extends Ability { int blockY = MathHelper.floor_double(checkY); int blockZ = MathHelper.floor_double(checkZ); + // Skip out-of-world coordinates + if (blockY < 0 || blockY >= 256) { + continue; + } + Block block = world.getBlock(blockX, blockY, blockZ); - if (block.getMaterial().isSolid() && block.isOpaqueCube()) { + if (block != null && block.getMaterial().isSolid() && block.isOpaqueCube()) { return false; } } @@ -270,7 +307,7 @@ public class AbilityTeleport extends Ability { return true; } - private Vec3 findValidPositionAlongLine(World world, EntityNPCInterface npc, + private Vec3 findValidPositionAlongLine(World world, EntityLivingBase caster, double x1, double y1, double z1, double x2, double y2, double z2) { double dx = x2 - x1; @@ -292,8 +329,8 @@ public class AbilityTeleport extends Ability { double checkY = y1 + dy * d; double checkZ = z1 + dz * d; - if (hasLineOfSight(world, x1, y1 + npc.getEyeHeight(), z1, - checkX, checkY + npc.getEyeHeight(), checkZ)) { + if (hasLineOfSight(world, x1, y1 + caster.getEyeHeight(), z1, + checkX, checkY + caster.getEyeHeight(), checkZ)) { int blockX = MathHelper.floor_double(checkX); int blockY = MathHelper.floor_double(checkY); int blockZ = MathHelper.floor_double(checkZ); @@ -327,16 +364,26 @@ public class AbilityTeleport extends Ability { } private boolean isSafeLocation(World world, int x, int y, int z) { + // Bounds check - ensure we're within valid world coordinates + if (y < 1 || y >= 255) { + return false; + } + Block groundBlock = world.getBlock(x, y - 1, z); Block feetBlock = world.getBlock(x, y, z); Block headBlock = world.getBlock(x, y + 1, z); + // Defensive null checks (shouldn't happen in MC 1.7.10, but safe) + if (groundBlock == null || feetBlock == null || headBlock == null) { + return false; + } + return groundBlock.getMaterial().isSolid() && !feetBlock.getMaterial().isSolid() && !headBlock.getMaterial().isSolid(); } - private void dealDamageAt(EntityNPCInterface npc, World world, double x, double y, double z) { + private void dealDamageAt(EntityLivingBase caster, World world, double x, double y, double z) { AxisAlignedBB aabb = AxisAlignedBB.getBoundingBox( x - damageRadius, y - 1, z - damageRadius, x + damageRadius, y + 2, z + damageRadius @@ -347,13 +394,16 @@ public class AbilityTeleport extends Ability { for (Entity entity : entities) { if (!(entity instanceof EntityLivingBase)) continue; - if (entity == npc) continue; + if (entity == caster) continue; EntityLivingBase living = (EntityLivingBase) entity; double dist = Math.sqrt(Math.pow(living.posX - x, 2) + Math.pow(living.posZ - z, 2)); if (dist <= damageRadius) { // Apply damage with scripted event support (no knockback for teleport damage) - applyAbilityDamage(npc, living, damage, 0); + boolean wasHit = applyAbilityDamage(caster, living, damage, 0); + if (wasHit) { + applyEffects(living); + } } } } @@ -416,14 +466,25 @@ public class AbilityTeleport extends Ability { } // Getters & Setters - public TeleportMode getMode() { + public TeleportMode getModeEnum() { return mode; } - public void setMode(TeleportMode mode) { + public void setModeEnum(TeleportMode mode) { this.mode = mode; } + @Override + public int getMode() { + return mode.ordinal(); + } + + @Override + public void setMode(int mode) { + TeleportMode[] values = TeleportMode.values(); + this.mode = mode >= 0 && mode < values.length ? values[mode] : TeleportMode.BLINK; + } + public int getBlinkCount() { return blinkCount; } @@ -495,4 +556,31 @@ public class AbilityTeleport extends Ability { public void setDamageRadius(float damageRadius) { this.damageRadius = damageRadius; } + + @SideOnly(Side.CLIENT) + @Override + public List getFieldDefinitions() { + return Arrays.asList( + FieldDef.enumField("ability.mode", TeleportMode.class, this::getModeEnum, this::setModeEnum), + FieldDef.floatField("ability.blinkRadius", this::getBlinkRadius, this::setBlinkRadius) + .visibleWhen(() -> this.getModeEnum() == TeleportMode.BLINK || this.getModeEnum() == TeleportMode.SINGLE), + FieldDef.intField("ability.blinkCount", this::getBlinkCount, this::setBlinkCount) + .visibleWhen(() -> this.getModeEnum() == TeleportMode.BLINK).column(ColumnHint.LEFT), + FieldDef.intField("ability.blinkDelay", this::getBlinkDelayTicks, this::setBlinkDelayTicks) + .visibleWhen(() -> this.getModeEnum() == TeleportMode.BLINK).column(ColumnHint.RIGHT), + FieldDef.floatField("ability.behindDistance", this::getBehindDistance, this::setBehindDistance) + .visibleWhen(() -> this.getModeEnum() == TeleportMode.BEHIND), + FieldDef.boolField("ability.lineOfSight", this::isRequireLineOfSight, this::setRequireLineOfSight) + .hover("ability.hover.lineOfSight") + .visibleWhen(() -> this.getModeEnum() == TeleportMode.BLINK || this.getModeEnum() == TeleportMode.SINGLE), + FieldDef.section("ability.section.damage"), + FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage).column(ColumnHint.LEFT), + FieldDef.floatField("gui.radius", this::getDamageRadius, this::setDamageRadius).column(ColumnHint.RIGHT), + FieldDef.boolField("ability.damageAtStart", this::isDamageAtStart, this::setDamageAtStart) + .hover("ability.hover.dmgAtStart").column(ColumnHint.LEFT), + FieldDef.boolField("ability.damageAtEnd", this::isDamageAtEnd, this::setDamageAtEnd) + .hover("ability.hover.dmgAtEnd").column(ColumnHint.RIGHT), + AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects) + ); + } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java index 05eb43798..65673f861 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java @@ -1,23 +1,27 @@ package kamkeel.npcs.controllers.data.ability.type; -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.LockMovementType; import kamkeel.npcs.controllers.data.ability.TargetingMode; import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; -import net.minecraft.potion.Potion; -import net.minecraft.potion.PotionEffect; import net.minecraft.util.AxisAlignedBB; import net.minecraft.util.DamageSource; import net.minecraft.world.World; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityTrap; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.entity.EntityNPCInterface; +import noppes.npcs.api.ability.type.IAbilityTrap; + +import noppes.npcs.client.gui.builder.ColumnHint; +import noppes.npcs.client.gui.builder.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.AbilityFieldDefs; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; + +import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Random; @@ -28,14 +32,25 @@ import java.util.UUID; * Trap ability: Places a proximity-triggered trap. * Features arm time, trigger radius, and various effects on trigger. */ -public class AbilityTrap extends Ability { +public class AbilityTrap extends Ability implements IAbilityTrap { public enum TrapPlacement { AT_CASTER, AT_TARGET, - AHEAD_OF_CASTER + AHEAD_OF_CASTER; + + @Override + public String toString() { + switch (this) { + case AT_CASTER: return "ability.trapPlace.atCaster"; + case AT_TARGET: return "ability.trapPlace.atTarget"; + case AHEAD_OF_CASTER: return "ability.trapPlace.aheadOfCaster"; + default: return name(); + } + } } + private int durationTicks = 200; private TrapPlacement placement = TrapPlacement.AT_TARGET; private float placementDistance = 5.0f; private float triggerRadius = 2.0f; @@ -45,12 +60,6 @@ public class AbilityTrap extends Ability { private float damage = 6.0f; private float damageRadius = 0.0f; private float knockback = 0.5f; - private int stunDuration = 0; - private int rootDuration = 40; - private int slowDuration = 0; - private int slowLevel = 1; - private int poisonDuration = 0; - private int poisonLevel = 0; private boolean visible = true; // Offset parameters - trap spawns near target, not exactly on them @@ -71,25 +80,12 @@ public class AbilityTrap extends Ability { this.name = "Trap"; this.targetingMode = TargetingMode.AGGRO_TARGET; this.maxRange = 15.0f; - this.lockMovement = true; - this.cooldownTicks = 150; + this.lockMovement = LockMovementType.WINDUP; + this.cooldownTicks = 0; this.windUpTicks = 20; - this.activeTicks = 200; - this.recoveryTicks = 10; this.telegraphType = TelegraphType.CIRCLE; } - @Override - public boolean hasTypeSettings() { - return true; - } - - @Override - @SideOnly(Side.CLIENT) - public SubGuiAbilityConfig createConfigGui(IAbilityConfigCallback callback) { - return new SubGuiAbilityTrap(this, callback); - } - @Override public boolean isTargetingModeLocked() { return true; @@ -106,8 +102,8 @@ public class AbilityTrap extends Ability { } @Override - public TelegraphInstance createTelegraph(EntityNPCInterface npc, EntityLivingBase target) { - TelegraphInstance instance = super.createTelegraph(npc, target); + public TelegraphInstance createTelegraph(EntityLivingBase caster, EntityLivingBase target) { + TelegraphInstance instance = super.createTelegraph(caster, target); if (instance == null) return null; // Control telegraph following based on placement mode @@ -117,14 +113,14 @@ public class AbilityTrap extends Ability { // Telegraph at caster or ahead, no following instance.setEntityIdToFollow(-1); if (placement == TrapPlacement.AHEAD_OF_CASTER) { - double yaw = Math.toRadians(npc.rotationYaw); - instance.setX(npc.posX - Math.sin(yaw) * placementDistance); - instance.setY(npc.posY); - instance.setZ(npc.posZ + Math.cos(yaw) * placementDistance); + double yaw = Math.toRadians(caster.rotationYaw); + instance.setX(caster.posX - Math.sin(yaw) * placementDistance); + instance.setY(caster.posY); + instance.setZ(caster.posZ + Math.cos(yaw) * placementDistance); } else { - instance.setX(npc.posX); - instance.setY(npc.posY); - instance.setZ(npc.posZ); + instance.setX(caster.posX); + instance.setY(caster.posY); + instance.setZ(caster.posZ); } break; case AT_TARGET: @@ -137,33 +133,8 @@ public class AbilityTrap extends Ability { return instance; } - /** - * Calculates offset position near the given coordinates. - */ - private double[] calculateOffsetPosition(double baseX, double baseY, double baseZ) { - if (maxOffset <= 0) { - return new double[]{baseX, baseY, baseZ}; - } - - double offsetDist; - double offsetAngle; - - if (randomOffset) { - offsetDist = minOffset + RANDOM.nextDouble() * (maxOffset - minOffset); - offsetAngle = RANDOM.nextDouble() * Math.PI * 2; - } else { - offsetDist = maxOffset; - offsetAngle = RANDOM.nextDouble() * Math.PI * 2; - } - - double offsetX = Math.cos(offsetAngle) * offsetDist; - double offsetZ = Math.sin(offsetAngle) * offsetDist; - - return new double[]{baseX + offsetX, baseY, baseZ + offsetZ}; - } - @Override - public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) { + public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { armed = false; triggerCount = 0; ticksSinceLastTrigger = armTime; @@ -174,39 +145,51 @@ public class AbilityTrap extends Ability { switch (placement) { case AT_CASTER: - trapX = npc.posX; - trapY = npc.posY; - trapZ = npc.posZ; + trapX = caster.posX; + trapY = caster.posY; + trapZ = caster.posZ; break; case AT_TARGET: // Use telegraph position with offset if (telegraph != null) { - double[] pos = calculateOffsetPosition(telegraph.getX(), telegraph.getY(), telegraph.getZ()); + double[] pos = Ability.calculateOffsetPosition(telegraph.getX(), telegraph.getY(), telegraph.getZ(), + minOffset, maxOffset, randomOffset, RANDOM); trapX = pos[0]; trapY = pos[1]; trapZ = pos[2]; + // Update telegraph to show actual trap position + telegraph.setX(trapX); + telegraph.setY(trapY); + telegraph.setZ(trapZ); } else if (target != null) { - double[] pos = calculateOffsetPosition(target.posX, target.posY, target.posZ); + double[] pos = Ability.calculateOffsetPosition(target.posX, target.posY, target.posZ, + minOffset, maxOffset, randomOffset, RANDOM); trapX = pos[0]; trapY = pos[1]; trapZ = pos[2]; } else { - trapX = npc.posX; - trapY = npc.posY; - trapZ = npc.posZ; + trapX = caster.posX; + trapY = caster.posY; + trapZ = caster.posZ; } break; case AHEAD_OF_CASTER: - double yaw = Math.toRadians(npc.rotationYaw); - trapX = npc.posX - Math.sin(yaw) * placementDistance; - trapY = npc.posY; - trapZ = npc.posZ + Math.cos(yaw) * placementDistance; + double yaw = Math.toRadians(caster.rotationYaw); + trapX = caster.posX - Math.sin(yaw) * placementDistance; + trapY = caster.posY; + trapZ = caster.posZ + Math.cos(yaw) * placementDistance; break; } } @Override - public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { + public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { + // Check if trap duration has ended + if (tick >= durationTicks) { + signalCompletion(); + return; + } + if (!armed) { if (tick >= armTime) { armed = true; @@ -217,6 +200,7 @@ public class AbilityTrap extends Ability { ticksSinceLastTrigger++; if (maxTriggers > 0 && triggerCount >= maxTriggers) { + signalCompletion(); // All triggers used, trap is done return; } @@ -233,7 +217,7 @@ public class AbilityTrap extends Ability { List entities = world.getEntitiesWithinAABB(EntityLivingBase.class, box); for (EntityLivingBase entity : entities) { - if (entity == npc) continue; + if (entity == caster) continue; if (entity.isDead) continue; if (maxTriggers == 1 && triggeredEntities.contains(entity.getUniqueID())) continue; @@ -242,13 +226,13 @@ public class AbilityTrap extends Ability { double dist = Math.sqrt(dx * dx + dz * dz); if (dist <= triggerRadius) { - triggerTrap(npc, entity, world); + triggerTrap(caster, entity, world); return; } } } - private void triggerTrap(EntityNPCInterface npc, EntityLivingBase triggerer, World world) { + private void triggerTrap(EntityLivingBase caster, EntityLivingBase triggerer, World world) { triggerCount++; ticksSinceLastTrigger = 0; triggeredEntities.add(triggerer.getUniqueID()); @@ -265,7 +249,7 @@ public class AbilityTrap extends Ability { List entities = world.getEntitiesWithinAABB(EntityLivingBase.class, box); for (EntityLivingBase entity : entities) { - if (entity == npc) continue; + if (entity == caster) continue; double dx = entity.posX - trapX; double dz = entity.posZ - trapZ; double dist = Math.sqrt(dx * dx + dz * dz); @@ -279,39 +263,24 @@ public class AbilityTrap extends Ability { for (EntityLivingBase entity : affected) { // Apply damage with scripted event support - boolean wasHit = applyAbilityDamage(npc, entity, damage, knockback); + boolean wasHit = applyAbilityDamage(caster, entity, damage, knockback); - // Only apply effects if the hit wasn't cancelled + // Apply effects if the hit wasn't cancelled if (wasHit) { - if (stunDuration > 0) { - entity.addPotionEffect(new PotionEffect(Potion.moveSlowdown.id, stunDuration, 10)); - entity.addPotionEffect(new PotionEffect(Potion.weakness.id, stunDuration, 2)); - } - - if (rootDuration > 0) { - entity.addPotionEffect(new PotionEffect(Potion.moveSlowdown.id, rootDuration, 127)); - } - - if (slowDuration > 0) { - entity.addPotionEffect(new PotionEffect(Potion.moveSlowdown.id, slowDuration, slowLevel)); - } - - if (poisonDuration > 0 && poisonLevel >= 0) { - entity.addPotionEffect(new PotionEffect(Potion.poison.id, poisonDuration, poisonLevel)); - } + applyEffects(entity); } } } @Override - public void onComplete(EntityNPCInterface npc, EntityLivingBase target) { + public void onComplete(EntityLivingBase caster, EntityLivingBase target) { armed = false; triggerCount = 0; triggeredEntities.clear(); } @Override - public void onInterrupt(EntityNPCInterface npc, DamageSource source, float damage) { + public void onInterrupt(EntityLivingBase caster, DamageSource source, float damage) { armed = false; triggerCount = 0; triggeredEntities.clear(); @@ -319,6 +288,7 @@ public class AbilityTrap extends Ability { @Override public void writeTypeNBT(NBTTagCompound nbt) { + nbt.setInteger("durationTicks", durationTicks); nbt.setString("placement", placement.name()); nbt.setFloat("placementDistance", placementDistance); nbt.setFloat("triggerRadius", triggerRadius); @@ -328,12 +298,6 @@ public class AbilityTrap extends Ability { nbt.setFloat("damage", damage); nbt.setFloat("damageRadius", damageRadius); nbt.setFloat("knockback", knockback); - nbt.setInteger("stunDuration", stunDuration); - nbt.setInteger("rootDuration", rootDuration); - nbt.setInteger("slowDuration", slowDuration); - nbt.setInteger("slowLevel", slowLevel); - nbt.setInteger("poisonDuration", poisonDuration); - nbt.setInteger("poisonLevel", poisonLevel); nbt.setBoolean("visible", visible); nbt.setFloat("minOffset", minOffset); nbt.setFloat("maxOffset", maxOffset); @@ -342,6 +306,7 @@ public class AbilityTrap extends Ability { @Override public void readTypeNBT(NBTTagCompound nbt) { + this.durationTicks = nbt.hasKey("durationTicks") ? nbt.getInteger("durationTicks") : 200; try { this.placement = TrapPlacement.valueOf(nbt.getString("placement")); } catch (Exception e) { @@ -355,12 +320,6 @@ public class AbilityTrap extends Ability { this.damage = nbt.hasKey("damage") ? nbt.getFloat("damage") : 6.0f; this.damageRadius = nbt.hasKey("damageRadius") ? nbt.getFloat("damageRadius") : 0.0f; this.knockback = nbt.hasKey("knockback") ? nbt.getFloat("knockback") : 0.5f; - this.stunDuration = nbt.hasKey("stunDuration") ? nbt.getInteger("stunDuration") : 0; - this.rootDuration = nbt.hasKey("rootDuration") ? nbt.getInteger("rootDuration") : 40; - this.slowDuration = nbt.hasKey("slowDuration") ? nbt.getInteger("slowDuration") : 0; - this.slowLevel = nbt.hasKey("slowLevel") ? nbt.getInteger("slowLevel") : 1; - this.poisonDuration = nbt.hasKey("poisonDuration") ? nbt.getInteger("poisonDuration") : 0; - this.poisonLevel = nbt.hasKey("poisonLevel") ? nbt.getInteger("poisonLevel") : 0; this.visible = !nbt.hasKey("visible") || nbt.getBoolean("visible"); this.minOffset = nbt.hasKey("minOffset") ? nbt.getFloat("minOffset") : 0.0f; this.maxOffset = nbt.hasKey("maxOffset") ? nbt.getFloat("maxOffset") : 1.5f; @@ -368,14 +327,33 @@ public class AbilityTrap extends Ability { } // Getters & Setters - public TrapPlacement getPlacement() { + public int getDurationTicks() { + return durationTicks; + } + + public void setDurationTicks(int durationTicks) { + this.durationTicks = Math.max(1, durationTicks); + } + + public TrapPlacement getPlacementEnum() { return placement; } - public void setPlacement(TrapPlacement placement) { + public void setPlacementEnum(TrapPlacement placement) { this.placement = placement; } + @Override + public int getPlacement() { + return placement.ordinal(); + } + + @Override + public void setPlacement(int placement) { + TrapPlacement[] values = TrapPlacement.values(); + this.placement = placement >= 0 && placement < values.length ? values[placement] : TrapPlacement.AT_TARGET; + } + public float getPlacementDistance() { return placementDistance; } @@ -440,54 +418,6 @@ public class AbilityTrap extends Ability { this.knockback = knockback; } - public int getStunDuration() { - return stunDuration; - } - - public void setStunDuration(int stunDuration) { - this.stunDuration = stunDuration; - } - - public int getRootDuration() { - return rootDuration; - } - - public void setRootDuration(int rootDuration) { - this.rootDuration = rootDuration; - } - - public int getSlowDuration() { - return slowDuration; - } - - public void setSlowDuration(int slowDuration) { - this.slowDuration = slowDuration; - } - - public int getSlowLevel() { - return slowLevel; - } - - public void setSlowLevel(int slowLevel) { - this.slowLevel = slowLevel; - } - - public int getPoisonDuration() { - return poisonDuration; - } - - public void setPoisonDuration(int poisonDuration) { - this.poisonDuration = poisonDuration; - } - - public int getPoisonLevel() { - return poisonLevel; - } - - public void setPoisonLevel(int poisonLevel) { - this.poisonLevel = poisonLevel; - } - public boolean isVisible() { return visible; } @@ -536,4 +466,25 @@ public class AbilityTrap extends Ability { public boolean isArmed() { return armed; } + + @SideOnly(Side.CLIENT) + @Override + public List getFieldDefinitions() { + return Arrays.asList( + FieldDef.intField("ability.duration", this::getDurationTicks, this::setDurationTicks) + .range(1, 2000), + FieldDef.enumField("ability.placement", TrapPlacement.class, this::getPlacementEnum, this::setPlacementEnum) + .hover("ability.hover.placement"), + FieldDef.section("ability.section.trigger"), + FieldDef.floatField("gui.radius", this::getTriggerRadius, this::setTriggerRadius).column(ColumnHint.LEFT), + FieldDef.intField("ability.armTime", this::getArmTime, this::setArmTime).column(ColumnHint.RIGHT), + FieldDef.intField("ability.maxTriggers", this::getMaxTriggers, this::setMaxTriggers).column(ColumnHint.LEFT), + FieldDef.intField("ability.triggerCooldown", this::getTriggerCooldown, this::setTriggerCooldown).column(ColumnHint.RIGHT), + FieldDef.section("ability.section.damage"), + FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage).column(ColumnHint.LEFT), + FieldDef.floatField("gui.radius", this::getDamageRadius, this::setDamageRadius).column(ColumnHint.RIGHT), + FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback), + AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects) + ); + } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityVortex.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityVortex.java index dee010da0..0df622235 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityVortex.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityVortex.java @@ -1,22 +1,24 @@ package kamkeel.npcs.controllers.data.ability.type; -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.LockMovementType; import kamkeel.npcs.controllers.data.ability.TargetingMode; import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; -import net.minecraft.potion.Potion; -import net.minecraft.potion.PotionEffect; import net.minecraft.util.AxisAlignedBB; -import net.minecraft.util.DamageSource; import net.minecraft.world.World; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityVortex; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.entity.EntityNPCInterface; +import noppes.npcs.client.gui.builder.ColumnHint; +import noppes.npcs.client.gui.builder.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.AbilityFieldDefs; +import noppes.npcs.api.ability.type.IAbilityVortex; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; + +import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -26,51 +28,42 @@ import java.util.UUID; * Vortex ability: Pulls targets toward the caster. * Can pull single target or AOE, with optional damage and stun on arrival. */ -public class AbilityVortex extends Ability { +public class AbilityVortex extends Ability implements IAbilityVortex { private float pullRadius = 8.0f; private float pullStrength = 0.8f; private float damage = 0.0f; private float knockback = 0.0f; - private int stunDuration = 0; - private int rootDuration = 0; private boolean aoe = false; private int maxTargets = 5; private boolean damageOnPull = false; private float pullDamage = 0.0f; // Runtime state - private transient Set pulledEntities = new HashSet<>(); + private transient Set pulledEntities; private transient boolean pullComplete = false; private transient int ticksSincePullDamage = 0; + private Set getPulledEntities() { + if (pulledEntities == null) { + pulledEntities = new HashSet<>(); + } + return pulledEntities; + } + public AbilityVortex() { this.typeId = "ability.cnpc.vortex"; this.name = "Vortex"; this.targetingMode = TargetingMode.AOE_SELF; this.maxRange = 15.0f; - this.lockMovement = true; - this.cooldownTicks = 120; + this.lockMovement = LockMovementType.WINDUP_AND_ACTIVE; + this.cooldownTicks = 0; this.windUpTicks = 30; - this.activeTicks = 40; - this.recoveryTicks = 20; this.telegraphType = TelegraphType.CIRCLE; this.windUpSound = "mob.ghast.charge"; this.activeSound = "mob.ghast.fireball"; } - @Override - public boolean hasTypeSettings() { - return true; - } - - @Override - @SideOnly(Side.CLIENT) - public SubGuiAbilityConfig createConfigGui( - IAbilityConfigCallback callback) { - return new SubGuiAbilityVortex(this, callback); - } - @Override public boolean isTargetingModeLocked() { return true; @@ -87,24 +80,24 @@ public class AbilityVortex extends Ability { } @Override - public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) { - pulledEntities.clear(); + public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { + getPulledEntities().clear(); pullComplete = false; ticksSincePullDamage = 0; if (aoe) { - AxisAlignedBB box = npc.boundingBox.expand(pullRadius, pullRadius / 2, pullRadius); + AxisAlignedBB box = caster.boundingBox.expand(pullRadius, pullRadius / 2, pullRadius); @SuppressWarnings("unchecked") List entities = world.getEntitiesWithinAABB(EntityLivingBase.class, box); int count = 0; for (EntityLivingBase entity : entities) { - if (entity == npc) continue; + if (entity == caster) continue; if (entity.isDead) continue; - double dist = npc.getDistanceToEntity(entity); + double dist = caster.getDistanceToEntity(entity); if (dist <= pullRadius) { - pulledEntities.add(entity.getUniqueID()); + getPulledEntities().add(entity.getUniqueID()); count++; if (count >= maxTargets) break; } @@ -112,31 +105,32 @@ public class AbilityVortex extends Ability { } else { // Single target mode - still check pullRadius if (target != null && !target.isDead) { - double dist = npc.getDistanceToEntity(target); + double dist = caster.getDistanceToEntity(target); if (dist <= pullRadius) { - pulledEntities.add(target.getUniqueID()); + getPulledEntities().add(target.getUniqueID()); } } } } @Override - public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { - if (pullComplete || pulledEntities.isEmpty()) { + public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { + if (pullComplete || getPulledEntities().isEmpty()) { + signalCompletion(); return; } - double destX = npc.posX; - double destY = npc.posY; - double destZ = npc.posZ; + double destX = caster.posX; + double destY = caster.posY; + double destZ = caster.posZ; boolean anyStillPulling = false; ticksSincePullDamage++; - for (UUID uuid : new HashSet<>(pulledEntities)) { - EntityLivingBase entity = findEntity(world, uuid); + for (UUID uuid : new HashSet<>(getPulledEntities())) { + EntityLivingBase entity = findEntity(caster, world, uuid); if (entity == null || entity.isDead) { - pulledEntities.remove(uuid); + getPulledEntities().remove(uuid); continue; } @@ -146,8 +140,8 @@ public class AbilityVortex extends Ability { double dist = Math.sqrt(dx * dx + dy * dy + dz * dz); if (dist <= 1.0f) { - pulledEntities.remove(uuid); - onTargetArrived(npc, entity, world); + getPulledEntities().remove(uuid); + onTargetArrived(caster, entity, world); continue; } @@ -160,7 +154,7 @@ public class AbilityVortex extends Ability { AxisAlignedBB nextBox = entity.boundingBox.copy().offset(nextX, nextY, nextZ); if (!world.getCollidingBoundingBoxes(entity, nextBox).isEmpty()) { - pulledEntities.remove(uuid); + getPulledEntities().remove(uuid); continue; } @@ -172,55 +166,47 @@ public class AbilityVortex extends Ability { if (damageOnPull && pullDamage > 0 && ticksSincePullDamage >= 10) { ticksSincePullDamage = 0; entity.hurtResistantTime = 0; - applyAbilityDamage(npc, entity, pullDamage * 0.5f, 0); + applyAbilityDamage(caster, entity, pullDamage * 0.5f, 0); } } - if (!anyStillPulling && pulledEntities.isEmpty()) { + if (!anyStillPulling && getPulledEntities().isEmpty()) { pullComplete = true; + signalCompletion(); } } - private void onTargetArrived(EntityNPCInterface npc, EntityLivingBase entity, World world) { + private void onTargetArrived(EntityLivingBase caster, EntityLivingBase entity, World world) { // Apply damage with scripted event support - boolean wasHit = applyAbilityDamage(npc, entity, damage, knockback * 0.5f); + boolean wasHit = applyAbilityDamage(caster, entity, damage, knockback * 0.5f); // Only apply effects if hit wasn't cancelled if (wasHit) { - if (stunDuration > 0) { - entity.addPotionEffect(new PotionEffect(Potion.moveSlowdown.id, stunDuration, 10)); - entity.addPotionEffect(new PotionEffect(Potion.weakness.id, stunDuration, 2)); - } - - if (rootDuration > 0) { - entity.addPotionEffect(new PotionEffect(Potion.moveSlowdown.id, rootDuration, 127)); - } + applyEffects(entity); } } + /** + * Find an entity by UUID within the vortex pull area. + * Uses AABB search instead of iterating all loaded entities. + */ @SuppressWarnings("unchecked") - private EntityLivingBase findEntity(World world, UUID uuid) { - for (Object obj : world.loadedEntityList) { - if (obj instanceof EntityLivingBase) { - EntityLivingBase entity = (EntityLivingBase) obj; - if (entity.getUniqueID().equals(uuid)) { - return entity; - } + private EntityLivingBase findEntity(EntityLivingBase caster, World world, UUID uuid) { + // Search within pull radius (entities being pulled should be within this area) + AxisAlignedBB searchBox = caster.boundingBox.expand(pullRadius, pullRadius / 2, pullRadius); + List nearbyEntities = world.getEntitiesWithinAABB(EntityLivingBase.class, searchBox); + + for (EntityLivingBase entity : nearbyEntities) { + if (entity.getUniqueID().equals(uuid)) { + return entity; } } return null; } @Override - public void onComplete(EntityNPCInterface npc, EntityLivingBase target) { - pulledEntities.clear(); - pullComplete = false; - ticksSincePullDamage = 0; - } - - @Override - public void onInterrupt(EntityNPCInterface npc, DamageSource source, float damage) { - pulledEntities.clear(); + public void cleanup() { + getPulledEntities().clear(); pullComplete = false; ticksSincePullDamage = 0; } @@ -231,8 +217,6 @@ public class AbilityVortex extends Ability { nbt.setFloat("pullStrength", pullStrength); nbt.setFloat("damage", damage); nbt.setFloat("knockback", knockback); - nbt.setInteger("stunDuration", stunDuration); - nbt.setInteger("rootDuration", rootDuration); nbt.setBoolean("aoe", aoe); nbt.setInteger("maxTargets", maxTargets); nbt.setBoolean("damageOnPull", damageOnPull); @@ -245,8 +229,6 @@ public class AbilityVortex extends Ability { this.pullStrength = nbt.hasKey("pullStrength") ? nbt.getFloat("pullStrength") : 0.8f; this.damage = nbt.hasKey("damage") ? nbt.getFloat("damage") : 0.0f; this.knockback = nbt.hasKey("knockback") ? nbt.getFloat("knockback") : 0.0f; - this.stunDuration = nbt.hasKey("stunDuration") ? nbt.getInteger("stunDuration") : 0; - this.rootDuration = nbt.hasKey("rootDuration") ? nbt.getInteger("rootDuration") : 0; this.aoe = nbt.hasKey("aoe") && nbt.getBoolean("aoe"); this.maxTargets = nbt.hasKey("maxTargets") ? nbt.getInteger("maxTargets") : 5; this.damageOnPull = nbt.hasKey("damageOnPull") && nbt.getBoolean("damageOnPull"); @@ -286,22 +268,6 @@ public class AbilityVortex extends Ability { this.knockback = knockback; } - public int getStunDuration() { - return stunDuration; - } - - public void setStunDuration(int stunDuration) { - this.stunDuration = stunDuration; - } - - public int getRootDuration() { - return rootDuration; - } - - public void setRootDuration(int rootDuration) { - this.rootDuration = rootDuration; - } - public boolean isAoe() { return aoe; } @@ -333,4 +299,27 @@ public class AbilityVortex extends Ability { public void setPullDamage(float pullDamage) { this.pullDamage = pullDamage; } + + @SideOnly(Side.CLIENT) + @Override + public List getFieldDefinitions() { + return Arrays.asList( + FieldDef.floatField("ability.pullRadius", this::getPullRadius, this::setPullRadius).column(ColumnHint.LEFT), + FieldDef.floatField("ability.pullStrength", this::getPullStrength, this::setPullStrength).column(ColumnHint.RIGHT), + FieldDef.section("ability.section.damage"), + FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage).column(ColumnHint.LEFT), + FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback).column(ColumnHint.RIGHT), + FieldDef.section("ability.section.aoe"), + FieldDef.boolField("gui.enabled", this::isAoe, this::setAoe) + .hover("ability.hover.aoe"), + FieldDef.intField("ability.maxTargets", this::getMaxTargets, this::setMaxTargets) + .visibleWhen(this::isAoe), + FieldDef.section("ability.section.pullDamage"), + FieldDef.boolField("gui.enabled", this::isDamageOnPull, this::setDamageOnPull) + .hover("ability.hover.dmgOnPull"), + FieldDef.floatField("enchantment.damage", this::getPullDamage, this::setPullDamage) + .visibleWhen(this::isDamageOnPull), + AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects) + ); + } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/telegraph/TelegraphInstance.java b/src/main/java/kamkeel/npcs/controllers/data/telegraph/TelegraphInstance.java index 9f90bc29b..a5d2f0d95 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/telegraph/TelegraphInstance.java +++ b/src/main/java/kamkeel/npcs/controllers/data/telegraph/TelegraphInstance.java @@ -1,6 +1,6 @@ package kamkeel.npcs.controllers.data.telegraph; -import net.minecraft.block.Block; +import kamkeel.npcs.controllers.data.ability.Ability; import net.minecraft.entity.Entity; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.world.World; @@ -17,15 +17,22 @@ public class TelegraphInstance { private String instanceId; private Telegraph telegraph; - // Position + // Position (current) private double x; private double y; private double z; private float yaw; + // Position (previous tick - for interpolation) + private double prevX; + private double prevY; + private double prevZ; + private float prevYaw; + // Entity tracking private int entityIdToFollow = -1; private int casterEntityId = -1; + private int targetEntityId = -1; // For LINE telegraphs: entity to face (yaw tracking) // Timing private int remainingTicks; @@ -49,6 +56,11 @@ public class TelegraphInstance { this.y = y; this.z = z; this.yaw = yaw; + // Initialize prev positions to current for first frame + this.prevX = x; + this.prevY = y; + this.prevZ = z; + this.prevYaw = yaw; this.remainingTicks = telegraph.getDurationTicks(); this.totalTicks = telegraph.getDurationTicks(); } @@ -68,6 +80,12 @@ public class TelegraphInstance { return false; } + // Save previous position for interpolation + this.prevX = this.x; + this.prevY = this.y; + this.prevZ = this.z; + this.prevYaw = this.yaw; + remainingTicks--; // Update position if following an entity @@ -77,7 +95,17 @@ public class TelegraphInstance { this.x = entity.posX; this.z = entity.posZ; // Find ground level below the entity for proper telegraph placement - this.y = findGroundLevel(world, entity.posX, entity.posY, entity.posZ); + this.y = Ability.findGroundLevel(world, entity.posX, entity.posY, entity.posZ); + } + } + + // Update yaw to face target entity (for LINE telegraphs) + if (targetEntityId >= 0 && world != null) { + Entity target = world.getEntityByID(targetEntityId); + if (target != null) { + double dx = target.posX - this.x; + double dz = target.posZ - this.z; + this.yaw = (float) (Math.atan2(-dx, dz) * 180.0 / Math.PI); } } @@ -91,34 +119,6 @@ public class TelegraphInstance { return true; } - /** - * Find the ground level at a given position. - * Searches downward from the given Y to find a solid block. - * - * @param world The world - * @param x X coordinate - * @param startY Starting Y coordinate (entity feet position) - * @param z Z coordinate - * @return The Y coordinate of the ground surface - */ - private double findGroundLevel(World world, double x, double startY, double z) { - int blockX = (int) Math.floor(x); - int blockZ = (int) Math.floor(z); - int startBlockY = (int) Math.floor(startY); - - // Search downward for solid ground (max 10 blocks down) - for (int checkY = startBlockY; checkY >= startBlockY - 10 && checkY >= 0; checkY--) { - Block block = world.getBlock(blockX, checkY, blockZ); - if (block != null && block.getMaterial().isSolid()) { - // Found solid block, telegraph goes on top of it - return checkY + 1; - } - } - - // No ground found, use original position - return startY; - } - /** * Get progress from 0.0 (just started) to 1.0 (finished). */ @@ -136,7 +136,7 @@ public class TelegraphInstance { } /** - * Get alpha-modulated color for pulsing animation. + * Get alpha-modulated color for breathing animation. */ public int getAnimatedColor(float partialTicks) { int baseColor = getCurrentColor(); @@ -144,9 +144,12 @@ public class TelegraphInstance { return baseColor; } - // Pulsing animation: modulate alpha based on time + // Breathing animation: slow, smooth modulation of alpha float time = (totalTicks - remainingTicks) + partialTicks; - float pulse = (float) (Math.sin(time * 0.3) * 0.3 + 0.7); // 0.4 to 1.0 range + // Slower breathing: 0.08 for ~4 second cycle, smoother sine curve + float breathPhase = (float) Math.sin(time * 0.08); + // Smooth easing: square the sine for more organic feel, range 0.5 to 1.0 + float pulse = 0.5f + (breathPhase * breathPhase * 0.5f * Math.signum(breathPhase)); int alpha = (baseColor >> 24) & 0xFF; int r = (baseColor >> 16) & 0xFF; @@ -158,6 +161,43 @@ public class TelegraphInstance { return (alpha << 24) | (r << 16) | (g << 8) | b; } + // ═══════════════════════════════════════════════════════════════════ + // INTERPOLATED POSITION (for smooth rendering) + // ═══════════════════════════════════════════════════════════════════ + + /** + * Get interpolated X position for smooth rendering. + */ + public double getInterpolatedX(float partialTicks) { + return prevX + (x - prevX) * partialTicks; + } + + /** + * Get interpolated Y position for smooth rendering. + */ + public double getInterpolatedY(float partialTicks) { + return prevY + (y - prevY) * partialTicks; + } + + /** + * Get interpolated Z position for smooth rendering. + */ + public double getInterpolatedZ(float partialTicks) { + return prevZ + (z - prevZ) * partialTicks; + } + + /** + * Get interpolated yaw for smooth rendering. + * Handles angle wrapping for smooth rotation. + */ + public float getInterpolatedYaw(float partialTicks) { + // Handle angle wrapping to prevent spinning the wrong way + float diff = yaw - prevYaw; + while (diff > 180.0f) diff -= 360.0f; + while (diff < -180.0f) diff += 360.0f; + return prevYaw + diff * partialTicks; + } + // ═══════════════════════════════════════════════════════════════════ // NBT SERIALIZATION // ═══════════════════════════════════════════════════════════════════ @@ -177,6 +217,7 @@ public class TelegraphInstance { nbt.setInteger("entityIdToFollow", entityIdToFollow); nbt.setInteger("casterEntityId", casterEntityId); + nbt.setInteger("targetEntityId", targetEntityId); nbt.setInteger("remainingTicks", remainingTicks); nbt.setInteger("totalTicks", totalTicks); @@ -201,6 +242,7 @@ public class TelegraphInstance { this.entityIdToFollow = nbt.getInteger("entityIdToFollow"); this.casterEntityId = nbt.getInteger("casterEntityId"); + this.targetEntityId = nbt.getInteger("targetEntityId"); this.remainingTicks = nbt.getInteger("remainingTicks"); this.totalTicks = nbt.getInteger("totalTicks"); @@ -269,11 +311,12 @@ public class TelegraphInstance { } /** - * Locks the telegraph at its current position. + * Locks the telegraph at its current position and direction. * Stops following any entity - used when ability commits to action. */ public void lockPosition() { this.entityIdToFollow = -1; + this.targetEntityId = -1; } public int getCasterEntityId() { @@ -284,6 +327,14 @@ public class TelegraphInstance { this.casterEntityId = casterEntityId; } + public int getTargetEntityId() { + return targetEntityId; + } + + public void setTargetEntityId(int targetEntityId) { + this.targetEntityId = targetEntityId; + } + public int getRemainingTicks() { return remainingTicks; } diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java new file mode 100644 index 000000000..6aed40c3f --- /dev/null +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java @@ -0,0 +1,851 @@ +package kamkeel.npcs.entity; + +import kamkeel.npcs.controllers.data.ability.AnchorPoint; +import kamkeel.npcs.controllers.data.ability.data.*; +import kamkeel.npcs.util.AnchorPointHelper; +import net.minecraft.entity.Entity; +import net.minecraft.entity.EntityLivingBase; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.nbt.NBTTagList; +import net.minecraft.util.AxisAlignedBB; +import net.minecraft.util.MovingObjectPosition; +import net.minecraft.util.Vec3; +import net.minecraft.world.World; +import noppes.npcs.LogWriter; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +/** + * Beam projectile - head with trailing path that curves with homing. + * Stays attached to origin point, trail shows path of head. + * + * IMPORTANT: Trail points are stored RELATIVE to startX/Y/Z (origin). + * This ensures rendering is stable regardless of entity position interpolation. + * + * Design inspired by LouisXIV's energy attack system. + */ +public class EntityAbilityBeam extends EntityAbilityProjectile { + + // Beam-specific movement properties + private float speed = 0.5f; + private boolean homing = true; + private float homingStrength = 0.35f; // Increased from 0.15 for better tracking + private float homingRange = 20.0f; + private UUID siblingUUID = null; + + // Beam shape properties + private float beamWidth = 0.3f; + private float headSize = 0.5f; + + // Trail - list of points showing beam path (RELATIVE to origin!) + private List trailPoints = new ArrayList<>(); + private static final int MAX_TRAIL_POINTS = 200; + private static final double MIN_POINT_DISTANCE = 0.2; + + // Head position relative to origin + private double headOffsetX, headOffsetY, headOffsetZ; + private double prevHeadOffsetX, prevHeadOffsetY, prevHeadOffsetZ; + + // Origin stays fixed (or follows owner) + private boolean attachedToOwner = true; + + // Whether to render tail orb (only when anchored) + private boolean renderTailOrb = true; + + // Charging state (during windup) + private boolean charging = false; + private int chargeDuration = 40; + private int chargeTick = 0; + private float chargeOffsetDistance = 1.0f; + private EnergyAnchorData anchorData = new EnergyAnchorData(AnchorPoint.FRONT); + + // Trail fading for non-anchored beams (comet effect) + private boolean fadeTrail = false; + private int trailFadeTime = 20; // Ticks for trail to fully fade + private List trailPointAges = new ArrayList<>(); + + // Data watcher index for charging state (synced to clients) + private static final int DW_CHARGING = 20; + + // Debug logging + private static final boolean DEBUG_LOGGING = false; + + public EntityAbilityBeam(World world) { + super(world); + } + + @Override + protected void entityInit() { + super.entityInit(); + // Register data watcher for charging state + this.dataWatcher.addObject(DW_CHARGING, (byte) 0); + } + + /** + * 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; + } + + /** + * Set charging state (server only, synced to clients via data watcher). + */ + private void setCharging(boolean value) { + this.charging = value; + if (!worldObj.isRemote) { + this.dataWatcher.updateObject(DW_CHARGING, (byte) (value ? 1 : 0)); + } + } + + /** + * Full constructor with all parameters using data classes. + * @param anchoredMode If true, origin follows owner and tail orb is rendered. + * If false, beam is free-moving with trailing length (no tail orb). + */ + public EntityAbilityBeam(World world, EntityLivingBase owner, EntityLivingBase target, + double x, double y, double z, + float beamWidth, float headSize, + EnergyColorData color, EnergyCombatData combat, + EnergyHomingData homing, EnergyLightningData lightning, + EnergyLifespanData lifespan, + boolean anchoredMode) { + super(world); + + // Initialize base properties via parent + initProjectile(owner, target, x, y, z, headSize, color, combat, lightning, lifespan); + + // Beam-specific properties from homing data + this.speed = homing.speed; + this.homing = homing.homing; + this.homingStrength = homing.homingStrength; + this.homingRange = homing.homingRange; + this.beamWidth = beamWidth; + this.headSize = headSize; + + // Anchored mode controls whether origin follows owner and tail orb is rendered + this.attachedToOwner = anchoredMode; + this.renderTailOrb = anchoredMode; + + // Initialize head offset at origin (0,0,0 relative) + this.headOffsetX = 0; + this.headOffsetY = 0; + this.headOffsetZ = 0; + this.prevHeadOffsetX = 0; + this.prevHeadOffsetY = 0; + this.prevHeadOffsetZ = 0; + + // Add initial trail point at origin (relative 0,0,0) + trailPoints.add(Vec3.createVectorHelper(0, 0, 0)); + + if (DEBUG_LOGGING && !world.isRemote) { + LogWriter.info("[Beam] Created at origin " + x + ", " + y + ", " + z + " maxDist=" + lifespan.maxDistance); + } + + // Calculate initial velocity toward target + if (target != null) { + double dx = target.posX - x; + double dy = (target.posY + target.getEyeHeight()) - y; + double dz = target.posZ - z; + double len = Math.sqrt(dx * dx + dy * dy + dz * dz); + if (len > 0) { + this.motionX = (dx / len) * speed; + this.motionY = (dy / len) * speed; + this.motionZ = (dz / len) * speed; + } + } else { + float yaw = (float) Math.toRadians(owner.rotationYaw); + float pitch = (float) Math.toRadians(owner.rotationPitch); + this.motionX = -Math.sin(yaw) * Math.cos(pitch) * speed; + this.motionY = -Math.sin(pitch) * speed; + this.motionZ = Math.cos(yaw) * Math.cos(pitch) * speed; + } + } + + public void setupCharging(EnergyAnchorData anchor, int chargeDuration, float chargeOffsetDistance) { + setCharging(true); + this.chargeDuration = chargeDuration; + this.chargeTick = 0; + this.chargeOffsetDistance = chargeOffsetDistance; + this.anchorData = anchor; + this.fadeTrail = !attachedToOwner; + this.motionX = 0; + this.motionY = 0; + this.motionZ = 0; + } + + public void setupPreview(EntityLivingBase owner, float beamWidth, float headSize, EnergyColorData color, EnergyLightningData lightning, EnergyAnchorData anchor, int chargeDuration, float chargeOffsetDistance) { + this.setPreviewMode(true); + this.setPreviewOwner(owner); + + // Set visual properties + this.beamWidth = beamWidth; + this.headSize = headSize; + this.size = headSize; + this.innerColor = color.innerColor; + this.outerColor = color.outerColor; + this.outerColorEnabled = color.outerColorEnabled; + this.outerColorWidth = color.outerColorWidth; + this.outerColorAlpha = color.outerColorAlpha; + this.rotationSpeed = color.rotationSpeed; + this.lightningEffect = lightning.lightningEffect; + this.lightningDensity = lightning.lightningDensity; + this.lightningRadius = lightning.lightningRadius; + + // Set charging state + this.setCharging(true); + this.chargeDuration = chargeDuration; + this.chargeTick = 0; + this.chargeOffsetDistance = chargeOffsetDistance; + this.anchorData = anchor; + + // Initial position at anchor point + Vec3 pos = AnchorPointHelper.calculateAnchorPosition(owner, anchorData, chargeOffsetDistance); + this.setPosition(pos.xCoord, pos.yCoord, pos.zCoord); + this.prevPosX = pos.xCoord; + this.prevPosY = pos.yCoord; + this.prevPosZ = pos.zCoord; + this.startX = pos.xCoord; + this.startY = pos.yCoord; + this.startZ = pos.zCoord; + + // Initialize head offsets + this.headOffsetX = 0; + this.headOffsetY = 0; + this.headOffsetZ = 0; + this.prevHeadOffsetX = 0; + this.prevHeadOffsetY = 0; + this.prevHeadOffsetZ = 0; + + // Attach to owner for anchor following + this.attachedToOwner = true; + this.renderTailOrb = true; + + // Clear motion + this.motionX = 0; + this.motionY = 0; + this.motionZ = 0; + } + + /** + * 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. + * + * For anchored beams: origin follows owner, head starts at charged position + * For non-anchored beams: origin fixed at charged position, head starts there + */ + public void startFiring(EntityLivingBase target) { + if (!isCharging()) return; + + setCharging(false); + + // Origin (tail) stays at the charged position - where the orb was + // This is the same for both anchored and non-anchored modes + startX = posX; + startY = posY; + startZ = posZ; + + // Head starts at the origin (tail position) + headOffsetX = 0; + headOffsetY = 0; + headOffsetZ = 0; + prevHeadOffsetX = 0; + prevHeadOffsetY = 0; + prevHeadOffsetZ = 0; + + // Origin no longer follows owner after firing starts + // (for anchored mode, tail is fixed in space; for non-anchored, there's no tail) + attachedToOwner = false; + + // Initialize trail with just the origin point + trailPoints.clear(); + trailPointAges.clear(); + trailPoints.add(Vec3.createVectorHelper(0, 0, 0)); + if (fadeTrail) trailPointAges.add(0); + + // Calculate velocity toward target (head starts at origin = startX/Y/Z) + Entity owner = getOwner(); + + if (target != null) { + double dx = target.posX - startX; + double dy = (target.posY + target.getEyeHeight()) - startY; + double dz = target.posZ - startZ; + double len = Math.sqrt(dx * dx + dy * dy + dz * dz); + if (len > 0) { + motionX = (dx / len) * speed; + motionY = (dy / len) * speed; + motionZ = (dz / len) * speed; + } + } else if (owner != null) { + float yaw = (float) Math.toRadians(owner.rotationYaw); + float pitch = (float) Math.toRadians(owner.rotationPitch); + motionX = -Math.sin(yaw) * Math.cos(pitch) * speed; + motionY = -Math.sin(pitch) * speed; + motionZ = Math.cos(yaw) * Math.cos(pitch) * speed; + } + + if (DEBUG_LOGGING && !worldObj.isRemote) { + LogWriter.info("[Beam] startFiring: origin=(" + startX + "," + startY + "," + startZ + + ") headOffset=(" + headOffsetX + "," + headOffsetY + "," + headOffsetZ + + ") motion=(" + motionX + "," + motionY + "," + motionZ + ")"); + } + } + + @Override + protected boolean checkMaxDistance() { + // Check distance using head offset (distance from origin) + double distFromOrigin = Math.sqrt( + headOffsetX * headOffsetX + + headOffsetY * headOffsetY + + headOffsetZ * headOffsetZ + ); + boolean exceeded = distFromOrigin >= maxDistance; + if (exceeded && !worldObj.isRemote && DEBUG_LOGGING) { + LogWriter.info("[Beam] DEAD: Max distance exceeded. dist=" + distFromOrigin + " max=" + maxDistance); + } + return exceeded; + } + + @Override + protected void updateProjectile() { + // Handle charging state (windup phase) - use isCharging() for synced value + if (isCharging()) { + updateCharging(); + return; + } + + // Store previous head offset for interpolation + prevHeadOffsetX = headOffsetX; + prevHeadOffsetY = headOffsetY; + prevHeadOffsetZ = headOffsetZ; + + // Age trail points for fading effect + if (fadeTrail) { + ageTrailPoints(); + } + + // In preview mode, run movement on client (no server) + if (previewMode) { + updatePreviewMovement(); + return; + } + + if (worldObj.isRemote) { + // Client: interpolate entity position + handleClientInterpolation(); + + // Client: derive head offset from entity position and origin + // Entity position = head world position, so headOffset = pos - origin + // Note: During charging, updateCharging() handles origin positioning + // After firing, origin is fixed at the charged position + headOffsetX = posX - startX; + headOffsetY = posY - startY; + headOffsetZ = posZ - startZ; + + // Add trail point on client + addTrailPoint(); + } else { + // Server-side logic + updateHoming(); + + // 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 for collision detection + double headWorldX = startX + headOffsetX; + double headWorldY = startY + headOffsetY; + double headWorldZ = startZ + headOffsetZ; + + // Update entity position to head (for network sync) + this.setPosition(headWorldX, headWorldY, headWorldZ); + + // Add trail point + addTrailPoint(); + + // Debug log periodically + if (DEBUG_LOGGING && ticksExisted % 20 == 0) { + LogWriter.info("[Beam] Server tick=" + ticksExisted + " headOffset=(" + + String.format("%.2f", headOffsetX) + "," + + String.format("%.2f", headOffsetY) + "," + + String.format("%.2f", headOffsetZ) + ") origin=(" + + String.format("%.2f", startX) + "," + + String.format("%.2f", startY) + "," + + String.format("%.2f", startZ) + ") trail=" + trailPoints.size()); + } + + // Skip collision checks on first few ticks + if (ticksExisted > 2) { + checkBlockCollision(headWorldX, headWorldY, headWorldZ); + checkEntityCollision(headWorldX, headWorldY, headWorldZ); + } + } + } + + /** + * Update during charging state - follow owner based on anchor point. + */ + private void updateCharging() { + chargeTick++; + + Entity owner = getOwner(); + if (owner == null || owner.isDead) { + setDead(); + return; + } + + // Calculate position based on anchor point + Vec3 pos; + if (owner instanceof EntityLivingBase) { + pos = AnchorPointHelper.calculateAnchorPosition((EntityLivingBase) owner, anchorData, chargeOffsetDistance); + } else { + // Fallback for non-living entities (shouldn't happen normally) + float yaw = (float) Math.toRadians(owner.rotationYaw); + double offsetX = -Math.sin(yaw) * chargeOffsetDistance; + double offsetZ = Math.cos(yaw) * chargeOffsetDistance; + pos = Vec3.createVectorHelper( + owner.posX + offsetX, + owner.posY + owner.getEyeHeight() * 0.7, + owner.posZ + offsetZ + ); + } + + // Offset downward by half headSize to center + double centeredY = pos.yCoord - headSize * 0.5; + setPosition(pos.xCoord, centeredY, pos.zCoord); + + // Also update origin for when firing starts + startX = pos.xCoord; + startY = centeredY; + startZ = pos.zCoord; + } + + /** + * Age trail points and remove old ones (for comet effect). + */ + private void ageTrailPoints() { + // Age all trail points + for (int i = 0; i < trailPointAges.size(); i++) { + trailPointAges.set(i, trailPointAges.get(i) + 1); + } + + // Remove trail points that have fully faded + while (!trailPointAges.isEmpty() && trailPointAges.get(0) >= trailFadeTime) { + trailPointAges.remove(0); + if (!trailPoints.isEmpty()) { + trailPoints.remove(0); + } + } + } + + private void addTrailPoint() { + // Trail points are RELATIVE to origin + if (trailPoints.isEmpty()) { + trailPoints.add(Vec3.createVectorHelper(headOffsetX, headOffsetY, headOffsetZ)); + if (fadeTrail) { + trailPointAges.add(0); + } + return; + } + + Vec3 lastPoint = trailPoints.get(trailPoints.size() - 1); + double dist = Math.sqrt( + (headOffsetX - lastPoint.xCoord) * (headOffsetX - lastPoint.xCoord) + + (headOffsetY - lastPoint.yCoord) * (headOffsetY - lastPoint.yCoord) + + (headOffsetZ - lastPoint.zCoord) * (headOffsetZ - lastPoint.zCoord) + ); + + if (dist >= MIN_POINT_DISTANCE) { + trailPoints.add(Vec3.createVectorHelper(headOffsetX, headOffsetY, headOffsetZ)); + if (fadeTrail) { + trailPointAges.add(0); + } + + // Limit trail length (only if not using fading - fading handles its own cleanup) + if (!fadeTrail) { + while (trailPoints.size() > MAX_TRAIL_POINTS) { + trailPoints.remove(0); + } + } + } + } + + /** + * 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; + + Entity target = getTarget(); + if (target == null || !target.isEntityAlive()) return; + + // Calculate world position of head + double headWorldX = startX + headOffsetX; + double headWorldY = startY + headOffsetY; + double headWorldZ = startZ + headOffsetZ; + + double dx = target.posX - headWorldX; + double dy = (target.posY + target.getEyeHeight()) - headWorldY; + double dz = target.posZ - headWorldZ; + double dist = Math.sqrt(dx * dx + dy * dy + dz * dz); + + if (dist <= homingRange && dist > 0) { + // Calculate effective homing strength - increases when closer to commit to target + float effectiveStrength = homingStrength; + if (dist < homingRange * 0.3) { + effectiveStrength = Math.min(1.0f, homingStrength * 2.5f); + } else if (dist < homingRange * 0.6) { + effectiveStrength = Math.min(0.8f, homingStrength * 1.5f); + } + + double desiredVX = (dx / dist) * speed; + double desiredVY = (dy / dist) * speed; + double desiredVZ = (dz / dist) * speed; + + motionX += (desiredVX - motionX) * effectiveStrength; + motionY += (desiredVY - motionY) * effectiveStrength; + motionZ += (desiredVZ - motionZ) * effectiveStrength; + + double vLen = Math.sqrt(motionX * motionX + motionY * motionY + motionZ * motionZ); + if (vLen > 0) { + motionX = (motionX / vLen) * speed; + motionY = (motionY / vLen) * speed; + motionZ = (motionZ / vLen) * speed; + } + } + } + + private void checkBlockCollision(double headX, double headY, double headZ) { + // Previous head world position + double prevHeadWorldX = startX + prevHeadOffsetX; + double prevHeadWorldY = startY + prevHeadOffsetY; + double prevHeadWorldZ = startZ + prevHeadOffsetZ; + + Vec3 currentPos = Vec3.createVectorHelper(prevHeadWorldX, prevHeadWorldY, prevHeadWorldZ); + Vec3 nextPos = Vec3.createVectorHelper(headX, headY, headZ); + // Use full raytrace that doesn't stop at liquids and checks all blocks + MovingObjectPosition blockHit = worldObj.func_147447_a(currentPos, nextPos, false, true, false); + + if (blockHit != null && blockHit.typeOfHit == MovingObjectPosition.MovingObjectType.BLOCK) { + hasHit = true; + if (explosive) { + posX = blockHit.hitVec.xCoord; + posY = blockHit.hitVec.yCoord; + posZ = blockHit.hitVec.zCoord; + doExplosion(); + } + this.setDead(); + } + } + + private void checkEntityCollision(double headX, double headY, double headZ) { + double hitSize = headSize * 0.5; + AxisAlignedBB hitBox = AxisAlignedBB.getBoundingBox( + headX - hitSize, headY - hitSize, headZ - hitSize, + headX + hitSize, headY + hitSize, headZ + hitSize + ); + + @SuppressWarnings("unchecked") + List entities = worldObj.getEntitiesWithinAABB(EntityLivingBase.class, hitBox); + + for (EntityLivingBase entity : entities) { + if (shouldIgnoreEntity(entity)) continue; + + if (DEBUG_LOGGING) { + LogWriter.info("[Beam] DEAD: Entity collision with " + entity.getClass().getSimpleName() + " at tick " + ticksExisted); + } + hasHit = true; + + if (explosive) { + posX = headX; + posY = headY; + posZ = headZ; + doExplosion(); + } else { + applyDamage(entity); + } + + this.setDead(); + return; + } + } + + @Override + protected boolean shouldIgnoreEntity(Entity entity) { + if (entity.getPersistentID().equals(siblingUUID)) return true; + return super.shouldIgnoreEntity(entity); + } + + // ==================== GETTERS FOR RENDERER ==================== + + public float getBeamWidth() { + return beamWidth; + } + + public float getHeadSize() { + return headSize; + } + + /** + * Get interpolated head offset for smooth rendering. + * Returns offset RELATIVE to origin. + */ + public double getInterpolatedHeadOffsetX(float partialTicks) { + return prevHeadOffsetX + (headOffsetX - prevHeadOffsetX) * partialTicks; + } + + public double getInterpolatedHeadOffsetY(float partialTicks) { + return prevHeadOffsetY + (headOffsetY - prevHeadOffsetY) * partialTicks; + } + + public double getInterpolatedHeadOffsetZ(float partialTicks) { + return prevHeadOffsetZ + (headOffsetZ - prevHeadOffsetZ) * partialTicks; + } + + /** + * Get trail points. These are RELATIVE to origin (startX/Y/Z). + */ + public List getTrailPoints() { + return trailPoints; + } + + public double getOriginX() { + return startX; + } + + public double getOriginY() { + return startY; + } + + public double getOriginZ() { + return startZ; + } + + /** + * Whether the tail orb should be rendered (only true in anchored mode). + */ + public boolean shouldRenderTailOrb() { + return renderTailOrb; + } + + /** + * Whether the beam is attached to its owner (anchored mode). + */ + public boolean isAttachedToOwner() { + return attachedToOwner; + } + + /** + * Get the charge progress (0-1). + */ + public float getChargeProgress() { + if (chargeDuration <= 0) return 1.0f; + return Math.min(1.0f, (float) chargeTick / chargeDuration); + } + + /** + * Get interpolated charge progress for smooth rendering. + */ + public float getInterpolatedChargeProgress(float partialTicks) { + if (chargeDuration <= 0) return 1.0f; + float prevProgress = Math.max(0, (float) (chargeTick - 1) / chargeDuration); + float currProgress = Math.min(1.0f, (float) chargeTick / chargeDuration); + return prevProgress + (currProgress - prevProgress) * partialTicks; + } + + /** + * Whether trail should fade (comet effect for non-anchored beams). + */ + public boolean hasFadingTrail() { + return fadeTrail; + } + + /** + * Get trail point ages for fading calculation. + */ + public List getTrailPointAges() { + return trailPointAges; + } + + /** + * Get trail fade time in ticks. + */ + public int getTrailFadeTime() { + return trailFadeTime; + } + + public UUID getSiblingUUID() { + return siblingUUID; + } + + public void setSiblingUUID(UUID siblingUUID) { + if (siblingUUID == null) + return; + + if (this.siblingUUID == null) + this.siblingUUID = siblingUUID; + } + + // ==================== NBT ==================== + + @Override + protected void readProjectileNBT(NBTTagCompound nbt) { + this.speed = nbt.hasKey("Speed") ? nbt.getFloat("Speed") : 0.5f; + this.homing = !nbt.hasKey("Homing") || nbt.getBoolean("Homing"); + this.homingStrength = nbt.hasKey("HomingStrength") ? nbt.getFloat("HomingStrength") : 0.15f; + this.homingRange = nbt.hasKey("HomingRange") ? nbt.getFloat("HomingRange") : 20.0f; + this.beamWidth = nbt.hasKey("BeamWidth") ? nbt.getFloat("BeamWidth") : 0.3f; + this.headSize = nbt.hasKey("HeadSize") ? nbt.getFloat("HeadSize") : 0.5f; + this.headOffsetX = nbt.hasKey("HeadOffsetX") ? nbt.getDouble("HeadOffsetX") : 0; + this.headOffsetY = nbt.hasKey("HeadOffsetY") ? nbt.getDouble("HeadOffsetY") : 0; + this.headOffsetZ = nbt.hasKey("HeadOffsetZ") ? nbt.getDouble("HeadOffsetZ") : 0; + this.prevHeadOffsetX = this.headOffsetX; + this.prevHeadOffsetY = this.headOffsetY; + this.prevHeadOffsetZ = this.headOffsetZ; + this.attachedToOwner = !nbt.hasKey("AttachedToOwner") || nbt.getBoolean("AttachedToOwner"); + this.renderTailOrb = !nbt.hasKey("RenderTailOrb") || nbt.getBoolean("RenderTailOrb"); + // Read charging state and sync to data watcher + boolean isCharging = nbt.hasKey("Charging") && nbt.getBoolean("Charging"); + this.charging = isCharging; + this.dataWatcher.updateObject(DW_CHARGING, (byte) (isCharging ? 1 : 0)); + this.chargeDuration = nbt.hasKey("ChargeDuration") ? nbt.getInteger("ChargeDuration") : 40; + this.chargeTick = nbt.hasKey("ChargeTick") ? nbt.getInteger("ChargeTick") : 0; + this.chargeOffsetDistance = nbt.hasKey("ChargeOffsetDistance") ? nbt.getFloat("ChargeOffsetDistance") : 1.0f; + this.fadeTrail = nbt.hasKey("FadeTrail") && nbt.getBoolean("FadeTrail"); + this.trailFadeTime = nbt.hasKey("TrailFadeTime") ? nbt.getInteger("TrailFadeTime") : 20; + + this.anchorData.readNBT(nbt); + + // Read trail points (relative to origin) + trailPoints.clear(); + if (nbt.hasKey("Trail")) { + NBTTagList trailList = nbt.getTagList("Trail", 10); + for (int i = 0; i < trailList.tagCount(); i++) { + NBTTagCompound point = trailList.getCompoundTagAt(i); + trailPoints.add(Vec3.createVectorHelper( + point.getDouble("X"), + point.getDouble("Y"), + point.getDouble("Z") + )); + } + } + + this.siblingUUID = nbt.hasKey("SiblingUUID") ? UUID.fromString(nbt.getString("SiblingUUID")) : null; + } + + @Override + protected void writeProjectileNBT(NBTTagCompound nbt) { + nbt.setFloat("Speed", speed); + nbt.setBoolean("Homing", homing); + nbt.setFloat("HomingStrength", homingStrength); + nbt.setFloat("HomingRange", homingRange); + nbt.setFloat("BeamWidth", beamWidth); + nbt.setFloat("HeadSize", headSize); + nbt.setDouble("HeadOffsetX", headOffsetX); + nbt.setDouble("HeadOffsetY", headOffsetY); + nbt.setDouble("HeadOffsetZ", headOffsetZ); + nbt.setBoolean("AttachedToOwner", attachedToOwner); + nbt.setBoolean("RenderTailOrb", renderTailOrb); + nbt.setBoolean("Charging", isCharging()); + nbt.setInteger("ChargeDuration", chargeDuration); + nbt.setInteger("ChargeTick", chargeTick); + nbt.setFloat("ChargeOffsetDistance", chargeOffsetDistance); + nbt.setBoolean("FadeTrail", fadeTrail); + nbt.setInteger("TrailFadeTime", trailFadeTime); + + this.anchorData.writeNBT(nbt); + + // Write trail points + NBTTagList trailList = new NBTTagList(); + for (Vec3 point : trailPoints) { + NBTTagCompound pointNbt = new NBTTagCompound(); + pointNbt.setDouble("X", point.xCoord); + pointNbt.setDouble("Y", point.yCoord); + pointNbt.setDouble("Z", point.zCoord); + trailList.appendTag(pointNbt); + } + nbt.setTag("Trail", trailList); + + if (siblingUUID instanceof UUID) { + nbt.setString("SiblingUUID", siblingUUID.toString()); + } + } +} diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java new file mode 100644 index 000000000..04afb2224 --- /dev/null +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java @@ -0,0 +1,633 @@ +package kamkeel.npcs.entity; + +import kamkeel.npcs.controllers.data.ability.AnchorPoint; +import kamkeel.npcs.controllers.data.ability.data.*; +import kamkeel.npcs.util.AnchorPointHelper; +import net.minecraft.entity.Entity; +import net.minecraft.entity.EntityLivingBase; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.util.AxisAlignedBB; +import net.minecraft.util.MovingObjectPosition; +import net.minecraft.util.Vec3; +import net.minecraft.world.World; + +import java.util.List; +import java.util.UUID; + +/** + * Disc projectile - flat spinning disc with optional boomerang behavior. + * Has a wider, thinner hitbox compared to Orb. + * + * Design inspired by LouisXIV's energy attack system. + */ +public class EntityAbilityDisc extends EntityAbilityProjectile { + + // Disc-specific movement properties + private float speed = 0.5f; + private boolean homing = true; + private float homingStrength = 0.35f; // Increased from 0.15 for better tracking + private float homingRange = 20.0f; + + // Boomerang properties + private boolean boomerang = false; + private int boomerangDelay = 40; // Ticks before returning + private boolean returning = false; + private int ticksSinceMiss = 0; + + // Disc shape properties + private float discRadius = 1.0f; // Width of disc + private float discThickness = 0.2f; // Height of disc + + // Charging state (during windup) + private boolean charging = false; + private int chargeDuration = 40; + private int chargeTick = 0; + private float targetDiscRadius = 1.0f; // Full radius to grow to during charging + private float targetDiscThickness = 0.2f; // Full thickness to grow to during charging + private EnergyAnchorData anchorData = new EnergyAnchorData(); + + // Data watcher index for charging state (synced to clients) + private static final int DW_CHARGING = 20; + + private UUID siblingUUID = null; + + public EntityAbilityDisc(World world) { + super(world); + } + + @Override + protected void entityInit() { + super.entityInit(); + // Register data watcher for charging state + this.dataWatcher.addObject(DW_CHARGING, (byte) 0); + } + + /** + * 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; + } + + /** + * Set charging state (server only, synced to clients via data watcher). + */ + private void setCharging(boolean value) { + this.charging = value; + if (!worldObj.isRemote) { + this.dataWatcher.updateObject(DW_CHARGING, (byte) (value ? 1 : 0)); + } + } + + /** + * Full constructor with all parameters using data classes. + */ + public EntityAbilityDisc(World world, EntityLivingBase owner, EntityLivingBase target, + double x, double y, double z, + float discRadius, float discThickness, + EnergyColorData color, EnergyCombatData combat, + EnergyHomingData homing, EnergyLightningData lightning, + EnergyLifespanData lifespan, + boolean boomerang, int boomerangDelay) { + super(world); + + // Initialize base properties + initProjectile(owner, target, x, y, z, discRadius, color, combat, lightning, lifespan); + + // Disc-specific properties from homing data + this.speed = homing.speed; + this.homing = homing.homing; + this.homingStrength = homing.homingStrength; + this.homingRange = homing.homingRange; + + this.boomerang = boomerang; + this.boomerangDelay = boomerangDelay; + + this.discRadius = discRadius; + this.discThickness = discThickness; + + // Calculate initial velocity toward target + if (target != null) { + double dx = target.posX - x; + double dy = (target.posY + target.getEyeHeight()) - y; + double dz = target.posZ - z; + double len = Math.sqrt(dx * dx + dy * dy + dz * dz); + if (len > 0) { + this.motionX = (dx / len) * speed; + this.motionY = (dy / len) * speed; + this.motionZ = (dz / len) * speed; + } + } else { + float yaw = (float) Math.toRadians(owner.rotationYaw); + float pitch = (float) Math.toRadians(owner.rotationPitch); + this.motionX = -Math.sin(yaw) * Math.cos(pitch) * speed; + this.motionY = -Math.sin(pitch) * speed; + this.motionZ = Math.cos(yaw) * Math.cos(pitch) * speed; + } + } + + /** + * Setup this disc in charging mode (for windup phase). + * The disc will grow from 0 to discRadius over chargeDuration ticks. + * Position follows the owner based on anchor point. + */ + public void setupCharging(EnergyAnchorData anchor, int chargeDuration) { + setCharging(true); + this.chargeDuration = chargeDuration; + this.chargeTick = 0; + this.anchorData = anchor; + this.targetDiscRadius = this.discRadius; + this.targetDiscThickness = this.discThickness; + this.discRadius = 0.01f; + this.discThickness = 0.01f; + this.size = 0.01f; + this.renderCurrentSize = 0.01f; + this.prevRenderSize = 0.01f; + this.motionX = 0; + this.motionY = 0; + this.motionZ = 0; + } + + /** + * Setup this 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 void setupPreview(EntityLivingBase owner, float discRadius, float discThickness, EnergyColorData color, EnergyLightningData lightning, EnergyAnchorData anchor, int chargeDuration) { + this.setPreviewMode(true); + this.setPreviewOwner(owner); + + // Set visual properties + this.innerColor = color.innerColor; + this.outerColor = color.outerColor; + this.outerColorEnabled = color.outerColorEnabled; + this.outerColorWidth = color.outerColorWidth; + this.rotationSpeed = color.rotationSpeed; + this.lightningEffect = lightning.lightningEffect; + this.lightningDensity = lightning.lightningDensity; + this.lightningRadius = lightning.lightningRadius; + + // Set charging state + this.setCharging(true); + this.chargeDuration = chargeDuration; + this.chargeTick = 0; + this.anchorData = anchor; + + // Store target size and start at 0 for grow effect + this.targetDiscRadius = discRadius; + this.targetDiscThickness = discThickness; + this.discRadius = 0.01f; + this.discThickness = 0.01f; + this.size = 0.01f; + + // Initial position at anchor point + Vec3 pos = AnchorPointHelper.calculateAnchorPosition(owner, anchorData); + this.setPosition(pos.xCoord, pos.yCoord, pos.zCoord); + this.prevPosX = pos.xCoord; + this.prevPosY = pos.yCoord; + this.prevPosZ = pos.zCoord; + this.startX = pos.xCoord; + this.startY = pos.yCoord; + this.startZ = pos.zCoord; + + // Clear motion + this.motionX = 0; + this.motionY = 0; + this.motionZ = 0; + } + + /** + * 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. + */ + public void startMoving(EntityLivingBase target) { + if (!isCharging()) return; + + setCharging(false); + + // Update start position to current position + startX = posX; + startY = posY; + startZ = posZ; + + // Calculate velocity toward target + Entity owner = getOwner(); + + if (target != null) { + double dx = target.posX - posX; + double dy = (target.posY + target.getEyeHeight()) - posY; + double dz = target.posZ - posZ; + double len = Math.sqrt(dx * dx + dy * dy + dz * dz); + if (len > 0) { + motionX = (dx / len) * speed; + motionY = (dy / len) * speed; + motionZ = (dz / len) * speed; + } + } else if (owner != null) { + float yaw = (float) Math.toRadians(owner.rotationYaw); + float pitch = (float) Math.toRadians(owner.rotationPitch); + motionX = -Math.sin(yaw) * Math.cos(pitch) * speed; + motionY = -Math.sin(pitch) * speed; + motionZ = Math.cos(yaw) * Math.cos(pitch) * speed; + } + } + + @Override + protected void updateRotation() { + // Disc spins ONLY on Y axis (flat spin like a saw blade) + this.rotationValY += rotationSpeed; + // No wobble - keep X and Z at 0 for stable flight + if (this.rotationValY > 360.0f) this.rotationValY -= 360.0f; + } + + @Override + protected void updateProjectile() { + // Handle charging state (windup phase) + if (isCharging()) { + updateCharging(); + return; + } + + // In preview mode, run movement on client (no server) + if (previewMode) { + updatePreviewMovement(); + return; + } + + if (worldObj.isRemote) { + handleClientInterpolation(); + } else { + // Server-side logic + if (returning) { + updateReturnToOwner(); + } else { + updateHoming(); + } + + checkBlockCollision(); + checkEntityCollision(); + this.moveEntity(motionX, motionY, motionZ); + + // Track ticks for boomerang delay (checkMaxDistance handles the actual return trigger) + if (boomerang && !returning && !hasHit) { + ticksSinceMiss++; + } + } + + // Check wall collision + if (this.isCollidedHorizontally || this.isCollidedVertically) { + if (!worldObj.isRemote) { + hasHit = true; + if (explosive) { + doExplosion(); + } + } + this.setDead(); + } + } + + @Override + protected boolean checkMaxDistance() { + double distTraveled = Math.sqrt( + (posX - startX) * (posX - startX) + + (posY - startY) * (posY - startY) + + (posZ - startZ) * (posZ - startZ) + ); + + if (boomerang) { + // If not returning yet, check if we should start returning + if (!returning && !hasHit) { + if (distTraveled >= maxDistance * 0.8 || ticksSinceMiss >= boomerangDelay) { + returning = true; + } + } + + // If returning, only die when close to owner + if (returning) { + Entity owner = getOwner(); + if (owner != null) { + double distToOwner = Math.sqrt( + (posX - owner.posX) * (posX - owner.posX) + + (posY - owner.posY) * (posY - owner.posY) + + (posZ - owner.posZ) * (posZ - owner.posZ) + ); + return distToOwner < 1.5; + } + // Owner gone but returning - keep going toward last known position + return false; + } + + // Not returning yet - don't die from distance (we'll trigger return above) + return false; + } + + // Non-boomerang: standard max distance check + 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; + + Entity target = getTarget(); + if (target == null || !target.isEntityAlive()) return; + + double dx = target.posX - posX; + double dy = (target.posY + target.getEyeHeight()) - posY; + double dz = target.posZ - posZ; + double dist = Math.sqrt(dx * dx + dy * dy + dz * dz); + + if (dist <= homingRange && dist > 0) { + // Calculate effective homing strength - increases when closer to commit to target + float effectiveStrength = homingStrength; + if (dist < homingRange * 0.3) { + effectiveStrength = Math.min(1.0f, homingStrength * 2.5f); + } else if (dist < homingRange * 0.6) { + effectiveStrength = Math.min(0.8f, homingStrength * 1.5f); + } + + double desiredVX = (dx / dist) * speed; + double desiredVY = (dy / dist) * speed; + double desiredVZ = (dz / dist) * speed; + + motionX += (desiredVX - motionX) * effectiveStrength; + motionY += (desiredVY - motionY) * effectiveStrength; + motionZ += (desiredVZ - motionZ) * effectiveStrength; + + double vLen = Math.sqrt(motionX * motionX + motionY * motionY + motionZ * motionZ); + if (vLen > 0) { + motionX = (motionX / vLen) * speed; + motionY = (motionY / vLen) * speed; + motionZ = (motionZ / vLen) * speed; + } + } + } + + private void updateReturnToOwner() { + Entity owner = getOwner(); + + // If owner is gone (unloaded/dead), head back toward start position + double targetX, targetY, targetZ; + if (owner != null) { + targetX = owner.posX; + targetY = owner.posY + owner.height * 0.5; + targetZ = owner.posZ; + } else { + // Return to start position if owner not available + targetX = startX; + targetY = startY; + targetZ = startZ; + } + + double dx = targetX - posX; + double dy = targetY - posY; + double dz = targetZ - posZ; + double dist = Math.sqrt(dx * dx + dy * dy + dz * dz); + + if (dist > 0) { + // Stronger homing when returning + double returnStrength = Math.min(1.0, homingStrength * 2.5); + double desiredVX = (dx / dist) * speed; + double desiredVY = (dy / dist) * speed; + double desiredVZ = (dz / dist) * speed; + + motionX += (desiredVX - motionX) * returnStrength; + motionY += (desiredVY - motionY) * returnStrength; + motionZ += (desiredVZ - motionZ) * returnStrength; + + double vLen = Math.sqrt(motionX * motionX + motionY * motionY + motionZ * motionZ); + if (vLen > 0) { + motionX = (motionX / vLen) * speed; + motionY = (motionY / vLen) * speed; + motionZ = (motionZ / vLen) * speed; + } + } + } + + private void checkBlockCollision() { + Vec3 currentPos = Vec3.createVectorHelper(posX, posY, posZ); + Vec3 nextPos = Vec3.createVectorHelper(posX + motionX, posY + motionY, posZ + motionZ); + // Use full raytrace that doesn't stop at liquids and checks all blocks + MovingObjectPosition blockHit = worldObj.func_147447_a(currentPos, nextPos, false, true, false); + + if (blockHit != null && blockHit.typeOfHit == MovingObjectPosition.MovingObjectType.BLOCK) { + hasHit = true; + if (explosive) { + posX = blockHit.hitVec.xCoord; + posY = blockHit.hitVec.yCoord; + posZ = blockHit.hitVec.zCoord; + doExplosion(); + } + this.setDead(); + } + } + + private void checkEntityCollision() { + // Disc hitbox: wider but thinner + double halfWidth = discRadius * 0.5; + double halfHeight = discThickness * 0.5; + AxisAlignedBB hitBox = AxisAlignedBB.getBoundingBox( + posX - halfWidth, posY - halfHeight, posZ - halfWidth, + posX + halfWidth, posY + halfHeight, posZ + halfWidth + ); + + @SuppressWarnings("unchecked") + List entities = worldObj.getEntitiesWithinAABB(EntityLivingBase.class, hitBox); + + for (EntityLivingBase entity : entities) { + if (shouldIgnoreEntity(entity)) continue; + + hasHit = true; + + if (explosive) { + doExplosion(); + } else { + applyDamage(entity); + } + + this.setDead(); + return; + } + } + + /** + * Update during charging state - follow owner based on anchor point. + */ + private void updateCharging() { + chargeTick++; + + Entity owner = getOwner(); + if (owner == null || owner.isDead) { + setDead(); + return; + } + + // Grow size based on charge progress + float progress = getChargeProgress(); + this.discRadius = targetDiscRadius * progress; + this.discThickness = targetDiscThickness * progress; + this.size = this.discRadius; // Base size for interpolation + + // Calculate position based on anchor point, offset downward by half radius to center + if (owner instanceof EntityLivingBase) { + Vec3 pos = AnchorPointHelper.calculateAnchorPosition((EntityLivingBase) owner, anchorData); + setPosition(pos.xCoord, pos.yCoord, pos.zCoord); + } + + // Update rotation during charging (disc still spins) + updateRotation(); + } + + /** + * Get the charge progress (0-1). + */ + public float getChargeProgress() { + if (chargeDuration <= 0) return 1.0f; + return Math.min(1.0f, (float) chargeTick / chargeDuration); + } + + /** + * Get interpolated charge progress for smooth rendering. + */ + public float getInterpolatedChargeProgress(float partialTicks) { + if (chargeDuration <= 0) return 1.0f; + float prevProgress = Math.max(0, (float) (chargeTick - 1) / chargeDuration); + float currProgress = Math.min(1.0f, (float) chargeTick / chargeDuration); + return prevProgress + (currProgress - prevProgress) * partialTicks; + } + + // ==================== GETTERS ==================== + + public float getSpeed() { + return speed; + } + + public boolean isHoming() { + return homing; + } + + public float getDiscRadius() { + return discRadius; + } + + public float getDiscThickness() { + return discThickness; + } + + public boolean isBoomerang() { + return boomerang; + } + + public boolean isReturning() { + return returning; + } + + public UUID getSiblingUUID() { + return siblingUUID; + } + + public void setSiblingUUID(UUID siblingUUID) { + if (siblingUUID == null) + return; + + if (this.siblingUUID == null) + this.siblingUUID = siblingUUID; + } + + // ==================== NBT ==================== + + @Override + protected void readProjectileNBT(NBTTagCompound nbt) { + this.speed = nbt.hasKey("Speed") ? nbt.getFloat("Speed") : 0.5f; + this.homing = !nbt.hasKey("Homing") || nbt.getBoolean("Homing"); + this.homingStrength = nbt.hasKey("HomingStrength") ? nbt.getFloat("HomingStrength") : 0.15f; + this.homingRange = nbt.hasKey("HomingRange") ? nbt.getFloat("HomingRange") : 20.0f; + this.boomerang = nbt.hasKey("Boomerang") && nbt.getBoolean("Boomerang"); + this.boomerangDelay = nbt.hasKey("BoomerangDelay") ? nbt.getInteger("BoomerangDelay") : 40; + this.discRadius = nbt.hasKey("DiscRadius") ? nbt.getFloat("DiscRadius") : 1.0f; + this.discThickness = nbt.hasKey("DiscThickness") ? nbt.getFloat("DiscThickness") : 0.2f; + this.returning = nbt.hasKey("Returning") && nbt.getBoolean("Returning"); + // Charging state + boolean isCharging = nbt.hasKey("Charging") && nbt.getBoolean("Charging"); + this.charging = isCharging; + this.dataWatcher.updateObject(DW_CHARGING, (byte) (isCharging ? 1 : 0)); + this.chargeDuration = nbt.hasKey("ChargeDuration") ? nbt.getInteger("ChargeDuration") : 40; + this.chargeTick = nbt.hasKey("ChargeTick") ? nbt.getInteger("ChargeTick") : 0; + this.targetDiscRadius = nbt.hasKey("TargetDiscRadius") ? nbt.getFloat("TargetDiscRadius") : this.discRadius; + this.targetDiscThickness = nbt.hasKey("TargetDiscThickness") ? nbt.getFloat("TargetDiscThickness") : this.discThickness; + this.anchorData.readNBT(nbt); + this.siblingUUID = nbt.hasKey("SiblingUUID") ? UUID.fromString(nbt.getString("SiblingUUID")) : null; + } + + @Override + protected void writeProjectileNBT(NBTTagCompound nbt) { + nbt.setFloat("Speed", speed); + nbt.setBoolean("Homing", homing); + nbt.setFloat("HomingStrength", homingStrength); + nbt.setFloat("HomingRange", homingRange); + nbt.setBoolean("Boomerang", boomerang); + nbt.setInteger("BoomerangDelay", boomerangDelay); + nbt.setFloat("DiscRadius", discRadius); + nbt.setFloat("DiscThickness", discThickness); + nbt.setBoolean("Returning", returning); + // Charging state + nbt.setBoolean("Charging", isCharging()); + nbt.setInteger("ChargeDuration", chargeDuration); + nbt.setInteger("ChargeTick", chargeTick); + nbt.setFloat("TargetDiscRadius", targetDiscRadius); + nbt.setFloat("TargetDiscThickness", targetDiscThickness); + + // Anchor data + this.anchorData.writeNBT(nbt); + + if (siblingUUID instanceof UUID) { + nbt.setString("SiblingUUID", siblingUUID.toString()); + } + } +} diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java new file mode 100644 index 000000000..e93ea5a77 --- /dev/null +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java @@ -0,0 +1,525 @@ +package kamkeel.npcs.entity; + +import kamkeel.npcs.controllers.data.ability.AnchorPoint; +import kamkeel.npcs.controllers.data.ability.data.*; +import kamkeel.npcs.util.AnchorPointHelper; +import net.minecraft.entity.Entity; +import net.minecraft.entity.EntityLivingBase; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.util.AxisAlignedBB; +import net.minecraft.util.MovingObjectPosition; +import net.minecraft.util.Vec3; +import net.minecraft.world.World; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Laser projectile - fast expanding thin line that pierces through multiple targets. + * No homing, travels in a straight line from origin to max distance. + * + * Design inspired by LouisXIV's energy attack system. + */ +public class EntityAbilityLaser extends EntityAbilityProjectile { + + // Laser-specific properties + private float laserWidth = 0.2f; + private float expansionSpeed = 2.0f; // Blocks per tick + private int lingerTicks = 10; // How long laser stays visible after reaching max + + // State + private float currentLength = 0.0f; + private boolean fullyExtended = false; + private int ticksSinceFullExtension = 0; + + // Direction (normalized) + private double dirX, dirY, dirZ; + + // Track hit entities to avoid double-damage + private Set hitEntities = new HashSet<>(); + + // End point for rendering + private double endX, endY, endZ; + + // Charging state (during windup) + private boolean charging = false; + private int chargeDuration = 40; + private int chargeTick = 0; + private EnergyAnchorData anchorData = new EnergyAnchorData(AnchorPoint.FRONT); + private float targetSize = 1.0f; // Full size to grow to during charging + + // Data watcher index for charging state (synced to clients) + private static final int DW_CHARGING = 20; + + public EntityAbilityLaser(World world) { + super(world); + } + + @Override + protected void entityInit() { + super.entityInit(); + // Register data watcher for charging state + this.dataWatcher.addObject(DW_CHARGING, (byte) 0); + } + + /** + * 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; + } + + /** + * Set charging state (server only, synced to clients via data watcher). + */ + private void setCharging(boolean value) { + this.charging = value; + if (!worldObj.isRemote) { + this.dataWatcher.updateObject(DW_CHARGING, (byte) (value ? 1 : 0)); + } + } + + /** + * Full constructor with all parameters using data classes. + */ + public EntityAbilityLaser(World world, EntityLivingBase owner, EntityLivingBase target, + double x, double y, double z, + float laserWidth, + EnergyColorData color, EnergyCombatData combat, + EnergyLightningData lightning, EnergyLifespanData lifespan, + float expansionSpeed, int lingerTicks) { + super(world); + + // Initialize base properties (laser doesn't rotate) + initProjectile(owner, target, x, y, z, laserWidth, color, combat, lightning, lifespan); + this.rotationSpeed = 0.0f; + + // Laser-specific properties + this.laserWidth = laserWidth; + this.expansionSpeed = expansionSpeed; + this.lingerTicks = lingerTicks; + + // Calculate direction toward target or forward + if (target != null) { + double dx = target.posX - x; + double dy = (target.posY + target.getEyeHeight() - 0.4) - y; + double dz = target.posZ - z; + double len = Math.sqrt(dx * dx + dy * dy + dz * dz); + if (len > 0) { + this.dirX = dx / len; + this.dirY = dy / len; + this.dirZ = dz / len; + } + } else { + // Fire in NPC's facing direction + float yaw = (float) Math.toRadians(owner.rotationYaw); + float pitch = (float) Math.toRadians(owner.rotationPitch); + this.dirX = -Math.sin(yaw) * Math.cos(pitch); + this.dirY = -Math.sin(pitch); + this.dirZ = Math.cos(yaw) * Math.cos(pitch); + } + + // Initialize end point + this.endX = x; + this.endY = y; + this.endZ = z; + } + + /** + * Setup this orb in charging mode (for windup phase). + * The orb will grow from 0 to orbSize over chargeDuration ticks. + * Position follows the owner based on anchor point. + */ + public void setupCharging(EnergyAnchorData anchor, int chargeDuration) { + setCharging(true); + this.chargeDuration = chargeDuration; + this.chargeTick = 0; + this.anchorData = anchor; + this.targetSize = this.size; + this.size = 0.01f; + this.renderCurrentSize = 0.01f; + this.prevRenderSize = 0.01f; + this.motionX = 0; + this.motionY = 0; + this.motionZ = 0; + } + + @Override + protected void updateRotation() { + // Laser doesn't rotate + } + + @Override + protected boolean checkMaxDistance() { + // Laser handles distance differently - it expands to max then lingers + return false; + } + + @Override + protected void updateProjectile() { + if (isCharging()) { + updateCharging(); + return; + } + + if (!fullyExtended) { + // Expand the laser + currentLength += expansionSpeed; + + if (currentLength >= maxDistance) { + currentLength = maxDistance; + fullyExtended = true; + } + + // Update end point + endX = startX + dirX * currentLength; + endY = startY + dirY * currentLength; + endZ = startZ + dirZ * currentLength; + + // Check for block collision along the new segment + if (!worldObj.isRemote) { + checkBlockCollision(); + checkEntityCollisionAlongLine(); + } + } else { + // Laser is fully extended, count down linger time + ticksSinceFullExtension++; + if (ticksSinceFullExtension >= lingerTicks) { + this.setDead(); + } + } + } + + public void startMoving(EntityLivingBase target) { + if (!isCharging()) return; + + setCharging(false); + + // Update start position to current position + startX = posX; + startY = posY; + startZ = posZ; + + // Calculate velocity toward target + Entity owner = getOwner(); + + if (target != null) { + double dx = target.posX - startX; + double dy = (target.posY + target.getEyeHeight() - 0.4) - startY; + double dz = target.posZ - startZ; + double len = Math.sqrt(dx * dx + dy * dy + dz * dz); + if (len > 0) { + this.dirX = dx / len; + this.dirY = dy / len; + this.dirZ = dz / len; + } + } else if (owner != null) { + // Fire in NPC's facing direction + float yaw = (float) Math.toRadians(owner.rotationYaw); + float pitch = (float) Math.toRadians(owner.rotationPitch); + this.dirX = -Math.sin(yaw) * Math.cos(pitch); + this.dirY = -Math.sin(pitch); + this.dirZ = Math.cos(yaw) * Math.cos(pitch);; + } + + setPosition(endX, endY, endZ); + + // Initialize end point + this.endX = posX; + this.endY = posY; + this.endZ = posZ; + } + + private void checkBlockCollision() { + Vec3 start = Vec3.createVectorHelper(startX, startY, startZ); + Vec3 end = Vec3.createVectorHelper(endX, endY, endZ); + // Use full raytrace that doesn't stop at liquids and checks all blocks + MovingObjectPosition blockHit = worldObj.func_147447_a(start, end, false, true, false); + + if (blockHit != null && blockHit.typeOfHit == MovingObjectPosition.MovingObjectType.BLOCK) { + // Laser stops at block + currentLength = (float) Math.sqrt( + (blockHit.hitVec.xCoord - startX) * (blockHit.hitVec.xCoord - startX) + + (blockHit.hitVec.yCoord - startY) * (blockHit.hitVec.yCoord - startY) + + (blockHit.hitVec.zCoord - startZ) * (blockHit.hitVec.zCoord - startZ) + ); + endX = blockHit.hitVec.xCoord; + endY = blockHit.hitVec.yCoord; + endZ = blockHit.hitVec.zCoord; + fullyExtended = true; + + if (explosive) { + // Explode at hit point + double oldPosX = posX; + double oldPosY = posY; + double oldPosZ = posZ; + posX = endX; + posY = endY; + posZ = endZ; + doExplosion(); + posX = oldPosX; + posY = oldPosY; + posZ = oldPosZ; + } + } + } + + /** + * Update during charging state - follow owner based on anchor point. + */ + private void updateCharging() { + chargeTick++; + + Entity owner = getOwner(); + if (owner == null || owner.isDead) { + setDead(); + return; + } + + // Grow size based on charge progress + float progress = getChargeProgress(); + this.size = targetSize * progress; + + // Calculate position based on anchor point, offset downward by half size to center + if (owner instanceof EntityLivingBase) { + Vec3 pos = AnchorPointHelper.calculateAnchorPosition((EntityLivingBase) owner, anchorData); + setPosition(pos.xCoord, pos.yCoord, pos.zCoord); + } + } + + /** + * Get the charge progress (0-1). + */ + public float getChargeProgress() { + if (chargeDuration <= 0) return 1.0f; + return Math.min(1.0f, (float) chargeTick / chargeDuration); + } + + public float getInterpolatedChargeProgress(float partialTicks) { + if (chargeDuration <= 0) return 1.0f; + float prevProgress = Math.max(0, (float) (chargeTick - 1) / chargeDuration); + float currProgress = Math.min(1.0f, (float) chargeTick / chargeDuration); + return prevProgress + (currProgress - prevProgress) * partialTicks; + } + + private void checkEntityCollisionAlongLine() { + // Check collision along the entire laser line + // Use a slightly expanded AABB for the full line + double minX = Math.min(startX, endX) - laserWidth; + double minY = Math.min(startY, endY) - laserWidth; + double minZ = Math.min(startZ, endZ) - laserWidth; + double maxX = Math.max(startX, endX) + laserWidth; + double maxY = Math.max(startY, endY) + laserWidth; + double maxZ = Math.max(startZ, endZ) + laserWidth; + + AxisAlignedBB searchBox = AxisAlignedBB.getBoundingBox(minX, minY, minZ, maxX, maxY, maxZ); + + @SuppressWarnings("unchecked") + List entities = worldObj.getEntitiesWithinAABB(EntityLivingBase.class, searchBox); + + for (EntityLivingBase entity : entities) { + if (shouldIgnoreEntity(entity)) continue; + if (hitEntities.contains(entity.getEntityId())) continue; + + // Check if entity is close to the laser line + if (isEntityOnLine(entity)) { + hitEntities.add(entity.getEntityId()); + applyDamage(entity); + + // Piercing - don't stop, continue to next entity + } + } + } + + /** + * Check if an entity intersects with the laser line. + */ + private boolean isEntityOnLine(EntityLivingBase entity) { + // Get entity center + double ex = entity.posX; + double ey = entity.posY + entity.height * 0.5; + double ez = entity.posZ; + + // Vector from start to entity + double vx = ex - startX; + double vy = ey - startY; + double vz = ez - startZ; + + // Project onto line direction + double dot = vx * dirX + vy * dirY + vz * dirZ; + + // Clamp to line segment + dot = Math.max(0, Math.min(dot, currentLength)); + + // Closest point on line + double closestX = startX + dirX * dot; + double closestY = startY + dirY * dot; + double closestZ = startZ + dirZ * dot; + + // Distance from entity to closest point + double distSq = (ex - closestX) * (ex - closestX) + + (ey - closestY) * (ey - closestY) + + (ez - closestZ) * (ez - closestZ); + + // Hit if within laser width + entity radius + double hitRadius = laserWidth * 0.5 + entity.width * 0.5; + return distSq <= hitRadius * hitRadius; + } + + // ==================== GETTERS FOR RENDERER ==================== + + public float getLaserWidth() { + return laserWidth; + } + + public float getCurrentLength() { + return currentLength; + } + + public boolean isFullyExtended() { + return fullyExtended; + } + + public double getStartX() { + return startX; + } + + public double getStartY() { + return startY; + } + + public double getStartZ() { + return startZ; + } + + public double getEndX() { + return endX; + } + + public double getEndY() { + return endY; + } + + public double getEndZ() { + return endZ; + } + + public double getDirX() { + return dirX; + } + + public double getDirY() { + return dirY; + } + + public double getDirZ() { + return dirZ; + } + + /** + * Get the alpha for fade-out effect during linger. + */ + public float getLingerAlpha() { + if (!fullyExtended) return 1.0f; + return 1.0f - ((float) ticksSinceFullExtension / lingerTicks); + } + + // ==================== NBT ==================== + + @Override + protected void readProjectileNBT(NBTTagCompound nbt) { + this.laserWidth = nbt.hasKey("LaserWidth") ? nbt.getFloat("LaserWidth") : 0.2f; + this.expansionSpeed = nbt.hasKey("ExpansionSpeed") ? nbt.getFloat("ExpansionSpeed") : 2.0f; + this.lingerTicks = nbt.hasKey("LingerTicks") ? nbt.getInteger("LingerTicks") : 10; + this.dirX = nbt.getDouble("DirX"); + this.dirY = nbt.getDouble("DirY"); + this.dirZ = nbt.getDouble("DirZ"); + this.currentLength = nbt.getFloat("CurrentLength"); + this.fullyExtended = nbt.getBoolean("FullyExtended"); + this.endX = nbt.getDouble("EndX"); + this.endY = nbt.getDouble("EndY"); + this.endZ = nbt.getDouble("EndZ"); + + // Charging state + boolean isCharging = nbt.hasKey("Charging") && nbt.getBoolean("Charging"); + this.charging = isCharging; + this.dataWatcher.updateObject(DW_CHARGING, (byte) (isCharging ? 1 : 0)); + this.chargeDuration = nbt.hasKey("ChargeDuration") ? nbt.getInteger("ChargeDuration") : 40; + this.chargeTick = nbt.hasKey("ChargeTick") ? nbt.getInteger("ChargeTick") : 0; + this.targetSize = nbt.hasKey("TargetSize") ? nbt.getFloat("TargetSize") : this.size; + anchorData.readNBT(nbt); + } + + @Override + protected void writeProjectileNBT(NBTTagCompound nbt) { + nbt.setFloat("LaserWidth", laserWidth); + nbt.setFloat("ExpansionSpeed", expansionSpeed); + nbt.setInteger("LingerTicks", lingerTicks); + nbt.setDouble("DirX", dirX); + nbt.setDouble("DirY", dirY); + nbt.setDouble("DirZ", dirZ); + nbt.setFloat("CurrentLength", currentLength); + nbt.setBoolean("FullyExtended", fullyExtended); + nbt.setDouble("EndX", endX); + nbt.setDouble("EndY", endY); + nbt.setDouble("EndZ", endZ); + + // Charging state + nbt.setBoolean("Charging", isCharging()); + nbt.setInteger("ChargeDuration", chargeDuration); + nbt.setInteger("ChargeTick", chargeTick); + nbt.setFloat("TargetSize", targetSize); + anchorData.writeNBT(nbt); + } + + /** + * Setup this laser in preview mode for GUI display. + * Laser doesn't have charging state - spawns at active phase and fires immediately. + */ + public void setupPreview(EntityLivingBase owner, float laserWidth, EnergyColorData color, + EnergyLightningData lightning, float expansionSpeed, float maxDistance) { + this.setPreviewMode(true); + this.setPreviewOwner(owner); + + // Set visual properties + this.laserWidth = laserWidth; + this.size = laserWidth; + this.innerColor = color.innerColor; + this.outerColor = color.outerColor; + this.outerColorEnabled = color.outerColorEnabled; + this.outerColorWidth = color.outerColorWidth; + this.expansionSpeed = expansionSpeed; + this.maxDistance = Math.min(maxDistance, 5.0f); // Limit for GUI preview + this.lightningEffect = lightning.lightningEffect; + this.lightningDensity = lightning.lightningDensity; + this.lightningRadius = lightning.lightningRadius; + + // Position at chest height + double x = owner.posX; + double y = owner.posY + owner.height * 0.7; + double z = owner.posZ; + this.setPosition(x, y, z); + this.prevPosX = x; + this.prevPosY = y; + this.prevPosZ = z; + this.startX = x; + this.startY = y; + this.startZ = z; + + // Fire in owner's facing direction + float yaw = (float) Math.toRadians(owner.rotationYaw); + this.dirX = -Math.sin(yaw); + this.dirY = 0; + this.dirZ = Math.cos(yaw); + + // Initialize end point (same as start, will expand) + this.endX = x; + this.endY = y; + this.endZ = z; + } +} diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java new file mode 100644 index 000000000..3c5895206 --- /dev/null +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java @@ -0,0 +1,497 @@ +package kamkeel.npcs.entity; + +import kamkeel.npcs.controllers.data.ability.AnchorPoint; +import kamkeel.npcs.controllers.data.ability.data.*; +import kamkeel.npcs.util.AnchorPointHelper; +import net.minecraft.entity.Entity; +import net.minecraft.entity.EntityLivingBase; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.util.AxisAlignedBB; +import net.minecraft.util.MovingObjectPosition; +import net.minecraft.util.Vec3; +import net.minecraft.world.World; + +import java.util.List; +import java.util.UUID; + +/** + * Orb projectile - spherical homing energy ball. + * Extends EntityAbilityProjectile for shared functionality. + * + * Design inspired by LouisXIV's energy attack system. + */ +public class EntityAbilityOrb extends EntityAbilityProjectile { + + // Orb-specific movement properties + private float speed = 0.5f; + private boolean homing = true; + private float homingStrength = 0.35f; // Increased from 0.15 for better tracking + private float homingRange = 20.0f; + + // Charging state (during windup) + private boolean charging = false; + private int chargeDuration = 40; + private int chargeTick = 0; + private EnergyAnchorData anchorData = new EnergyAnchorData(AnchorPoint.FRONT); + private float targetSize = 1.0f; // Full size to grow to during charging + + // Data watcher index for charging state (synced to clients) + private static final int DW_CHARGING = 20; + + private UUID siblingUUID = null; + + public EntityAbilityOrb(World world) { + super(world); + } + + @Override + protected void entityInit() { + super.entityInit(); + // Register data watcher for charging state + this.dataWatcher.addObject(DW_CHARGING, (byte) 0); + } + + /** + * 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; + } + + /** + * Set charging state (server only, synced to clients via data watcher). + */ + private void setCharging(boolean value) { + this.charging = value; + if (!worldObj.isRemote) { + this.dataWatcher.updateObject(DW_CHARGING, (byte) (value ? 1 : 0)); + } + } + + /** + * Full constructor using data classes for grouped parameters. + */ + public EntityAbilityOrb(World world, EntityLivingBase owner, EntityLivingBase target, + double x, double y, double z, float orbSize, + EnergyColorData color, EnergyCombatData combat, + EnergyHomingData homing, EnergyLightningData lightning, + EnergyLifespanData lifespan) { + super(world); + + // Initialize base properties + initProjectile(owner, target, x, y, z, orbSize, color, combat, lightning, lifespan); + + // Orb-specific properties + this.speed = homing.speed; + this.homing = homing.homing; + this.homingStrength = homing.homingStrength; + this.homingRange = homing.homingRange; + + // Calculate initial velocity toward target + if (target != null) { + double dx = target.posX - x; + double dy = (target.posY + target.getEyeHeight()) - y; + double dz = target.posZ - z; + double len = Math.sqrt(dx * dx + dy * dy + dz * dz); + if (len > 0) { + this.motionX = (dx / len) * homing.speed; + this.motionY = (dy / len) * homing.speed; + this.motionZ = (dz / len) * homing.speed; + } + } else { + // Fire in NPC's facing direction + float yaw = (float) Math.toRadians(owner.rotationYaw); + float pitch = (float) Math.toRadians(owner.rotationPitch); + this.motionX = -Math.sin(yaw) * Math.cos(pitch) * homing.speed; + this.motionY = -Math.sin(pitch) * homing.speed; + this.motionZ = Math.cos(yaw) * Math.cos(pitch) * homing.speed; + } + } + + /** + * Setup this orb in charging mode (for windup phase). + * The orb will grow from 0 to orbSize over chargeDuration ticks. + * Position follows the owner based on anchor point. + */ + public void setupCharging(EnergyAnchorData anchor, int chargeDuration) { + setCharging(true); + this.chargeDuration = chargeDuration; + this.chargeTick = 0; + this.anchorData = anchor; + this.targetSize = this.size; + this.size = 0.01f; + this.renderCurrentSize = 0.01f; + this.prevRenderSize = 0.01f; + this.motionX = 0; + this.motionY = 0; + this.motionZ = 0; + } + + /** + * Setup this 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 void setupPreview(EntityLivingBase owner, float orbSize, EnergyColorData color, EnergyLightningData lightning, EnergyAnchorData anchor, int chargeDuration) { + this.setPreviewMode(true); + this.setPreviewOwner(owner); + + // Set visual properties + this.innerColor = color.innerColor; + this.outerColor = color.outerColor; + this.outerColorEnabled = color.outerColorEnabled; + this.outerColorWidth = color.outerColorWidth; + this.rotationSpeed = color.rotationSpeed; + this.lightningEffect = lightning.lightningEffect; + this.lightningDensity = lightning.lightningDensity; + this.lightningRadius = lightning.lightningRadius; + this.lightningFadeTime = lightning.lightningFadeTime; + + // Set charging state + this.setCharging(true); + this.chargeDuration = chargeDuration; + this.chargeTick = 0; + this.anchorData = anchor; + + // Store target size and start at 0 for grow effect + this.targetSize = orbSize; + this.size = 0.01f; + this.renderCurrentSize = 0.01f; + this.prevRenderSize = 0.01f; + + // Initial position at anchor point + Vec3 pos = AnchorPointHelper.calculateAnchorPosition(owner, anchorData); + this.setPosition(pos.xCoord, pos.yCoord, pos.zCoord); + this.prevPosX = pos.xCoord; + this.prevPosY = pos.yCoord; + this.prevPosZ = pos.zCoord; + this.startX = pos.xCoord; + this.startY = pos.yCoord; + this.startZ = pos.zCoord; + + // Clear motion + this.motionX = 0; + this.motionY = 0; + this.motionZ = 0; + } + + /** + * 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. + */ + public void startMoving(EntityLivingBase target) { + if (!isCharging()) return; + + setCharging(false); + + // Update start position to current position + startX = posX; + startY = posY; + startZ = posZ; + + // Calculate velocity toward target + Entity owner = getOwner(); + + if (target != null) { + double dx = target.posX - posX; + double dy = (target.posY + target.getEyeHeight()) - posY; + double dz = target.posZ - posZ; + double len = Math.sqrt(dx * dx + dy * dy + dz * dz); + if (len > 0) { + motionX = (dx / len) * speed; + motionY = (dy / len) * speed; + motionZ = (dz / len) * speed; + } + } else if (owner != null) { + float yaw = (float) Math.toRadians(owner.rotationYaw); + float pitch = (float) Math.toRadians(owner.rotationPitch); + motionX = -Math.sin(yaw) * Math.cos(pitch) * speed; + motionY = -Math.sin(pitch) * speed; + motionZ = Math.cos(yaw) * Math.cos(pitch) * speed; + } + } + + @Override + protected void updateProjectile() { + // Handle charging state (windup phase) + if (isCharging()) { + updateCharging(); + return; + } + + // In preview mode, run movement on client (no server) + if (previewMode) { + updatePreviewMovement(); + return; + } + + if (worldObj.isRemote) { + handleClientInterpolation(); + } else { + // Server-side logic + updateHoming(); + checkBlockCollision(); + checkEntityCollision(); + this.moveEntity(motionX, motionY, motionZ); + } + + // Check wall collision + if (this.isCollidedHorizontally || this.isCollidedVertically) { + if (!worldObj.isRemote) { + hasHit = true; + if (explosive) { + doExplosion(); + } + } + this.setDead(); + } + } + + /** + * 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; + + Entity target = getTarget(); + if (target == null || !target.isEntityAlive()) return; + + double dx = target.posX - posX; + double dy = (target.posY + target.getEyeHeight()) - posY; + double dz = target.posZ - posZ; + double dist = Math.sqrt(dx * dx + dy * dy + dz * dz); + + if (dist <= homingRange && dist > 0) { + // Calculate effective homing strength - increases when closer to commit to target + // This prevents orbiting behavior at close range + float effectiveStrength = homingStrength; + if (dist < homingRange * 0.3) { + // Within 30% of homing range, dramatically increase strength to commit + effectiveStrength = Math.min(1.0f, homingStrength * 2.5f); + } else if (dist < homingRange * 0.6) { + // Within 60% of homing range, moderately increase strength + effectiveStrength = Math.min(0.8f, homingStrength * 1.5f); + } + + double desiredVX = (dx / dist) * speed; + double desiredVY = (dy / dist) * speed; + double desiredVZ = (dz / dist) * speed; + + motionX += (desiredVX - motionX) * effectiveStrength; + motionY += (desiredVY - motionY) * effectiveStrength; + motionZ += (desiredVZ - motionZ) * effectiveStrength; + + // Normalize to maintain speed + double vLen = Math.sqrt(motionX * motionX + motionY * motionY + motionZ * motionZ); + if (vLen > 0) { + motionX = (motionX / vLen) * speed; + motionY = (motionY / vLen) * speed; + motionZ = (motionZ / vLen) * speed; + } + } + } + + private void checkBlockCollision() { + Vec3 currentPos = Vec3.createVectorHelper(posX, posY, posZ); + Vec3 nextPos = Vec3.createVectorHelper(posX + motionX, posY + motionY, posZ + motionZ); + // Use full raytrace that doesn't stop at liquids and checks all blocks + MovingObjectPosition blockHit = worldObj.func_147447_a(currentPos, nextPos, false, true, false); + + if (blockHit != null && blockHit.typeOfHit == MovingObjectPosition.MovingObjectType.BLOCK) { + hasHit = true; + if (explosive) { + posX = blockHit.hitVec.xCoord; + posY = blockHit.hitVec.yCoord; + posZ = blockHit.hitVec.zCoord; + doExplosion(); + } + this.setDead(); + } + } + + private void checkEntityCollision() { + double hitSize = size * 0.5; + AxisAlignedBB hitBox = AxisAlignedBB.getBoundingBox( + posX - hitSize, posY - hitSize, posZ - hitSize, + posX + hitSize, posY + hitSize, posZ + hitSize + ); + + @SuppressWarnings("unchecked") + List entities = worldObj.getEntitiesWithinAABB(EntityLivingBase.class, hitBox); + + for (EntityLivingBase entity : entities) { + if (shouldIgnoreEntity(entity)) continue; + + hasHit = true; + + if (explosive) { + doExplosion(); + } else { + applyDamage(entity); + } + + this.setDead(); + return; + } + } + + /** + * Update during charging state - follow owner based on anchor point. + */ + private void updateCharging() { + chargeTick++; + + Entity owner = getOwner(); + if (owner == null || owner.isDead) { + setDead(); + return; + } + + // Grow size based on charge progress + float progress = getChargeProgress(); + this.size = targetSize * progress; + + // Calculate position based on anchor point, offset downward by half size to center + if (owner instanceof EntityLivingBase) { + Vec3 pos = AnchorPointHelper.calculateAnchorPosition((EntityLivingBase) owner, anchorData); + setPosition(pos.xCoord, pos.yCoord, pos.zCoord); + } + } + + /** + * Get the charge progress (0-1). + */ + public float getChargeProgress() { + if (chargeDuration <= 0) return 1.0f; + return Math.min(1.0f, (float) chargeTick / chargeDuration); + } + + /** + * Get interpolated charge progress for smooth rendering. + */ + public float getInterpolatedChargeProgress(float partialTicks) { + if (chargeDuration <= 0) return 1.0f; + float prevProgress = Math.max(0, (float) (chargeTick - 1) / chargeDuration); + float currProgress = Math.min(1.0f, (float) chargeTick / chargeDuration); + return prevProgress + (currProgress - prevProgress) * partialTicks; + } + + // ==================== GETTERS ==================== + + public float getSpeed() { + return speed; + } + + public boolean isHoming() { + return homing; + } + + public float getHomingStrength() { + return homingStrength; + } + + public float getHomingRange() { + return homingRange; + } + + // Legacy getter for renderer compatibility + public float getOrbSize() { + return size; + } + + public UUID getSiblingUUID() { + return siblingUUID; + } + + public void setSiblingUUID(UUID siblingUUID) { + if (siblingUUID == null) + return; + + if (this.siblingUUID == null) + this.siblingUUID = siblingUUID; + } + + // ==================== NBT ==================== + + @Override + protected void readProjectileNBT(NBTTagCompound nbt) { + this.speed = nbt.hasKey("OrbSpeed") ? nbt.getFloat("OrbSpeed") : 0.5f; + this.homing = !nbt.hasKey("Homing") || nbt.getBoolean("Homing"); + this.homingStrength = nbt.hasKey("HomingStrength") ? nbt.getFloat("HomingStrength") : 0.15f; + this.homingRange = nbt.hasKey("HomingRange") ? nbt.getFloat("HomingRange") : 20.0f; + // Charging state + boolean isCharging = nbt.hasKey("Charging") && nbt.getBoolean("Charging"); + this.charging = isCharging; + this.dataWatcher.updateObject(DW_CHARGING, (byte) (isCharging ? 1 : 0)); + this.chargeDuration = nbt.hasKey("ChargeDuration") ? nbt.getInteger("ChargeDuration") : 40; + this.chargeTick = nbt.hasKey("ChargeTick") ? nbt.getInteger("ChargeTick") : 0; + this.targetSize = nbt.hasKey("TargetSize") ? nbt.getFloat("TargetSize") : this.size; + // Anchor data + this.anchorData.readNBT(nbt); + + this.siblingUUID = nbt.hasKey("SiblingUUID") ? UUID.fromString(nbt.getString("SiblingUUID")) : null; + } + + @Override + protected void writeProjectileNBT(NBTTagCompound nbt) { + nbt.setFloat("OrbSpeed", speed); + nbt.setBoolean("Homing", homing); + nbt.setFloat("HomingStrength", homingStrength); + nbt.setFloat("HomingRange", homingRange); + // Charging state + nbt.setBoolean("Charging", isCharging()); + nbt.setInteger("ChargeDuration", chargeDuration); + nbt.setInteger("ChargeTick", chargeTick); + anchorData.writeNBT(nbt); + nbt.setFloat("TargetSize", targetSize); + + if (siblingUUID instanceof UUID) { + nbt.setString("SiblingUUID", siblingUUID.toString()); + } + } +} diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityProjectile.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityProjectile.java new file mode 100644 index 000000000..57f99e1c8 --- /dev/null +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityProjectile.java @@ -0,0 +1,687 @@ +package kamkeel.npcs.entity; + +import cpw.mods.fml.common.registry.IEntityAdditionalSpawnData; +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import io.netty.buffer.ByteBuf; +import kamkeel.npcs.controllers.data.ability.AbilityEffect; +import kamkeel.npcs.controllers.data.ability.data.EnergyColorData; +import kamkeel.npcs.controllers.data.ability.data.EnergyCombatData; +import kamkeel.npcs.controllers.data.ability.data.EnergyLifespanData; +import kamkeel.npcs.controllers.data.ability.data.EnergyLightningData; +import net.minecraft.entity.Entity; +import net.minecraft.entity.EntityLivingBase; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.nbt.NBTTagList; +import net.minecraft.util.AxisAlignedBB; +import net.minecraft.util.DamageSource; +import net.minecraft.util.Vec3; +import net.minecraft.world.World; +import noppes.npcs.NpcDamageSource; +import noppes.npcs.entity.EntityNPCInterface; + +import java.util.ArrayList; +import java.util.List; + +/** + * Base class for all ability projectiles (Orb, Disc, Beam, Laser). + * Provides common functionality for visuals, combat, effects, and interpolation. + * + * Design inspired by LouisXIV's energy attack system. + */ +public abstract class EntityAbilityProjectile extends Entity implements IEntityAdditionalSpawnData { + + // ==================== VISUAL PROPERTIES ==================== + protected float size = 1.0f; + protected int innerColor = 0xFFFFFF; + protected int outerColor = 0x8888FF; + protected boolean outerColorEnabled = true; + protected float outerColorWidth = 0.4f; // Additive offset from inner size + protected float outerColorAlpha = 0.5f; + protected float rotationSpeed = 4.0f; + + // ==================== COMBAT PROPERTIES ==================== + protected float damage = 7.0f; + protected float knockback = 1.0f; + protected float knockbackUp = 0.1f; + + // ==================== EFFECT PROPERTIES ==================== + protected List effects = new ArrayList<>(); + + // ==================== EXPLOSION PROPERTIES ==================== + protected boolean explosive = false; + protected float explosionRadius = 3.0f; + protected float explosionDamageFalloff = 0.5f; + + // ==================== LIGHTNING EFFECT PROPERTIES ==================== + protected boolean lightningEffect = false; + protected float lightningDensity = 0.15f; // Bolts spawned per tick (0.15 = ~15% chance per frame) + protected float lightningRadius = 0.5f; // Max distance lightning can arc from center + protected int lightningFadeTime = 6; // Ticks before lightning fades out + + // Client-side lightning state (not saved to NBT) + @cpw.mods.fml.relauncher.SideOnly(cpw.mods.fml.relauncher.Side.CLIENT) + public transient Object lightningState; // Actually AttachedLightningRenderer.LightningState + + // ==================== LIFESPAN ==================== + protected float maxDistance = 30.0f; + protected int maxLifetime = 200; + protected long deathWorldTime = -1; // World time when entity should die (-1 = not set) + + // ==================== TRACKING ==================== + protected double startX, startY, startZ; + protected int ownerEntityId = -1; + protected int targetEntityId = -1; + + // ==================== STATE ==================== + protected boolean hasHit = false; + protected boolean previewMode = false; // Client-side preview mode (no damage/effects) + protected EntityLivingBase previewOwner = null; // Direct reference for GUI preview (no world lookup) + + // ==================== ROTATION INTERPOLATION ==================== + public float prevRotationValX, prevRotationValY, prevRotationValZ; + public float rotationValX, rotationValY, rotationValZ; + + // ==================== SIZE INTERPOLATION ==================== + protected float renderCurrentSize; + protected float prevRenderSize; + + // ==================== POSITION INTERPOLATION ==================== + protected double interpTargetX, interpTargetY, interpTargetZ; + protected double interpTargetMotionX, interpTargetMotionY, interpTargetMotionZ; + protected int interpSteps; + + public EntityAbilityProjectile(World world) { + super(world); + this.setSize(0.5f, 0.5f); + this.noClip = false; + this.isImmuneToFire = true; + this.ignoreFrustumCheck = true; + } + + /** + * Initialize common properties using data classes. Call from subclass constructors. + */ + protected void initProjectile(EntityLivingBase owner, EntityLivingBase target, + double x, double y, double z, float size, + EnergyColorData color, EnergyCombatData combat, + EnergyLightningData lightning, EnergyLifespanData lifespan) { + this.setPosition(x, y, z); + this.startX = x; + this.startY = y; + this.startZ = z; + + this.ownerEntityId = owner.getEntityId(); + this.targetEntityId = target != null ? target.getEntityId() : -1; + + // Visual + this.size = size; + this.innerColor = color.innerColor; + this.outerColor = color.outerColor; + this.outerColorEnabled = color.outerColorEnabled; + this.outerColorWidth = color.outerColorWidth; + this.outerColorAlpha = color.outerColorAlpha; + this.rotationSpeed = color.rotationSpeed; + + // Combat + this.damage = combat.damage; + this.knockback = combat.knockback; + this.knockbackUp = combat.knockbackUp; + + // Explosion + this.explosive = combat.explosive; + this.explosionRadius = combat.explosionRadius; + this.explosionDamageFalloff = combat.explosionDamageFalloff; + + // Lifespan + this.maxDistance = lifespan.maxDistance; + this.maxLifetime = lifespan.maxLifetime; + // deathWorldTime will be set on first tick when world is available + + // Lightning + this.lightningEffect = lightning.lightningEffect; + this.lightningDensity = lightning.lightningDensity; + this.lightningRadius = lightning.lightningRadius; + this.lightningFadeTime = lightning.lightningFadeTime; + + // Initialize render size + this.renderCurrentSize = size; + this.prevRenderSize = size; + } + + @Override + protected void entityInit() { + // No DataWatcher needed - using IEntityAdditionalSpawnData with NBT + } + + @Override + @SideOnly(Side.CLIENT) + public boolean isInRangeToRenderDist(double distance) { + double d1 = this.boundingBox.getAverageEdgeLength() * 4.0D; + d1 *= 128.0D; + return distance < d1 * d1; + } + + @Override + public boolean shouldRenderInPass(int pass) { + return pass == 1; // Translucent pass + } + + @Override + public void onUpdate() { + // Store previous values for interpolation + this.prevPosX = this.posX; + this.prevPosY = this.posY; + this.prevPosZ = this.posZ; + this.prevRotationValX = this.rotationValX; + this.prevRotationValY = this.rotationValY; + this.prevRotationValZ = this.rotationValZ; + this.prevRenderSize = this.renderCurrentSize; + + // Skip super.onUpdate() in preview mode to avoid world checks + if (!previewMode) { + super.onUpdate(); + } + + // Update rotation + updateRotation(); + + // Lerp render size toward actual size + this.renderCurrentSize = this.renderCurrentSize + (this.size - this.renderCurrentSize) * 0.15f; + + // 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 max distance (subclass can override if needed) + if (checkMaxDistance()) { + this.setDead(); + return; + } + + if (hasHit) { + this.setDead(); + return; + } + } + + // Subclass-specific update + updateProjectile(); + } + + /** + * Update rotation values. Override for custom rotation behavior. + */ + protected void updateRotation() { + this.rotationValX += rotationSpeed * 0.7f; + this.rotationValY += rotationSpeed; + this.rotationValZ += rotationSpeed * 0.5f; + + if (this.rotationValX > 360.0f) this.rotationValX -= 360.0f; + if (this.rotationValY > 360.0f) this.rotationValY -= 360.0f; + if (this.rotationValZ > 360.0f) this.rotationValZ -= 360.0f; + } + + /** + * Check if projectile has exceeded max distance. + * @return true if should die + */ + protected boolean checkMaxDistance() { + double distTraveled = Math.sqrt( + (posX - startX) * (posX - startX) + + (posY - startY) * (posY - startY) + + (posZ - startZ) * (posZ - startZ) + ); + return distTraveled >= maxDistance; + } + + /** + * Subclass-specific update logic. Called every tick. + */ + protected abstract void updateProjectile(); + + // ==================== POSITION INTERPOLATION ==================== + + @Override + public void setPositionAndRotation2(double x, double y, double z, float yaw, float pitch, int posRotationIncrements) { + this.interpTargetX = x; + this.interpTargetY = y; + this.interpTargetZ = z; + this.interpSteps = posRotationIncrements; + } + + @Override + public void setVelocity(double motionX, double motionY, double motionZ) { + this.interpTargetMotionX = motionX; + this.interpTargetMotionY = motionY; + this.interpTargetMotionZ = motionZ; + this.motionX = motionX; + this.motionY = motionY; + this.motionZ = motionZ; + } + + /** + * Handle client-side position interpolation. + */ + protected void handleClientInterpolation() { + if (this.interpSteps > 0) { + double newX = this.posX + (this.interpTargetX - this.posX) / this.interpSteps; + double newY = this.posY + (this.interpTargetY - this.posY) / this.interpSteps; + double newZ = this.posZ + (this.interpTargetZ - this.posZ) / this.interpSteps; + + this.motionX = this.motionX + (this.interpTargetMotionX - this.motionX) / this.interpSteps; + this.motionY = this.motionY + (this.interpTargetMotionY - this.motionY) / this.interpSteps; + this.motionZ = this.motionZ + (this.interpTargetMotionZ - this.motionZ) / this.interpSteps; + + this.setPosition(newX, newY, newZ); + this.interpSteps--; + } else { + this.posX += this.motionX; + this.posY += this.motionY; + this.posZ += this.motionZ; + this.setPosition(this.posX, this.posY, this.posZ); + } + } + + // ==================== 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(); + + if (owner instanceof EntityNPCInterface) { + target.attackEntityFrom(new NpcDamageSource("npc_ability", (EntityNPCInterface) owner), dmg); + } else if (owner instanceof EntityPlayer) { + target.attackEntityFrom(DamageSource.causePlayerDamage((EntityPlayer) owner), dmg); + } else if (owner instanceof EntityLivingBase) { + target.attackEntityFrom(DamageSource.causeMobDamage((EntityLivingBase) owner), dmg); + } else { + target.attackEntityFrom(new NpcDamageSource("npc_ability", null), dmg); + } + + if (kb > 0) { + double dx = target.posX - posX; + double dz = target.posZ - posZ; + double len = Math.sqrt(dx * dx + dz * dz); + if (len > 0) { + double kbX = (dx / len) * kb * 0.5; + double kbZ = (dz / len) * kb * 0.5; + double kbY = knockbackUp > 0 ? knockbackUp : 0.1; + target.addVelocity(kbX, kbY, kbZ); + target.velocityChanged = true; + } + } + + applyEffects(target); + } + + protected void applyEffects(EntityLivingBase target) { + for (AbilityEffect effect : effects) { + effect.apply(target); + } + } + + /** + * Set the effects list from the ability's configured effects. + */ + public void setEffects(List effects) { + this.effects = effects != null ? effects : new ArrayList<>(); + } + + 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(); + + AxisAlignedBB explosionBox = AxisAlignedBB.getBoundingBox( + posX - explosionRadius, posY - explosionRadius, posZ - explosionRadius, + posX + explosionRadius, posY + explosionRadius, posZ + explosionRadius + ); + + @SuppressWarnings("unchecked") + List targets = worldObj.getEntitiesWithinAABB(EntityLivingBase.class, explosionBox); + + for (EntityLivingBase target : targets) { + if (target == owner) continue; + + double dist = Math.sqrt( + Math.pow(target.posX - posX, 2) + + Math.pow(target.posY - posY, 2) + + Math.pow(target.posZ - posZ, 2) + ); + + if (dist <= explosionRadius) { + float falloff = 1.0f - (float) (dist / explosionRadius) * explosionDamageFalloff; + applyDamage(target, damage * falloff, knockback * falloff); + } + } + } + + // ==================== 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(EntityLivingBase owner) { + this.previewOwner = owner; + } + + protected Entity getTarget() { + if (targetEntityId == -1) return null; + return worldObj.getEntityByID(targetEntityId); + } + + /** + * Check if an entity should be ignored for collision. + */ + protected boolean shouldIgnoreEntity(Entity entity) { + Entity owner = getOwner(); + if (entity == owner) return true; + + if (owner instanceof EntityNPCInterface && entity instanceof EntityNPCInterface) { + EntityNPCInterface ownerNpc = (EntityNPCInterface) owner; + EntityNPCInterface targetNpc = (EntityNPCInterface) entity; + if (ownerNpc.faction.id == targetNpc.faction.id) return true; + } + return false; + } + + // ==================== VISUAL GETTERS ==================== + + public float getSize() { + return size; + } + + public int getInnerColor() { + return innerColor; + } + + public int getOuterColor() { + return outerColor; + } + + public boolean isOuterColorEnabled() { + return outerColorEnabled; + } + + public float getOuterColorWidth() { + return outerColorWidth; + } + + public float getOuterColorAlpha() { + return outerColorAlpha; + } + + public float getRotationSpeed() { + return rotationSpeed; + } + + public float getInterpolatedRotationX(float partialTicks) { + return this.prevRotationValX + (this.rotationValX - this.prevRotationValX) * partialTicks; + } + + public float getInterpolatedRotationY(float partialTicks) { + return this.prevRotationValY + (this.rotationValY - this.prevRotationValY) * partialTicks; + } + + public float getInterpolatedRotationZ(float partialTicks) { + return this.prevRotationValZ + (this.rotationValZ - this.prevRotationValZ) * partialTicks; + } + + public float getInterpolatedSize(float partialTicks) { + return this.prevRenderSize + (this.renderCurrentSize - this.prevRenderSize) * partialTicks; + } + + // ==================== LIGHTNING GETTERS ==================== + + public boolean hasLightningEffect() { + return lightningEffect; + } + + public float getLightningDensity() { + return lightningDensity; + } + + public float getLightningRadius() { + return lightningRadius; + } + + public int getLightningFadeTime() { + return lightningFadeTime; + } + + // ==================== BRIGHTNESS ==================== + + @Override + public float getBrightness(float partialTicks) { + return 1.0f; + } + + @Override + @SideOnly(Side.CLIENT) + public int getBrightnessForRender(float partialTicks) { + return 0xF000F0; + } + + // ==================== NBT ==================== + + @Override + protected void readEntityFromNBT(NBTTagCompound nbt) { + readBaseNBT(nbt); + readProjectileNBT(nbt); + } + + @Override + protected void writeEntityToNBT(NBTTagCompound nbt) { + writeBaseNBT(nbt); + writeProjectileNBT(nbt); + } + + /** + * Read base projectile properties from NBT. + */ + protected void readBaseNBT(NBTTagCompound nbt) { + this.size = nbt.getFloat("Size"); + this.innerColor = nbt.getInteger("InnerColor"); + this.outerColor = nbt.getInteger("OuterColor"); + this.outerColorEnabled = !nbt.hasKey("OuterColorEnabled") || nbt.getBoolean("OuterColorEnabled"); + this.outerColorAlpha = nbt.hasKey("OuterColorAlpha") ? nbt.getFloat("OuterColorAlpha") : 0.5f; + this.outerColorWidth = nbt.hasKey("OuterColorWidth") ? nbt.getFloat("OuterColorWidth") : 0.4f; + this.rotationSpeed = nbt.hasKey("RotationSpeed") ? nbt.getFloat("RotationSpeed") : 4.0f; + + this.damage = nbt.getFloat("Damage"); + this.knockback = nbt.getFloat("Knockback"); + this.knockbackUp = nbt.getFloat("KnockbackUp"); + + this.explosive = nbt.getBoolean("Explosive"); + this.explosionRadius = nbt.getFloat("ExplosionRadius"); + this.explosionDamageFalloff = nbt.getFloat("ExplosionDamageFalloff"); + + // Read effects list + this.effects.clear(); + if (nbt.hasKey("Effects")) { + NBTTagList effectsList = nbt.getTagList("Effects", 10); // 10 = compound tag + for (int i = 0; i < effectsList.tagCount(); i++) { + effects.add(AbilityEffect.fromNBT(effectsList.getCompoundTagAt(i))); + } + } + + this.maxDistance = nbt.getFloat("MaxDistance"); + this.maxLifetime = nbt.getInteger("MaxLifetime"); + this.deathWorldTime = nbt.hasKey("DeathWorldTime") ? nbt.getLong("DeathWorldTime") : -1; + + this.startX = nbt.getDouble("StartX"); + this.startY = nbt.getDouble("StartY"); + this.startZ = nbt.getDouble("StartZ"); + + this.ownerEntityId = nbt.getInteger("OwnerId"); + this.targetEntityId = nbt.getInteger("TargetId"); + + this.motionX = nbt.getDouble("MotionX"); + this.motionY = nbt.getDouble("MotionY"); + this.motionZ = nbt.getDouble("MotionZ"); + + // Lightning + this.lightningEffect = nbt.getBoolean("LightningEffect"); + this.lightningDensity = nbt.hasKey("LightningDensity") ? nbt.getFloat("LightningDensity") : 0.15f; + this.lightningRadius = nbt.hasKey("LightningRadius") ? nbt.getFloat("LightningRadius") : 0.5f; + this.lightningFadeTime = nbt.hasKey("LightningFadeTime") ? nbt.getInteger("LightningFadeTime") : 6; + + if (this.worldObj != null && this.worldObj.isRemote) { + this.renderCurrentSize = this.size; + this.prevRenderSize = this.size; + } + } + + /** + * Write base projectile properties to NBT. + */ + protected void writeBaseNBT(NBTTagCompound nbt) { + nbt.setFloat("Size", size); + nbt.setInteger("InnerColor", innerColor); + nbt.setInteger("OuterColor", outerColor); + nbt.setBoolean("OuterColorEnabled", outerColorEnabled); + nbt.setFloat("OuterColorWidth", outerColorWidth); + nbt.setFloat("OuterColorAlpha", outerColorAlpha); + nbt.setFloat("RotationSpeed", rotationSpeed); + + nbt.setFloat("Damage", damage); + nbt.setFloat("Knockback", knockback); + nbt.setFloat("KnockbackUp", knockbackUp); + + nbt.setBoolean("Explosive", explosive); + nbt.setFloat("ExplosionRadius", explosionRadius); + nbt.setFloat("ExplosionDamageFalloff", explosionDamageFalloff); + + // Write effects list + if (!effects.isEmpty()) { + NBTTagList effectsList = new NBTTagList(); + for (AbilityEffect effect : effects) { + effectsList.appendTag(effect.writeNBT()); + } + nbt.setTag("Effects", effectsList); + } + + nbt.setFloat("MaxDistance", maxDistance); + nbt.setLong("DeathWorldTime", deathWorldTime); + nbt.setInteger("MaxLifetime", maxLifetime); + + nbt.setDouble("StartX", startX); + nbt.setDouble("StartY", startY); + nbt.setDouble("StartZ", startZ); + + nbt.setInteger("OwnerId", ownerEntityId); + nbt.setInteger("TargetId", targetEntityId); + + nbt.setDouble("MotionX", motionX); + nbt.setDouble("MotionY", motionY); + nbt.setDouble("MotionZ", motionZ); + + // Lightning + nbt.setBoolean("LightningEffect", lightningEffect); + nbt.setFloat("LightningDensity", lightningDensity); + nbt.setFloat("LightningRadius", lightningRadius); + nbt.setInteger("LightningFadeTime", lightningFadeTime); + } + + /** + * Subclass-specific NBT reading. + */ + protected abstract void readProjectileNBT(NBTTagCompound nbt); + + /** + * Subclass-specific NBT writing. + */ + protected abstract void writeProjectileNBT(NBTTagCompound nbt); + + // ==================== SPAWN DATA ==================== + + @Override + public void writeSpawnData(ByteBuf buffer) { + try { + NBTTagCompound compound = new NBTTagCompound(); + this.writeEntityToNBT(compound); + cpw.mods.fml.common.network.ByteBufUtils.writeTag(buffer, compound); + } catch (Exception ignored) {} + } + + @Override + public void readSpawnData(ByteBuf buffer) { + try { + NBTTagCompound compound = cpw.mods.fml.common.network.ByteBufUtils.readTag(buffer); + if (compound != null) { + this.readEntityFromNBT(compound); + } + } catch (Exception ignored) {} + } + + // ==================== COLLISION SETTINGS ==================== + + @Override + public boolean canBeCollidedWith() { + return false; + } + + @Override + public boolean canBePushed() { + return false; + } + + @Override + protected boolean canTriggerWalking() { + return false; + } + + @Override + public boolean isBurning() { + return false; + } + + @SideOnly(Side.CLIENT) + public float getShadowSize() { + return 0.0f; + } +} diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilitySweeper.java b/src/main/java/kamkeel/npcs/entity/EntityAbilitySweeper.java new file mode 100644 index 000000000..8540d4df9 --- /dev/null +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilitySweeper.java @@ -0,0 +1,462 @@ +package kamkeel.npcs.entity; + +import cpw.mods.fml.common.registry.IEntityAdditionalSpawnData; +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import io.netty.buffer.ByteBuf; +import kamkeel.npcs.controllers.data.ability.data.EnergyColorData; +import net.minecraft.entity.Entity; +import net.minecraft.entity.EntityLivingBase; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.util.AxisAlignedBB; +import net.minecraft.util.DamageSource; +import net.minecraft.world.World; +import noppes.npcs.LogWriter; +import noppes.npcs.NpcDamageSource; +import noppes.npcs.entity.EntityNPCInterface; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Sweeper ability entity - a rotating beam attached to the NPC. + * Unlike other projectiles, this handles its own damage logic since it's + * attached to and rotates around the caster. + * + * Design: Similar to Beam trail visuals but rotating around origin. + */ +public class EntityAbilitySweeper extends Entity implements IEntityAdditionalSpawnData { + + // Beam properties + private float beamLength = 10.0f; + private float beamWidth = 0.3f; // Thin like beam trail + private float beamHeight = 0.8f; + private int innerColor = 0xFF6600; + private int outerColor = 0xFF0000; + private boolean outerColorEnabled = true; + private float outerColorWidth = 1.8f; + + // Combat properties + private float damage = 5.0f; + private int damageInterval = 5; + private boolean piercing = true; + + // Rotation + private float currentAngle = 0; + private float prevAngle = 0; + private float sweepSpeed = 3.0f; + private int numberOfRotations = 2; + private int completedRotations = 0; + private float baseYaw = 0; // Starting yaw direction + private boolean lockOnTarget = false; + + // Owner tracking + private int ownerEntityId = -1; + private int targetEntityId = -1; + + // Lifetime + private int maxTicks = 400; + private long deathWorldTime = -1; + + // Damage state + private transient int ticksSinceDamage = 0; + private transient Set hitThisTick = new HashSet<>(); + + public EntityAbilitySweeper(World world) { + super(world); + this.setSize(0.1f, 0.1f); + this.noClip = true; + this.isImmuneToFire = true; + this.ignoreFrustumCheck = true; + } + + public EntityAbilitySweeper(World world, EntityLivingBase owner, EntityLivingBase target, + float beamLength, float beamWidth, float beamHeight, + EnergyColorData colorData, + float sweepSpeed, int numberOfRotations, + float damage, int damageInterval, boolean piercing, + boolean lockOnTarget) { + this(world); + + this.ownerEntityId = owner.getEntityId(); + this.targetEntityId = target != null ? target.getEntityId() : -1; + this.beamLength = beamLength; + this.beamWidth = beamWidth; + this.beamHeight = beamHeight; + this.innerColor = colorData.innerColor; + this.outerColor = colorData.outerColor; + this.outerColorEnabled = colorData.outerColorEnabled; + this.outerColorWidth = colorData.outerColorWidth; + this.sweepSpeed = sweepSpeed; + this.numberOfRotations = numberOfRotations; + this.damage = damage; + this.damageInterval = damageInterval; + this.piercing = piercing; + this.lockOnTarget = lockOnTarget; + + // Calculate max ticks based on rotations + this.maxTicks = (int) ((360.0f * numberOfRotations) / sweepSpeed) + 10; + + // Calculate base yaw + if (lockOnTarget && target != null) { + double dx = target.posX - owner.posX; + double dz = target.posZ - owner.posZ; + this.baseYaw = (float) Math.toDegrees(Math.atan2(-dx, dz)); + } else { + this.baseYaw = owner.rotationYaw; + } + + // Position at owner + this.setPosition(owner.posX, owner.posY + beamHeight, owner.posZ); + + // Ready to deal damage immediately + this.ticksSinceDamage = damageInterval; + } + + @Override + protected void entityInit() { + // No DataWatcher needed + } + + @Override + public void onUpdate() { + this.prevPosX = this.posX; + this.prevPosY = this.posY; + this.prevPosZ = this.posZ; + this.prevAngle = this.currentAngle; + + super.onUpdate(); + + // Set death time on first tick + if (deathWorldTime < 0 && worldObj != null) { + deathWorldTime = worldObj.getTotalWorldTime() + maxTicks; + } + + // Check lifetime + if (deathWorldTime > 0 && worldObj.getTotalWorldTime() >= deathWorldTime) { + this.setDead(); + return; + } + + // Follow owner + Entity owner = getOwner(); + if (owner == null) { + this.setDead(); + return; + } + + // Update position to follow owner + this.setPosition(owner.posX, owner.posY + beamHeight, owner.posZ); + + // Check if sweep is done + if (completedRotations >= numberOfRotations) { + this.setDead(); + return; + } + + // Update rotation + currentAngle += sweepSpeed; + if (currentAngle >= 360) { + currentAngle -= 360; + completedRotations++; + if (!worldObj.isRemote) { + LogWriter.info("[Sweeper] Completed rotation " + completedRotations + "/" + numberOfRotations); + } + } + + // Server handles damage + if (!worldObj.isRemote) { + handleDamage(owner); + } + } + + /** + * Handle damage detection and application. + * This runs on server only. + */ + private void handleDamage(Entity owner) { + hitThisTick.clear(); + ticksSinceDamage++; + + if (ticksSinceDamage < damageInterval) { + return; + } + ticksSinceDamage = 0; + + // Calculate beam direction using the ACTUAL render angle + float beamYaw = baseYaw + currentAngle; + float yawRad = (float) Math.toRadians(beamYaw); + + double startX = posX; + double startY = posY; + double startZ = posZ; + + double dirX = -Math.sin(yawRad); + double dirZ = Math.cos(yawRad); + + // Sample points along the beam for collision + int samples = (int) (beamLength / 0.5); + for (int i = 1; i <= samples; i++) { + double progress = (double) i / samples; + double checkX = startX + dirX * beamLength * progress; + double checkY = startY; + double checkZ = startZ + dirZ * beamLength * progress; + + // Narrow hitbox for thin beam + AxisAlignedBB checkBox = AxisAlignedBB.getBoundingBox( + checkX - beamWidth * 2, checkY - 0.5, checkZ - beamWidth * 2, + checkX + beamWidth * 2, checkY + 1.0, checkZ + beamWidth * 2 + ); + + @SuppressWarnings("unchecked") + List entities = worldObj.getEntitiesWithinAABB(EntityLivingBase.class, checkBox); + + for (Entity entity : entities) { + if (!(entity instanceof EntityLivingBase)) continue; + if (entity == owner) continue; + if (hitThisTick.contains(entity.getEntityId())) continue; + + EntityLivingBase livingEntity = (EntityLivingBase) entity; + + // Check if actually in beam path + if (isInBeamPath(livingEntity, startX, startY, startZ, dirX, dirZ)) { + hitThisTick.add(entity.getEntityId()); + applyDamage(livingEntity, owner); + + if (!piercing) { + return; + } + } + } + } + } + + private boolean isInBeamPath(EntityLivingBase entity, double startX, double startY, double startZ, + double dirX, double dirZ) { + double entityX = entity.posX - startX; + double entityZ = entity.posZ - startZ; + + // Dot product to find projection distance + double projectionDist = entityX * dirX + entityZ * dirZ; + + // Check if within beam length + if (projectionDist < 0 || projectionDist > beamLength) { + return false; + } + + // Calculate perpendicular distance + double projX = dirX * projectionDist; + double projZ = dirZ * projectionDist; + double perpX = entityX - projX; + double perpZ = entityZ - projZ; + double perpDist = Math.sqrt(perpX * perpX + perpZ * perpZ); + + // Check if within beam width (use wider hitbox than visual) + if (perpDist > beamWidth * 3 + entity.width / 2) { + return false; + } + + // Height check - entity's feet must be below beam top (allows jumping over) + double entityFeetY = entity.posY; + double beamTopY = startY + 0.5; + + return entityFeetY < beamTopY; + } + + private void applyDamage(EntityLivingBase target, Entity owner) { + if (owner instanceof EntityNPCInterface) { + target.attackEntityFrom(new NpcDamageSource("npc_ability", (EntityNPCInterface) owner), damage); + } else if (owner instanceof EntityPlayer) { + target.attackEntityFrom(DamageSource.causePlayerDamage((EntityPlayer) owner), damage); + } else if (owner instanceof EntityLivingBase) { + target.attackEntityFrom(DamageSource.causeMobDamage((EntityLivingBase) owner), damage); + } else { + target.attackEntityFrom(new NpcDamageSource("npc_ability", null), damage); + } + + // Small upward knockback to help escape + target.addVelocity(0, 0.2, 0); + target.velocityChanged = true; + } + + private Entity getOwner() { + if (ownerEntityId == -1) return null; + return worldObj.getEntityByID(ownerEntityId); + } + + // ==================== RENDERING ==================== + + @Override + @SideOnly(Side.CLIENT) + public boolean isInRangeToRenderDist(double distance) { + return distance < 16384.0D; // 128 blocks squared + } + + @Override + public boolean shouldRenderInPass(int pass) { + return pass == 1; // Translucent pass + } + + @Override + public float getBrightness(float partialTicks) { + return 1.0f; + } + + @Override + @SideOnly(Side.CLIENT) + public int getBrightnessForRender(float partialTicks) { + return 0xF000F0; + } + + // ==================== GETTERS FOR RENDERER ==================== + + public float getBeamLength() { + return beamLength; + } + + public float getBeamWidth() { + return beamWidth; + } + + public float getBeamHeight() { + return beamHeight; + } + + public int getInnerColor() { + return innerColor; + } + + public int getOuterColor() { + return outerColor; + } + + public boolean isOuterColorEnabled() { + return outerColorEnabled; + } + + public float getOuterColorWidth() { + return outerColorWidth; + } + + /** + * Get the full interpolated angle INCLUDING base yaw for rendering. + */ + public float getInterpolatedAngle(float partialTicks) { + // Handle wrap-around for the rotating part + float diff = currentAngle - prevAngle; + if (diff > 180) diff -= 360; + if (diff < -180) diff += 360; + float interpAngle = prevAngle + diff * partialTicks; + // Add base yaw for correct world orientation + return baseYaw + interpAngle; + } + + public float getCurrentAngle() { + return currentAngle; + } + + public float getBaseYaw() { + return baseYaw; + } + + // ==================== NBT ==================== + + @Override + protected void readEntityFromNBT(NBTTagCompound nbt) { + this.beamLength = nbt.getFloat("BeamLength"); + this.beamWidth = nbt.getFloat("BeamWidth"); + this.beamHeight = nbt.getFloat("BeamHeight"); + this.innerColor = nbt.getInteger("InnerColor"); + this.outerColor = nbt.getInteger("OuterColor"); + this.outerColorEnabled = !nbt.hasKey("OuterColorEnabled") || nbt.getBoolean("OuterColorEnabled"); + this.outerColorWidth = nbt.hasKey("OuterColorWidth") ? nbt.getFloat("OuterColorWidth") : 1.8f; + this.sweepSpeed = nbt.getFloat("SweepSpeed"); + this.numberOfRotations = nbt.getInteger("NumRotations"); + this.completedRotations = nbt.getInteger("CompletedRotations"); + this.maxTicks = nbt.getInteger("MaxTicks"); + this.ownerEntityId = nbt.getInteger("OwnerId"); + this.targetEntityId = nbt.getInteger("TargetId"); + this.currentAngle = nbt.getFloat("CurrentAngle"); + this.baseYaw = nbt.getFloat("BaseYaw"); + this.deathWorldTime = nbt.getLong("DeathWorldTime"); + this.damage = nbt.getFloat("Damage"); + this.damageInterval = nbt.getInteger("DamageInterval"); + this.piercing = nbt.getBoolean("Piercing"); + this.lockOnTarget = nbt.getBoolean("LockOnTarget"); + } + + @Override + protected void writeEntityToNBT(NBTTagCompound nbt) { + nbt.setFloat("BeamLength", beamLength); + nbt.setFloat("BeamWidth", beamWidth); + nbt.setFloat("BeamHeight", beamHeight); + nbt.setInteger("InnerColor", innerColor); + nbt.setInteger("OuterColor", outerColor); + nbt.setBoolean("OuterColorEnabled", outerColorEnabled); + nbt.setFloat("OuterColorWidth", outerColorWidth); + nbt.setFloat("SweepSpeed", sweepSpeed); + nbt.setInteger("NumRotations", numberOfRotations); + nbt.setInteger("CompletedRotations", completedRotations); + nbt.setInteger("MaxTicks", maxTicks); + nbt.setInteger("OwnerId", ownerEntityId); + nbt.setInteger("TargetId", targetEntityId); + nbt.setFloat("CurrentAngle", currentAngle); + nbt.setFloat("BaseYaw", baseYaw); + nbt.setLong("DeathWorldTime", deathWorldTime); + nbt.setFloat("Damage", damage); + nbt.setInteger("DamageInterval", damageInterval); + nbt.setBoolean("Piercing", piercing); + nbt.setBoolean("LockOnTarget", lockOnTarget); + } + + // ==================== SPAWN DATA ==================== + + @Override + public void writeSpawnData(ByteBuf buffer) { + buffer.writeFloat(beamLength); + buffer.writeFloat(beamWidth); + buffer.writeFloat(beamHeight); + buffer.writeInt(innerColor); + buffer.writeInt(outerColor); + buffer.writeBoolean(outerColorEnabled); + buffer.writeFloat(outerColorWidth); + buffer.writeFloat(sweepSpeed); + buffer.writeInt(numberOfRotations); + buffer.writeInt(completedRotations); + buffer.writeInt(maxTicks); + buffer.writeInt(ownerEntityId); + buffer.writeInt(targetEntityId); + buffer.writeFloat(currentAngle); + buffer.writeFloat(baseYaw); + buffer.writeFloat(damage); + buffer.writeInt(damageInterval); + buffer.writeBoolean(piercing); + buffer.writeBoolean(lockOnTarget); + } + + @Override + public void readSpawnData(ByteBuf buffer) { + this.beamLength = buffer.readFloat(); + this.beamWidth = buffer.readFloat(); + this.beamHeight = buffer.readFloat(); + this.innerColor = buffer.readInt(); + this.outerColor = buffer.readInt(); + this.outerColorEnabled = buffer.readBoolean(); + this.outerColorWidth = buffer.readFloat(); + this.sweepSpeed = buffer.readFloat(); + this.numberOfRotations = buffer.readInt(); + this.completedRotations = buffer.readInt(); + this.maxTicks = buffer.readInt(); + this.ownerEntityId = buffer.readInt(); + this.targetEntityId = buffer.readInt(); + this.currentAngle = buffer.readFloat(); + this.baseYaw = buffer.readFloat(); + this.damage = buffer.readFloat(); + this.damageInterval = buffer.readInt(); + this.piercing = buffer.readBoolean(); + this.lockOnTarget = buffer.readBoolean(); + this.prevAngle = this.currentAngle; + } +} diff --git a/src/main/java/kamkeel/npcs/network/PacketHandler.java b/src/main/java/kamkeel/npcs/network/PacketHandler.java index d09882cfc..63ab9b192 100644 --- a/src/main/java/kamkeel/npcs/network/PacketHandler.java +++ b/src/main/java/kamkeel/npcs/network/PacketHandler.java @@ -26,6 +26,7 @@ import kamkeel.npcs.network.packets.data.SoundManagementPacket; import kamkeel.npcs.network.packets.data.SwingPlayerArmPacket; import kamkeel.npcs.network.packets.data.UpdateAnimationsPacket; import kamkeel.npcs.network.packets.data.VillagerListPacket; +import kamkeel.npcs.network.packets.data.ability.PlayerAbilitySyncPacket; import kamkeel.npcs.network.packets.data.telegraph.TelegraphRemovePacket; import kamkeel.npcs.network.packets.data.telegraph.TelegraphSpawnPacket; import kamkeel.npcs.network.packets.data.gui.GuiClosePacket; @@ -92,10 +93,15 @@ 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.CustomAbilitiesGetPacket; +import kamkeel.npcs.network.packets.request.ability.CustomAbilityGetPacket; +import kamkeel.npcs.network.packets.request.ability.CustomAbilityRemovePacket; +import kamkeel.npcs.network.packets.request.ability.CustomAbilitySavePacket; import kamkeel.npcs.network.packets.request.animation.AnimationGetPacket; import kamkeel.npcs.network.packets.request.animation.AnimationRemovePacket; import kamkeel.npcs.network.packets.request.animation.AnimationSavePacket; import kamkeel.npcs.network.packets.request.animation.AnimationsGetPacket; +import kamkeel.npcs.network.packets.request.animation.BuiltInAnimationGetPacket; import kamkeel.npcs.network.packets.request.bank.BankGetPacket; import kamkeel.npcs.network.packets.request.bank.BankRemovePacket; import kamkeel.npcs.network.packets.request.bank.BankSavePacket; @@ -469,6 +475,7 @@ public class PacketHandler { REQUEST_PACKET.registerPacket(new AnimationGetPacket()); REQUEST_PACKET.registerPacket(new AnimationRemovePacket()); REQUEST_PACKET.registerPacket(new AnimationSavePacket()); + REQUEST_PACKET.registerPacket(new BuiltInAnimationGetPacket()); // Moving Path REQUEST_PACKET.registerPacket(new MovingPathSavePacket()); @@ -499,6 +506,10 @@ public class PacketHandler { REQUEST_PACKET.registerPacket(new AbilitiesGetAllPacket()); REQUEST_PACKET.registerPacket(new AbilitiesNpcGetPacket()); REQUEST_PACKET.registerPacket(new AbilitiesNpcSavePacket()); + REQUEST_PACKET.registerPacket(new CustomAbilitiesGetPacket()); + REQUEST_PACKET.registerPacket(new CustomAbilityGetPacket()); + REQUEST_PACKET.registerPacket(new CustomAbilityRemovePacket()); + REQUEST_PACKET.registerPacket(new CustomAbilitySavePacket()); // Other Packets REQUEST_PACKET.registerPacket(new IsGuiOpenInform()); @@ -568,6 +579,7 @@ public class PacketHandler { // Data | Ability Packets DATA_PACKET.registerPacket(new TelegraphSpawnPacket()); DATA_PACKET.registerPacket(new TelegraphRemovePacket()); + DATA_PACKET.registerPacket(new PlayerAbilitySyncPacket()); } public void registerPlayerPackets() { diff --git a/src/main/java/kamkeel/npcs/network/enums/EnumDataPacket.java b/src/main/java/kamkeel/npcs/network/enums/EnumDataPacket.java index a5c6c8ba9..18321d0a4 100644 --- a/src/main/java/kamkeel/npcs/network/enums/EnumDataPacket.java +++ b/src/main/java/kamkeel/npcs/network/enums/EnumDataPacket.java @@ -62,6 +62,7 @@ public enum EnumDataPacket { // Ability System TELEGRAPH_SPAWN, TELEGRAPH_REMOVE, + PLAYER_ABILITY_SYNC, // Auction System AUCTION_DATA, diff --git a/src/main/java/kamkeel/npcs/network/enums/EnumRequestPacket.java b/src/main/java/kamkeel/npcs/network/enums/EnumRequestPacket.java index 005da40a7..8183941d0 100644 --- a/src/main/java/kamkeel/npcs/network/enums/EnumRequestPacket.java +++ b/src/main/java/kamkeel/npcs/network/enums/EnumRequestPacket.java @@ -93,6 +93,7 @@ public enum EnumRequestPacket { AnimationGet, AnimationRemove, AnimationSave, + BuiltInAnimationGet, RemoteNpcsGet, RemoteTpToNpc, @@ -197,6 +198,10 @@ public enum EnumRequestPacket { NpcAbilitiesGet, NpcAbilitiesSave, AbilitiesGetAll, + CustomAbilitiesGet, + CustomAbilityGet, + CustomAbilityRemove, + CustomAbilitySave, Hammer, RecipeScript, diff --git a/src/main/java/kamkeel/npcs/network/enums/EnumSyncType.java b/src/main/java/kamkeel/npcs/network/enums/EnumSyncType.java index a84e62392..1ac0ec828 100644 --- a/src/main/java/kamkeel/npcs/network/enums/EnumSyncType.java +++ b/src/main/java/kamkeel/npcs/network/enums/EnumSyncType.java @@ -12,5 +12,6 @@ public enum EnumSyncType { WORKBENCH_RECIPES, CARPENTRY_RECIPES, ANVIL_RECIPES, - CUSTOM_EFFECTS + CUSTOM_EFFECTS, + CUSTOM_ABILITY } diff --git a/src/main/java/kamkeel/npcs/network/packets/data/ability/PlayerAbilitySyncPacket.java b/src/main/java/kamkeel/npcs/network/packets/data/ability/PlayerAbilitySyncPacket.java new file mode 100644 index 000000000..08df836aa --- /dev/null +++ b/src/main/java/kamkeel/npcs/network/packets/data/ability/PlayerAbilitySyncPacket.java @@ -0,0 +1,73 @@ +package kamkeel.npcs.network.packets.data.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.enums.EnumDataPacket; +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.controllers.PlayerDataController; +import noppes.npcs.controllers.data.PlayerData; + +import java.io.IOException; + +/** + * Syncs the player's ability data (unlocked abilities, selected index) to the client. + * Sent on login and whenever abilities change (unlock/lock/select). + */ +public final class PlayerAbilitySyncPacket extends AbstractPacket { + public static final String packetName = "Data|PlayerAbilitySync"; + + private NBTTagCompound abilityNBT; + + public PlayerAbilitySyncPacket() { + } + + public PlayerAbilitySyncPacket(NBTTagCompound abilityNBT) { + this.abilityNBT = abilityNBT; + } + + @Override + public Enum getType() { + return EnumDataPacket.PLAYER_ABILITY_SYNC; + } + + @Override + public PacketChannel getChannel() { + return PacketHandler.DATA_PACKET; + } + + @Override + public void sendData(ByteBuf out) throws IOException { + ByteBufUtils.writeNBT(out, abilityNBT); + } + + @SideOnly(Side.CLIENT) + @Override + public void receiveData(ByteBuf in, EntityPlayer player) throws IOException { + NBTTagCompound nbt = ByteBufUtils.readNBT(in); + if (nbt == null) return; + + PlayerData data = PlayerData.get(player); + if (data != null) { + data.abilityData.readFromNBT(nbt); + } + } + + /** + * Send the player's ability data to their client. + */ + public static void sendToPlayer(EntityPlayerMP player) { + PlayerData data = PlayerDataController.Instance.getPlayerData(player); + if (data == null) return; + + NBTTagCompound nbt = new NBTTagCompound(); + data.abilityData.writeToNBT(nbt); + PacketHandler.Instance.sendToPlayer(new PlayerAbilitySyncPacket(nbt), player); + } +} diff --git a/src/main/java/kamkeel/npcs/network/packets/player/SpecialKeyStatePacket.java b/src/main/java/kamkeel/npcs/network/packets/player/SpecialKeyStatePacket.java index 3b9f56690..48ac3391b 100644 --- a/src/main/java/kamkeel/npcs/network/packets/player/SpecialKeyStatePacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/player/SpecialKeyStatePacket.java @@ -54,7 +54,13 @@ public final class SpecialKeyStatePacket extends AbstractPacket { PlayerData data = PlayerDataController.Instance.getPlayerData((EntityPlayerMP) player); if (data != null) { + boolean wasDown = data.isSpecialKeyDown(); data.setSpecialKeyDown(keyDown); + + // Trigger ability activation on key press (rising edge) + if (keyDown && !wasDown) { + data.abilityData.activateAbility(player); + } } } } diff --git a/src/main/java/kamkeel/npcs/network/packets/request/ability/CustomAbilitiesGetPacket.java b/src/main/java/kamkeel/npcs/network/packets/request/ability/CustomAbilitiesGetPacket.java new file mode 100644 index 000000000..8febd9fc0 --- /dev/null +++ b/src/main/java/kamkeel/npcs/network/packets/request/ability/CustomAbilitiesGetPacket.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 custom ability preset UUIDs/names. + */ +public final class CustomAbilitiesGetPacket extends AbstractPacket { + public static String packetName = "Request|CustomAbilitiesGet"; + + public CustomAbilitiesGetPacket() { + } + + @Override + public Enum getType() { + return EnumRequestPacket.CustomAbilitiesGet; + } + + @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.sendCustomAbilitiesData((EntityPlayerMP) player); + } +} diff --git a/src/main/java/kamkeel/npcs/network/packets/request/ability/CustomAbilityGetPacket.java b/src/main/java/kamkeel/npcs/network/packets/request/ability/CustomAbilityGetPacket.java new file mode 100644 index 000000000..d95051a66 --- /dev/null +++ b/src/main/java/kamkeel/npcs/network/packets/request/ability/CustomAbilityGetPacket.java @@ -0,0 +1,70 @@ +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 custom ability by UUID. + */ +public final class CustomAbilityGetPacket extends AbstractPacket { + public static String packetName = "Request|CustomAbilityGet"; + + private String uuid; + + public CustomAbilityGetPacket() { + } + + public CustomAbilityGetPacket(String uuid) { + this.uuid = uuid; + } + + @Override + public Enum getType() { + return EnumRequestPacket.CustomAbilityGet; + } + + @Override + public PacketChannel getChannel() { + return PacketHandler.REQUEST_PACKET; + } + + @SideOnly(Side.CLIENT) + @Override + public void sendData(ByteBuf out) throws IOException { + ByteBufUtils.writeString(out, uuid); + } + + @Override + public void receiveData(ByteBuf in, EntityPlayer player) throws IOException { + if (!(player instanceof EntityPlayerMP)) + return; + + if (!PacketUtil.verifyItemPacket(packetName, EnumItemPacketType.WAND, player)) + return; + + String id = ByteBufUtils.readString(in); + Ability ability = AbilityController.Instance.getCustomAbility(id); + if (ability != null) { + NBTTagCompound compound = ability.writeNBT(); + // Include the UUID so the client can track it + compound.setString("_uuid", id); + GuiDataPacket.sendGuiData((EntityPlayerMP) player, compound); + } + } +} diff --git a/src/main/java/kamkeel/npcs/network/packets/request/ability/CustomAbilityRemovePacket.java b/src/main/java/kamkeel/npcs/network/packets/request/ability/CustomAbilityRemovePacket.java new file mode 100644 index 000000000..3ef040700 --- /dev/null +++ b/src/main/java/kamkeel/npcs/network/packets/request/ability/CustomAbilityRemovePacket.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 custom ability by UUID. + */ +public final class CustomAbilityRemovePacket extends AbstractPacket { + public static String packetName = "Request|CustomAbilityRemove"; + + private String uuid; + + public CustomAbilityRemovePacket() { + } + + public CustomAbilityRemovePacket(String uuid) { + this.uuid = uuid; + } + + @Override + public Enum getType() { + return EnumRequestPacket.CustomAbilityRemove; + } + + @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, uuid); + } + + @Override + public void receiveData(ByteBuf in, EntityPlayer player) throws IOException { + if (!(player instanceof EntityPlayerMP)) + return; + + if (!PacketUtil.verifyItemPacket(packetName, EnumItemPacketType.WAND, player)) + return; + + String id = ByteBufUtils.readString(in); + AbilityController.Instance.deleteCustomAbility(id); + NoppesUtilServer.sendCustomAbilitiesData((EntityPlayerMP) player); + } +} diff --git a/src/main/java/kamkeel/npcs/network/packets/request/ability/CustomAbilitySavePacket.java b/src/main/java/kamkeel/npcs/network/packets/request/ability/CustomAbilitySavePacket.java new file mode 100644 index 000000000..d5ca1c556 --- /dev/null +++ b/src/main/java/kamkeel/npcs/network/packets/request/ability/CustomAbilitySavePacket.java @@ -0,0 +1,78 @@ +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 a custom ability preset. + */ +public final class CustomAbilitySavePacket extends AbstractPacket { + public static String packetName = "Request|CustomAbilitySave"; + + private NBTTagCompound abilityNBT; + + public CustomAbilitySavePacket() { + } + + public CustomAbilitySavePacket(NBTTagCompound abilityNBT) { + this.abilityNBT = abilityNBT; + } + + @Override + public Enum getType() { + return EnumRequestPacket.CustomAbilitySave; + } + + @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.saveCustomAbility(ability); + NoppesUtilServer.sendCustomAbilitiesData((EntityPlayerMP) player); + NBTTagCompound responseNbt = ability.writeNBT(); + responseNbt.setString("_uuid", ability.getId()); + GuiDataPacket.sendGuiData((EntityPlayerMP) player, responseNbt); + } + } +} diff --git a/src/main/java/kamkeel/npcs/network/packets/request/animation/AnimationsGetPacket.java b/src/main/java/kamkeel/npcs/network/packets/request/animation/AnimationsGetPacket.java index a2bc51d0a..39b60a40f 100644 --- a/src/main/java/kamkeel/npcs/network/packets/request/animation/AnimationsGetPacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/request/animation/AnimationsGetPacket.java @@ -44,5 +44,6 @@ public final class AnimationsGetPacket extends AbstractPacket { return; NoppesUtilServer.sendAnimationDataAll((EntityPlayerMP) player); + NoppesUtilServer.sendBuiltInAnimationData((EntityPlayerMP) player); } } diff --git a/src/main/java/kamkeel/npcs/network/packets/request/animation/BuiltInAnimationGetPacket.java b/src/main/java/kamkeel/npcs/network/packets/request/animation/BuiltInAnimationGetPacket.java new file mode 100644 index 000000000..e214689d8 --- /dev/null +++ b/src/main/java/kamkeel/npcs/network/packets/request/animation/BuiltInAnimationGetPacket.java @@ -0,0 +1,72 @@ +package kamkeel.npcs.network.packets.request.animation; + +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 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.controllers.AnimationController; +import noppes.npcs.controllers.data.Animation; +import noppes.npcs.controllers.data.BuiltInAnimation; + +import java.io.IOException; + +/** + * Packet to request a built-in animation by name. + * Built-in animations don't have IDs, so we fetch by name. + */ +public final class BuiltInAnimationGetPacket extends AbstractPacket { + public static String packetName = "Request|BuiltInAnimationGet"; + + private String animationName; + + public BuiltInAnimationGetPacket() { + } + + public BuiltInAnimationGetPacket(String animationName) { + this.animationName = animationName; + } + + @Override + public Enum getType() { + return EnumRequestPacket.BuiltInAnimationGet; + } + + @Override + public PacketChannel getChannel() { + return PacketHandler.REQUEST_PACKET; + } + + @SideOnly(Side.CLIENT) + @Override + public void sendData(ByteBuf out) throws IOException { + ByteBufUtils.writeString(out, animationName); + } + + @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); + BuiltInAnimation animation = AnimationController.getInstance().getBuiltInAnimation(name); + if (animation != null) { + NBTTagCompound compound = animation.writeToNBT(); + // Mark as built-in so client knows + compound.setBoolean("BuiltIn", true); + GuiDataPacket.sendGuiData((EntityPlayerMP) player, compound); + } + } +} diff --git a/src/main/java/kamkeel/npcs/network/packets/request/script/ScriptFilesPacket.java b/src/main/java/kamkeel/npcs/network/packets/request/script/ScriptFilesPacket.java index 37383ea6c..700513d78 100644 --- a/src/main/java/kamkeel/npcs/network/packets/request/script/ScriptFilesPacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/request/script/ScriptFilesPacket.java @@ -39,7 +39,7 @@ public final class ScriptFilesPacket extends AbstractPacket { @Override public PacketChannel getChannel() { - return PacketHandler.DATA_PACKET; + return PacketHandler.REQUEST_PACKET; } @Override @@ -47,7 +47,6 @@ public final class ScriptFilesPacket extends AbstractPacket { return CustomNpcsPermissions.SCRIPT_GLOBAL; } - @SideOnly(Side.CLIENT) @Override public void sendData(ByteBuf out) throws IOException { ByteBufUtils.writeNBT(out, getScriptsNbt(lang)); diff --git a/src/main/java/kamkeel/npcs/util/AnchorPointHelper.java b/src/main/java/kamkeel/npcs/util/AnchorPointHelper.java new file mode 100644 index 000000000..e095dc61b --- /dev/null +++ b/src/main/java/kamkeel/npcs/util/AnchorPointHelper.java @@ -0,0 +1,348 @@ +package kamkeel.npcs.util; + +import kamkeel.npcs.controllers.data.ability.AnchorPoint; +import kamkeel.npcs.controllers.data.ability.data.EnergyAnchorData; +import net.minecraft.entity.EntityLivingBase; +import net.minecraft.util.Vec3; +import noppes.npcs.constants.EnumAnimationPart; +import noppes.npcs.controllers.data.AnimationData; +import noppes.npcs.controllers.data.Frame; +import noppes.npcs.controllers.data.FramePart; +import noppes.npcs.entity.EntityCustomNpc; +import noppes.npcs.entity.EntityNPCInterface; +import noppes.npcs.entity.data.ModelScalePart; + +/** + * Utility class for calculating world positions based on anchor points. + * Supports both EntityNPCInterface and EntityPlayer with animation and model size awareness. + */ +public class AnchorPointHelper { + + // Default biped model constants (in model units, 1 unit = 1/16 block) + private static final float MODEL_SCALE = 1f / 16f; + + // Shoulder position relative to model origin + private static final float SHOULDER_Y = 2f; + private static final float SHOULDER_X_OFFSET = 5f; // Distance from center to shoulder + + // Palm position (in arm's local space, relative to shoulder) + // Arm is 12 units long; palm is near the end + private static final float PALM_DOWN = 11f; // Distance down the arm to palm center + private static final float PALM_FORWARD = 2f; // Forward offset to palm surface (not center of hand) + private static final float PALM_INWARD = 0f; // Lateral offset (0 = centered on arm) + + // Default height offsets as fraction of entity height + private static final float FRONT_HEIGHT = 0.7f; + private static final float CENTER_HEIGHT = 0.5f; + private static final float ARM_HEIGHT = 0.75f; + private static final float ABOVE_HEAD_HEIGHT = 1.2f; + private static final float CHEST_HEIGHT = 0.65f; + + // Distance in front for FRONT anchor + private static final float DEFAULT_FRONT_DISTANCE = 1.0f; + + // Fallback arm positioning (when no animation, arms hang at sides) + private static final float FALLBACK_LATERAL = 0.35f; // Distance from body center to arm + private static final float FALLBACK_FORWARD = 0.1f; // Slight forward offset + private static final float FALLBACK_ARM_HEIGHT = 0.45f; // Hand height when arms hang down (near hip) + + /** + * Calculate world position for an anchor point on an entity. + */ + public static Vec3 calculateAnchorPosition(EntityLivingBase entity, EnergyAnchorData anchorData) { + return calculateAnchorPosition(entity, anchorData, DEFAULT_FRONT_DISTANCE); + } + + /** + * Calculate world position for an anchor point with custom front offset. + */ + public static Vec3 calculateAnchorPosition(EntityLivingBase entity, EnergyAnchorData anchorData, float frontOffset) { + float scale = getModelScale(entity); + float height = entity.height; + + AnchorPoint anchor = anchorData.anchorPoint; + + float anchorX = anchorData.anchorOffsetX; + float anchorY = anchorData.anchorOffsetY; + float anchorZ = anchorData.anchorOffsetZ; + + double x = entity.posX; + double y = entity.posY; + double z = entity.posZ; + + float headYaw = (float) Math.toRadians(entity.rotationYawHead); + float bodyYaw = (float) Math.toRadians(entity.renderYawOffset); + + switch (anchor) { + case FRONT: + Vec3 look = entity.getLookVec(); + x += look.xCoord * frontOffset * scale; + y += height * FRONT_HEIGHT + look.yCoord * frontOffset * scale; + z += look.zCoord * frontOffset * scale; + break; + + case CENTER: + y += height * CENTER_HEIGHT; + break; + + case RIGHT_HAND: + return calculateHandPosition(entity, anchorData, true, scale); + + case LEFT_HAND: + return calculateHandPosition(entity, anchorData, false, scale); + + case ABOVE_HEAD: + y += height * ABOVE_HEAD_HEIGHT; + break; + + case CHEST: + y += height * CHEST_HEIGHT; + break; + } + + x += anchorX * scale; + y += anchorY * scale; + z += anchorZ * scale; + + return Vec3.createVectorHelper(x, y, z); + } + + /** + * Get model scale factor for an entity. + * NPCs have configurable model size, players use default scale. + */ + private static float getModelScale(EntityLivingBase entity) { + if (entity instanceof EntityNPCInterface) { + int modelSize = ((EntityNPCInterface) entity).display.modelSize; + return modelSize / 5f; // modelSize 5 = 100% scale + } + return 1f; + } + + // Default arm scale (no scaling) + private static final ModelScalePart DEFAULT_ARM_SCALE = new ModelScalePart(); + + /** + * Get arm scale for an entity. + * EntityCustomNpc has per-part scaling, others use default (1,1,1). + */ + private static ModelScalePart getArmScale(EntityLivingBase entity) { + if (entity instanceof EntityCustomNpc) { + return ((EntityCustomNpc) entity).modelData.modelScale.arms; + } + return DEFAULT_ARM_SCALE; + } + + /** + * Calculate hand position using animation data if available. + */ + private static Vec3 calculateHandPosition(EntityLivingBase entity, EnergyAnchorData anchor, boolean rightHand, float scale) { + // Try to get animated position first + Vec3 animatedPos = getAnimatedHandPosition(entity, anchor, rightHand, scale); + if (animatedPos != null) { + return animatedPos; + } + + // Fallback to static position + return calculateFallbackHandPosition(entity, anchor, rightHand, scale); + } + + /** + * Calculate hand position from animation data. + * Works for both NPCs and Players. + */ + private static Vec3 getAnimatedHandPosition(EntityLivingBase entity, EnergyAnchorData anchor, boolean rightHand, float scale) { + AnimationData animData = AnimationData.getData(entity); + if (animData == null || !animData.isActive() || animData.animation == null) { + return null; + } + + Frame frame = (Frame) animData.animation.currentFrame(); + if (frame == null) { + return null; + } + + EnumAnimationPart armPart = rightHand ? EnumAnimationPart.RIGHT_ARM : EnumAnimationPart.LEFT_ARM; + FramePart part = frame.frameParts.get(armPart); + if (part == null) { + return null; + } + + // Get arm scale for EntityCustomNpc + ModelScalePart armScale = getArmScale(entity); + + // Get rotation and pivot values + float[] rotations = getRotations(part, entity); + float[] pivots = getPivots(part, entity); + + // Shoulder position in model space + float shoulderX = rightHand ? -SHOULDER_X_OFFSET : SHOULDER_X_OFFSET; + float shoulderY = SHOULDER_Y; + float shoulderZ = 0f; + + // Add pivot offsets + shoulderX += pivots[0]; + shoulderY += pivots[1]; + shoulderZ += pivots[2]; + + // Palm offset in arm's local space (before rotation) + // Apply arm scaling to the palm position + float palmX = (rightHand ? -PALM_INWARD : PALM_INWARD) * armScale.scaleX; + float palmY = PALM_DOWN * armScale.scaleY; + float palmZ = PALM_FORWARD * armScale.scaleZ; + + // Apply arm rotations (order: X -> Y -> Z, matching GL's reverse application) + double hx = palmX, hy = palmY, hz = palmZ; + + // X rotation first + double cosX = Math.cos(rotations[0]); + double sinX = Math.sin(rotations[0]); + double newY = hy * cosX - hz * sinX; + double newZ = hy * sinX + hz * cosX; + hy = newY; + hz = newZ; + + // Y rotation second + double cosY = Math.cos(rotations[1]); + double sinY = Math.sin(rotations[1]); + double newX = hx * cosY + hz * sinY; + newZ = -hx * sinY + hz * cosY; + hx = newX; + hz = newZ; + + // Z rotation last + double cosZ = Math.cos(rotations[2]); + double sinZ = Math.sin(rotations[2]); + newX = hx * cosZ - hy * sinZ; + newY = hx * sinZ + hy * cosZ; + hx = newX; + hy = newY; + + // Combine shoulder + rotated hand offset + double modelX = shoulderX + hx; + double modelY = shoulderY + hy; + double modelZ = shoulderZ + hz; + + // Convert to blocks and apply model scale + double blockX = modelX * MODEL_SCALE * scale; + double blockY = modelY * MODEL_SCALE * scale; + double blockZ = modelZ * MODEL_SCALE * scale; + + // Apply body yaw rotation + // Renderer uses glRotate(180 - yaw), so: cos(180-yaw) = -cos(yaw), sin(180-yaw) = sin(yaw) + // Y-axis rotation: x' = x*cos + z*sin, z' = -x*sin + z*cos + // Substituting: x' = -x*cos(yaw) + z*sin(yaw), z' = -x*sin(yaw) - z*cos(yaw) + float bodyYaw = (float) Math.toRadians(entity.renderYawOffset); + double cosYaw = Math.cos(bodyYaw); + double sinYaw = Math.sin(bodyYaw); + + double worldOffsetX = blockX * cosYaw + blockZ * sinYaw; + double worldOffsetZ = blockX * sinYaw - blockZ * cosYaw; + + // Calculate shoulder height based on entity and scale + float shoulderHeight = entity.height * ARM_HEIGHT; + + // Final world position + double worldX = entity.posX + worldOffsetX; + double worldY = entity.posY + shoulderHeight - blockY; + double worldZ = entity.posZ + worldOffsetZ; + + worldX += anchor.anchorOffsetX * scale; + worldY += anchor.anchorOffsetY * scale; + worldZ += anchor.anchorOffsetZ * scale; + + return Vec3.createVectorHelper(worldX, worldY, worldZ); + } + + /** + * Get rotation values from FramePart. + * Uses interpolated values on client, raw values on server. + */ + private static float[] getRotations(FramePart part, EntityLivingBase entity) { + if (entity.worldObj.isRemote) { + // Client side - use interpolated values (already in radians) + part.interpolateAngles(); + return part.prevRotations; + } else { + // Server side - convert raw degrees to radians + float pi = (float) Math.PI / 180f; + return new float[]{ + part.rotation[0] * pi, + part.rotation[1] * pi, + part.rotation[2] * pi + }; + } + } + + /** + * Get pivot values from FramePart. + * Uses interpolated values on client, raw values on server. + */ + private static float[] getPivots(FramePart part, EntityLivingBase entity) { + if (entity.worldObj.isRemote) { + // Client side - use interpolated values + part.interpolateOffset(); + return part.prevPivots; + } else { + // Server side - use raw values + return part.pivot; + } + } + + /** + * Calculate fallback hand position when no animation is active. + * Represents arms hanging at sides in default idle pose. + */ + private static Vec3 calculateFallbackHandPosition(EntityLivingBase entity, EnergyAnchorData anchor, boolean rightHand, float scale) { + float bodyYaw = (float) Math.toRadians(entity.renderYawOffset); + + // Get arm scale for EntityCustomNpc + ModelScalePart armScale = getArmScale(entity); + + // Perpendicular direction for lateral offset (right = +90°, left = -90° from body facing) + float perpYaw = bodyYaw + (rightHand ? (float) (Math.PI / 2) : (float) (-Math.PI / 2)); + + // Calculate lateral offset - arms hang at sides + // Scale by model size, but not by arm scale (lateral distance is fixed by skeleton) + double lateralX = -Math.sin(perpYaw) * FALLBACK_LATERAL * scale; + double lateralZ = Math.cos(perpYaw) * FALLBACK_LATERAL * scale; + + // Small forward offset + double forwardX = -Math.sin(bodyYaw) * FALLBACK_FORWARD * scale; + double forwardZ = Math.cos(bodyYaw) * FALLBACK_FORWARD * scale; + + // Arm length affects vertical position - longer arms = lower hands + double armLengthOffset = armScale.scaleY * 0.3 * scale; + + double x = entity.posX + lateralX + forwardX; + double y = entity.posY + entity.height * FALLBACK_ARM_HEIGHT + armLengthOffset; + double z = entity.posZ + lateralZ + forwardZ; + + x += anchor.anchorOffsetX * scale; + y += anchor.anchorOffsetY * scale; + z += anchor.anchorOffsetZ * scale; + + return Vec3.createVectorHelper(x, y, z); + } + + /** + * Get default height multiplier for an anchor point. + */ + public static float getHeightMultiplier(AnchorPoint anchor) { + switch (anchor) { + case FRONT: + return FRONT_HEIGHT; + case CENTER: + return CENTER_HEIGHT; + case RIGHT_HAND: + case LEFT_HAND: + return ARM_HEIGHT; + case ABOVE_HEAD: + return ABOVE_HEAD_HEIGHT; + case CHEST: + return CHEST_HEIGHT; + default: + return FRONT_HEIGHT; + } + } +} diff --git a/src/main/java/kamkeel/npcs/util/BukkitUtil.java b/src/main/java/kamkeel/npcs/util/BukkitUtil.java index bc35cae6c..14f6b3609 100644 --- a/src/main/java/kamkeel/npcs/util/BukkitUtil.java +++ b/src/main/java/kamkeel/npcs/util/BukkitUtil.java @@ -2,8 +2,8 @@ package kamkeel.npcs.util; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; - -import java.lang.reflect.Method; +import org.bukkit.Bukkit; +import org.bukkit.plugin.RegisteredServiceProvider; /** * Utility class for Bukkit integration detection and common operations. @@ -15,24 +15,6 @@ public class BukkitUtil { private static boolean initialized = false; private static boolean bukkitEnabled = false; - // Bukkit classes - private static Class bukkitClass; - private static Class serverClass; - private static Class servicesManagerClass; - private static Class registeredServiceProviderClass; - private static Class offlinePlayerClass; - private static Class playerClass; - - // Bukkit methods - private static Method getServer; - private static Method getServicesManager; - private static Method getRegistration; - private static Method getProvider; - private static Method getOfflinePlayer; - private static Method getPlayer; - private static Method getPluginManager; - private static Method isPluginEnabled; - /** * Initializes Bukkit integration using reflection. * Should be called during FMLServerStartedEvent to ensure Bukkit is fully loaded. @@ -44,25 +26,24 @@ public class BukkitUtil { initialized = true; try { - // Load Bukkit classes - bukkitClass = Class.forName("org.bukkit.Bukkit"); - serverClass = Class.forName("org.bukkit.Server"); - servicesManagerClass = Class.forName("org.bukkit.plugin.ServicesManager"); - registeredServiceProviderClass = Class.forName("org.bukkit.plugin.RegisteredServiceProvider"); - offlinePlayerClass = Class.forName("org.bukkit.OfflinePlayer"); - playerClass = Class.forName("org.bukkit.entity.Player"); + // Make sure all necessary bukkit methods and classes are available. + Class bukkitClass = Class.forName("org.bukkit.Bukkit"); + Class.forName("org.bukkit.Server"); + Class servicesManagerClass =Class.forName("org.bukkit.plugin.ServicesManager"); + Class registeredServiceProviderClass = Class.forName("org.bukkit.plugin.RegisteredServiceProvider"); + Class.forName("org.bukkit.OfflinePlayer"); + Class.forName("org.bukkit.entity.Player"); - // Get Bukkit methods - getServer = bukkitClass.getMethod("getServer"); - getServicesManager = bukkitClass.getMethod("getServicesManager"); - getRegistration = servicesManagerClass.getMethod("getRegistration", Class.class); - getProvider = registeredServiceProviderClass.getMethod("getProvider"); - getOfflinePlayer = bukkitClass.getMethod("getOfflinePlayer", String.class); - getPlayer = bukkitClass.getMethod("getPlayer", String.class); - getPluginManager = bukkitClass.getMethod("getPluginManager"); + bukkitClass.getMethod("getServer"); + bukkitClass.getMethod("getServicesManager"); + servicesManagerClass.getMethod("getRegistration", Class.class); + registeredServiceProviderClass.getMethod("getProvider"); + bukkitClass.getMethod("getOfflinePlayer", String.class); + bukkitClass.getMethod("getPlayer", String.class); + bukkitClass.getMethod("getPluginManager"); Class pluginManagerClass = Class.forName("org.bukkit.plugin.PluginManager"); - isPluginEnabled = pluginManagerClass.getMethod("isPluginEnabled", String.class); + pluginManagerClass.getMethod("isPluginEnabled", String.class); bukkitEnabled = true; logger.info("Bukkit integration enabled"); @@ -70,7 +51,7 @@ public class BukkitUtil { // Initialize Vault after Bukkit is confirmed VaultUtil.init(); - } catch (ClassNotFoundException e) { + } catch (ClassNotFoundException | NoClassDefFoundError e) { logger.debug("Bukkit not found, Bukkit integration disabled"); } catch (NoSuchMethodException e) { logger.error("Bukkit API method not found", e); @@ -100,30 +81,45 @@ public class BukkitUtil { * @return true if the plugin is enabled */ public static boolean isPluginEnabled(String pluginName) { - if (!bukkitEnabled) return false; + if (!isEnabled()) return false; try { - Object pluginManager = getPluginManager.invoke(null); - return (Boolean) isPluginEnabled.invoke(pluginManager, pluginName); + return Bukkit.getPluginManager().isPluginEnabled(pluginName); } catch (Exception e) { - logger.error("Error checking if plugin is enabled: " + pluginName, e); + logger.error("Error checking if plugin is enabled: {}", pluginName, e); return false; } } + /** + * @param pluginName name of the plugin to get. + * @return {@linkplain org.bukkit.plugin.Plugin Plugin} object for the name. Cast to correct class manually. + */ + public static Object getPlugin(String pluginName) { + if (!isEnabled()) return null; + + try { + return Bukkit.getPluginManager().getPlugin(pluginName); + } catch (Exception e) { + logger.error("Error getting plugin object: {}", pluginName, e); + return null; + } + } + /** * Gets a Bukkit OfflinePlayer by name. * * @param playerName the player name - * @return the OfflinePlayer object, or null if not available + * @return the {@link org.bukkit.OfflinePlayer} object, or null if not available */ + @SuppressWarnings({"deprecation"}) public static Object getOfflinePlayer(String playerName) { - if (!bukkitEnabled) return null; + if (!isEnabled()) return null; try { - return getOfflinePlayer.invoke(null, playerName); + return Bukkit.getOfflinePlayer(playerName); } catch (Exception e) { - logger.error("Error getting OfflinePlayer: " + playerName, e); + logger.error("Error getting OfflinePlayer: {}", playerName, e); return null; } } @@ -132,15 +128,15 @@ public class BukkitUtil { * Gets a Bukkit Player by name (online players only). * * @param playerName the player name - * @return the Player object, or null if not online or not available + * @return the {@link org.bukkit.entity.Player} object, or null if not online or not available */ public static Object getPlayer(String playerName) { - if (!bukkitEnabled) return null; + if (!isEnabled()) return null; try { - return getPlayer.invoke(null, playerName); + return Bukkit.getPlayer(playerName); } catch (Exception e) { - logger.error("Error getting Player: " + playerName, e); + logger.error("Error getting Player: {}", playerName, e); return null; } } @@ -151,39 +147,17 @@ public class BukkitUtil { * @param serviceClass the service class to get * @return the service provider, or null if not available */ - public static Object getServiceProvider(Class serviceClass) { - if (!bukkitEnabled) return null; + public static T getServiceProvider(Class serviceClass) { + if (!isEnabled()) return null; try { - Object servicesManager = getServicesManager.invoke(null); - Object registration = getRegistration.invoke(servicesManager, serviceClass); + RegisteredServiceProvider registration = Bukkit.getServicesManager().getRegistration(serviceClass); if (registration != null) { - return getProvider.invoke(registration); + return registration.getProvider(); } } catch (Exception e) { - logger.error("Error getting service provider: " + serviceClass.getName(), e); + logger.error("Error getting service provider: {}", serviceClass.getName(), e); } return null; } - - // Getters for classes (for use by other utils like VaultUtil) - public static Class getBukkitClass() { - return bukkitClass; - } - - public static Class getServicesManagerClass() { - return servicesManagerClass; - } - - public static Class getRegisteredServiceProviderClass() { - return registeredServiceProviderClass; - } - - public static Class getOfflinePlayerClass() { - return offlinePlayerClass; - } - - public static Class getPlayerClass() { - return playerClass; - } } diff --git a/src/main/java/kamkeel/npcs/util/Register.java b/src/main/java/kamkeel/npcs/util/Register.java new file mode 100644 index 000000000..0f1400ab3 --- /dev/null +++ b/src/main/java/kamkeel/npcs/util/Register.java @@ -0,0 +1,68 @@ +package kamkeel.npcs.util; + +import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.AbilityController; +import noppes.npcs.LogWriter; +import noppes.npcs.controllers.AnimationController; +import noppes.npcs.controllers.data.Animation; +import noppes.npcs.controllers.data.BuiltInAnimation; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.function.Supplier; + +public class Register { + private final String registryKey; + protected final String namespace; + protected final Map> entries = new LinkedHashMap<>(); + + public Register(String registryKey, String namespace) { + this.registryKey = registryKey; + this.namespace = namespace; + } + + public T register(String factoryName, Supplier factory) { + entries.put(registryKey + "." + namespace + ":" + factoryName, factory); + return factory.get(); + } + + public static class Abilities extends Register { + public Abilities(String namespace) { + super("ability", namespace); + } + + public void register() { + for (Map.Entry> entry : entries.entrySet()) { + AbilityController.Instance.registerType(entry.getKey(), entry.getValue()); + } + } + } + + public static class Animations extends Register { + private final Class modClass; + private final String animationsPath; + + public Animations(Class modClass, String animationsPath, String namespace) { + super("ability", namespace); + this.animationsPath = animationsPath; + this.modClass = modClass; + } + + @Override + public Animation register(String factoryName, Supplier factory) { + return super.register(factoryName, factory); + } + + public void register() { + try { + String path = "/assets/" + namespace + "/" + animationsPath; + for (Map.Entry> entry : entries.entrySet()) { + String name = entry.getKey().substring(entry.getKey().indexOf(":") + 1); + AnimationController.Instance.loadBuiltInAnimation(modClass, path, name); + } + } catch (Exception e) { + LogWriter.error("Error scanning built-in animations folder", e); + } + } + } +} diff --git a/src/main/java/kamkeel/npcs/util/VaultUtil.java b/src/main/java/kamkeel/npcs/util/VaultUtil.java index 64743ffba..ab485e186 100644 --- a/src/main/java/kamkeel/npcs/util/VaultUtil.java +++ b/src/main/java/kamkeel/npcs/util/VaultUtil.java @@ -3,6 +3,8 @@ package kamkeel.npcs.util; import net.minecraft.entity.player.EntityPlayer; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.bukkit.Bukkit; +import org.bukkit.plugin.Plugin; import java.lang.reflect.Method; @@ -68,15 +70,19 @@ public class VaultUtil { } try { - Class offlinePlayerClass = BukkitUtil.getOfflinePlayerClass(); + Class offlinePlayerClass = Class.forName("org.bukkit.OfflinePlayer"); if (offlinePlayerClass == null) { logger.error("Could not get OfflinePlayer class from BukkitUtil"); return; } // Load Vault classes - economyClass = Class.forName("net.milkbowl.vault.economy.Economy"); - economyResponseClass = Class.forName("net.milkbowl.vault.economy.EconomyResponse"); + Plugin vault = Bukkit.getPluginManager().getPlugin("Vault"); + if (vault == null) + return; + ClassLoader vaultLoader = vault.getClass().getClassLoader(); + economyClass = vaultLoader.loadClass("net.milkbowl.vault.economy.Economy"); + economyResponseClass = vaultLoader.loadClass("net.milkbowl.vault.economy.EconomyResponse"); // Get Economy methods (using deprecated String methods for broader compatibility) isEnabled = economyClass.getMethod("isEnabled"); @@ -174,7 +180,6 @@ public class VaultUtil { /** * Checks if a player has an account in the economy system. - * * @param player the player to check * @return true if the player has an account */ @@ -184,7 +189,6 @@ public class VaultUtil { /** * Checks if a player has an account in the economy system. - * * @param playerName the player name to check * @return true if the player has an account */ @@ -205,7 +209,6 @@ public class VaultUtil { /** * Gets the balance of a player. - * * @param player the player * @return the player's balance, or 0 if not available */ @@ -215,7 +218,6 @@ public class VaultUtil { /** * Gets the balance of a player by name. - * * @param playerName the player name * @return the player's balance, or 0 if not available */ @@ -236,7 +238,6 @@ public class VaultUtil { /** * Checks if a player has at least the specified amount. - * * @param player the player * @param amount the amount to check * @return true if the player has at least the specified amount @@ -247,9 +248,8 @@ public class VaultUtil { /** * Checks if a player has at least the specified amount. - * * @param playerName the player name - * @param amount the amount to check + * @param amount the amount to check * @return true if the player has at least the specified amount */ public static boolean has(String playerName, double amount) { @@ -269,7 +269,6 @@ public class VaultUtil { /** * Withdraws money from a player's account. - * * @param player the player * @param amount the amount to withdraw * @return true if the transaction was successful @@ -280,9 +279,8 @@ public class VaultUtil { /** * Withdraws money from a player's account. - * * @param playerName the player name - * @param amount the amount to withdraw + * @param amount the amount to withdraw * @return true if the transaction was successful */ public static boolean withdrawMoney(String playerName, double amount) { @@ -305,7 +303,6 @@ public class VaultUtil { /** * Adds money to a player's account (deposit). - * * @param player the player * @param amount the amount to add * @return true if the transaction was successful @@ -316,9 +313,8 @@ public class VaultUtil { /** * Adds money to a player's account (deposit). - * * @param playerName the player name - * @param amount the amount to add + * @param amount the amount to add * @return true if the transaction was successful */ public static boolean addMoney(String playerName, double amount) { @@ -342,7 +338,6 @@ public class VaultUtil { /** * Sets a player's balance to the specified amount. * This withdraws all money first, then deposits the new amount. - * * @param player the player * @param amount the new balance * @return true if the transaction was successful @@ -354,9 +349,8 @@ public class VaultUtil { /** * Sets a player's balance to the specified amount. * This withdraws all money first, then deposits the new amount. - * * @param playerName the player name - * @param amount the new balance + * @param amount the new balance * @return true if the transaction was successful */ public static boolean setBalance(String playerName, double amount) { @@ -376,7 +370,6 @@ public class VaultUtil { /** * Formats an amount according to the economy plugin's settings. - * * @param amount the amount to format * @return the formatted string, or the raw number as string if not available */ @@ -393,7 +386,6 @@ public class VaultUtil { /** * Gets the plural currency name (e.g., "Dollars"). - * * @return the plural currency name, or "coins" if not available */ public static String getCurrencyNamePlural() { @@ -409,7 +401,6 @@ public class VaultUtil { /** * Gets the singular currency name (e.g., "Dollar"). - * * @return the singular currency name, or "coin" if not available */ public static String getCurrencyNameSingular() { @@ -425,7 +416,6 @@ public class VaultUtil { /** * Creates an account for the player if one doesn't exist. - * * @param player the player * @return true if the account was created or already exists */ @@ -435,7 +425,6 @@ public class VaultUtil { /** * Creates an account for the player if one doesn't exist. - * * @param playerName the player name * @return true if the account was created or already exists */ @@ -472,7 +461,6 @@ public class VaultUtil { /** * Gets a list of all bank names. - * * @return list of bank names, or empty list if not available */ @SuppressWarnings("unchecked") @@ -489,7 +477,6 @@ public class VaultUtil { /** * Gets the balance of a bank. - * * @param bankName the bank name * @return the bank balance, or 0 if not available */ @@ -507,9 +494,8 @@ public class VaultUtil { /** * Checks if a bank has at least the specified amount. - * * @param bankName the bank name - * @param amount the amount to check + * @param amount the amount to check * @return true if the bank has at least the specified amount */ public static boolean bankHas(String bankName, double amount) { @@ -526,9 +512,8 @@ public class VaultUtil { /** * Withdraws money from a bank. - * * @param bankName the bank name - * @param amount the amount to withdraw + * @param amount the amount to withdraw * @return true if the transaction was successful */ public static boolean bankWithdraw(String bankName, double amount) { @@ -545,9 +530,8 @@ public class VaultUtil { /** * Deposits money into a bank. - * * @param bankName the bank name - * @param amount the amount to deposit + * @param amount the amount to deposit * @return true if the transaction was successful */ public static boolean bankDeposit(String bankName, double amount) { @@ -564,9 +548,8 @@ public class VaultUtil { /** * Transfers money from one player to another. - * - * @param from the player to take money from - * @param to the player to give money to + * @param from the player to take money from + * @param to the player to give money to * @param amount the amount to transfer * @return true if the transfer was successful */ @@ -576,10 +559,9 @@ public class VaultUtil { /** * Transfers money from one player to another. - * * @param fromPlayerName the player name to take money from - * @param toPlayerName the player name to give money to - * @param amount the amount to transfer + * @param toPlayerName the player name to give money to + * @param amount the amount to transfer * @return true if the transfer was successful */ public static boolean transfer(String fromPlayerName, String toPlayerName, double amount) { diff --git a/src/main/java/noppes/npcs/CustomNpcs.java b/src/main/java/noppes/npcs/CustomNpcs.java index ed084c578..0bc081429 100644 --- a/src/main/java/noppes/npcs/CustomNpcs.java +++ b/src/main/java/noppes/npcs/CustomNpcs.java @@ -86,6 +86,11 @@ import noppes.npcs.entity.EntityNpcDragon; import noppes.npcs.entity.EntityNpcPony; import noppes.npcs.entity.EntityNpcSlime; import noppes.npcs.entity.EntityProjectile; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityAbilitySweeper; import noppes.npcs.entity.old.EntityNPCDwarfFemale; import noppes.npcs.entity.old.EntityNPCDwarfMale; import noppes.npcs.entity.old.EntityNPCElfFemale; @@ -153,7 +158,13 @@ public class CustomNpcs { @SideOnly(Side.CLIENT) private static IDynamicCompiler clientJaninoCompiler; @SideOnly(Side.CLIENT) - private final static Set> clientClassFilterConsumer = new HashSet<>(); + private static Set> clientClassFilterConsumer; + + static { + if (FMLCommonHandler.instance().getEffectiveSide().isClient()) { + clientClassFilterConsumer = new HashSet<>(); + } + } @SideOnly(Side.CLIENT) public static void addClassesToClientClassFilter(Consumer consumer) { @@ -276,6 +287,11 @@ public class CustomNpcs { registerNewEntity(EntityChairMount.class, "CustomNpcChairMount", 64, 10, false); registerNewEntity(EntityProjectile.class, "throwableitem", 64, 3, true); registerNewEntity(EntityMagicProjectile.class, "magicprojectile", 64, 3, true); + registerNewEntity(EntityAbilityOrb.class, "abilityorb", 64, 3, true); + registerNewEntity(EntityAbilityDisc.class, "abilitydisc", 64, 3, true); + registerNewEntity(EntityAbilityLaser.class, "abilitylaser", 64, 3, true); + registerNewEntity(EntityAbilityBeam.class, "abilitybeam", 64, 3, true); + registerNewEntity(EntityAbilitySweeper.class, "abilitysweeper", 64, 3, true); new RecipeController(); @@ -330,7 +346,6 @@ public class CustomNpcs { new SpawnController(); new LinkedNpcController(); new AnimationController(); - new AbilityController(); AbilityController.Instance.load(); TelegraphController.init(); diff --git a/src/main/java/noppes/npcs/CustomNpcsPermissions.java b/src/main/java/noppes/npcs/CustomNpcsPermissions.java index 0d758ad41..6f99dd8fc 100644 --- a/src/main/java/noppes/npcs/CustomNpcsPermissions.java +++ b/src/main/java/noppes/npcs/CustomNpcsPermissions.java @@ -7,6 +7,7 @@ import kamkeel.npcs.util.BukkitUtil; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.entity.player.EntityPlayerMP; import org.apache.logging.log4j.LogManager; +import org.bukkit.entity.Player; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; @@ -66,6 +67,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"); @@ -114,7 +116,7 @@ public class CustomNpcsPermissions { public static final Permission AUCTION_TRADES_MAX = new Permission("customnpcs.auction.trades.*"); public static CustomNpcsPermissions Instance; - private Method hasPermission; + private static boolean isEnabled; public CustomNpcsPermissions() { Instance = this; @@ -130,16 +132,16 @@ public class CustomNpcsPermissions { } try { - hasPermission = BukkitUtil.getPlayerClass().getMethod("hasPermission", String.class); + Class.forName("org.bukkit.entity.Player").getMethod("hasPermission", String.class); + isEnabled = true; + LogManager.getLogger(CustomNpcs.class).info("Bukkit permissions enabled"); LogManager.getLogger(CustomNpcs.class).info("Permissions available:"); Collections.sort(Permission.permissions, String.CASE_INSENSITIVE_ORDER); for (String p : Permission.permissions) { LogManager.getLogger(CustomNpcs.class).info(p); } - } catch (NoSuchMethodException e) { - e.printStackTrace(); - } catch (SecurityException e) { + } catch (Exception e) { e.printStackTrace(); } } @@ -173,17 +175,13 @@ public class CustomNpcsPermissions { } private boolean bukkitPermission(String username, String permission) { - if (hasPermission == null) return false; + if (!BukkitUtil.isEnabled() || !isEnabled) return false; try { Object player = BukkitUtil.getPlayer(username); if (player == null) return false; - return (Boolean) hasPermission.invoke(player, permission); - } catch (IllegalAccessException e) { - e.printStackTrace(); - } catch (IllegalArgumentException e) { - e.printStackTrace(); - } catch (InvocationTargetException e) { + return ((Player) player).hasPermission(permission); + } catch (Exception e) { e.printStackTrace(); } return false; @@ -208,6 +206,6 @@ public class CustomNpcsPermissions { } public static boolean enabled() { - return BukkitUtil.isEnabled(); + return BukkitUtil.isEnabled() && isEnabled; } } diff --git a/src/main/java/noppes/npcs/DataAbilities.java b/src/main/java/noppes/npcs/DataAbilities.java index 8d390f5cc..8c44c482e 100644 --- a/src/main/java/noppes/npcs/DataAbilities.java +++ b/src/main/java/noppes/npcs/DataAbilities.java @@ -3,6 +3,8 @@ package noppes.npcs; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.AbilityController; import kamkeel.npcs.controllers.data.ability.AbilityPhase; +import kamkeel.npcs.controllers.data.ability.AbilitySlot; +import kamkeel.npcs.controllers.data.ability.type.AbilityGuard; import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; import kamkeel.npcs.network.packets.data.telegraph.TelegraphRemovePacket; import kamkeel.npcs.network.packets.data.telegraph.TelegraphSpawnPacket; @@ -17,9 +19,7 @@ import noppes.npcs.scripted.NpcAPI; import noppes.npcs.scripted.event.AbilityEvent; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; -import java.util.Map; import java.util.Random; /** @@ -36,9 +36,9 @@ public class DataAbilities { // ═══════════════════════════════════════════════════════════════════ /** - * List of abilities this NPC can use + * List of ability slots this NPC can use (inline or reference). */ - private List abilities = new ArrayList<>(); + private List abilitySlots = new ArrayList<>(); /** * Whether the ability system is enabled for this NPC @@ -46,9 +46,14 @@ public class DataAbilities { public boolean enabled = false; /** - * Minimum ticks between ability selections (global cooldown) + * Minimum cooldown ticks between abilities (global minimum) */ - public int globalCooldown = 20; + public int minCooldown = 80; + + /** + * Maximum cooldown ticks between abilities (global maximum) + */ + public int maxCooldown = 200; // ═══════════════════════════════════════════════════════════════════ // RUNTIME STATE (not saved) @@ -60,14 +65,9 @@ public class DataAbilities { private transient Ability currentAbility; /** - * Per-ability cooldown timers (ability ID -> world time when cooldown ends) + * World time when NPC cooldown ends (can select next ability) */ - private transient Map cooldowns = new HashMap<>(); - - /** - * Ticks until next ability can be selected (global cooldown timer) - */ - private transient int globalCooldownTimer = 0; + private transient long cooldownEndTime = 0; /** * Last target used for ability execution @@ -79,6 +79,16 @@ public class DataAbilities { */ private transient List recentHitTimes = new ArrayList<>(); + /** + * Locked rotation values for ACTIVE phase when movement is locked. + * Stored when entering ACTIVE phase, applied every tick to prevent rotation changes. + */ + private transient boolean rotationLocked = false; + private transient float lockedYaw = 0; + private transient float lockedYawHead = 0; + private transient float lockedRenderYawOffset = 0; + private transient float lockedPitch = 0; + // ═══════════════════════════════════════════════════════════════════ // CONSTRUCTOR // ═══════════════════════════════════════════════════════════════════ @@ -100,11 +110,6 @@ public class DataAbilities { return; } - // Tick global cooldown - if (globalCooldownTimer > 0) { - globalCooldownTimer--; - } - // Tick current ability if executing if (currentAbility != null && currentAbility.isExecuting()) { tickCurrentAbility(); @@ -142,9 +147,15 @@ public class DataAbilities { // Remove telegraph - it has served its purpose removeTelegraph(currentAbility); + // Lock rotation if movement is locked during ACTIVE phase + // Capture current rotation values to force them every tick + if (currentAbility.isMovementLockedDuringActive()) { + captureLockedRotation(); + } + // Play active sound and animation playAbilitySound(currentAbility.getActiveSound()); - playAbilityAnimation(currentAbility.getActiveAnimationId()); + playAbilityAnimation(currentAbility.getActiveAnimation()); // Fire execute event (cancelable) AbilityEvent.ExecuteEvent executeEvent = new AbilityEvent.ExecuteEvent( @@ -156,59 +167,103 @@ public class DataAbilities { // Call onExecute currentAbility.onExecute(npc, target, npc.worldObj); + + // Check if ability completed during onExecute (signalCompletion was called) + if (currentAbility.getPhase() == AbilityPhase.IDLE) { + handleAbilityCompletion(target); + return; + } } currentAbility.onActiveTick(npc, target, npc.worldObj, currentAbility.getCurrentTick()); + + // Check if ability completed during onActiveTick (signalCompletion was called) + if (currentAbility.getPhase() == AbilityPhase.IDLE) { + handleAbilityCompletion(target); + return; + } break; - case RECOVERY: - // Recovery phase - just wait + case DAZED: + // Dazed phase - NPC cannot attack, just wait for daze to end break; case IDLE: - // Ability completed - currentAbility.onComplete(npc, target); - - // Fire complete event - AbilityEvent.CompleteEvent completeEvent = new AbilityEvent.CompleteEvent( - npc.wrappedNPC, currentAbility, target); - NpcAPI.EVENT_BUS.post(completeEvent); - - onAbilityComplete(); + // Ability completed (reached via tick() phase transition from DAZED) + handleAbilityCompletion(target); break; } - // Clear AI pathfinding if ability is controlling movement - // But DON'T zero motion - let the ability set it if needed + // Apply movement control if ability is still executing if (currentAbility != null && currentAbility.isExecuting()) { - if (isAbilityControllingMovement()) { - // Clear navigator so AI doesn't fight with ability movement - npc.getNavigator().clearPathEntity(); + applyMovementControl(); + } + } - // Only zero motion if lockMovement is true AND ability doesn't have its own movement - // This allows stationary abilities to freeze the NPC, while movement abilities can set their own motion - if (currentAbility.isLockMovement() && !currentAbility.hasAbilityMovement()) { - npc.motionX = 0; - npc.motionZ = 0; - } + /** + * Apply movement control based on lock movement settings. + * Called every tick during ability execution. + */ + private void applyMovementControl() { + if (currentAbility == null) return; + + // Check if movement should be locked for the current phase + if (currentAbility.isMovementLockedForCurrentPhase()) { + // Clear navigator to prevent AI pathfinding + npc.getNavigator().clearPathEntity(); + + // Zero motion if ability doesn't have its own movement + // This allows abilities like Charge/Slam to control their own motion + if (!currentAbility.hasAbilityMovement()) { + npc.motionX = 0; + npc.motionZ = 0; } } } + /** + * Handle ability completion - called when ability phase becomes IDLE. + * This can happen from: + * - signalCompletion() called in onExecute() or onActiveTick() + * - tick() transitioning from DAZED to IDLE + */ + private void handleAbilityCompletion(EntityLivingBase target) { + if (currentAbility == null) return; + + // Call onComplete callback + currentAbility.onComplete(npc, target); + + // Fire complete event + AbilityEvent.CompleteEvent completeEvent = new AbilityEvent.CompleteEvent( + npc.wrappedNPC, currentAbility, target); + NpcAPI.EVENT_BUS.post(completeEvent); + + // Release rotation lock before cleanup + releaseLockedRotation(); + + // Clean up and roll cooldown (this sets currentAbility to null) + onAbilityComplete(); + } + /** * Called when an ability completes. + * Calculates cooldown as: random(minCooldown, maxCooldown) + ability's cooldown offset */ private void onAbilityComplete() { if (currentAbility != null) { // Stop any ability animation stopAbilityAnimation(); - // Start per-ability cooldown - startCooldown(currentAbility); + // Release rotation lock + releaseLockedRotation(); + + // Calculate cooldown: random(min, max) + ability offset + int abilityCooldownOffset = currentAbility.getCooldownTicks(); + rollCooldown(); + // Add ability-specific cooldown offset + cooldownEndTime += abilityCooldownOffset; + currentAbility = null; lastTarget = null; - - // Start global cooldown - globalCooldownTimer = globalCooldown; } } @@ -240,13 +295,14 @@ public class DataAbilities { * Check if an ability can be selected right now. */ public boolean canSelectAbility() { - if (!enabled || abilities.isEmpty()) { + if (!enabled || abilitySlots.isEmpty()) { return false; } if (currentAbility != null && currentAbility.isExecuting()) { return false; } - if (globalCooldownTimer > 0) { + // Check if still on cooldown + if (npc.worldObj.getTotalWorldTime() < cooldownEndTime) { return false; } return true; @@ -259,7 +315,7 @@ public class DataAbilities { List eligible = new ArrayList<>(); int totalWeight = 0; - for (Ability ability : abilities) { + for (Ability ability : getAbilities()) { if (isAbilityEligible(ability, target)) { eligible.add(ability); totalWeight += ability.getWeight(); @@ -292,11 +348,6 @@ public class DataAbilities { return false; } - // Check cooldown - if (isOnCooldown(ability)) { - return false; - } - // Check if already executing if (ability.isExecuting()) { return false; @@ -341,7 +392,7 @@ public class DataAbilities { playAbilitySound(ability.getWindUpSound()); // Play wind up animation if configured - playAbilityAnimation(ability.getWindUpAnimationId()); + playAbilityAnimation(ability.getWindUpAnimation()); return true; } @@ -357,17 +408,25 @@ public class DataAbilities { /** * Play an animation on the NPC by ID. + * Public so abilities can trigger animations directly if needed. + * + * @param animation The animation to play, or null if none */ - private void playAbilityAnimation(int animationId) { - if (animationId < 0) return; + public void playAbilityAnimation(Animation animation) { + if (animation == null) return; if (AnimationController.Instance == null) return; - Animation animation = AnimationController.Instance.animations.get(animationId); - if (animation != null) { - npc.display.animationData.setEnabled(true); - npc.display.animationData.setAnimation(animation); - npc.display.animationData.updateClient(); - } + npc.display.animationData.setEnabled(true); + npc.display.animationData.setAnimation(animation); + npc.display.animationData.updateClient(); + } + + public void playAbilityAnimation(int animation) { + if (animation < 0) return; + if (AnimationController.Instance == null) return; + if (AnimationController.Instance.get(animation) == null) return; + + playAbilityAnimation((Animation) AnimationController.Instance.get(animation)); } /** @@ -406,37 +465,25 @@ public class DataAbilities { // ═══════════════════════════════════════════════════════════════════ /** - * Check if an ability is on cooldown. + * Check if NPC is on cooldown (cannot use any ability). */ - public boolean isOnCooldown(Ability ability) { - Long endTime = cooldowns.get(ability.getId()); - if (endTime == null) { - return false; - } - return npc.worldObj.getTotalWorldTime() < endTime; + public boolean isOnCooldown() { + return npc.worldObj.getTotalWorldTime() < cooldownEndTime; } /** - * Start cooldown for an ability. + * Get remaining cooldown ticks. */ - private void startCooldown(Ability ability) { - long endTime = npc.worldObj.getTotalWorldTime() + ability.getCooldownTicks(); - cooldowns.put(ability.getId(), endTime); + public long getRemainingCooldown() { + long remaining = cooldownEndTime - npc.worldObj.getTotalWorldTime(); + return remaining > 0 ? remaining : 0; } /** - * Reset cooldown for a specific ability. + * Reset cooldown (allow immediate ability use). */ - public void resetCooldown(String abilityId) { - cooldowns.remove(abilityId); - } - - /** - * Reset all cooldowns. - */ - public void resetAllCooldowns() { - cooldowns.clear(); - globalCooldownTimer = 0; + public void resetCooldown() { + cooldownEndTime = 0; } // ═══════════════════════════════════════════════════════════════════ @@ -444,7 +491,7 @@ public class DataAbilities { // ═══════════════════════════════════════════════════════════════════ /** - * Handle damage taken - may interrupt current ability. + * Handle damage taken - may interrupt current ability or trigger guard counter. * * @param source The damage source * @param amount The damage amount @@ -454,6 +501,14 @@ public class DataAbilities { // Track hit for hit count condition recordHit(); + // Check if currently executing a Guard ability and notify it of damage taken + if (currentAbility instanceof AbilityGuard) { + AbilityGuard guard = (AbilityGuard) currentAbility; + if (guard.isGuarding() && source.getEntity() instanceof EntityLivingBase) { + guard.onDamageTaken(npc, (EntityLivingBase) source.getEntity(), source, amount); + } + } + if (currentAbility == null || !currentAbility.isExecuting()) { return false; } @@ -466,11 +521,32 @@ public class DataAbilities { return false; } + /** + * Get the damage reduction factor if a Guard ability is currently active. + * + * @return The damage reduction factor (0.0 = no reduction, 1.0 = full immunity), or 0 if not guarding + */ + public float getGuardDamageReduction() { + if (currentAbility instanceof AbilityGuard) { + return ((AbilityGuard) currentAbility).getDamageReductionFactor(); + } + return 0.0f; + } + /** * Record a hit for the hit count condition. + * Includes periodic cleanup to prevent memory leak. */ private void recordHit() { - recentHitTimes.add(npc.worldObj.getTotalWorldTime()); + long currentTime = npc.worldObj.getTotalWorldTime(); + recentHitTimes.add(currentTime); + + // Cleanup old entries periodically to prevent unbounded growth + // Keep only hits from last 5 minutes (6000 ticks) max + if (recentHitTimes.size() > 50) { + long cutoff = currentTime - 6000; + recentHitTimes.removeIf(time -> time < cutoff); + } } /** @@ -496,6 +572,8 @@ public class DataAbilities { /** * Interrupt the currently executing ability. + * If interrupted during WINDUP, the ability will transition to DAZED phase. + * When DAZED completes, the ability will move to IDLE and onAbilityComplete() is called. */ public void interruptCurrentAbility(DamageSource source, float damage) { if (currentAbility != null) { @@ -505,15 +583,19 @@ public class DataAbilities { // Stop any ability animation stopAbilityAnimation(); + // Release rotation lock + releaseLockedRotation(); + // Fire interrupt event AbilityEvent.InterruptEvent interruptEvent = new AbilityEvent.InterruptEvent( npc.wrappedNPC, currentAbility, lastTarget, source, damage); NpcAPI.EVENT_BUS.post(interruptEvent); currentAbility.onInterrupt(npc, source, damage); - currentAbility.interrupt(); - currentAbility = null; - lastTarget = null; + currentAbility.interrupt(); // Transitions to DAZED if interrupted during WINDUP + + // Don't clear currentAbility - let it tick through DAZED phase + // When DAZED ends and phase becomes IDLE, onAbilityComplete() will be called } } @@ -524,12 +606,57 @@ public class DataAbilities { if (currentAbility != null) { removeTelegraph(currentAbility); stopAbilityAnimation(); + releaseLockedRotation(); currentAbility.interrupt(); currentAbility = null; lastTarget = null; } } + /** + * Force start a specific ability, bypassing normal selection and cooldown. + * If an ability is currently executing, it will be cancelled. + * + * @param ability The ability to start + * @param target The target entity (can be null for self-targeted abilities) + * @return true if the ability was started successfully + */ + public boolean forceStartAbility(Ability ability, EntityLivingBase target) { + if (ability == null || npc.worldObj.isRemote) { + return false; + } + + // Stop any currently executing ability + stopCurrentAbility(); + + // Reset the ability state + ability.reset(); + + // Start the ability directly + return startAbility(ability, target); + } + + /** + * Execute an ability on this NPC by key (built-in name or custom UUID). + * The NPC does NOT need to have this ability assigned. + * + * @param key The ability key (built-in name or custom UUID) + * @param target The target entity (can be null for self-targeted abilities) + * @return true if the ability was started successfully + */ + public boolean executeAbility(String key, EntityLivingBase target) { + if (key == null || key.isEmpty() || npc.worldObj.isRemote) { + return false; + } + + Ability resolved = AbilityController.Instance.resolveAbility(key); + if (resolved == null) { + return false; + } + + return forceStartAbility(resolved, target); + } + // ═══════════════════════════════════════════════════════════════════ // SCRIPT EVENTS // ═══════════════════════════════════════════════════════════════════ @@ -553,7 +680,7 @@ public class DataAbilities { * @param hitEntity The entity being hit * @param damage The damage amount * @param knockback The horizontal knockback - * @param knockbackUp The vertical knockback (deprecated, ignored) + * @param knockbackUp The vertical knockback * @return The event (with possibly modified values), or null if cancelled */ public AbilityEvent.HitEvent fireHitEvent(Ability ability, EntityLivingBase target, @@ -581,54 +708,109 @@ public class DataAbilities { /** * Reset all runtime state (cooldowns, current ability). * Called when NPC respawns or combat ends. + * Rolls cooldown so NPC doesn't immediately use an ability. */ public void reset() { stopCurrentAbility(); - resetAllCooldowns(); - // Reset execution state on all abilities - for (Ability ability : abilities) { + // Roll cooldown so NPC doesn't immediately attack after reset + rollCooldown(); + + // Reset execution state on all resolved abilities + for (Ability ability : getAbilities()) { ability.reset(); } } + /** + * Roll a new cooldown using the min/max range. + * Called after ability completes and on reset. + */ + private void rollCooldown() { + int baseCooldown = minCooldown; + if (maxCooldown > minCooldown) { + baseCooldown = minCooldown + random.nextInt(maxCooldown - minCooldown + 1); + } + cooldownEndTime = npc.worldObj.getTotalWorldTime() + baseCooldown; + } + // ═══════════════════════════════════════════════════════════════════ // ABILITY LIST MANAGEMENT // ═══════════════════════════════════════════════════════════════════ + /** + * Get resolved abilities from all slots, filtering out broken references. + */ public List getAbilities() { - return abilities; + List resolved = new ArrayList<>(); + for (AbilitySlot slot : abilitySlots) { + Ability a = slot.getAbility(); + if (a != null) { + resolved.add(a); + } + } + return resolved; } + /** Get the raw slot list (for GUI access). */ + public List getAbilitySlots() { + return abilitySlots; + } + + /** Add an inline ability. */ public void addAbility(Ability ability) { - abilities.add(ability); + abilitySlots.add(AbilitySlot.inline(ability)); + } + + /** Add a reference ability by key (built-in name or custom UUID). */ + public void addAbilityReference(String key) { + abilitySlots.add(AbilitySlot.reference(key)); } public void removeAbility(int index) { - if (index >= 0 && index < abilities.size()) { - abilities.remove(index); + if (index >= 0 && index < abilitySlots.size()) { + abilitySlots.remove(index); } } public void removeAbility(String id) { - abilities.removeIf(a -> a.getId().equals(id)); + abilitySlots.removeIf(slot -> { + if (slot.isReference()) { + return slot.getReferenceId().equals(id); + } + Ability a = slot.getAbility(); + return a != null && a.getId().equals(id); + }); } public Ability getAbility(String id) { - for (Ability ability : abilities) { - if (ability.getId().equals(id)) { - return ability; + for (AbilitySlot slot : abilitySlots) { + Ability a = slot.getAbility(); + if (a != null && a.getId().equals(id)) { + return a; } } return null; } + /** Check if a slot at a given index is a reference. */ + public boolean isSlotReference(int index) { + if (index < 0 || index >= abilitySlots.size()) return false; + return abilitySlots.get(index).isReference(); + } + + /** Convert a reference slot to inline. Returns false if resolution fails. */ + public boolean convertToInline(int index) { + if (index < 0 || index >= abilitySlots.size()) return false; + return abilitySlots.get(index).convertToInline(); + } + public void clearAbilities() { - abilities.clear(); + abilitySlots.clear(); } public boolean isEmpty() { - return abilities.isEmpty(); + return abilitySlots.isEmpty(); } // ═══════════════════════════════════════════════════════════════════ @@ -654,9 +836,9 @@ public class DataAbilities { * Called by AI tasks to determine if they should try to path. *

* Rules: - * - WINDUP phase: Block AI if lockMovement is true - * - ACTIVE phase: Block AI if lockMovement OR hasAbilityMovement is true - * - RECOVERY phase: Block AI if lockMovement is true + * - WINDUP phase: Block AI if movement is locked during windup + * - ACTIVE phase: Block AI if movement is locked during active OR hasAbilityMovement is true + * - DAZED phase: Always block AI (NPC is stunned) */ public boolean isAbilityControllingMovement() { if (currentAbility == null || !currentAbility.isExecuting()) { @@ -667,19 +849,19 @@ public class DataAbilities { switch (phase) { case WINDUP: - // During windup, only block if lockMovement is set - // This allows NPC to keep chasing while winding up if lockMovement=false - return currentAbility.isLockMovement(); + // During windup, only block if movement is locked during windup + // This allows NPC to keep chasing while winding up if not locked + return currentAbility.isMovementLockedDuringWindup(); case ACTIVE: // During active phase, block if either: - // - lockMovement is true (stationary ability) + // - movement is locked during active (stationary ability) // - hasAbilityMovement is true (ability is moving the NPC itself) - return currentAbility.isLockMovement() || currentAbility.hasAbilityMovement(); + return currentAbility.isMovementLockedDuringActive() || currentAbility.hasAbilityMovement(); - case RECOVERY: - // During recovery, block if lockMovement is set - return currentAbility.isLockMovement(); + case DAZED: + // During dazed phase, always block - NPC is stunned from interrupt + return true; default: return false; @@ -687,13 +869,21 @@ public class DataAbilities { } /** - * Check if NPC movement should be blocked due to ability execution. - * - * @deprecated Use isAbilityControllingMovement() instead + * Check if NPC's look direction should be locked to target. + * Only locks during ACTIVE phase when movement is locked during active. + * This allows the NPC to freely track target during WINDUP and DAZED. */ - @Deprecated - public boolean isMovementBlocked() { - return isAbilityControllingMovement(); + public boolean shouldLockLookDirection() { + if (currentAbility == null || !currentAbility.isExecuting()) { + return false; + } + + // Only lock look direction during ACTIVE phase + if (currentAbility.getPhase() != AbilityPhase.ACTIVE) { + return false; + } + + return currentAbility.isMovementLockedDuringActive(); } /** @@ -707,17 +897,72 @@ public class DataAbilities { return phase == AbilityPhase.WINDUP || phase == AbilityPhase.ACTIVE; } + // ═══════════════════════════════════════════════════════════════════ + // ROTATION LOCKING + // ═══════════════════════════════════════════════════════════════════ + + /** + * Capture current rotation values to lock NPC's look direction. + * Called when entering ACTIVE phase with movement locked during active. + */ + private void captureLockedRotation() { + lockedYaw = npc.rotationYaw; + lockedYawHead = npc.rotationYawHead; + lockedRenderYawOffset = npc.renderYawOffset; + lockedPitch = npc.rotationPitch; + rotationLocked = true; + } + + /** + * Release the rotation lock. + * Called when leaving ACTIVE phase or ability completes. + */ + private void releaseLockedRotation() { + rotationLocked = false; + } + + /** + * Apply locked rotation values to the NPC. + * Called AFTER super.onLivingUpdate() in EntityNPCInterface to override + * any rotation changes made by the look helper or AI tasks. + */ + public void applyLockedRotation() { + if (!rotationLocked) { + return; + } + + // Force rotation back to locked values + npc.rotationYaw = lockedYaw; + npc.rotationYawHead = lockedYawHead; + npc.renderYawOffset = lockedRenderYawOffset; + npc.rotationPitch = lockedPitch; + + // Also set prev values to prevent interpolation jitter + npc.prevRotationYaw = lockedYaw; + npc.prevRotationYawHead = lockedYawHead; + npc.prevRenderYawOffset = lockedRenderYawOffset; + npc.prevRotationPitch = lockedPitch; + } + + /** + * Check if rotation is currently locked. + */ + public boolean isRotationLocked() { + return rotationLocked; + } + // ═══════════════════════════════════════════════════════════════════ // NBT SERIALIZATION // ═══════════════════════════════════════════════════════════════════ public NBTTagCompound writeToNBT(NBTTagCompound compound) { compound.setBoolean("AbilitiesEnabled", enabled); - compound.setInteger("AbilityGlobalCooldown", globalCooldown); + compound.setInteger("AbilityMinCooldown", minCooldown); + compound.setInteger("AbilityMaxCooldown", maxCooldown); NBTTagList abilityList = new NBTTagList(); - for (Ability ability : abilities) { - abilityList.appendTag(ability.writeNBT()); + for (AbilitySlot slot : abilitySlots) { + abilityList.appendTag(slot.writeNBT()); } compound.setTag("Abilities", abilityList); @@ -726,16 +971,16 @@ public class DataAbilities { public void readFromNBT(NBTTagCompound compound) { enabled = compound.getBoolean("AbilitiesEnabled"); - globalCooldown = compound.hasKey("AbilityGlobalCooldown") ? - compound.getInteger("AbilityGlobalCooldown") : 20; + minCooldown = compound.getInteger("AbilityMinCooldown"); + maxCooldown = compound.getInteger("AbilityMaxCooldown"); - abilities.clear(); + abilitySlots.clear(); NBTTagList abilityList = compound.getTagList("Abilities", 10); for (int i = 0; i < abilityList.tagCount(); i++) { NBTTagCompound abilityNBT = abilityList.getCompoundTagAt(i); - Ability ability = AbilityController.Instance.fromNBT(abilityNBT); - if (ability != null) { - abilities.add(ability); + AbilitySlot slot = AbilitySlot.fromNBT(abilityNBT); + if (slot != null) { + abilitySlots.add(slot); } } } diff --git a/src/main/java/noppes/npcs/EventHooks.java b/src/main/java/noppes/npcs/EventHooks.java index 5e086abce..5f9d1388f 100644 --- a/src/main/java/noppes/npcs/EventHooks.java +++ b/src/main/java/noppes/npcs/EventHooks.java @@ -62,6 +62,7 @@ import noppes.npcs.scripted.event.RecipeScriptEvent; import noppes.npcs.scripted.event.player.CustomGuiEvent; import noppes.npcs.scripted.event.player.DialogEvent; import noppes.npcs.scripted.event.player.FactionEvent; +import noppes.npcs.scripted.event.player.PlayerAbilityEvent; import noppes.npcs.scripted.event.player.PlayerEvent; import noppes.npcs.scripted.event.player.PlayerEvent.ChatEvent; import noppes.npcs.scripted.event.player.PlayerEvent.ContainerOpen; @@ -692,6 +693,11 @@ public class EventHooks { return NpcAPI.EVENT_BUS.post(event); } + public static boolean onPlayerAbilityHit(PlayerDataScript handler, PlayerAbilityEvent.HitEvent event) { + handler.callScript(EnumScriptType.ABILITY_HIT, event); + return NpcAPI.EVENT_BUS.post(event); + } + public static void onPlayerChangeDim(PlayerDataScript handler, IPlayer player, int fromDim, int toDim) { PlayerEvent.ChangedDimension event = new PlayerEvent.ChangedDimension(player, fromDim, toDim); handler.callScript(EnumScriptType.CHANGED_DIM, event); diff --git a/src/main/java/noppes/npcs/NoppesUtilPlayer.java b/src/main/java/noppes/npcs/NoppesUtilPlayer.java index 90e3c91ec..7f9753104 100644 --- a/src/main/java/noppes/npcs/NoppesUtilPlayer.java +++ b/src/main/java/noppes/npcs/NoppesUtilPlayer.java @@ -306,18 +306,18 @@ public class NoppesUtilPlayer { if (dialog == null) return; DialogOption option = dialog.options.get(optionId); - if (EventHooks.onDialogOption(player, new DialogEvent.DialogOption((IPlayer) NpcAPI.Instance().getIEntity(player), dialog, optionId))) + if (EventHooks.onDialogOption(player, new DialogEvent.DialogOption((IPlayer) NpcAPI.Instance().getIEntity(player), dialogId, optionId, dialog))) return; if (!npc.isRemote()) { EventHooks.onNPCDialogClosed(npc, player, dialogId, optionId + 1, dialog); if (!dialog.hasDialogs(player) && !dialog.hasOtherOptions()) { - EventHooks.onDialogClosed(player, new DialogEvent.DialogClosed((IPlayer) NpcAPI.Instance().getIEntity(player), dialog)); + EventHooks.onDialogClosed(player, new DialogEvent.DialogClosed((IPlayer) NpcAPI.Instance().getIEntity(player), dialogId, optionId, dialog)); return; } if (option == null || option.optionType == EnumOptionType.DialogOption && (!option.isAvailable(player) || !option.hasDialog()) || option.optionType == EnumOptionType.Disabled || option.optionType == EnumOptionType.QuitOption) { - EventHooks.onDialogClosed(player, new DialogEvent.DialogClosed((IPlayer) NpcAPI.Instance().getIEntity(player), dialog)); + EventHooks.onDialogClosed(player, new DialogEvent.DialogClosed((IPlayer) NpcAPI.Instance().getIEntity(player), dialogId, optionId, dialog)); return; } } diff --git a/src/main/java/noppes/npcs/NoppesUtilServer.java b/src/main/java/noppes/npcs/NoppesUtilServer.java index 08c04f6f4..0799e7a6c 100644 --- a/src/main/java/noppes/npcs/NoppesUtilServer.java +++ b/src/main/java/noppes/npcs/NoppesUtilServer.java @@ -163,7 +163,16 @@ public class NoppesUtilServer { for (Animation animation : AnimationController.getInstance().animations.values()) { map.put(animation.name, animation.id); } - sendScrollData(player, map, EnumScrollData.OPTIONAL); + sendScrollData(player, map, EnumScrollData.ANIMATIONS); + } + + public static void sendBuiltInAnimationData(EntityPlayerMP player) { + Map map = new HashMap(); + for (String name : AnimationController.getInstance().builtInAnimations.keySet()) { + // Use -1 as a marker for built-in animations (they have no ID) + map.put(AnimationController.getInstance().builtInAnimations.get(name).getName(), -1); + } + sendScrollData(player, map, EnumScrollData.BUILTIN_ANIMATIONS); } public static void sendTagDataAll(EntityPlayerMP player) { @@ -241,7 +250,7 @@ public class NoppesUtilServer { if (EventHooks.onNPCDialog(npc, player, dialog.id, optionId, dialog)) { return; } - if (EventHooks.onDialogOpen(player, new DialogEvent.DialogOpen((IPlayer) NpcAPI.Instance().getIEntity(player), dialog))) { + if (EventHooks.onDialogOpen(player, new DialogEvent.DialogOpen((IPlayer) NpcAPI.Instance().getIEntity(player), dialog.id, optionId, dialog))) { return; } } @@ -776,6 +785,20 @@ public class NoppesUtilServer { sendScrollData(player, map, EnumScrollData.ABILITY_TYPES); } + public static void sendCustomAbilitiesData(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 uuid : controller.getCustomAbilityIds()) { + String name = controller.getCustomAbilityName(uuid); + if (name == null || name.isEmpty()) name = uuid; + map.put(name + "\t" + uuid, index++); + } + } + sendScrollData(player, map, EnumScrollData.CUSTOM_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/ScriptPlayerEventHandler.java b/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java index a37e6ba08..0541f64c4 100644 --- a/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java +++ b/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java @@ -97,6 +97,13 @@ public class ScriptPlayerEventHandler { if (PlayerDataController.Instance != null) { PlayerData playerData = PlayerData.get(player); playerData.actionManager.tick(); + playerData.abilityData.tick(player); + + // Lock player movement during ability phases that require it + if (playerData.abilityData.isMovementLocked()) { + player.motionX = 0; + player.motionZ = 0; + } if (playerData.updateClient) { SyncController.syncPlayerData((EntityPlayerMP) player, true); diff --git a/src/main/java/noppes/npcs/ai/CombatHandler.java b/src/main/java/noppes/npcs/ai/CombatHandler.java index e3eb85fbe..750725551 100644 --- a/src/main/java/noppes/npcs/ai/CombatHandler.java +++ b/src/main/java/noppes/npcs/ai/CombatHandler.java @@ -179,10 +179,10 @@ public class CombatHandler { } /** - * Check if NPC movement should be blocked due to ability execution. + * Check if ability is controlling NPC movement (AI pathfinding should be blocked). */ - public boolean isMovementBlocked() { - return npc.abilities.isMovementBlocked(); + public boolean isAbilityControllingMovement() { + return npc.abilities.isAbilityControllingMovement(); } /** diff --git a/src/main/java/noppes/npcs/client/ClientProxy.java b/src/main/java/noppes/npcs/client/ClientProxy.java index 6a65e71b6..42d5808a1 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; @@ -146,6 +147,17 @@ import noppes.npcs.client.renderer.RenderNpcCrystal; import noppes.npcs.client.renderer.RenderNpcDragon; import noppes.npcs.client.renderer.RenderNpcSlime; import noppes.npcs.client.renderer.RenderProjectile; +import kamkeel.npcs.client.renderer.RenderAbilityOrb; +import kamkeel.npcs.client.renderer.RenderAbilityDisc; +import kamkeel.npcs.client.renderer.RenderAbilityLaser; +import kamkeel.npcs.client.renderer.RenderAbilityBeam; +import kamkeel.npcs.client.renderer.RenderAbilitySweeper; +import kamkeel.npcs.client.renderer.lightning.LightningHandler; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityAbilitySweeper; import noppes.npcs.client.renderer.blocks.BlockBannerRenderer; import noppes.npcs.client.renderer.blocks.BlockBarrelRenderer; import noppes.npcs.client.renderer.blocks.BlockBeamRenderer; @@ -253,6 +265,11 @@ public class ClientProxy extends CommonProxy { RenderingRegistry.registerEntityRenderingHandler(EntityNpcDragon.class, new RenderNpcDragon(new ModelNpcDragon(0.0F), 0.5F)); RenderingRegistry.registerEntityRenderingHandler(EntityNpcSlime.class, new RenderNpcSlime(new ModelNpcSlime(16), new ModelNpcSlime(0), 0.25F)); RenderingRegistry.registerEntityRenderingHandler(EntityProjectile.class, new RenderProjectile()); + RenderingRegistry.registerEntityRenderingHandler(EntityAbilityOrb.class, new RenderAbilityOrb()); + RenderingRegistry.registerEntityRenderingHandler(EntityAbilityDisc.class, new RenderAbilityDisc()); + RenderingRegistry.registerEntityRenderingHandler(EntityAbilityLaser.class, new RenderAbilityLaser()); + RenderingRegistry.registerEntityRenderingHandler(EntityAbilityBeam.class, new RenderAbilityBeam()); + RenderingRegistry.registerEntityRenderingHandler(EntityAbilitySweeper.class, new RenderAbilitySweeper()); RenderingRegistry.registerEntityRenderingHandler(EntityCustomNpc.class, new RenderCustomNpc()); @@ -305,7 +322,7 @@ public class ClientProxy extends CommonProxy { Minecraft mc = Minecraft.getMinecraft(); NPCButton = new KeyBinding("NPC Inventory", Keyboard.KEY_N, "key.categories.customnpc"); - SpecialKey = new KeyBinding("key.customnpcs.special", Keyboard.KEY_C, "key.categories.customnpc"); + SpecialKey = new KeyBinding("key.customnpcs.special", Keyboard.KEY_B, "key.categories.customnpc"); ClientRegistry.registerKeyBinding(NPCButton); ClientRegistry.registerKeyBinding(SpecialKey); @@ -326,6 +343,9 @@ public class ClientProxy extends CommonProxy { TelegraphManager.initClient(); MinecraftForge.EVENT_BUS.register(new TelegraphRenderer()); + // Lightning effect rendering system + MinecraftForge.EVENT_BUS.register(new LightningHandler()); + if (ConfigClient.InventoryGuiEnabled) { MinecraftForge.EVENT_BUS.register(new TabRegistry()); if (TabRegistry.getTabList().isEmpty()) { @@ -480,6 +500,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/ClientTickHandler.java b/src/main/java/noppes/npcs/client/ClientTickHandler.java index 6142de16c..203cef3e2 100644 --- a/src/main/java/noppes/npcs/client/ClientTickHandler.java +++ b/src/main/java/noppes/npcs/client/ClientTickHandler.java @@ -5,6 +5,7 @@ import cpw.mods.fml.common.eventhandler.SubscribeEvent; import cpw.mods.fml.common.gameevent.InputEvent; import cpw.mods.fml.common.gameevent.TickEvent; import cpw.mods.fml.common.gameevent.TickEvent.Phase; +import kamkeel.npcs.client.renderer.lightning.LightningBolt; import kamkeel.npcs.controllers.data.telegraph.TelegraphManager; import kamkeel.npcs.network.PacketClient; import kamkeel.npcs.network.packets.player.CheckPlayerValue; @@ -115,6 +116,9 @@ public class ClientTickHandler { if (TelegraphManager.ClientInstance != null) { TelegraphManager.ClientInstance.tick(mc.theWorld); } + + // Update lightning bolts for ability effects + LightningBolt.updateAll(); if (Minecraft.getMinecraft().thePlayer != null && (prevWidth != mc.displayWidth || prevHeight != mc.displayHeight)) { prevWidth = mc.displayWidth; prevHeight = mc.displayHeight; diff --git a/src/main/java/noppes/npcs/client/gui/SubGuiColorSelector.java b/src/main/java/noppes/npcs/client/gui/SubGuiColorSelector.java index 947825f5a..be8c07f8a 100644 --- a/src/main/java/noppes/npcs/client/gui/SubGuiColorSelector.java +++ b/src/main/java/noppes/npcs/client/gui/SubGuiColorSelector.java @@ -25,10 +25,10 @@ public class SubGuiColorSelector extends SubGuiInterface implements ITextfieldLi public int color; public SubGuiColorSelector(int color) { - xSize = 176; + xSize = 182; ySize = 222; this.color = color; - setBackground("smallbg.png"); + setBackground("menubg.png"); } @Override @@ -40,7 +40,7 @@ public class SubGuiColorSelector extends SubGuiInterface implements ITextfieldLi this.addTextField(textfield = new GuiNpcTextField(0, this, guiLeft + 53, guiTop + 20, 70, 20, getColor())); textfield.setTextColor(color); - addButton(new GuiNpcButton(66, guiLeft + 112, guiTop + 198, 60, 20, "gui.done")); + addButton(new GuiNpcButton(66, guiLeft + 112, guiTop + 175, 60, 20, "gui.done")); } public String getColor() { diff --git a/src/main/java/noppes/npcs/client/gui/SubGuiNpcDialogVisual.java b/src/main/java/noppes/npcs/client/gui/SubGuiNpcDialogVisual.java index 72481f133..807a3e719 100644 --- a/src/main/java/noppes/npcs/client/gui/SubGuiNpcDialogVisual.java +++ b/src/main/java/noppes/npcs/client/gui/SubGuiNpcDialogVisual.java @@ -98,7 +98,7 @@ public class SubGuiNpcDialogVisual extends SubGuiInterface implements ISubGuiLis getTextField(18).setVisible(dialog.renderGradual); getLabel(18).enabled = dialog.renderGradual; - addTextField(new GuiNpcTextField(19, this, guiLeft + 125, y += 22, 40, 20, String.valueOf(dialog.textPitch))); + addTextField(new GuiNpcTextField(19, this, guiLeft + 125, y += 22, 40, 20, "" + dialog.textPitch)); addLabel(new GuiNpcLabel(19, "dialog.textPitch", guiLeft + 4, y + 5)); getTextField(19).floatsOnly = true; getTextField(19).setVisible(dialog.renderGradual); @@ -110,48 +110,48 @@ public class SubGuiNpcDialogVisual extends SubGuiInterface implements ISubGuiLis addButton(new GuiNpcButton(24, guiLeft + 192, y, 50, 20, new String[]{"display.bottom", "display.top"}, dialog.alignment)); addLabel(new GuiNpcLabel(27, "display.alignment", guiLeft + 126, y + 5)); - addTextField(new GuiNpcTextField(11, this, guiLeft + 120, y += 25, 40, 20, String.valueOf(dialog.textWidth))); - addTextField(new GuiNpcTextField(12, this, guiLeft + 165, y, 40, 20, String.valueOf(dialog.textHeight))); + addTextField(new GuiNpcTextField(11, this, guiLeft + 120, y += 25, 40, 20, "" + dialog.textWidth)); + addTextField(new GuiNpcTextField(12, this, guiLeft + 165, y, 40, 20, "" + dialog.textHeight)); addLabel(new GuiNpcLabel(12, "gui.widthHeight", guiLeft + 4, y + 5)); getTextField(11).integersOnly = true; getTextField(11).setMinMaxDefault(-Integer.MAX_VALUE, Integer.MAX_VALUE, 0); getTextField(12).integersOnly = true; getTextField(12).setMinMaxDefault(-Integer.MAX_VALUE, Integer.MAX_VALUE, 0); - addTextField(new GuiNpcTextField(13, this, guiLeft + 120, y += 25, 40, 20, String.valueOf(dialog.textOffsetX))); - addTextField(new GuiNpcTextField(14, this, guiLeft + 165, y, 40, 20, String.valueOf(dialog.textOffsetY))); + addTextField(new GuiNpcTextField(13, this, guiLeft + 120, y += 25, 40, 20, "" + dialog.textOffsetX)); + addTextField(new GuiNpcTextField(14, this, guiLeft + 165, y, 40, 20, "" + dialog.textOffsetY)); addLabel(new GuiNpcLabel(15, "dialog.textOffset", guiLeft + 4, y + 5)); getTextField(13).integersOnly = true; getTextField(13).setMinMaxDefault(-Integer.MAX_VALUE, Integer.MAX_VALUE, 0); getTextField(14).integersOnly = true; getTextField(14).setMinMaxDefault(-Integer.MAX_VALUE, Integer.MAX_VALUE, 0); - addTextField(new GuiNpcTextField(15, this, guiLeft + 120, y += 25, 40, 20, String.valueOf(dialog.titleOffsetX))); - addTextField(new GuiNpcTextField(16, this, guiLeft + 165, y, 40, 20, String.valueOf(dialog.titleOffsetY))); + addTextField(new GuiNpcTextField(15, this, guiLeft + 120, y += 25, 40, 20, "" + dialog.titleOffsetX)); + addTextField(new GuiNpcTextField(16, this, guiLeft + 165, y, 40, 20, "" + dialog.titleOffsetY)); addLabel(new GuiNpcLabel(18, "dialog.titleOffset", guiLeft + 4, y + 5)); getTextField(15).integersOnly = true; getTextField(15).setMinMaxDefault(-Integer.MAX_VALUE, Integer.MAX_VALUE, 0); getTextField(16).integersOnly = true; getTextField(16).setMinMaxDefault(-Integer.MAX_VALUE, Integer.MAX_VALUE, 0); - addTextField(new GuiNpcTextField(17, this, guiLeft + 120, y += 25, 40, 20, String.valueOf(dialog.optionOffsetX))); - addTextField(new GuiNpcTextField(18, this, guiLeft + 165, y, 40, 20, String.valueOf(dialog.optionOffsetY))); + addTextField(new GuiNpcTextField(17, this, guiLeft + 120, y += 25, 40, 20, "" + dialog.optionOffsetX)); + addTextField(new GuiNpcTextField(18, this, guiLeft + 165, y, 40, 20, "" + dialog.optionOffsetY)); addLabel(new GuiNpcLabel(21, "dialog.optionOffset", guiLeft + 4, y + 5)); getTextField(17).integersOnly = true; getTextField(17).setMinMaxDefault(-Integer.MAX_VALUE, Integer.MAX_VALUE, 0); getTextField(18).integersOnly = true; getTextField(18).setMinMaxDefault(-Integer.MAX_VALUE, Integer.MAX_VALUE, 0); - addTextField(new GuiNpcTextField(19, this, guiLeft + 120, y += 25, 40, 20, String.valueOf(dialog.optionSpaceX))); - addTextField(new GuiNpcTextField(20, this, guiLeft + 165, y, 40, 20, String.valueOf(dialog.optionSpaceY))); + addTextField(new GuiNpcTextField(19, this, guiLeft + 120, y += 25, 40, 20, "" + dialog.optionSpaceX)); + addTextField(new GuiNpcTextField(20, this, guiLeft + 165, y, 40, 20, "" + dialog.optionSpaceY)); addLabel(new GuiNpcLabel(24, "dialog.optionSpacing", guiLeft + 4, y + 5)); getTextField(19).integersOnly = true; getTextField(19).setMinMaxDefault(-Integer.MAX_VALUE, Integer.MAX_VALUE, 0); getTextField(20).integersOnly = true; getTextField(20).setMinMaxDefault(-Integer.MAX_VALUE, Integer.MAX_VALUE, 0); - addTextField(new GuiNpcTextField(21, this, guiLeft + 120, y += 25, 40, 20, String.valueOf(dialog.npcOffsetX))); - addTextField(new GuiNpcTextField(22, this, guiLeft + 165, y, 40, 20, String.valueOf(dialog.npcOffsetY))); + addTextField(new GuiNpcTextField(21, this, guiLeft + 120, y += 25, 40, 20, "" + dialog.npcOffsetX)); + addTextField(new GuiNpcTextField(22, this, guiLeft + 165, y, 40, 20, "" + dialog.npcOffsetY)); addLabel(new GuiNpcLabel(25, "dialog.npcOffset", guiLeft + 4, y + 5)); getTextField(21).integersOnly = true; getTextField(21).setMinMaxDefault(-Integer.MAX_VALUE, Integer.MAX_VALUE, 0); @@ -161,7 +161,7 @@ public class SubGuiNpcDialogVisual extends SubGuiInterface implements ISubGuiLis getTextField(22).setVisible(!dialog.hideNPC); getLabel(25).enabled = !dialog.hideNPC; - addTextField(new GuiNpcTextField(23, this, guiLeft + 120, y += 25, 40, 20, String.valueOf(dialog.npcScale))); + addTextField(new GuiNpcTextField(23, this, guiLeft + 120, y += 25, 40, 20, "" + dialog.npcScale)); addLabel(new GuiNpcLabel(26, "dialog.npcScale", guiLeft + 4, y + 5)); getTextField(23).floatsOnly = true; getTextField(23).setMinMaxDefaultFloat(-Float.MAX_VALUE, Float.MAX_VALUE, 0); @@ -182,30 +182,30 @@ public class SubGuiNpcDialogVisual extends SubGuiInterface implements ISubGuiLis DialogImage dialogImage = getSelectedImage(); addButton(new GuiNpcButton(2, guiLeft + xSize + 24, guiTop + 5, 20, 20, "-")); - addTextField(new GuiNpcTextField(10, this, guiLeft + 17, y += 25, 30, 20, String.valueOf(dialogImage.id))); + addTextField(new GuiNpcTextField(10, this, guiLeft + 17, y += 25, 30, 20, "" + dialogImage.id)); addTextField(new GuiNpcTextField(11, this, guiLeft + 92, y, 150, 20, dialogImage.texture)); addLabel(new GuiNpcLabel(10, "gui.id", guiLeft + 4, y + 5)); addLabel(new GuiNpcLabel(11, "display.texture", guiLeft + 50, y + 5)); getTextField(10).integersOnly = true; - addTextField(new GuiNpcTextField(12, this, guiLeft + 120, y += 25, 40, 20, String.valueOf(dialogImage.x))); - addTextField(new GuiNpcTextField(13, this, guiLeft + 165, y, 40, 20, String.valueOf(dialogImage.y))); + addTextField(new GuiNpcTextField(12, this, guiLeft + 120, y += 25, 40, 20, "" + dialogImage.x)); + addTextField(new GuiNpcTextField(13, this, guiLeft + 165, y, 40, 20, "" + dialogImage.y)); addLabel(new GuiNpcLabel(12, "gui.position", guiLeft + 4, y + 5)); getTextField(12).integersOnly = true; getTextField(12).setMinMaxDefault(-Integer.MAX_VALUE, Integer.MAX_VALUE, 0); getTextField(13).integersOnly = true; getTextField(13).setMinMaxDefault(-Integer.MAX_VALUE, Integer.MAX_VALUE, 0); - addTextField(new GuiNpcTextField(14, this, guiLeft + 120, y += 25, 40, 20, String.valueOf(dialogImage.width))); - addTextField(new GuiNpcTextField(15, this, guiLeft + 165, y, 40, 20, String.valueOf(dialogImage.height))); + addTextField(new GuiNpcTextField(14, this, guiLeft + 120, y += 25, 40, 20, "" + dialogImage.width)); + addTextField(new GuiNpcTextField(15, this, guiLeft + 165, y, 40, 20, "" + dialogImage.height)); addLabel(new GuiNpcLabel(13, "gui.widthHeight", guiLeft + 4, y + 5)); getTextField(14).integersOnly = true; getTextField(14).setMinMaxDefault(-Integer.MAX_VALUE, Integer.MAX_VALUE, 0); getTextField(15).integersOnly = true; getTextField(15).setMinMaxDefault(-Integer.MAX_VALUE, Integer.MAX_VALUE, 0); - addTextField(new GuiNpcTextField(16, this, guiLeft + 120, y += 25, 40, 20, String.valueOf(dialogImage.textureX))); - addTextField(new GuiNpcTextField(17, this, guiLeft + 165, y, 40, 20, String.valueOf(dialogImage.textureY))); + addTextField(new GuiNpcTextField(16, this, guiLeft + 120, y += 25, 40, 20, "" + dialogImage.textureX)); + addTextField(new GuiNpcTextField(17, this, guiLeft + 165, y, 40, 20, "" + dialogImage.textureY)); addLabel(new GuiNpcLabel(14, "dialog.textureOffset", guiLeft + 4, y + 5)); getTextField(16).integersOnly = true; getTextField(16).setMinMaxDefault(-Integer.MAX_VALUE, Integer.MAX_VALUE, 0); @@ -229,9 +229,9 @@ public class SubGuiNpcDialogVisual extends SubGuiInterface implements ISubGuiLis getButton(19).setVisible(dialogImage.imageType == 2); getLabel(16).enabled = dialogImage.imageType == 2; - addTextField(new GuiNpcTextField(20, this, guiLeft + 33, y += 25, 30, 20, String.valueOf(dialogImage.scale))); - addTextField(new GuiNpcTextField(21, this, guiLeft + 102, y, 30, 20, String.valueOf(dialogImage.alpha))); - addTextField(new GuiNpcTextField(22, this, guiLeft + 183, y, 45, 20, String.valueOf(dialogImage.rotation))); + addTextField(new GuiNpcTextField(20, this, guiLeft + 33, y += 25, 30, 20, "" + dialogImage.scale)); + addTextField(new GuiNpcTextField(21, this, guiLeft + 102, y, 30, 20, "" + dialogImage.alpha)); + addTextField(new GuiNpcTextField(22, this, guiLeft + 183, y, 45, 20, "" + dialogImage.rotation)); addLabel(new GuiNpcLabel(17, "model.scale", guiLeft + 4, y + 5)); addLabel(new GuiNpcLabel(18, "display.alpha", guiLeft + 72, y + 5)); addLabel(new GuiNpcLabel(19, "movement.rotation", guiLeft + 140, y + 5)); @@ -306,13 +306,13 @@ public class SubGuiNpcDialogVisual extends SubGuiInterface implements ISubGuiLis Collections.sort(ids); ArrayList strings = new ArrayList<>(); for (int i : ids) { - strings.add(String.valueOf(i)); + strings.add("" + i); } imageScroll.setUnsortedList(strings); if (changedId != -1) { - selected = imageScroll.getList().indexOf(String.valueOf(changedId)); - imageScroll.setSelected(String.valueOf(changedId)); + selected = imageScroll.getList().indexOf("" + changedId); + imageScroll.setSelected("" + changedId); changedId = -1; } @@ -402,7 +402,7 @@ public class SubGuiNpcDialogVisual extends SubGuiInterface implements ISubGuiLis dialog.dialogImages.put(addId, new DialogImage(addId)); updateScrollData(); - selected = imageScroll.getList().indexOf(String.valueOf(addId)); + selected = imageScroll.getList().indexOf("" + addId); } if (button.id == 2) { if (imageScroll.getSelected() != null) { @@ -598,8 +598,8 @@ public class SubGuiNpcDialogVisual extends SubGuiInterface implements ISubGuiLis } if (textfield.id == 10) { - if (imageScroll.getList().contains(String.valueOf(textfield.getInteger()))) { - textfield.setText(String.valueOf(dialogImage.id)); + if (imageScroll.getList().contains("" + textfield.getInteger())) { + textfield.setText("" + dialogImage.id); return; } diff --git a/src/main/java/noppes/npcs/client/gui/SubGuiNpcPartyOptions.java b/src/main/java/noppes/npcs/client/gui/SubGuiNpcPartyOptions.java index 10420a65d..c5a0769a3 100644 --- a/src/main/java/noppes/npcs/client/gui/SubGuiNpcPartyOptions.java +++ b/src/main/java/noppes/npcs/client/gui/SubGuiNpcPartyOptions.java @@ -71,7 +71,7 @@ public class SubGuiNpcPartyOptions extends SubGuiInterface implements ITextfield y += 23; //min party size - number field GuiNpcTextField minField = new GuiNpcTextField(21, this, guiLeft + 60, guiTop + 10 + y, - 30, 20, String.valueOf(options.minPartySize)); + 30, 20, "" + options.minPartySize); minField.integersOnly = true; minField.setMinMaxDefault(1, Integer.MAX_VALUE, 1); addTextField(minField); @@ -79,7 +79,7 @@ public class SubGuiNpcPartyOptions extends SubGuiInterface implements ITextfield //max party size - number field GuiNpcTextField maxField = new GuiNpcTextField(20, this, guiLeft + 160, guiTop + 10 + y, - 30, 20, String.valueOf(options.maxPartySize)); + 30, 20, "" + options.maxPartySize); maxField.integersOnly = true; maxField.setMinMaxDefault(1, Integer.MAX_VALUE, 4); addTextField(maxField); diff --git a/src/main/java/noppes/npcs/client/gui/SubGuiSpawningOptions.java b/src/main/java/noppes/npcs/client/gui/SubGuiSpawningOptions.java index c15f9162f..29c33352b 100644 --- a/src/main/java/noppes/npcs/client/gui/SubGuiSpawningOptions.java +++ b/src/main/java/noppes/npcs/client/gui/SubGuiSpawningOptions.java @@ -36,9 +36,9 @@ public class SubGuiSpawningOptions extends SubGuiInterface implements ITextfield addButton(new GuiNpcButtonYesNo(13, guiLeft + 120, y += 22, data.airSpawning)); addLabel(new GuiNpcLabel(13, "spawning.airSpawning", guiLeft + 4, y + 5)); - addTextField(new GuiNpcTextField(14, this, guiLeft + 120, y += 22, 40, 20, String.valueOf(data.spawnHeightMin))); + addTextField(new GuiNpcTextField(14, this, guiLeft + 120, y += 22, 40, 20, "" + data.spawnHeightMin)); addLabel(new GuiNpcLabel(14, "spawning.minHeight", guiLeft + 4, y + 5)); - addTextField(new GuiNpcTextField(15, this, guiLeft + 120, y += 22, 40, 20, String.valueOf(data.spawnHeightMax))); + addTextField(new GuiNpcTextField(15, this, guiLeft + 120, y += 22, 40, 20, "" + data.spawnHeightMax)); addLabel(new GuiNpcLabel(15, "spawning.maxHeight", guiLeft + 4, y + 5)); getTextField(14).integersOnly = true; getTextField(14).setMinMaxDefault(-Integer.MAX_VALUE, Integer.MAX_VALUE, 0); 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 0f41347d4..59046ef77 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/GuiNPCAbilities.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/GuiNPCAbilities.java @@ -2,6 +2,7 @@ package noppes.npcs.client.gui.advanced; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.AbilityController; +import kamkeel.npcs.controllers.data.ability.AbilitySlot; import kamkeel.npcs.network.PacketClient; import kamkeel.npcs.network.packets.request.ability.AbilitiesGetAllPacket; import kamkeel.npcs.network.packets.request.ability.AbilitiesNpcGetPacket; @@ -46,12 +47,13 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC // Display name to typeId mapping for the available types scroll private final HashMap displayNameToTypeId = new HashMap<>(); - // NPC's current abilities - private final List npcAbilities = new ArrayList<>(); + // NPC's current ability slots + private final List npcSlots = new ArrayList<>(); // Settings private boolean abilitiesEnabled = false; - private int globalCooldown = 20; + private int minCooldown = 20; + private int maxCooldown = 60; private String search = ""; private int selectedAbilityIndex = -1; @@ -72,12 +74,18 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC addLabel(new GuiNpcLabel(100, "gui.enabled", guiLeft + 5, y + 5)); addButton(new GuiNpcButton(100, guiLeft + 55, y, 50, 20, new String[]{"gui.no", "gui.yes"}, abilitiesEnabled ? 1 : 0)); - // Global cooldown (spaced out since combat only is removed) - addLabel(new GuiNpcLabel(101, "ability.globalCooldown", guiLeft + 140, y + 5)); - GuiNpcTextField cooldownField = new GuiNpcTextField(101, this, fontRendererObj, guiLeft + 230, y, 50, 20, String.valueOf(globalCooldown)); - cooldownField.setIntegersOnly(); - cooldownField.setMinMaxDefault(0, 10000, 20); - addTextField(cooldownField); + // Cooldown range: Min Cooldown and Max Cooldown (spaced apart more) + addLabel(new GuiNpcLabel(101, "ability.minCooldown", guiLeft + 115, y + 5)); + GuiNpcTextField minField = new GuiNpcTextField(101, this, fontRendererObj, guiLeft + 165, y, 40, 20, "" + minCooldown); + minField.setIntegersOnly(); + minField.setMinMaxDefault(0, 10000, 20); + addTextField(minField); + + addLabel(new GuiNpcLabel(102, "ability.maxCooldown", guiLeft + 260, y + 5)); + GuiNpcTextField maxField = new GuiNpcTextField(102, this, fontRendererObj, guiLeft + 310, y, 40, 20, "" + maxCooldown); + maxField.setIntegersOnly(); + maxField.setMinMaxDefault(0, 10000, 60); + addTextField(maxField); y += 28; @@ -116,6 +124,14 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC // Load button (under the add/remove arrows) addButton(new GuiNpcButton(75, centerX, y + 90, 40, 20, "gui.load")); + // On/Off toggle for selected ability (under Load button) + if (selectedAbilityIndex >= 0 && selectedAbilityIndex < npcSlots.size()) { + Ability selectedAbility = npcSlots.get(selectedAbilityIndex).getAbility(); + GuiNpcButton toggleBtn = new GuiNpcButton(77, centerX, y + 112, 40, 20, + new String[]{"gui.off", "gui.on"}, selectedAbility.isEnabled() ? 1 : 0); + addButton(toggleBtn); + } + // Right side buttons: Edit and Up/Down carrots with Save button addButton(new GuiNpcButton(72, guiLeft + 210, y + 145, 55, 20, "gui.edit")); getButton(72).setEnabled(selectedAbilityIndex >= 0); @@ -124,7 +140,7 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC addButton(new GuiNpcButton(73, guiLeft + 270, y + 145, 20, 20, "<")); addButton(new GuiNpcButton(74, guiLeft + 292, y + 145, 20, 20, ">")); getButton(73).setEnabled(selectedAbilityIndex > 0); - getButton(74).setEnabled(selectedAbilityIndex >= 0 && selectedAbilityIndex < npcAbilities.size() - 1); + getButton(74).setEnabled(selectedAbilityIndex >= 0 && selectedAbilityIndex < npcSlots.size() - 1); // Save button (to the right of carrots, only enabled when ability is selected) addButton(new GuiNpcButton(76, guiLeft + 320, y + 145, 50, 20, "gui.save")); @@ -151,10 +167,8 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC Ability newAbility = AbilityController.Instance.create(typeId); if (newAbility != null) { newAbility.setId(UUID.randomUUID().toString()); - // Don't set custom name - let it default to type name - npcAbilities.add(newAbility); - // Auto-select the newly added ability - selectedAbilityIndex = npcAbilities.size() - 1; + npcSlots.add(AbilitySlot.inline(newAbility)); + selectedAbilityIndex = npcSlots.size() - 1; updateNpcAbilitiesList(); selectAbilityByIndex(selectedAbilityIndex); initGui(); @@ -167,8 +181,8 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC // Remove ability if (id == 71) { - if (selectedAbilityIndex >= 0 && selectedAbilityIndex < npcAbilities.size()) { - npcAbilities.remove(selectedAbilityIndex); + if (selectedAbilityIndex >= 0 && selectedAbilityIndex < npcSlots.size()) { + npcSlots.remove(selectedAbilityIndex); selectedAbilityIndex = -1; updateNpcAbilitiesList(); initGui(); @@ -179,9 +193,17 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC // Edit ability if (id == 72) { - if (selectedAbilityIndex >= 0 && selectedAbilityIndex < npcAbilities.size()) { - Ability ability = npcAbilities.get(selectedAbilityIndex); - setSubGui(ability.createConfigGui(this)); + if (selectedAbilityIndex >= 0 && selectedAbilityIndex < npcSlots.size()) { + AbilitySlot slot = npcSlots.get(selectedAbilityIndex); + if (slot.isReference()) { + // Reference slot - ask clone-and-modify vs modify-parent + setSubGui(new SubGuiAbilityEditMode()); + } else { + Ability ability = slot.getAbility(); + if (ability != null) { + setSubGui(ability.createConfigGui(this)); + } + } } return; } @@ -189,9 +211,9 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC // Move up (carrot left = earlier in list) if (id == 73) { if (selectedAbilityIndex > 0) { - Ability temp = npcAbilities.get(selectedAbilityIndex); - npcAbilities.set(selectedAbilityIndex, npcAbilities.get(selectedAbilityIndex - 1)); - npcAbilities.set(selectedAbilityIndex - 1, temp); + AbilitySlot temp = npcSlots.get(selectedAbilityIndex); + npcSlots.set(selectedAbilityIndex, npcSlots.get(selectedAbilityIndex - 1)); + npcSlots.set(selectedAbilityIndex - 1, temp); selectedAbilityIndex--; updateNpcAbilitiesList(); selectAbilityByIndex(selectedAbilityIndex); @@ -203,10 +225,10 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC // Move down (carrot right = later in list) if (id == 74) { - if (selectedAbilityIndex >= 0 && selectedAbilityIndex < npcAbilities.size() - 1) { - Ability temp = npcAbilities.get(selectedAbilityIndex); - npcAbilities.set(selectedAbilityIndex, npcAbilities.get(selectedAbilityIndex + 1)); - npcAbilities.set(selectedAbilityIndex + 1, temp); + if (selectedAbilityIndex >= 0 && selectedAbilityIndex < npcSlots.size() - 1) { + AbilitySlot temp = npcSlots.get(selectedAbilityIndex); + npcSlots.set(selectedAbilityIndex, npcSlots.get(selectedAbilityIndex + 1)); + npcSlots.set(selectedAbilityIndex + 1, temp); selectedAbilityIndex++; updateNpcAbilitiesList(); selectAbilityByIndex(selectedAbilityIndex); @@ -222,15 +244,31 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC return; } - // Save selected ability + // Save selected ability as custom preset if (id == 76) { - if (selectedAbilityIndex >= 0 && selectedAbilityIndex < npcAbilities.size()) { - Ability abilityToSave = npcAbilities.get(selectedAbilityIndex); - if (abilityToSave.getName() == null || abilityToSave.getName().isEmpty()) { - // Need a name to save - open edit dialog first - setSubGui(abilityToSave.createConfigGui(this)); - } else { - setSubGui(new SubGuiAbilitySaveConfirm(abilityToSave)); + if (selectedAbilityIndex >= 0 && selectedAbilityIndex < npcSlots.size()) { + Ability abilityToSave = npcSlots.get(selectedAbilityIndex).getAbility(); + if (abilityToSave != null) { + if (abilityToSave.getName() == null || abilityToSave.getName().isEmpty()) { + setSubGui(abilityToSave.createConfigGui(this)); + } else { + setSubGui(new SubGuiAbilitySaveConfirm(abilityToSave)); + } + } + } + return; + } + + // Toggle selected ability on/off + if (id == 77) { + if (selectedAbilityIndex >= 0 && selectedAbilityIndex < npcSlots.size()) { + Ability ability = npcSlots.get(selectedAbilityIndex).getAbility(); + if (ability != null) { + ability.setEnabled(((GuiNpcButton) guibutton).getValue() == 1); + updateNpcAbilitiesList(); + selectAbilityByIndex(selectedAbilityIndex); + initGui(); + save(); } } return; @@ -253,16 +291,29 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC @Override public void setGuiData(NBTTagCompound compound) { - abilitiesEnabled = compound.getBoolean("AbilitiesEnabled"); - globalCooldown = compound.hasKey("AbilityGlobalCooldown") ? compound.getInteger("AbilityGlobalCooldown") : 20; + // 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; + } - npcAbilities.clear(); + abilitiesEnabled = compound.getBoolean("AbilitiesEnabled"); + + minCooldown = compound.getInteger("AbilityMinCooldown"); + maxCooldown = compound.getInteger("AbilityMaxCooldown"); + if (minCooldown == 0 && maxCooldown == 0) { + minCooldown = 20; + maxCooldown = 60; + } + + npcSlots.clear(); NBTTagList abilityList = compound.getTagList("Abilities", 10); for (int i = 0; i < abilityList.tagCount(); i++) { NBTTagCompound abilityNBT = abilityList.getCompoundTagAt(i); - Ability ability = AbilityController.Instance.fromNBT(abilityNBT); - if (ability != null) { - npcAbilities.add(ability); + AbilitySlot slot = AbilitySlot.fromNBT(abilityNBT); + if (slot != null) { + npcSlots.add(slot); } } @@ -305,7 +356,7 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC private void updateNpcAbilitiesList() { List list = new ArrayList<>(); - for (int i = 0; i < npcAbilities.size(); i++) { + for (int i = 0; i < npcSlots.size(); i++) { list.add(getAbilityListEntry(i)); } if (npcAbilitiesScroll != null) { @@ -314,26 +365,41 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC } /** - * Creates the display entry for an ability in the list. - * Format: "[●] 1. Custom Name (Type)" or "[●] 1. Type" if no custom name - * Circle is green (§a) for enabled, red (§c) for disabled + * Creates the display entry for an ability slot in the list. + * Inline: "[●] 1. Custom Name (Type)" or "[●] 1. Type" + * Reference: "[●] 1. > Name (Type)" in yellow + * Broken: "[●] 1. > [Missing: id...]" in red */ private String getAbilityListEntry(int index) { - Ability ability = npcAbilities.get(index); + AbilitySlot slot = npcSlots.get(index); + Ability ability = slot.getAbility(); + + if (ability == null) { + // Broken reference + String refId = slot.getReferenceId(); + String shortId = refId != null && refId.length() > 8 ? refId.substring(0, 8) + "..." : refId; + return "\u00A7c\u25CF\u00A7r " + (index + 1) + ". \u00A7c> [Missing: " + shortId + "]\u00A7r"; + } + String typeName = getDisplayName(ability.getTypeId()); String customName = ability.getName(); - - // Color prefix: green circle for enabled, red for disabled String colorPrefix = ability.isEnabled() ? "\u00A7a\u25CF\u00A7r " : "\u00A7c\u25CF\u00A7r "; + String nameDisplay; if (customName != null && !customName.isEmpty() && !customName.equals(typeName)) { - return colorPrefix + (index + 1) + ". " + customName + " (" + typeName + ")"; + nameDisplay = customName + " (" + typeName + ")"; + } else { + nameDisplay = typeName; } - return colorPrefix + (index + 1) + ". " + typeName; + + if (slot.isReference()) { + return colorPrefix + (index + 1) + ". \u00A7e> " + nameDisplay + "\u00A7r"; + } + return colorPrefix + (index + 1) + ". " + nameDisplay; } private void selectAbilityByIndex(int index) { - if (index >= 0 && index < npcAbilities.size()) { + if (index >= 0 && index < npcSlots.size()) { String entry = getAbilityListEntry(index); if (npcAbilitiesScroll != null) { npcAbilitiesScroll.setSelected(entry); @@ -352,11 +418,12 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC public void save() { NBTTagCompound compound = new NBTTagCompound(); compound.setBoolean("AbilitiesEnabled", abilitiesEnabled); - compound.setInteger("AbilityGlobalCooldown", globalCooldown); + compound.setInteger("AbilityMinCooldown", minCooldown); + compound.setInteger("AbilityMaxCooldown", maxCooldown); NBTTagList abilityList = new NBTTagList(); - for (Ability ability : npcAbilities) { - abilityList.appendTag(ability.writeNBT()); + for (AbilitySlot slot : npcSlots) { + abilityList.appendTag(slot.writeNBT()); } compound.setTag("Abilities", abilityList); @@ -364,15 +431,29 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC } /** - * Called from SubGuiAbilityLoad when a saved ability is loaded. - * Adds the ability to the NPC's ability list. + * Called from SubGuiAbilityLoad when a cloned ability is loaded. + * Adds as an inline slot. */ public void loadAbility(Ability loadedAbility) { if (loadedAbility != null) { loadedAbility.setId(UUID.randomUUID().toString()); - npcAbilities.add(loadedAbility); - // Auto-select the newly added ability - selectedAbilityIndex = npcAbilities.size() - 1; + npcSlots.add(AbilitySlot.inline(loadedAbility)); + selectedAbilityIndex = npcSlots.size() - 1; + updateNpcAbilitiesList(); + selectAbilityByIndex(selectedAbilityIndex); + initGui(); + save(); + } + } + + /** + * Called from SubGuiAbilityLoad when a reference is loaded. + * Adds as a reference slot. + */ + public void loadAbilityReference(String referenceId) { + if (referenceId != null && !referenceId.isEmpty()) { + npcSlots.add(AbilitySlot.reference(referenceId)); + selectedAbilityIndex = npcSlots.size() - 1; updateNpcAbilitiesList(); selectAbilityByIndex(selectedAbilityIndex); initGui(); @@ -412,24 +493,68 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC @Override public void customScrollDoubleClicked(String selection, GuiCustomScroll scroll) { - if (scroll == npcAbilitiesScroll && selectedAbilityIndex >= 0 && selectedAbilityIndex < npcAbilities.size()) { - Ability ability = npcAbilities.get(selectedAbilityIndex); - setSubGui(ability.createConfigGui(this)); + if (scroll == npcAbilitiesScroll && selectedAbilityIndex >= 0 && selectedAbilityIndex < npcSlots.size()) { + AbilitySlot slot = npcSlots.get(selectedAbilityIndex); + if (slot.isReference()) { + setSubGui(new SubGuiAbilityEditMode()); + } else { + Ability ability = slot.getAbility(); + if (ability != null) { + setSubGui(ability.createConfigGui(this)); + } + } } } @Override public void unFocused(GuiNpcTextField textField) { if (textField.id == 101) { - globalCooldown = textField.getInteger(); + minCooldown = textField.getInteger(); + // Ensure min <= max + if (minCooldown > maxCooldown) { + maxCooldown = minCooldown; + initGui(); + } + save(); + } else if (textField.id == 102) { + maxCooldown = textField.getInteger(); + // Ensure min <= max + if (maxCooldown < minCooldown) { + minCooldown = maxCooldown; + initGui(); + } save(); } } @Override public void subGuiClosed(SubGuiInterface subgui) { - if (subgui instanceof SubGuiAbilityConfig) { - // Ability was saved in the SubGui, refresh list + if (subgui instanceof SubGuiAbilityEditMode) { + int mode = ((SubGuiAbilityEditMode) subgui).getResult(); + if (mode < 0) return; // cancelled + + if (selectedAbilityIndex >= 0 && selectedAbilityIndex < npcSlots.size()) { + AbilitySlot slot = npcSlots.get(selectedAbilityIndex); + if (mode == SubGuiAbilityEditMode.MODE_CLONE_MODIFY) { + // Convert reference to inline, then edit + if (slot.convertToInline()) { + Ability ability = slot.getAbility(); + if (ability != null) { + ability.setId(UUID.randomUUID().toString()); + setSubGui(ability.createConfigGui(this)); + } + updateNpcAbilitiesList(); + save(); + } + } else if (mode == SubGuiAbilityEditMode.MODE_MODIFY_PARENT) { + // Edit the resolved ability directly (changes propagate to controller) + Ability ability = slot.getAbility(); + if (ability != null) { + setSubGui(ability.createConfigGui(this)); + } + } + } + } else if (subgui instanceof SubGuiAbilityConfig) { updateNpcAbilitiesList(); save(); } @@ -439,8 +564,15 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC * Called from SubGuiAbilityConfig when ability is saved. */ public void onAbilitySaved(Ability ability) { - if (selectedAbilityIndex >= 0 && selectedAbilityIndex < npcAbilities.size()) { - npcAbilities.set(selectedAbilityIndex, ability); + if (selectedAbilityIndex >= 0 && selectedAbilityIndex < npcSlots.size()) { + AbilitySlot slot = npcSlots.get(selectedAbilityIndex); + if (slot.isReference()) { + // For reference slots, save back to controller + // (The resolved copy was edited in-place) + } else { + // For inline slots, replace with the updated ability + npcSlots.set(selectedAbilityIndex, AbilitySlot.inline(ability)); + } updateNpcAbilitiesList(); save(); } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java index 58e133bc8..6954b36f1 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java @@ -1,19 +1,19 @@ package noppes.npcs.client.gui.advanced; import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.AbilityEffect; import kamkeel.npcs.controllers.data.ability.Condition; -import kamkeel.npcs.controllers.data.ability.TargetingMode; -import kamkeel.npcs.controllers.data.telegraph.TelegraphType; +import kamkeel.npcs.controllers.data.ability.gui.AbilityFieldBuilder; +import noppes.npcs.client.gui.builder.FieldDef; +import noppes.npcs.client.gui.builder.FieldType; import net.minecraft.client.gui.GuiButton; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.StatCollector; -import noppes.npcs.client.gui.SubGuiColorSelector; -import noppes.npcs.client.gui.select.GuiAnimationSelection; -import noppes.npcs.client.gui.select.GuiSoundSelection; import noppes.npcs.client.gui.util.GuiMenuTopButton; 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.GuiScrollWindow; import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.client.gui.util.ISubGuiListener; import noppes.npcs.client.gui.util.ITextfieldListener; @@ -22,147 +22,97 @@ import noppes.npcs.client.gui.util.SubGuiInterface; import java.util.ArrayList; import java.util.List; -/** - * Base SubGui for editing ability configuration with tabbed interface. - * Uses GuiMenuTopButton for navigation tabs. - *

- * Tabs: - * - General: Name, Weight, Lock Movement, Interruptible, Timing - * - Type: Type-specific settings per Ability (override in subclasses) - * - Target: Min/Max Range, Targeting Mode, Conditions (up to 3) - * - Effects: Sounds, Animations, Telegraph settings, Colors - *

- * Subclasses should override initTypeTab() and handleTypeButton()/handleTypeTextField() - * to provide type-specific settings UI. - */ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldListener, ISubGuiListener { - // Tab constants - protected static final int TAB_GENERAL = 0; - protected static final int TAB_TYPE = 1; - protected static final int TAB_TARGET = 2; - protected static final int TAB_EFFECTS = 3; + private static final int DECLARATIVE_ID_START = 1000; + private static final int CLEAR_ID_START = 2000; + private static final int LABEL_ID_START = 3000; - // Core references - protected final Ability ability; - protected final IAbilityConfigCallback callback; - protected int activeTab = TAB_GENERAL; + // Fixed tab indices + private static final int TAB_GENERAL = 0; + private static final int TAB_TYPE = 1; + private static final int TAB_TARGET = 2; + private static final int TAB_EFFECTS = 3; + // Custom tabs start at index 4 - // ═══════════════════════════════════════════════════════════════════════════ - // CACHED VALUES - General Tab - // ═══════════════════════════════════════════════════════════════════════════ - protected String name; - protected boolean enabled; - protected int weight; - protected boolean lockMovement; - protected boolean interruptible; + // Tab name strings (matching FieldDef.tab() values) + private static final String TAB_NAME_GENERAL = "General"; + private static final String TAB_NAME_TYPE = "Type"; + private static final String TAB_NAME_TARGET = "Target"; + private static final String TAB_NAME_EFFECTS = "Effects"; - // Timing - protected int cooldownTicks; - protected int windUpTicks; - protected int activeTicks; - protected int recoveryTicks; + private static final int L_LABEL_X = 5; + private static final int ROW_H = 24; - // ═══════════════════════════════════════════════════════════════════════════ - // CACHED VALUES - Target Tab - // ═══════════════════════════════════════════════════════════════════════════ - protected float minRange; - protected float maxRange; - protected TargetingMode targetingMode; - protected List conditions; + private final Ability ability; + private final IAbilityConfigCallback callback; + private int activeTab = TAB_GENERAL; - // ═══════════════════════════════════════════════════════════════════════════ - // CACHED VALUES - Effects Tab - // ═══════════════════════════════════════════════════════════════════════════ - protected String windUpSound; - protected String activeSound; - protected int windUpAnimationId; - protected int activeAnimationId; - protected String windUpAnimationName = null; - protected String activeAnimationName = null; + private List fieldDefs; + private List customTabNames; - // Telegraph - protected boolean showTelegraph; - protected int windUpColor; - protected int activeColor; + private List conditions; + private int editingConditionIndex = -1; - // Editing state - protected int editingColorId = 0; - protected int editingSoundId = 0; - protected int editingAnimationId = 0; - protected int editingConditionIndex = -1; + private AbilityFieldBuilder builder; + private FieldDef activeSubGuiField = null; - // ═══════════════════════════════════════════════════════════════════════════ - // ABILITY TYPE FEATURES - // ═══════════════════════════════════════════════════════════════════════════ - protected final boolean supportsTelegraph; - protected final boolean targetingModeLocked; - protected final boolean hasTypeSettings; + // Scroll position preservation per tab + private float[] tabScrollY; public SubGuiAbilityConfig(Ability ability, IAbilityConfigCallback callback) { this.ability = ability; this.callback = callback; - - // Cache values from ability - General - this.name = ability.getName() != null ? ability.getName() : ""; - this.enabled = ability.isEnabled(); - this.weight = ability.getWeight(); - this.lockMovement = ability.isLockMovement(); - this.interruptible = ability.isInterruptible(); - - // Timing - this.cooldownTicks = ability.getCooldownTicks(); - this.windUpTicks = ability.getWindUpTicks(); - this.activeTicks = ability.getActiveTicks(); - this.recoveryTicks = ability.getRecoveryTicks(); - - // Target - this.minRange = ability.getMinRange(); - this.maxRange = ability.getMaxRange(); - this.targetingMode = ability.getTargetingMode(); this.conditions = new ArrayList<>(ability.getConditions()); - // Effects - this.windUpSound = ability.getWindUpSound(); - this.activeSound = ability.getActiveSound(); - this.windUpAnimationId = ability.getWindUpAnimationId(); - this.activeAnimationId = ability.getActiveAnimationId(); + this.fieldDefs = ability.getAllFieldDefinitions(); + discoverCustomTabs(); - // Telegraph - this.showTelegraph = ability.isShowTelegraph(); - this.windUpColor = ability.getWindUpColor(); - this.activeColor = ability.getActiveColor(); - - // Determine ability type features - TelegraphType defaultType = ability.getTelegraphType(); - this.supportsTelegraph = defaultType != null && defaultType != TelegraphType.NONE; - this.targetingModeLocked = ability.isTargetingModeLocked(); - this.hasTypeSettings = ability.hasTypeSettings(); - - setBackground("menubg.png"); + setBackground("menubg.png", 217); xSize = 356; ySize = 200; } + private void discoverCustomTabs() { + customTabNames = new ArrayList<>(); + for (FieldDef def : fieldDefs) { + String tab = def.getTab(); + if (tab != null + && !TAB_NAME_GENERAL.equals(tab) + && !TAB_NAME_TYPE.equals(tab) + && !TAB_NAME_TARGET.equals(tab) + && !TAB_NAME_EFFECTS.equals(tab) + && !customTabNames.contains(tab)) { + customTabNames.add(tab); + } + } + tabScrollY = new float[4 + customTabNames.size()]; + } + + // ═══════════════════════════════════════════════════════════════════════════ + // INIT GUI + // ═══════════════════════════════════════════════════════════════════════════ + @Override public void initGui() { + // Save scroll position before super clears scroll windows + GuiScrollWindow oldSw = getScrollableGui(0); + if (oldSw != null && activeTab < tabScrollY.length) { + tabScrollY[activeTab] = oldSw.nextScrollY; + } + super.initGui(); - // ═══════════════════════════════════════════════════════════════════════ - // TOP TABS using GuiMenuTopButton - // ═══════════════════════════════════════════════════════════════════════ + // Tab buttons — always show General, Type, Target, Effects GuiMenuTopButton generalTab = new GuiMenuTopButton(90, guiLeft + 4, guiTop - 17, "menu.general"); generalTab.active = (activeTab == TAB_GENERAL); addTopButton(generalTab); - GuiMenuTopButton lastTab = generalTab; - if (hasTypeSettings) { - GuiMenuTopButton typeTab = new GuiMenuTopButton(91, lastTab, "gui.type"); - typeTab.active = (activeTab == TAB_TYPE); - addTopButton(typeTab); - lastTab = typeTab; - } + GuiMenuTopButton typeTab = new GuiMenuTopButton(91, lastTab, "gui.type"); + typeTab.active = (activeTab == TAB_TYPE); + addTopButton(typeTab); + lastTab = typeTab; GuiMenuTopButton targetTab = new GuiMenuTopButton(92, lastTab, "script.target"); targetTab.active = (activeTab == TAB_TARGET); @@ -172,210 +122,115 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi GuiMenuTopButton effectsTab = new GuiMenuTopButton(93, lastTab, "ability.tab.effects"); effectsTab.active = (activeTab == TAB_EFFECTS); addTopButton(effectsTab); + lastTab = effectsTab; + + // Custom tabs (Visual, etc.) + for (int i = 0; i < customTabNames.size(); i++) { + GuiMenuTopButton ct = new GuiMenuTopButton(94 + i, lastTab, customTabNames.get(i)); + ct.active = (activeTab == 4 + i); + addTopButton(ct); + lastTab = ct; + } - // Close button (X) in top menu bar - use -1000 to avoid any ID conflicts GuiMenuTopButton closeBtn = new GuiMenuTopButton(-1000, guiLeft + xSize - 22, guiTop - 17, "X"); addTopButton(closeBtn); - int contentY = guiTop + 5; + // Build scroll window dimensions + int swX = guiLeft + 4; + int swY = guiTop + 5; + int swW = xSize - 8; + int swH = ySize - 10; - // ═══════════════════════════════════════════════════════════════════════ - // TAB CONTENT - // ═══════════════════════════════════════════════════════════════════════ - switch (activeTab) { - case TAB_GENERAL: - initGeneralTab(contentY); - break; - case TAB_TYPE: - initTypeTab(contentY); - break; - case TAB_TARGET: - initTargetTab(contentY); - break; - case TAB_EFFECTS: - initEffectsTab(contentY); - break; + int y = 5; + int labelCounter = LABEL_ID_START; + + // General tab: type label at top — need to reserve space before building + if (activeTab == TAB_GENERAL) { + y += ROW_H; } - // ═══════════════════════════════════════════════════════════════════════ - // BOTTOM ROW - Enabled toggle - // ═══════════════════════════════════════════════════════════════════════ - addLabel(new GuiNpcLabel(99, "gui.enabled", guiLeft + 8, guiTop + ySize - 18)); - GuiNpcButton enabledBtn = new GuiNpcButton(2, guiLeft + 55, guiTop + ySize - 24, 40, 20, new String[]{"gui.no", "gui.yes"}, enabled ? 1 : 0); - enabledBtn.setTextColor(enabled ? 0x00FF00 : 0xFF0000); - addButton(enabledBtn); + // Build declarative fields into an auto-created scroll window + List tabFields = getVisibleFieldsForTab(activeTab); + builder = new AbilityFieldBuilder(this, fontRendererObj); + builder.startIds(DECLARATIVE_ID_START, CLEAR_ID_START, labelCounter); + builder.startY(y); + + GuiScrollWindow sw = builder.buildScrollWindow(tabFields, swX, swY, swW, swH); + + // General tab: add type label at top of scroll window + if (activeTab == TAB_GENERAL) { + sw.addLabel(new GuiNpcLabel(builder.getNextLabelId(), "gui.type", 5, 5 + 5, 0xFFFFFF)); + sw.addLabel(new GuiNpcLabel(builder.getNextLabelId() + 1, ability.getTypeId(), 55, 5 + 5, 0xFFFFFF)); + } + + // Conditions on target tab — appended after field defs + if (activeTab == TAB_TARGET) { + int condY = renderConditions(sw, builder.getLastBuildY(), builder.getNextLabelId()); + sw.maxScrollY = Math.max(condY - swH, 0); + } + + // Restore scroll position + if (activeTab < tabScrollY.length) { + float restored = Math.min(tabScrollY[activeTab], sw.maxScrollY); + sw.nextScrollY = restored; + sw.scrollY = restored; + } } - // ═══════════════════════════════════════════════════════════════════════════ - // GENERAL TAB - // ═══════════════════════════════════════════════════════════════════════════ + private List getVisibleFieldsForTab(int tabIndex) { + List result = new ArrayList<>(); - private void initGeneralTab(int startY) { - int y = startY; - int col1LabelX = guiLeft + 8; - int col1FieldX = guiLeft + 70; - int col2LabelX = guiLeft + 180; - int col2FieldX = guiLeft + 248; - - // Row 1: Type (read-only) + Name - addLabel(new GuiNpcLabel(1, "gui.type", col1LabelX, y + 5)); - addLabel(new GuiNpcLabel(2, ability.getTypeId(), col1FieldX, y + 5)); - - addLabel(new GuiNpcLabel(3, "gui.name", col2LabelX, y + 5)); - GuiNpcTextField nameField = new GuiNpcTextField(1, this, fontRendererObj, col2FieldX, y, 100, 20, name); - addTextField(nameField); - - y += 24; - - // Row 2: Weight + Lock Movement - addLabel(new GuiNpcLabel(4, "ability.weight", col1LabelX, y + 5)); - GuiNpcTextField weightField = new GuiNpcTextField(3, this, fontRendererObj, col1FieldX, y, 40, 20, String.valueOf(weight)); - weightField.setIntegersOnly(); - weightField.setMinMaxDefault(1, 1000, 10); - addTextField(weightField); - - addLabel(new GuiNpcLabel(5, "ability.lockMove", col2LabelX, y + 5)); - addButton(new GuiNpcButton(16, col2FieldX, y, 40, 20, new String[]{"gui.no", "gui.yes"}, lockMovement ? 1 : 0)); - - y += 24; - - // Row 3: Interruptible - addLabel(new GuiNpcLabel(6, "ability.interruptible", col1LabelX, y + 5)); - addButton(new GuiNpcButton(14, guiLeft + 85, y, 40, 20, new String[]{"gui.no", "gui.yes"}, interruptible ? 1 : 0)); - - y += 28; - - // Separator - Timing section header - addLabel(new GuiNpcLabel(7, "ability.timing", col1LabelX, y)); - y += 14; - - // Row 4: Windup Ticks + Active Ticks - addLabel(new GuiNpcLabel(10, "ability.windup", col1LabelX, y + 5)); - GuiNpcTextField windupField = new GuiNpcTextField(11, this, fontRendererObj, col1FieldX, y, 40, 20, String.valueOf(windUpTicks)); - windupField.setIntegersOnly(); - windupField.setMinMaxDefault(0, 1000, 20); - addTextField(windupField); - - addLabel(new GuiNpcLabel(11, "ability.active", col2LabelX, y + 5)); - GuiNpcTextField activeField = new GuiNpcTextField(12, this, fontRendererObj, col2FieldX, y, 40, 20, String.valueOf(activeTicks)); - activeField.setIntegersOnly(); - activeField.setMinMaxDefault(1, 1000, 10); - addTextField(activeField); - - y += 24; - - // Row 5: Recovery Ticks + Cooldown Ticks - addLabel(new GuiNpcLabel(12, "ability.recovery", col1LabelX, y + 5)); - GuiNpcTextField recoveryField = new GuiNpcTextField(13, this, fontRendererObj, col1FieldX, y, 40, 20, String.valueOf(recoveryTicks)); - recoveryField.setIntegersOnly(); - recoveryField.setMinMaxDefault(0, 1000, 20); - addTextField(recoveryField); - - addLabel(new GuiNpcLabel(13, "ability.cooldown", col2LabelX, y + 5)); - GuiNpcTextField cooldownField = new GuiNpcTextField(10, this, fontRendererObj, col2FieldX, y, 40, 20, String.valueOf(cooldownTicks)); - cooldownField.setIntegersOnly(); - cooldownField.setMinMaxDefault(0, 10000, 100); - addTextField(cooldownField); - } - - // ═══════════════════════════════════════════════════════════════════════════ - // TYPE TAB - Override in subclasses for type-specific settings - // ═══════════════════════════════════════════════════════════════════════════ - - /** - * Override this method to render type-specific settings in the Type tab. - * Use the helper methods createFloatField(), createIntField(), etc. - * Button/TextField IDs should start at 100. - * - * @param startY The Y position to start rendering from - */ - protected void initTypeTab(int startY) { - // Default: show message that no type settings are available - addLabel(new GuiNpcLabel(100, "ability.noTypeSettings", guiLeft + 8, startY + 5)); - } - - /** - * Override this method to handle button clicks for type-specific buttons. - * Button IDs 100+ are reserved for type-specific use. - */ - protected void handleTypeButton(int id, GuiNpcButton button) { - // Default: no-op - } - - /** - * Override this method to handle text field changes for type-specific fields. - * Field IDs 100+ are reserved for type-specific use. - */ - protected void handleTypeTextField(int id, GuiNpcTextField field) { - // Default: no-op - } - - // ═══════════════════════════════════════════════════════════════════════════ - // TARGET TAB - // ═══════════════════════════════════════════════════════════════════════════ - - private void initTargetTab(int startY) { - int y = startY; - int col1LabelX = guiLeft + 8; - int col1FieldX = guiLeft + 70; - int col2LabelX = guiLeft + 180; - int col2FieldX = guiLeft + 248; - - // Row 1: Min Range + Max Range - addLabel(new GuiNpcLabel(20, "ability.minRange", col1LabelX, y + 5)); - GuiNpcTextField minRangeField = new GuiNpcTextField(5, this, fontRendererObj, col1FieldX, y, 40, 20, String.valueOf((int) minRange)); - minRangeField.setIntegersOnly(); - minRangeField.setMinMaxDefault(0, 100, 0); - addTextField(minRangeField); - - addLabel(new GuiNpcLabel(21, "ability.maxRange", col2LabelX, y + 5)); - GuiNpcTextField maxRangeField = new GuiNpcTextField(6, this, fontRendererObj, col2FieldX, y, 40, 20, String.valueOf((int) maxRange)); - maxRangeField.setIntegersOnly(); - maxRangeField.setMinMaxDefault(1, 100, 20); - addTextField(maxRangeField); - - y += 24; - - // Row 2: Targeting Mode (if not locked) - addLabel(new GuiNpcLabel(22, "ability.targeting", col1LabelX, y + 5)); - if (!targetingModeLocked) { - String[] targetingModes = getAvailableTargetingModes(); - int selectedIndex = getTargetingModeIndex(targetingMode); - addButton(new GuiNpcButton(4, col1FieldX, y, 90, 20, targetingModes, selectedIndex)); + String tabName; + if (tabIndex >= 4) { + int customIndex = tabIndex - 4; + if (customIndex >= 0 && customIndex < customTabNames.size()) { + tabName = customTabNames.get(customIndex); + } else { + return result; + } } else { - String modeName = "ability.target." + targetingMode.name().toLowerCase(); - addLabel(new GuiNpcLabel(23, modeName, col1FieldX, y + 5)); + switch (tabIndex) { + case TAB_GENERAL: tabName = TAB_NAME_GENERAL; break; + case TAB_TYPE: tabName = TAB_NAME_TYPE; break; + case TAB_TARGET: tabName = TAB_NAME_TARGET; break; + case TAB_EFFECTS: tabName = TAB_NAME_EFFECTS; break; + default: tabName = TAB_NAME_TYPE; break; + } } - y += 28; + for (FieldDef def : fieldDefs) { + if (tabName.equals(def.getTab()) && def.isVisible()) { + result.add(def); + } + } - // ───────────────────────────────────────────────────────────────────── - // CONDITIONS SECTION (up to 3) - // ───────────────────────────────────────────────────────────────────── - addLabel(new GuiNpcLabel(24, "ability.conditions", col1LabelX, y)); - y += 14; + return result; + } + + // ═══════════════════════════════════════════════════════════════════════════ + // CONDITIONS (Target tab) + // ═══════════════════════════════════════════════════════════════════════════ + + private int renderConditions(GuiScrollWindow sw, int y, int labelCounter) { + y += 3; + sw.addLabel(new GuiNpcLabel(labelCounter, "ability.conditions", L_LABEL_X, y + 2, 0xFFFF55)); + y += 15; - // Display existing conditions (similar to Marks pattern) for (int i = 0; i < conditions.size() && i < 3; i++) { Condition cond = conditions.get(i); String condName = getConditionDisplayName(cond); - - // Condition type button - addButton(new GuiNpcButton(50 + i * 10, col1LabelX, y, 140, 20, condName)); - - // Edit button - addButton(new GuiNpcButton(51 + i * 10, col1LabelX + 145, y, 40, 20, "gui.edit")); - - // Delete button - addButton(new GuiNpcButton(52 + i * 10, col1LabelX + 190, y, 20, 20, "X")); - + sw.addButton(new GuiNpcButton(50 + i * 10, L_LABEL_X, y, 140, 20, condName)); + sw.addButton(new GuiNpcButton(51 + i * 10, L_LABEL_X + 145, y, 40, 20, "gui.edit")); + sw.addButton(new GuiNpcButton(52 + i * 10, L_LABEL_X + 190, y, 20, 20, "X")); y += 22; } - // Add button (if less than 3 conditions) if (conditions.size() < 3) { - addButton(new GuiNpcButton(80, col1LabelX, y, 50, 20, "gui.add")); + sw.addButton(new GuiNpcButton(80, L_LABEL_X, y, 50, 20, "gui.add")); + y += ROW_H; } + + return y; } private String getConditionDisplayName(Condition cond) { @@ -391,286 +246,157 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi case "target_hp_below": return StatCollector.translateToLocal("condition.target_hp_below") + " " + (int) (getConditionThreshold(cond) * 100) + "%"; case "hit_count": - Condition.ConditionHitCount hitCount = (Condition.ConditionHitCount) cond; - return StatCollector.translateToLocal("condition.hit_count") + ": " + hitCount.getRequiredHits() + "/" + hitCount.getWithinTicks() + "t"; + Condition.ConditionHitCount hc = (Condition.ConditionHitCount) cond; + return StatCollector.translateToLocal("condition.hit_count") + ": " + hc.getRequiredHits() + "/" + hc.getWithinTicks() + "t"; default: return typeId; } } private float getConditionThreshold(Condition cond) { - // Read threshold from NBT since inner classes don't expose it NBTTagCompound nbt = cond.writeNBT(); return nbt.hasKey("threshold") ? nbt.getFloat("threshold") : 0.5f; } - private String[] getAvailableTargetingModes() { - TargetingMode[] modes = ability.getAllowedTargetingModes(); - if (modes == null) { - modes = TargetingMode.values(); - } - String[] result = new String[modes.length]; - for (int i = 0; i < modes.length; i++) { - result[i] = "ability.target." + modes[i].name().toLowerCase(); - } - return result; - } - - private int getTargetingModeIndex(TargetingMode mode) { - TargetingMode[] modes = ability.getAllowedTargetingModes(); - if (modes == null) { - return mode.ordinal(); - } - for (int i = 0; i < modes.length; i++) { - if (modes[i] == mode) return i; - } - return 0; - } - - // ═══════════════════════════════════════════════════════════════════════════ - // EFFECTS TAB - // ═══════════════════════════════════════════════════════════════════════════ - - private void initEffectsTab(int startY) { - int y = startY; - int col1LabelX = guiLeft + 8; - int col1FieldX = guiLeft + 85; - int col1ClearX = guiLeft + 160; - int col2LabelX = guiLeft + 185; - int col2FieldX = guiLeft + 255; - - // ───────────────────────────────────────────────────────────────────── - // SOUNDS SECTION - // ───────────────────────────────────────────────────────────────────── - addLabel(new GuiNpcLabel(30, "advanced.sounds", col1LabelX, y)); - y += 14; - - // Wind Up Sound - addLabel(new GuiNpcLabel(31, "ability.windUpSound", col1LabelX, y + 5)); - String windUpSoundDisplay = windUpSound != null && !windUpSound.isEmpty() ? truncateString(windUpSound, 10) : StatCollector.translateToLocal("gui.none"); - addButton(new GuiNpcButton(30, col1FieldX, y, 70, 20, windUpSoundDisplay)); - addButton(new GuiNpcButton(35, col1ClearX, y, 20, 20, "X")); - - // Active Sound - addLabel(new GuiNpcLabel(32, "ability.activeSound", col2LabelX, y + 5)); - String activeSoundDisplay = activeSound != null && !activeSound.isEmpty() ? truncateString(activeSound, 10) : StatCollector.translateToLocal("gui.none"); - addButton(new GuiNpcButton(31, col2FieldX, y, 70, 20, activeSoundDisplay)); - addButton(new GuiNpcButton(36, col2FieldX + 75, y, 20, 20, "X")); - - y += 26; - - // ───────────────────────────────────────────────────────────────────── - // ANIMATIONS SECTION - // ───────────────────────────────────────────────────────────────────── - addLabel(new GuiNpcLabel(33, "menu.animations", col1LabelX, y)); - y += 14; - - // Wind Up Animation - addLabel(new GuiNpcLabel(34, "ability.windUpAnim", col1LabelX, y + 5)); - String windUpAnimName = getAnimationName(windUpAnimationId, true); - addButton(new GuiNpcButton(32, col1FieldX, y, 70, 20, windUpAnimName)); - addButton(new GuiNpcButton(37, col1ClearX, y, 20, 20, "X")); - - // Active Animation - addLabel(new GuiNpcLabel(35, "ability.activeAnim", col2LabelX, y + 5)); - String activeAnimName = getAnimationName(activeAnimationId, false); - addButton(new GuiNpcButton(33, col2FieldX, y, 70, 20, activeAnimName)); - addButton(new GuiNpcButton(38, col2FieldX + 75, y, 20, 20, "X")); - - y += 26; - - // ───────────────────────────────────────────────────────────────────── - // TELEGRAPH SECTION (only if supported) - // ───────────────────────────────────────────────────────────────────── - if (supportsTelegraph) { - addLabel(new GuiNpcLabel(40, "ability.telegraph", col1LabelX, y)); - y += 14; - - // Show Telegraph - addLabel(new GuiNpcLabel(41, "ability.showTelegraph", col1LabelX, y + 5)); - addButton(new GuiNpcButton(20, guiLeft + 95, y, 40, 20, new String[]{"gui.no", "gui.yes"}, showTelegraph ? 1 : 0)); - - // Telegraph Type (separate column) - String typeKey = ability.getTelegraphType().name().toLowerCase(); - addLabel(new GuiNpcLabel(42, "ability.telegraphType", guiLeft + 200, y + 5)); - String telegraphLangKey = typeKey.equals("none") ? "gui.none" : "telegraph." + typeKey; - addLabel(new GuiNpcLabel(43, telegraphLangKey, guiLeft + 280, y + 5)); - - y += 24; - - // Colors - addLabel(new GuiNpcLabel(44, "ability.windUpColor", col1LabelX, y + 5)); - String windUpHex = String.format("%06X", windUpColor & 0xFFFFFF); - GuiNpcButton windUpColorBtn = new GuiNpcButton(22, guiLeft + 95, y, 55, 20, windUpHex); - windUpColorBtn.setEnabled(showTelegraph); - windUpColorBtn.setTextColor(windUpColor & 0xFFFFFF); - addButton(windUpColorBtn); - - addLabel(new GuiNpcLabel(45, "ability.activeColor", guiLeft + 200, y + 5)); - String activeHex = String.format("%06X", activeColor & 0xFFFFFF); - GuiNpcButton activeColorBtn = new GuiNpcButton(24, guiLeft + 280, y, 55, 20, activeHex); - activeColorBtn.setEnabled(showTelegraph); - activeColorBtn.setTextColor(activeColor & 0xFFFFFF); - addButton(activeColorBtn); - } - } - - private String truncateString(String str, int maxLen) { - if (str == null) return ""; - if (str.length() <= maxLen) return str; - int lastSlash = str.lastIndexOf('/'); - if (lastSlash >= 0 && str.length() - lastSlash <= maxLen) { - return "..." + str.substring(lastSlash); - } - return "..." + str.substring(str.length() - maxLen + 3); - } - - private String getAnimationName(int animId, boolean isWindUp) { - if (animId < 0) return StatCollector.translateToLocal("gui.none"); - String cachedName = isWindUp ? windUpAnimationName : activeAnimationName; - if (cachedName != null && !cachedName.isEmpty()) { - return truncateString(cachedName, 8); - } - return "ID: " + animId; - } - // ═══════════════════════════════════════════════════════════════════════════ // BUTTON EVENTS // ═══════════════════════════════════════════════════════════════════════════ @Override + @SuppressWarnings({"unchecked", "rawtypes"}) public void buttonEvent(GuiButton guibutton) { int id = guibutton.id; - // Tab switching - if (id == 90) { - activeTab = TAB_GENERAL; - initGui(); - return; - } - if (id == 91 && hasTypeSettings) { - activeTab = TAB_TYPE; - initGui(); - return; - } - if (id == 92) { - activeTab = TAB_TARGET; - initGui(); - return; - } - if (id == 93) { - activeTab = TAB_EFFECTS; + // Tab switching — fixed tabs + if (id == 90) { activeTab = TAB_GENERAL; initGui(); return; } + if (id == 91) { activeTab = TAB_TYPE; initGui(); return; } + if (id == 92) { activeTab = TAB_TARGET; initGui(); return; } + if (id == 93) { activeTab = TAB_EFFECTS; initGui(); return; } + // Custom tabs (94+) + if (id >= 94 && id < 94 + customTabNames.size()) { + activeTab = 4 + (id - 94); initGui(); return; } + if (id == -1000) { close(); return; } - // Enabled button (bottom left) - if (id == 2) { - enabled = ((GuiNpcButton) guibutton).getValue() == 1; - initGui(); - } - - // General tab buttons - else if (id == 14) { - interruptible = ((GuiNpcButton) guibutton).getValue() == 1; - } else if (id == 16) { - lockMovement = ((GuiNpcButton) guibutton).getValue() == 1; - } - - // Target tab - Targeting mode - else if (id == 4) { - TargetingMode[] modes = ability.getAllowedTargetingModes(); - int idx = ((GuiNpcButton) guibutton).getValue(); - if (modes != null && idx < modes.length) { - targetingMode = modes[idx]; - } else { - targetingMode = TargetingMode.values()[idx]; - } - } - - // Close button (X in top menu) - use -1000 to avoid any ID conflicts - else if (id == -1000) { - close(); - return; - } - - // Target tab - Condition buttons (50-79) - else if (id >= 50 && id < 80) { + // Condition buttons (50-80) + if (id >= 50 && id < 80) { int condIndex = (id - 50) / 10; int action = (id - 50) % 10; - if (action == 0 || action == 1) { - // Click on condition name or Edit button - open edit subgui if (condIndex < conditions.size()) { editingConditionIndex = condIndex; setSubGui(new SubGuiConditionEdit(conditions.get(condIndex))); } } else if (action == 2) { - // Delete button if (condIndex < conditions.size()) { conditions.remove(condIndex); initGui(); } } + return; } - // Add condition button - else if (id == 80) { + if (id == 80) { if (conditions.size() < 3) { editingConditionIndex = conditions.size(); setSubGui(new SubGuiConditionEdit(null)); } + return; } - // Effects tab - Telegraph - else if (id == 20) { - showTelegraph = ((GuiNpcButton) guibutton).getValue() == 1; - initGui(); - } else if (id == 22) { - editingColorId = 22; - setSubGui(new SubGuiColorSelector(windUpColor)); - } else if (id == 24) { - editingColorId = 24; - setSubGui(new SubGuiColorSelector(activeColor)); + // Clear buttons (effects delete or SUB_GUI clear) + if (id >= CLEAR_ID_START) { + // Check effects list delete first + int[] meta = builder.getEffectWidgetMeta().get(id); + if (meta != null && meta[1] == 3) { + FieldDef def = builder.getClearFieldMap().get(id); + if (def != null && def.getType() == FieldType.EFFECTS_LIST) { + handleEffectButton(def, meta, guibutton); + return; + } + } + // SUB_GUI clear + FieldDef def = builder.getClearFieldMap().get(id); + if (def != null && def.hasClearAction()) { + def.getClearAction().run(); + initGui(); + } + return; } - // Effects tab - Sound buttons - else if (id == 30) { - editingSoundId = 30; - setSubGui(new GuiSoundSelection(windUpSound)); - } else if (id == 31) { - editingSoundId = 31; - setSubGui(new GuiSoundSelection(activeSound)); - } - // Sound clear buttons - else if (id == 35) { - windUpSound = ""; - initGui(); - } else if (id == 36) { - activeSound = ""; - initGui(); - } - // Animation buttons - else if (id == 32) { - editingAnimationId = 32; - setSubGui(new GuiAnimationSelection(windUpAnimationId)); - } else if (id == 33) { - editingAnimationId = 33; - setSubGui(new GuiAnimationSelection(activeAnimationId)); - } - // Animation clear buttons - else if (id == 37) { - windUpAnimationId = -1; - windUpAnimationName = null; - initGui(); - } else if (id == 38) { - activeAnimationId = -1; - activeAnimationName = null; - initGui(); - } + // Declarative field buttons + if (id >= DECLARATIVE_ID_START) { + FieldDef def = builder.getButtonFieldMap().get(id); + if (def == null) return; - // Type-specific buttons (100+) - delegate to subclass - else if (id >= 100) { - handleTypeButton(id, (GuiNpcButton) guibutton); + // Effects list buttons (type, amp, add) + int[] meta = builder.getEffectWidgetMeta().get(id); + if (meta != null && def.getType() == FieldType.EFFECTS_LIST) { + handleEffectButton(def, meta, guibutton); + return; + } + + switch (def.getType()) { + case BOOLEAN: + def.setValue(((GuiNpcButton) guibutton).getValue() == 1); + initGui(); + break; + case ENUM: { + int idx = ((GuiNpcButton) guibutton).getValue(); + Class> ec = def.getEnumClass(); + if (ec != null) { + Enum[] constants = ec.getEnumConstants(); + if (idx >= 0 && idx < constants.length) def.setValue(constants[idx]); + } + initGui(); + break; + } + case STRING_ENUM: { + int idx = ((GuiNpcButton) guibutton).getValue(); + String[] values = def.getStringEnumValues(); + if (values != null && idx >= 0 && idx < values.length) def.setStringEnumValue(values[idx]); + initGui(); + break; + } + case SUB_GUI: + activeSubGuiField = def; + if (def.getSubGuiFactory() != null) setSubGui(def.getSubGuiFactory().get()); + break; + } + } + } + + @SuppressWarnings("unchecked") + private void handleEffectButton(FieldDef def, int[] meta, GuiButton btn) { + List effects = (List) def.getValue(); + if (effects == null) return; + int effectIdx = meta[0]; + int action = meta[1]; + + switch (action) { + case 0: // Type changed + if (effectIdx < effects.size()) { + effects.get(effectIdx).setType(AbilityEffect.EffectType.fromOrdinal(((GuiNpcButton) btn).getValue())); + } + break; + case 2: // Amp changed + if (effectIdx < effects.size()) { + effects.get(effectIdx).setAmplifier(((GuiNpcButton) btn).getValue()); + } + break; + case 3: // Delete + if (effectIdx < effects.size()) { + effects.remove(effectIdx); + initGui(); + } + break; + case 4: // Add + if (effects.size() < 5) { + effects.add(new AbilityEffect(AbilityEffect.EffectType.SLOWNESS, 60, 0)); + initGui(); + } + break; } } @@ -679,80 +405,57 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi // ═══════════════════════════════════════════════════════════════════════════ @Override + @SuppressWarnings("unchecked") public void unFocused(GuiNpcTextField textField) { int id = textField.id; + if (id < DECLARATIVE_ID_START) return; - // General tab - if (id == 1) { - name = textField.getText(); - } else if (id == 3) { - weight = textField.getInteger(); + // Check effects list duration fields + int[] meta = builder.getEffectWidgetMeta().get(id); + if (meta != null && meta[1] == 1) { + FieldDef def = builder.getTextFieldMap().get(id); + if (def != null && def.getType() == FieldType.EFFECTS_LIST) { + List effects = (List) def.getValue(); + if (effects != null && meta[0] < effects.size()) { + effects.get(meta[0]).setDurationTicks(textField.getInteger()); + } + return; + } } - // Target tab - else if (id == 5) { - minRange = textField.getInteger(); - } else if (id == 6) { - maxRange = textField.getInteger(); - } + FieldDef def = builder.getTextFieldMap().get(id); + if (def == null) return; - // Timing (General tab) - else if (id == 10) { - cooldownTicks = textField.getInteger(); - } else if (id == 11) { - windUpTicks = textField.getInteger(); - } else if (id == 12) { - activeTicks = textField.getInteger(); - } else if (id == 13) { - recoveryTicks = textField.getInteger(); - } - - // Type-specific fields (100+) - delegate to subclass - else if (id >= 100) { - handleTypeTextField(id, textField); + switch (def.getType()) { + case FLOAT: + try { def.setValue(Float.parseFloat(textField.getText())); } catch (NumberFormatException ignored) {} + break; + case INT: + try { def.setValue(Integer.parseInt(textField.getText())); } catch (NumberFormatException ignored) {} + break; + case STRING: + def.setValue(textField.getText()); + break; } } // ═══════════════════════════════════════════════════════════════════════════ - // SUBGUI CLOSED HANDLER + // SUB GUI CLOSED // ═══════════════════════════════════════════════════════════════════════════ @Override public void subGuiClosed(SubGuiInterface subgui) { - if (subgui instanceof SubGuiColorSelector) { - SubGuiColorSelector colorSelector = (SubGuiColorSelector) subgui; - int rgb = colorSelector.color & 0x00FFFFFF; - if (editingColorId == 22) { - windUpColor = 0x80000000 | rgb; - } else if (editingColorId == 24) { - activeColor = 0xC0000000 | rgb; + if (activeSubGuiField != null) { + FieldDef def = activeSubGuiField; + activeSubGuiField = null; + if (def.getSubGuiResultHandler() != null) { + def.getSubGuiResultHandler().accept(subgui); } - editingColorId = 0; initGui(); - } else if (subgui instanceof GuiSoundSelection) { - GuiSoundSelection soundSelector = (GuiSoundSelection) subgui; - if (soundSelector.selectedResource != null) { - String sound = soundSelector.selectedResource.toString(); - if (editingSoundId == 30) { - windUpSound = sound; - } else if (editingSoundId == 31) { - activeSound = sound; - } - } - editingSoundId = 0; - initGui(); - } else if (subgui instanceof GuiAnimationSelection) { - GuiAnimationSelection animSelector = (GuiAnimationSelection) subgui; - if (editingAnimationId == 32) { - windUpAnimationId = animSelector.selectedAnimationId; - windUpAnimationName = animSelector.getSelectedName(); - } else if (editingAnimationId == 33) { - activeAnimationId = animSelector.selectedAnimationId; - activeAnimationName = animSelector.getSelectedName(); - } - editingAnimationId = 0; - initGui(); - } else if (subgui instanceof SubGuiConditionEdit) { + return; + } + + if (subgui instanceof SubGuiConditionEdit) { SubGuiConditionEdit condEdit = (SubGuiConditionEdit) subgui; Condition result = condEdit.getResult(); if (result != null && editingConditionIndex >= 0) { @@ -768,135 +471,33 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi } // ═══════════════════════════════════════════════════════════════════════════ - // CLOSE - Save before closing + // CLOSE / APPLY // ═══════════════════════════════════════════════════════════════════════════ @Override public void close() { - // Apply all cached values to the ability before closing applyToAbility(); - - // Notify callback that ability was saved callback.onAbilitySaved(ability); - - // Close the GUI super.close(); } - // ═══════════════════════════════════════════════════════════════════════════ - // APPLY TO ABILITY - // ═══════════════════════════════════════════════════════════════════════════ - protected void applyToAbility() { - // General - ability.setName(name); - ability.setEnabled(enabled); - ability.setWeight(weight); - ability.setLockMovement(lockMovement); - ability.setInterruptible(interruptible); - - // Timing - ability.setCooldownTicks(cooldownTicks); - ability.setWindUpTicks(windUpTicks); - ability.setActiveTicks(activeTicks); - ability.setRecoveryTicks(recoveryTicks); - - // Target - ability.setMinRange(minRange); - ability.setMaxRange(maxRange); - ability.setTargetingMode(targetingMode); - - // Conditions ability.getConditions().clear(); - for (Condition c : conditions) { - ability.addCondition(c); - } - - // Effects - ability.setWindUpSound(windUpSound); - ability.setActiveSound(activeSound); - ability.setWindUpAnimationId(windUpAnimationId); - ability.setActiveAnimationId(activeAnimationId); - - // Telegraph - ability.setShowTelegraph(showTelegraph); - ability.setWindUpColor(windUpColor); - ability.setActiveColor(activeColor); - - // Type-specific values are applied directly in handleTypeTextField and handleTypeButton + for (Condition c : conditions) ability.addCondition(c); } - /** - * Called when an ability is loaded from saved presets. - */ public void loadAbility(Ability loadedAbility) { if (loadedAbility == null) return; - - // Copy all values from loaded ability to current ability NBTTagCompound nbt = loadedAbility.writeNBT(); - // Keep the current typeId since we can't change types mid-edit nbt.setString("typeId", ability.getTypeId()); ability.readNBT(nbt); - // Update cached values - this.name = ability.getName() != null ? ability.getName() : ""; - this.enabled = ability.isEnabled(); - this.weight = ability.getWeight(); - this.lockMovement = ability.isLockMovement(); - this.interruptible = ability.isInterruptible(); - - this.cooldownTicks = ability.getCooldownTicks(); - this.windUpTicks = ability.getWindUpTicks(); - this.activeTicks = ability.getActiveTicks(); - this.recoveryTicks = ability.getRecoveryTicks(); - - this.minRange = ability.getMinRange(); - this.maxRange = ability.getMaxRange(); - this.targetingMode = ability.getTargetingMode(); this.conditions = new ArrayList<>(ability.getConditions()); - - this.showTelegraph = ability.isShowTelegraph(); - this.windUpColor = ability.getWindUpColor(); - this.activeColor = ability.getActiveColor(); - + this.fieldDefs = ability.getAllFieldDefinitions(); + discoverCustomTabs(); initGui(); } - // ═══════════════════════════════════════════════════════════════════════════ - // HELPER METHODS FOR SUBCLASSES - // ═══════════════════════════════════════════════════════════════════════════ - - /** - * Creates a float text field for type-specific settings. - */ - protected GuiNpcTextField createFloatField(int id, int x, int y, int width, float value) { - GuiNpcTextField field = new GuiNpcTextField(id, this, fontRendererObj, x, y, width, 20, String.format("%.1f", value)); - return field; - } - - /** - * Creates an integer text field for type-specific settings. - */ - protected GuiNpcTextField createIntField(int id, int x, int y, int width, int value) { - GuiNpcTextField field = new GuiNpcTextField(id, this, fontRendererObj, x, y, width, 20, String.valueOf(value)); - field.setIntegersOnly(); - return field; - } - - /** - * Parses a float from a text field with a fallback default value. - */ - protected float parseFloat(GuiNpcTextField field, float defaultValue) { - try { - return Float.parseFloat(field.getText()); - } catch (NumberFormatException e) { - return defaultValue; - } - } - - /** - * Gets the ability being edited. - */ protected Ability getAbility() { return ability; } 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..c0517257b 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.CustomAbilityRemovePacket; 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 CustomAbilityRemovePacket(abilityName)); + if (onDeleted != null) { + onDeleted.run(); + } close(); } else if (id == 1) { close(); diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityEditMode.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityEditMode.java new file mode 100644 index 000000000..4d21f12ca --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityEditMode.java @@ -0,0 +1,56 @@ +package noppes.npcs.client.gui.advanced; + +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.SubGuiInterface; + +/** + * Dialog asking the user whether to clone-and-modify (convert to inline) or modify the parent ability. + */ +public class SubGuiAbilityEditMode extends SubGuiInterface { + + public static final int MODE_CLONE_MODIFY = 0; + public static final int MODE_MODIFY_PARENT = 1; + + private int result = -1; + + public SubGuiAbilityEditMode() { + setBackground("menubg.png"); + xSize = 220; + ySize = 90; + } + + @Override + public void initGui() { + super.initGui(); + + int y = guiTop + 10; + addLabel(new GuiNpcLabel(0, "ability.edit.mode", guiLeft + 10, y)); + + y += 25; + addButton(new GuiNpcButton(0, guiLeft + 10, y, 95, 20, "ability.cloneModify")); + addButton(new GuiNpcButton(1, guiLeft + 115, y, 95, 20, "ability.modifyParent")); + + y += 25; + addButton(new GuiNpcButton(2, guiLeft + 65, y, 90, 20, "gui.cancel")); + } + + @Override + public void buttonEvent(GuiButton guibutton) { + if (guibutton.id == 0) { + result = MODE_CLONE_MODIFY; + close(); + } else if (guibutton.id == 1) { + result = MODE_MODIFY_PARENT; + close(); + } else if (guibutton.id == 2) { + result = -1; + close(); + } + } + + public int getResult() { + return result; + } +} 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..68a74cb8b 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,45 @@ 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.CustomAbilitiesGetPacket; +import kamkeel.npcs.network.packets.request.ability.CustomAbilityGetPacket; 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.ISubGuiListener; 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.UUID; +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). + * Shows custom abilities from the server, then asks Clone/Reference via SubGuiAbilityLoadMode. */ -public class SubGuiAbilityLoad extends SubGuiInterface implements ICustomScrollListener { +public class SubGuiAbilityLoad extends SubGuiInterface implements ICustomScrollListener, IScrollData, IGuiData, ISubGuiListener { private final SubGuiAbilityConfig parentConfig; private final GuiNPCAbilities parentAbilities; - private String selectedName = null; - private List abilityNames = new ArrayList<>(); + private String selectedDisplayName = null; + private String selectedUuid = null; + + /** Maps display name -> UUID for custom abilities. */ + private final HashMap displayToUuid = new HashMap<>(); + private HashMap rawData = new HashMap<>(); + private GuiCustomScroll scroll; + + private boolean waitingForLoad = false; + private int pendingLoadMode = -1; public SubGuiAbilityLoad(SubGuiAbilityConfig parentConfig) { this.parentConfig = parentConfig; @@ -32,6 +49,8 @@ public class SubGuiAbilityLoad extends SubGuiInterface implements ICustomScrollL setBackground("menubg.png"); xSize = 220; ySize = 180; + + PacketClient.sendClient(new CustomAbilitiesGetPacket()); } public SubGuiAbilityLoad(GuiNPCAbilities parentAbilities) { @@ -41,71 +60,90 @@ public class SubGuiAbilityLoad extends SubGuiInterface implements ICustomScrollL setBackground("menubg.png"); xSize = 220; ySize = 180; + + PacketClient.sendClient(new CustomAbilitiesGetPacket()); } @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.setSelected(selectedName); + scroll.setList(getFilteredList()); + if (selectedDisplayName != null) { + scroll.setSelected(selectedDisplayName); } addScroll(scroll); 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 - getButton(0).setEnabled(selectedName != null && !selectedName.isEmpty()); - getButton(1).setEnabled(selectedName != null && !selectedName.isEmpty()); + getButton(0).setEnabled(selectedUuid != null); + } + + private List getFilteredList() { + return new ArrayList<>(displayToUuid.keySet()); } @Override public void buttonEvent(GuiButton guibutton) { 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); - } + if (id == 0 && selectedUuid != null) { + if (parentAbilities != null) { + // From NPC abilities GUI - ask clone/reference + setSubGui(new SubGuiAbilityLoadMode()); + } else { + // From config GUI - always clone + waitingForLoad = true; + pendingLoadMode = SubGuiAbilityLoadMode.MODE_CLONE; + PacketClient.sendClient(new CustomAbilityGetPacket(selectedUuid)); } - close(); - } else if (id == 1 && selectedName != null) { - // Delete confirmation - setSubGui(new SubGuiAbilityDeleteConfirm(selectedName, this)); } else if (id == 2) { close(); } } + @Override + public void subGuiClosed(SubGuiInterface subgui) { + if (subgui instanceof SubGuiAbilityLoadMode) { + int mode = ((SubGuiAbilityLoadMode) subgui).getResult(); + if (mode < 0) return; // cancelled + + if (mode == SubGuiAbilityLoadMode.MODE_REFERENCE) { + // Reference mode - just pass the reference ID + if (parentAbilities != null && selectedUuid != null) { + parentAbilities.loadAbilityReference(selectedUuid); + } + close(); + } else { + // Clone mode - request full data + waitingForLoad = true; + pendingLoadMode = SubGuiAbilityLoadMode.MODE_CLONE; + PacketClient.sendClient(new CustomAbilityGetPacket(selectedUuid)); + } + } + } + @Override public void customScrollClicked(int i, int j, int k, GuiCustomScroll scroll) { if (scroll.id == 0) { - selectedName = scroll.getSelected(); + selectedDisplayName = scroll.getSelected(); + if (selectedDisplayName != null) { + selectedUuid = displayToUuid.get(selectedDisplayName); + } initGui(); } } @@ -113,9 +151,57 @@ 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); + selectedDisplayName = selection; + selectedUuid = displayToUuid.get(selectedDisplayName); + if (selectedUuid != null) { + if (parentAbilities != null) { + setSubGui(new SubGuiAbilityLoadMode()); + } else { + waitingForLoad = true; + pendingLoadMode = SubGuiAbilityLoadMode.MODE_CLONE; + PacketClient.sendClient(new CustomAbilityGetPacket(selectedUuid)); + } + } + } + } + + @Override + public void setData(Vector list, HashMap data, EnumScrollData type) { + if (type == EnumScrollData.CUSTOM_ABILITIES) { + displayToUuid.clear(); + rawData = data; + for (String key : data.keySet()) { + // key format: "displayName\tUUID" + int tabIndex = key.indexOf('\t'); + if (tabIndex > 0) { + String displayName = key.substring(0, tabIndex); + String uuid = key.substring(tabIndex + 1); + displayToUuid.put(displayName, uuid); + } else { + displayToUuid.put(key, key); + } + } + if (scroll != null) { + scroll.setList(getFilteredList()); + if (selectedDisplayName != null && displayToUuid.containsKey(selectedDisplayName)) { + scroll.setSelected(selectedDisplayName); + } + } + initGui(); + } + } + + @Override + public void setGuiData(NBTTagCompound compound) { + if (waitingForLoad) { + waitingForLoad = false; + Ability loaded = AbilityController.Instance.fromNBT(compound); if (loaded != null) { + if (pendingLoadMode == SubGuiAbilityLoadMode.MODE_CLONE) { + // Give it a new UUID so it's a unique inline copy + loaded.setId(UUID.randomUUID().toString()); + } + if (parentConfig != null) { parentConfig.loadAbility(loaded); } else if (parentAbilities != null) { @@ -126,13 +212,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.selectedDisplayName = selected; + if (scroll != null) { + scroll.setSelected(selected); } - initGui(); } } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityLoadMode.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityLoadMode.java new file mode 100644 index 000000000..28a7bd0bb --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityLoadMode.java @@ -0,0 +1,56 @@ +package noppes.npcs.client.gui.advanced; + +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.SubGuiInterface; + +/** + * Dialog asking the user whether to Clone (inline copy) or Reference the loaded ability. + */ +public class SubGuiAbilityLoadMode extends SubGuiInterface { + + public static final int MODE_CLONE = 0; + public static final int MODE_REFERENCE = 1; + + private int result = -1; + + public SubGuiAbilityLoadMode() { + setBackground("menubg.png"); + xSize = 200; + ySize = 90; + } + + @Override + public void initGui() { + super.initGui(); + + int y = guiTop + 10; + addLabel(new GuiNpcLabel(0, "ability.load.mode", guiLeft + 10, y)); + + y += 25; + addButton(new GuiNpcButton(0, guiLeft + 15, y, 80, 20, "ability.clone")); + addButton(new GuiNpcButton(1, guiLeft + 105, y, 80, 20, "ability.reference")); + + y += 25; + addButton(new GuiNpcButton(2, guiLeft + 55, y, 90, 20, "gui.cancel")); + } + + @Override + public void buttonEvent(GuiButton guibutton) { + if (guibutton.id == 0) { + result = MODE_CLONE; + close(); + } else if (guibutton.id == 1) { + result = MODE_REFERENCE; + close(); + } else if (guibutton.id == 2) { + result = -1; + close(); + } + } + + public int getResult() { + return result; + } +} 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..9407d20c5 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.CustomAbilitySavePacket; 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 CustomAbilitySavePacket(ability.writeNBT())); + if (callback != null) { + callback.onAbilitySaved(ability); + } close(); } else if (id == 1) { close(); diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiConditionEdit.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiConditionEdit.java index 3d2874dbc..d941f19ec 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiConditionEdit.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiConditionEdit.java @@ -88,7 +88,9 @@ public class SubGuiConditionEdit extends SubGuiInterface implements ITextfieldLi // Condition Type selector addLabel(new GuiNpcLabel(1, "gui.type", labelX, y + 5)); - addButton(new GuiNpcButton(1, fieldX, y, 115, 20, CONDITION_TYPES, selectedTypeIndex)); + GuiNpcButton typeBtn = new GuiNpcButton(1, fieldX, y, 115, 20, CONDITION_TYPES, selectedTypeIndex); + typeBtn.setHoverText("condition.hover.type"); + addButton(typeBtn); y += 26; // Type-specific fields @@ -96,24 +98,27 @@ public class SubGuiConditionEdit extends SubGuiInterface implements ITextfieldLi if (typeId.equals("hit_count")) { // Hit count fields addLabel(new GuiNpcLabel(2, "condition.hits", labelX, y + 5)); - GuiNpcTextField hitsField = new GuiNpcTextField(2, this, fontRendererObj, fieldX, y, 40, 20, String.valueOf(requiredHits)); + GuiNpcTextField hitsField = new GuiNpcTextField(2, this, fontRendererObj, fieldX, y, 40, 20, "" + requiredHits); hitsField.setIntegersOnly(); hitsField.setMinMaxDefault(1, 100, 3); + hitsField.setHoverText("condition.hover.hits"); addTextField(hitsField); y += 24; addLabel(new GuiNpcLabel(3, "condition.within", labelX, y + 5)); - GuiNpcTextField ticksField = new GuiNpcTextField(3, this, fontRendererObj, fieldX, y, 40, 20, String.valueOf(withinTicks)); + GuiNpcTextField ticksField = new GuiNpcTextField(3, this, fontRendererObj, fieldX, y, 40, 20, "" + withinTicks); ticksField.setIntegersOnly(); ticksField.setMinMaxDefault(1, 1200, 60); + ticksField.setHoverText("condition.hover.within"); addTextField(ticksField); } else { // HP threshold field (as percentage) addLabel(new GuiNpcLabel(2, "condition.percent", labelX, y + 5)); int percent = (int) (threshold * 100); - GuiNpcTextField thresholdField = new GuiNpcTextField(4, this, fontRendererObj, fieldX, y, 40, 20, String.valueOf(percent)); + GuiNpcTextField thresholdField = new GuiNpcTextField(4, this, fontRendererObj, fieldX, y, 40, 20, "" + percent); thresholdField.setIntegersOnly(); thresholdField.setMinMaxDefault(1, 99, 50); + thresholdField.setHoverText("condition.hover.threshold"); addTextField(thresholdField); addLabel(new GuiNpcLabel(4, "%", fieldX + 45, y + 5)); } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityBeam.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityBeam.java deleted file mode 100644 index ce56f809e..000000000 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityBeam.java +++ /dev/null @@ -1,110 +0,0 @@ -package noppes.npcs.client.gui.advanced.ability; - -import kamkeel.npcs.controllers.data.ability.type.AbilityBeam; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -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.IAbilityConfigCallback; - -/** - * GUI for configuring Beam ability type-specific settings. - */ -public class SubGuiAbilityBeam extends SubGuiAbilityConfig { - - private final AbilityBeam beam; - - public SubGuiAbilityBeam(AbilityBeam ability, IAbilityConfigCallback callback) { - super(ability, callback); - this.beam = ability; - } - - @Override - protected void initTypeTab(int startY) { - int y = startY; - int labelX = guiLeft + 8; - int fieldX = guiLeft + 85; - int col2LabelX = guiLeft + 145; - int col2FieldX = guiLeft + 205; - - // Row 1: Damage + Damage Interval - addLabel(new GuiNpcLabel(100, "enchantment.damage", labelX, y + 5)); - addTextField(createFloatField(100, fieldX, y, 50, beam.getDamage())); - - addLabel(new GuiNpcLabel(101, "ability.dmgInterval", col2LabelX, y + 5)); - addTextField(createIntField(101, col2FieldX, y, 50, beam.getDamageInterval())); - - y += 24; - - // Row 2: Beam Length + Beam Width - addLabel(new GuiNpcLabel(102, "ability.beamLength", labelX, y + 5)); - addTextField(createFloatField(102, fieldX, y, 50, beam.getBeamLength())); - - addLabel(new GuiNpcLabel(103, "ability.beamWidth", col2LabelX, y + 5)); - addTextField(createFloatField(103, col2FieldX, y, 50, beam.getBeamWidth())); - - y += 24; - - // Row 3: Sweep Angle + Sweep Speed - addLabel(new GuiNpcLabel(104, "ability.sweepAngle", labelX, y + 5)); - addTextField(createFloatField(104, fieldX, y, 50, beam.getSweepAngle())); - - addLabel(new GuiNpcLabel(105, "ability.sweepSpeed", col2LabelX, y + 5)); - addTextField(createFloatField(105, col2FieldX, y, 50, beam.getSweepSpeed())); - - y += 24; - - // Row 4: Piercing + Lock On Target - addLabel(new GuiNpcLabel(106, "ability.piercing", labelX, y + 5)); - addButton(new GuiNpcButton(106, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, beam.isPiercing() ? 1 : 0)); - - addLabel(new GuiNpcLabel(107, "ability.lockTarget", col2LabelX, y + 5)); - addButton(new GuiNpcButton(107, col2FieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, beam.isLockOnTarget() ? 1 : 0)); - - y += 24; - - // Row 5: Sweep Back & Forth - addLabel(new GuiNpcLabel(108, "ability.sweepBackForth", labelX, y + 5)); - addButton(new GuiNpcButton(108, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, beam.isSweepBackAndForth() ? 1 : 0)); - } - - @Override - protected void handleTypeButton(int id, GuiNpcButton button) { - int value = button.getValue(); - switch (id) { - case 106: - beam.setPiercing(value == 1); - break; - case 107: - beam.setLockOnTarget(value == 1); - break; - case 108: - beam.setSweepBackAndForth(value == 1); - break; - } - } - - @Override - protected void handleTypeTextField(int id, GuiNpcTextField field) { - switch (id) { - case 100: - beam.setDamage(parseFloat(field, beam.getDamage())); - break; - case 101: - beam.setDamageInterval(field.getInteger()); - break; - case 102: - beam.setBeamLength(parseFloat(field, beam.getBeamLength())); - break; - case 103: - beam.setBeamWidth(parseFloat(field, beam.getBeamWidth())); - break; - case 104: - beam.setSweepAngle(parseFloat(field, beam.getSweepAngle())); - break; - case 105: - beam.setSweepSpeed(parseFloat(field, beam.getSweepSpeed())); - break; - } - } -} diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityCharge.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityCharge.java deleted file mode 100644 index 4d7486c5e..000000000 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityCharge.java +++ /dev/null @@ -1,67 +0,0 @@ -package noppes.npcs.client.gui.advanced.ability; - -import kamkeel.npcs.controllers.data.ability.type.AbilityCharge; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -import noppes.npcs.client.gui.util.GuiNpcLabel; -import noppes.npcs.client.gui.util.GuiNpcTextField; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; - -/** - * GUI for configuring Charge ability type-specific settings. - */ -public class SubGuiAbilityCharge extends SubGuiAbilityConfig { - - private final AbilityCharge charge; - - public SubGuiAbilityCharge(AbilityCharge ability, IAbilityConfigCallback callback) { - super(ability, callback); - this.charge = ability; - } - - @Override - protected void initTypeTab(int startY) { - int y = startY; - int labelX = guiLeft + 8; - int fieldX = guiLeft + 85; - int col2LabelX = guiLeft + 145; - int col2FieldX = guiLeft + 205; - - // Row 1: Damage + Speed - addLabel(new GuiNpcLabel(100, "enchantment.damage", labelX, y + 5)); - addTextField(createFloatField(100, fieldX, y, 50, charge.getDamage())); - - addLabel(new GuiNpcLabel(101, "stats.speed", col2LabelX, y + 5)); - addTextField(createFloatField(101, col2FieldX, y, 50, charge.getChargeSpeed())); - - y += 24; - - // Row 2: Knockback - addLabel(new GuiNpcLabel(102, "ability.knockback", labelX, y + 5)); - addTextField(createFloatField(102, fieldX, y, 50, charge.getKnockback())); - - y += 24; - - // Row 3: Hit Width - addLabel(new GuiNpcLabel(104, "ability.hitWidth", labelX, y + 5)); - addTextField(createFloatField(104, fieldX, y, 50, charge.getHitWidth())); - - } - - @Override - protected void handleTypeTextField(int id, GuiNpcTextField field) { - switch (id) { - case 100: - charge.setDamage(parseFloat(field, charge.getDamage())); - break; - case 101: - charge.setChargeSpeed(parseFloat(field, charge.getChargeSpeed())); - break; - case 102: - charge.setKnockback(parseFloat(field, charge.getKnockback())); - break; - case 104: - charge.setHitWidth(parseFloat(field, charge.getHitWidth())); - break; - } - } -} diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityCutter.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityCutter.java deleted file mode 100644 index 514391f8a..000000000 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityCutter.java +++ /dev/null @@ -1,126 +0,0 @@ -package noppes.npcs.client.gui.advanced.ability; - -import kamkeel.npcs.controllers.data.ability.type.AbilityCutter; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -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.IAbilityConfigCallback; - -/** - * GUI for configuring Cutter ability type-specific settings. - */ -public class SubGuiAbilityCutter extends SubGuiAbilityConfig { - - private final AbilityCutter cutter; - - public SubGuiAbilityCutter(AbilityCutter ability, IAbilityConfigCallback callback) { - super(ability, callback); - this.cutter = ability; - } - - @Override - protected void initTypeTab(int startY) { - int y = startY; - int labelX = guiLeft + 8; - int fieldX = guiLeft + 85; - int col2LabelX = guiLeft + 145; - int col2FieldX = guiLeft + 205; - - // Row 1: Damage + Range - addLabel(new GuiNpcLabel(100, "enchantment.damage", labelX, y + 5)); - addTextField(createFloatField(100, fieldX, y, 50, cutter.getDamage())); - - addLabel(new GuiNpcLabel(101, "gui.range", col2LabelX, y + 5)); - addTextField(createFloatField(101, col2FieldX, y, 50, cutter.getRange())); - - y += 24; - - // Row 2: Arc Angle + Inner Radius - addLabel(new GuiNpcLabel(102, "ability.arcAngle", labelX, y + 5)); - addTextField(createFloatField(102, fieldX, y, 50, cutter.getArcAngle())); - - addLabel(new GuiNpcLabel(103, "ability.innerRadius", col2LabelX, y + 5)); - addTextField(createFloatField(103, col2FieldX, y, 50, cutter.getInnerRadius())); - - y += 24; - - // Row 3: Knockback + Sweep Speed - addLabel(new GuiNpcLabel(104, "ability.knockback", labelX, y + 5)); - addTextField(createFloatField(104, fieldX, y, 50, cutter.getKnockback())); - - addLabel(new GuiNpcLabel(105, "ability.sweepSpeed", col2LabelX, y + 5)); - addTextField(createFloatField(105, col2FieldX, y, 50, cutter.getSweepSpeed())); - - y += 24; - - // Row 4: Sweep Mode + Piercing - addLabel(new GuiNpcLabel(106, "ability.sweepMode", labelX, y + 5)); - String[] sweepModes = {"Swipe", "Spin"}; - addButton(new GuiNpcButton(106, fieldX, y, 70, 20, sweepModes, cutter.getSweepMode().ordinal())); - - addLabel(new GuiNpcLabel(107, "ability.piercing", col2LabelX, y + 5)); - addButton(new GuiNpcButton(107, col2FieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, cutter.isPiercing() ? 1 : 0)); - - y += 24; - - // Row 5: Stun Duration + Poison Time - addLabel(new GuiNpcLabel(108, "ability.stunDuration", labelX, y + 5)); - addTextField(createIntField(108, fieldX, y, 50, cutter.getStunDuration())); - - addLabel(new GuiNpcLabel(109, "ability.poisonTime", col2LabelX, y + 5)); - addTextField(createIntField(109, col2FieldX, y, 50, cutter.getPoisonDurationSeconds())); - - y += 24; - - // Row 6: Poison Level - addLabel(new GuiNpcLabel(110, "ability.poisonLvl", labelX, y + 5)); - addTextField(createIntField(110, fieldX, y, 50, cutter.getPoisonLevel())); - } - - @Override - protected void handleTypeButton(int id, GuiNpcButton button) { - int value = button.getValue(); - switch (id) { - case 106: - cutter.setSweepMode(AbilityCutter.SweepMode.values()[value]); - break; - case 107: - cutter.setPiercing(value == 1); - break; - } - } - - @Override - protected void handleTypeTextField(int id, GuiNpcTextField field) { - switch (id) { - case 100: - cutter.setDamage(parseFloat(field, cutter.getDamage())); - break; - case 101: - cutter.setRange(parseFloat(field, cutter.getRange())); - break; - case 102: - cutter.setArcAngle(parseFloat(field, cutter.getArcAngle())); - break; - case 103: - cutter.setInnerRadius(parseFloat(field, cutter.getInnerRadius())); - break; - case 104: - cutter.setKnockback(parseFloat(field, cutter.getKnockback())); - break; - case 105: - cutter.setSweepSpeed(parseFloat(field, cutter.getSweepSpeed())); - break; - case 108: - cutter.setStunDuration(field.getInteger()); - break; - case 109: - cutter.setPoisonDurationSeconds(field.getInteger()); - break; - case 110: - cutter.setPoisonLevel(field.getInteger()); - break; - } - } -} diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDash.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDash.java deleted file mode 100644 index 26bfd6610..000000000 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDash.java +++ /dev/null @@ -1,63 +0,0 @@ -package noppes.npcs.client.gui.advanced.ability; - -import kamkeel.npcs.controllers.data.ability.type.AbilityDash; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -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.IAbilityConfigCallback; - -/** - * GUI for configuring Dash ability type-specific settings. - */ -public class SubGuiAbilityDash extends SubGuiAbilityConfig { - - private final AbilityDash dash; - - public SubGuiAbilityDash(AbilityDash ability, IAbilityConfigCallback callback) { - super(ability, callback); - this.dash = ability; - } - - @Override - protected void initTypeTab(int startY) { - int y = startY; - int labelX = guiLeft + 8; - int fieldX = guiLeft + 85; - int col2LabelX = guiLeft + 145; - int col2FieldX = guiLeft + 205; - - // Row 1: Dash Mode - addLabel(new GuiNpcLabel(100, "ability.mode", labelX, y + 5)); - String[] dashModes = {"Aggressive", "Defensive"}; - addButton(new GuiNpcButton(100, fieldX, y, 80, 20, dashModes, dash.getDashMode().ordinal())); - - y += 24; - - // Row 2: Dash Distance + Dash Speed - addLabel(new GuiNpcLabel(101, "ability.dashDist", labelX, y + 5)); - addTextField(createFloatField(101, fieldX, y, 50, dash.getDashDistance())); - - addLabel(new GuiNpcLabel(102, "ability.dashSpeed", col2LabelX, y + 5)); - addTextField(createFloatField(102, col2FieldX, y, 50, dash.getDashSpeed())); - } - - @Override - protected void handleTypeButton(int id, GuiNpcButton button) { - if (id == 100) { - dash.setDashMode(AbilityDash.DashMode.values()[button.getValue()]); - } - } - - @Override - protected void handleTypeTextField(int id, GuiNpcTextField field) { - switch (id) { - case 101: - dash.setDashDistance(parseFloat(field, dash.getDashDistance())); - break; - case 102: - dash.setDashSpeed(parseFloat(field, dash.getDashSpeed())); - break; - } - } -} diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityGuard.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityGuard.java deleted file mode 100644 index c5240d862..000000000 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityGuard.java +++ /dev/null @@ -1,106 +0,0 @@ -package noppes.npcs.client.gui.advanced.ability; - -import kamkeel.npcs.controllers.data.ability.type.AbilityGuard; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -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.IAbilityConfigCallback; - -/** - * GUI for configuring Guard ability type-specific settings. - */ -public class SubGuiAbilityGuard extends SubGuiAbilityConfig { - - private final AbilityGuard guard; - - public SubGuiAbilityGuard(AbilityGuard ability, IAbilityConfigCallback callback) { - super(ability, callback); - this.guard = ability; - } - - @Override - protected void initTypeTab(int startY) { - int y = startY; - int labelX = guiLeft + 8; - int fieldX = guiLeft + 105; - int col2LabelX = guiLeft + 165; - int col2FieldX = guiLeft + 225; - - // Row 1: Damage Reduction - addLabel(new GuiNpcLabel(100, "ability.dmgReduce", labelX, y + 5)); - addTextField(createFloatField(100, fieldX, y, 50, guard.getDamageReduction())); - - y += 24; - - // Row 2: Can Counter + Counter Type - addLabel(new GuiNpcLabel(101, "ability.canCounter", labelX, y + 5)); - addButton(new GuiNpcButton(101, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, guard.isCanCounter() ? 1 : 0)); - - addLabel(new GuiNpcLabel(102, "ability.counterType", col2LabelX, y + 5)); - String[] types = {"Flat", "Percent"}; - GuiNpcButton counterTypeButton = new GuiNpcButton(102, col2FieldX, y, 60, 20, types, guard.getCounterType().ordinal()); - counterTypeButton.setEnabled(guard.isCanCounter()); - addButton(counterTypeButton); - - y += 24; - - // Row 3: Counter Value + Counter Chance - addLabel(new GuiNpcLabel(103, "ability.counterValue", labelX, y + 5)); - GuiNpcTextField counterValueField = createFloatField(103, fieldX, y, 50, guard.getCounterValue()); - counterValueField.setEnabled(guard.isCanCounter()); - addTextField(counterValueField); - - addLabel(new GuiNpcLabel(104, "ability.counterChance", col2LabelX, y + 5)); - GuiNpcTextField counterChanceField = createFloatField(104, col2FieldX, y, 50, guard.getCounterChance()); - counterChanceField.setEnabled(guard.isCanCounter()); - addTextField(counterChanceField); - - y += 24; - - // Row 4: Counter Sound + Counter Animation - addLabel(new GuiNpcLabel(105, "ability.counterSound", labelX, y + 5)); - GuiNpcTextField counterSoundField = new GuiNpcTextField(105, this, fontRendererObj, fieldX, y, 100, 20, guard.getCounterSound()); - counterSoundField.setEnabled(guard.isCanCounter()); - addTextField(counterSoundField); - - addLabel(new GuiNpcLabel(106, "ability.counterAnim", col2LabelX, y + 5)); - GuiNpcTextField counterAnimField = createIntField(106, col2FieldX, y, 50, guard.getCounterAnimationId()); - counterAnimField.setEnabled(guard.isCanCounter()); - addTextField(counterAnimField); - } - - @Override - protected void handleTypeButton(int id, GuiNpcButton button) { - switch (id) { - case 101: - guard.setCanCounter(button.getValue() == 1); - initGui(); - break; - case 102: - guard.setCounterType(AbilityGuard.CounterType.values()[button.getValue()]); - break; - } - } - - @Override - protected void handleTypeTextField(int id, GuiNpcTextField field) { - switch (id) { - case 100: - guard.setDamageReduction(parseFloat(field, guard.getDamageReduction())); - break; - case 103: - guard.setCounterValue(parseFloat(field, guard.getCounterValue())); - break; - case 104: - guard.setCounterChance(parseFloat(field, guard.getCounterChance())); - break; - case 105: - guard.setCounterSound(field.getText()); - break; - case 106: - guard.setCounterAnimationId(field.getInteger()); - break; - } - } -} diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHazard.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHazard.java deleted file mode 100644 index 1ec615d7a..000000000 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHazard.java +++ /dev/null @@ -1,118 +0,0 @@ -package noppes.npcs.client.gui.advanced.ability; - -import kamkeel.npcs.controllers.data.ability.type.AbilityHazard; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -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.IAbilityConfigCallback; - -/** - * GUI for configuring Hazard ability type-specific settings. - */ -public class SubGuiAbilityHazard extends SubGuiAbilityConfig { - - private final AbilityHazard hazard; - - public SubGuiAbilityHazard(AbilityHazard ability, IAbilityConfigCallback callback) { - super(ability, callback); - this.hazard = ability; - } - - @Override - protected void initTypeTab(int startY) { - int y = startY; - int labelX = guiLeft + 8; - int fieldX = guiLeft + 85; - int col2LabelX = guiLeft + 145; - int col2FieldX = guiLeft + 205; - - // Row 1: Shape + Placement - addLabel(new GuiNpcLabel(100, "ability.shape", labelX, y + 5)); - String[] shapes = {"Circle", "Ring", "Cone"}; - addButton(new GuiNpcButton(100, fieldX, y, 55, 20, shapes, hazard.getShape().ordinal())); - - addLabel(new GuiNpcLabel(101, "ability.placement", col2LabelX, y + 5)); - String[] placements = {"Caster", "Target", "FollowCast", "FollowTgt"}; - addButton(new GuiNpcButton(101, col2FieldX, y, 60, 20, placements, hazard.getPlacement().ordinal())); - - y += 24; - - // Row 2: Radius + Inner Radius - addLabel(new GuiNpcLabel(102, "ability.radius", labelX, y + 5)); - addTextField(createFloatField(102, fieldX, y, 50, hazard.getRadius())); - - addLabel(new GuiNpcLabel(103, "ability.innerRadius", col2LabelX, y + 5)); - addTextField(createFloatField(103, col2FieldX, y, 50, hazard.getInnerRadius())); - - y += 24; - - // Row 3: Damage Per Second + Damage Interval - addLabel(new GuiNpcLabel(104, "ability.dmgPerSec", labelX, y + 5)); - addTextField(createFloatField(104, fieldX, y, 50, hazard.getDamagePerSecond())); - - addLabel(new GuiNpcLabel(105, "ability.dmgInterval", col2LabelX, y + 5)); - addTextField(createIntField(105, col2FieldX, y, 50, hazard.getDamageInterval())); - - y += 24; - - // Row 4: Slowness Level + Debuff Duration - addLabel(new GuiNpcLabel(106, "ability.slownessLvl", labelX, y + 5)); - addTextField(createIntField(106, fieldX, y, 50, hazard.getSlownessLevel())); - - addLabel(new GuiNpcLabel(107, "ability.debuffDur", col2LabelX, y + 5)); - addTextField(createIntField(107, col2FieldX, y, 50, hazard.getDebuffDuration())); - - y += 24; - - // Row 5: Poison Level + Affects Caster - addLabel(new GuiNpcLabel(108, "ability.poisonLvl", labelX, y + 5)); - addTextField(createIntField(108, fieldX, y, 50, hazard.getPoisonLevel())); - - addLabel(new GuiNpcLabel(109, "ability.affectsCaster", col2LabelX, y + 5)); - addButton(new GuiNpcButton(109, col2FieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, hazard.isAffectsCaster() ? 1 : 0)); - } - - @Override - protected void handleTypeButton(int id, GuiNpcButton button) { - int value = button.getValue(); - switch (id) { - case 100: - hazard.setShape(AbilityHazard.HazardShape.values()[value]); - break; - case 101: - hazard.setPlacement(AbilityHazard.PlacementMode.values()[value]); - break; - case 109: - hazard.setAffectsCaster(value == 1); - break; - } - } - - @Override - protected void handleTypeTextField(int id, GuiNpcTextField field) { - switch (id) { - case 102: - hazard.setRadius(parseFloat(field, hazard.getRadius())); - break; - case 103: - hazard.setInnerRadius(parseFloat(field, hazard.getInnerRadius())); - break; - case 104: - hazard.setDamagePerSecond(parseFloat(field, hazard.getDamagePerSecond())); - break; - case 105: - hazard.setDamageInterval(field.getInteger()); - break; - case 106: - hazard.setSlownessLevel(field.getInteger()); - break; - case 107: - hazard.setDebuffDuration(field.getInteger()); - break; - case 108: - hazard.setPoisonLevel(field.getInteger()); - break; - } - } -} diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHeal.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHeal.java deleted file mode 100644 index 8b679fb25..000000000 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHeal.java +++ /dev/null @@ -1,86 +0,0 @@ -package noppes.npcs.client.gui.advanced.ability; - -import kamkeel.npcs.controllers.data.ability.type.AbilityHeal; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -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.IAbilityConfigCallback; - -/** - * GUI for configuring Heal ability type-specific settings. - */ -public class SubGuiAbilityHeal extends SubGuiAbilityConfig { - - private final AbilityHeal heal; - - public SubGuiAbilityHeal(AbilityHeal ability, IAbilityConfigCallback callback) { - super(ability, callback); - this.heal = ability; - } - - @Override - protected void initTypeTab(int startY) { - int y = startY; - int labelX = guiLeft + 8; - int fieldX = guiLeft + 85; - int col2LabelX = guiLeft + 145; - int col2FieldX = guiLeft + 205; - - // Row 1: Heal Amount + Heal Percent - addLabel(new GuiNpcLabel(100, "ability.healAmount", labelX, y + 5)); - addTextField(createFloatField(100, fieldX, y, 50, heal.getHealAmount())); - - addLabel(new GuiNpcLabel(101, "ability.healPercent", col2LabelX, y + 5)); - addTextField(createFloatField(101, col2FieldX, y, 50, heal.getHealPercent())); - - y += 24; - - // Row 2: Heal Radius + Instant Heal - addLabel(new GuiNpcLabel(102, "ability.healRadius", labelX, y + 5)); - addTextField(createFloatField(102, fieldX, y, 50, heal.getHealRadius())); - - addLabel(new GuiNpcLabel(103, "dialog.instant", col2LabelX, y + 5)); - addButton(new GuiNpcButton(103, col2FieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, heal.isInstantHeal() ? 1 : 0)); - - y += 24; - - // Row 3: Heal Self + Heal Allies - addLabel(new GuiNpcLabel(104, "ability.healSelf", labelX, y + 5)); - addButton(new GuiNpcButton(104, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, heal.isHealSelf() ? 1 : 0)); - - addLabel(new GuiNpcLabel(105, "ability.healAllies", col2LabelX, y + 5)); - addButton(new GuiNpcButton(105, col2FieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, heal.isHealAllies() ? 1 : 0)); - } - - @Override - protected void handleTypeButton(int id, GuiNpcButton button) { - int value = button.getValue(); - switch (id) { - case 103: - heal.setInstantHeal(value == 1); - break; - case 104: - heal.setHealSelf(value == 1); - break; - case 105: - heal.setHealAllies(value == 1); - break; - } - } - - @Override - protected void handleTypeTextField(int id, GuiNpcTextField field) { - switch (id) { - case 100: - heal.setHealAmount(parseFloat(field, heal.getHealAmount())); - break; - case 101: - heal.setHealPercent(parseFloat(field, heal.getHealPercent())); - break; - case 102: - heal.setHealRadius(parseFloat(field, heal.getHealRadius())); - break; - } - } -} diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHeavyHit.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHeavyHit.java deleted file mode 100644 index ea21127dc..000000000 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHeavyHit.java +++ /dev/null @@ -1,72 +0,0 @@ -package noppes.npcs.client.gui.advanced.ability; - -import kamkeel.npcs.controllers.data.ability.type.AbilityHeavyHit; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -import noppes.npcs.client.gui.util.GuiNpcLabel; -import noppes.npcs.client.gui.util.GuiNpcTextField; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; - -/** - * GUI for configuring Heavy Hit ability type-specific settings. - */ -public class SubGuiAbilityHeavyHit extends SubGuiAbilityConfig { - - private final AbilityHeavyHit heavyHit; - - public SubGuiAbilityHeavyHit(AbilityHeavyHit ability, IAbilityConfigCallback callback) { - super(ability, callback); - this.heavyHit = ability; - } - - @Override - protected void initTypeTab(int startY) { - int y = startY; - int labelX = guiLeft + 8; - int fieldX = guiLeft + 85; - int col2LabelX = guiLeft + 145; - int col2FieldX = guiLeft + 205; - - // Row 1: Damage + Potion Time - addLabel(new GuiNpcLabel(100, "enchantment.damage", labelX, y + 5)); - addTextField(createFloatField(100, fieldX, y, 50, heavyHit.getDamage())); - - addLabel(new GuiNpcLabel(101, "ability.potionTime", col2LabelX, y + 5)); - addTextField(createIntField(101, col2FieldX, y, 50, heavyHit.getPotionDurationSeconds())); - - y += 24; - - // Row 2: Knockback + Slowness Level - addLabel(new GuiNpcLabel(102, "ability.knockback", labelX, y + 5)); - addTextField(createFloatField(102, fieldX, y, 50, heavyHit.getKnockback())); - - addLabel(new GuiNpcLabel(103, "ability.slownessLvl", col2LabelX, y + 5)); - addTextField(createIntField(103, col2FieldX, y, 50, heavyHit.getSlownessLevel())); - - y += 24; - - // Row 3: Weakness Level - addLabel(new GuiNpcLabel(104, "ability.weaknessLvl", labelX, y + 5)); - addTextField(createIntField(104, fieldX, y, 50, heavyHit.getWeaknessLevel())); - } - - @Override - protected void handleTypeTextField(int id, GuiNpcTextField field) { - switch (id) { - case 100: - heavyHit.setDamage(parseFloat(field, heavyHit.getDamage())); - break; - case 101: - heavyHit.setPotionDurationSeconds(field.getInteger()); - break; - case 102: - heavyHit.setKnockback(parseFloat(field, heavyHit.getKnockback())); - break; - case 103: - heavyHit.setSlownessLevel(field.getInteger()); - break; - case 104: - heavyHit.setWeaknessLevel(field.getInteger()); - break; - } - } -} diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityOrb.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityOrb.java deleted file mode 100644 index f94e8d0cf..000000000 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityOrb.java +++ /dev/null @@ -1,116 +0,0 @@ -package noppes.npcs.client.gui.advanced.ability; - -import kamkeel.npcs.controllers.data.ability.type.AbilityOrb; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -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.IAbilityConfigCallback; - -/** - * GUI for configuring Orb ability type-specific settings. - */ -public class SubGuiAbilityOrb extends SubGuiAbilityConfig { - - private final AbilityOrb orb; - - public SubGuiAbilityOrb(AbilityOrb ability, IAbilityConfigCallback callback) { - super(ability, callback); - this.orb = ability; - } - - @Override - protected void initTypeTab(int startY) { - int y = startY; - int labelX = guiLeft + 8; - int fieldX = guiLeft + 85; - int col2LabelX = guiLeft + 145; - int col2FieldX = guiLeft + 205; - - // Row 1: Damage + Speed - addLabel(new GuiNpcLabel(100, "enchantment.damage", labelX, y + 5)); - addTextField(createFloatField(100, fieldX, y, 50, orb.getDamage())); - - addLabel(new GuiNpcLabel(101, "stats.speed", col2LabelX, y + 5)); - addTextField(createFloatField(101, col2FieldX, y, 50, orb.getOrbSpeed())); - - y += 24; - - // Row 2: Orb Size + Max Distance - addLabel(new GuiNpcLabel(102, "stats.size", labelX, y + 5)); - addTextField(createFloatField(102, fieldX, y, 50, orb.getOrbSize())); - - addLabel(new GuiNpcLabel(103, "ability.maxDist", col2LabelX, y + 5)); - addTextField(createFloatField(103, col2FieldX, y, 50, orb.getMaxDistance())); - - y += 24; - - // Row 3: Homing + Homing Strength - addLabel(new GuiNpcLabel(104, "ability.homing", labelX, y + 5)); - addButton(new GuiNpcButton(104, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, orb.isHoming() ? 1 : 0)); - - addLabel(new GuiNpcLabel(105, "ability.homingStr", col2LabelX, y + 5)); - addTextField(createFloatField(105, col2FieldX, y, 50, orb.getHomingStrength())); - - y += 24; - - // Row 4: Explosive + Explosion Radius - addLabel(new GuiNpcLabel(106, "ability.explosive", labelX, y + 5)); - addButton(new GuiNpcButton(106, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, orb.isExplosive() ? 1 : 0)); - - addLabel(new GuiNpcLabel(107, "ability.explosionRad", col2LabelX, y + 5)); - addTextField(createFloatField(107, col2FieldX, y, 50, orb.getExplosionRadius())); - - y += 24; - - // Row 5: Knockback + Max Lifetime - addLabel(new GuiNpcLabel(108, "ability.knockback", labelX, y + 5)); - addTextField(createFloatField(108, fieldX, y, 50, orb.getKnockback())); - - addLabel(new GuiNpcLabel(109, "ability.lifetime", col2LabelX, y + 5)); - addTextField(createIntField(109, col2FieldX, y, 50, orb.getMaxLifetime())); - } - - @Override - protected void handleTypeButton(int id, GuiNpcButton button) { - int value = button.getValue(); - switch (id) { - case 104: - orb.setHoming(value == 1); - break; - case 106: - orb.setExplosive(value == 1); - break; - } - } - - @Override - protected void handleTypeTextField(int id, GuiNpcTextField field) { - switch (id) { - case 100: - orb.setDamage(parseFloat(field, orb.getDamage())); - break; - case 101: - orb.setOrbSpeed(parseFloat(field, orb.getOrbSpeed())); - break; - case 102: - orb.setOrbSize(parseFloat(field, orb.getOrbSize())); - break; - case 103: - orb.setMaxDistance(parseFloat(field, orb.getMaxDistance())); - break; - case 105: - orb.setHomingStrength(parseFloat(field, orb.getHomingStrength())); - break; - case 107: - orb.setExplosionRadius(parseFloat(field, orb.getExplosionRadius())); - break; - case 108: - orb.setKnockback(parseFloat(field, orb.getKnockback())); - break; - case 109: - orb.setMaxLifetime(field.getInteger()); - break; - } - } -} diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityProjectile.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityProjectile.java deleted file mode 100644 index c9be00e38..000000000 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityProjectile.java +++ /dev/null @@ -1,111 +0,0 @@ -package noppes.npcs.client.gui.advanced.ability; - -import kamkeel.npcs.controllers.data.ability.type.AbilityProjectile; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -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.IAbilityConfigCallback; - -/** - * GUI for configuring Projectile ability type-specific settings. - */ -public class SubGuiAbilityProjectile extends SubGuiAbilityConfig { - - private final AbilityProjectile projectile; - - public SubGuiAbilityProjectile(AbilityProjectile ability, IAbilityConfigCallback callback) { - super(ability, callback); - this.projectile = ability; - } - - @Override - protected void initTypeTab(int startY) { - int y = startY; - int labelX = guiLeft + 8; - int fieldX = guiLeft + 85; - int col2LabelX = guiLeft + 145; - int col2FieldX = guiLeft + 205; - - // Row 1: Damage + Speed - addLabel(new GuiNpcLabel(100, "enchantment.damage", labelX, y + 5)); - addTextField(createFloatField(100, fieldX, y, 50, projectile.getDamage())); - - addLabel(new GuiNpcLabel(101, "stats.speed", col2LabelX, y + 5)); - addTextField(createFloatField(101, col2FieldX, y, 50, projectile.getSpeed())); - - y += 24; - - // Row 2: Knockback + Projectile Type - addLabel(new GuiNpcLabel(102, "ability.knockback", labelX, y + 5)); - addTextField(createFloatField(102, fieldX, y, 50, projectile.getKnockback())); - - addLabel(new GuiNpcLabel(103, "ability.projType", col2LabelX, y + 5)); - String[] projTypes = {"fireball", "arrow", "magic"}; - int typeIdx = 0; - String currType = projectile.getProjectileType(); - for (int i = 0; i < projTypes.length; i++) { - if (projTypes[i].equals(currType)) { - typeIdx = i; - break; - } - } - addButton(new GuiNpcButton(103, col2FieldX, y, 60, 20, projTypes, typeIdx)); - - y += 24; - - // Row 3: Explosive + Explosion Radius - addLabel(new GuiNpcLabel(104, "ability.explosive", labelX, y + 5)); - addButton(new GuiNpcButton(104, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, projectile.isExplosive() ? 1 : 0)); - - addLabel(new GuiNpcLabel(105, "ability.explosionRad", col2LabelX, y + 5)); - addTextField(createFloatField(105, col2FieldX, y, 50, projectile.getExplosionRadius())); - - y += 24; - - // Row 4: Homing + Homing Strength - addLabel(new GuiNpcLabel(106, "ability.homing", labelX, y + 5)); - addButton(new GuiNpcButton(106, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, projectile.isHoming() ? 1 : 0)); - - addLabel(new GuiNpcLabel(107, "ability.homingStr", col2LabelX, y + 5)); - addTextField(createFloatField(107, col2FieldX, y, 50, projectile.getHomingStrength())); - } - - @Override - protected void handleTypeButton(int id, GuiNpcButton button) { - int value = button.getValue(); - switch (id) { - case 103: - String[] projTypes = {"fireball", "arrow", "magic"}; - projectile.setProjectileType(projTypes[value]); - break; - case 104: - projectile.setExplosive(value == 1); - break; - case 106: - projectile.setHoming(value == 1); - break; - } - } - - @Override - protected void handleTypeTextField(int id, GuiNpcTextField field) { - switch (id) { - case 100: - projectile.setDamage(parseFloat(field, projectile.getDamage())); - break; - case 101: - projectile.setSpeed(parseFloat(field, projectile.getSpeed())); - break; - case 102: - projectile.setKnockback(parseFloat(field, projectile.getKnockback())); - break; - case 105: - projectile.setExplosionRadius(parseFloat(field, projectile.getExplosionRadius())); - break; - case 107: - projectile.setHomingStrength(parseFloat(field, projectile.getHomingStrength())); - break; - } - } -} diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityShockwave.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityShockwave.java deleted file mode 100644 index a03a85736..000000000 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityShockwave.java +++ /dev/null @@ -1,75 +0,0 @@ -package noppes.npcs.client.gui.advanced.ability; - -import kamkeel.npcs.controllers.data.ability.type.AbilityShockwave; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -import noppes.npcs.client.gui.util.GuiNpcLabel; -import noppes.npcs.client.gui.util.GuiNpcTextField; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; - -/** - * GUI for configuring Shockwave ability type-specific settings. - */ -public class SubGuiAbilityShockwave extends SubGuiAbilityConfig { - - private final AbilityShockwave shockwave; - - public SubGuiAbilityShockwave(AbilityShockwave ability, IAbilityConfigCallback callback) { - super(ability, callback); - this.shockwave = ability; - } - - @Override - protected void initTypeTab(int startY) { - int y = startY; - int labelX = guiLeft + 8; - int fieldX = guiLeft + 85; - int col2LabelX = guiLeft + 145; - int col2FieldX = guiLeft + 205; - - // Row 1: Damage + Push Radius - addLabel(new GuiNpcLabel(100, "enchantment.damage", labelX, y + 5)); - addTextField(createFloatField(100, fieldX, y, 50, shockwave.getDamage())); - - addLabel(new GuiNpcLabel(101, "ability.pushRadius", col2LabelX, y + 5)); - addTextField(createFloatField(101, col2FieldX, y, 50, shockwave.getPushRadius())); - - y += 24; - - // Row 2: Push Strength - addLabel(new GuiNpcLabel(102, "ability.pushStrength", labelX, y + 5)); - addTextField(createFloatField(102, fieldX, y, 50, shockwave.getPushStrength())); - - y += 24; - - // Row 3: Stun Duration + Max Targets - addLabel(new GuiNpcLabel(103, "ability.stunDuration", labelX, y + 5)); - addTextField(createIntField(103, fieldX, y, 50, shockwave.getStunDuration())); - - addLabel(new GuiNpcLabel(104, "ability.maxTargets", col2LabelX, y + 5)); - addTextField(createIntField(104, col2FieldX, y, 50, shockwave.getMaxTargets())); - - y += 24; - addLabel(new GuiNpcLabel(105, "ability.shockwaveFalloff", labelX, y + 5)); - } - - @Override - protected void handleTypeTextField(int id, GuiNpcTextField field) { - switch (id) { - case 100: - shockwave.setDamage(parseFloat(field, shockwave.getDamage())); - break; - case 101: - shockwave.setPushRadius(parseFloat(field, shockwave.getPushRadius())); - break; - case 102: - shockwave.setPushStrength(parseFloat(field, shockwave.getPushStrength())); - break; - case 103: - shockwave.setStunDuration(field.getInteger()); - break; - case 104: - shockwave.setMaxTargets(field.getInteger()); - break; - } - } -} diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilitySlam.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilitySlam.java deleted file mode 100644 index 989202037..000000000 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilitySlam.java +++ /dev/null @@ -1,72 +0,0 @@ -package noppes.npcs.client.gui.advanced.ability; - -import kamkeel.npcs.controllers.data.ability.type.AbilitySlam; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -import noppes.npcs.client.gui.util.GuiNpcLabel; -import noppes.npcs.client.gui.util.GuiNpcTextField; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; - -/** - * GUI for configuring Slam ability type-specific settings. - */ -public class SubGuiAbilitySlam extends SubGuiAbilityConfig { - - private final AbilitySlam slam; - - public SubGuiAbilitySlam(AbilitySlam ability, IAbilityConfigCallback callback) { - super(ability, callback); - this.slam = ability; - } - - @Override - protected void initTypeTab(int startY) { - int y = startY; - int labelX = guiLeft + 8; - int fieldX = guiLeft + 85; - int col2LabelX = guiLeft + 145; - int col2FieldX = guiLeft + 205; - - // Row 1: Damage + Radius - addLabel(new GuiNpcLabel(100, "enchantment.damage", labelX, y + 5)); - addTextField(createFloatField(100, fieldX, y, 50, slam.getDamage())); - - addLabel(new GuiNpcLabel(101, "ability.radius", col2LabelX, y + 5)); - addTextField(createFloatField(101, col2FieldX, y, 50, slam.getRadius())); - - y += 24; - - // Row 2: Knockback + Leap Speed - addLabel(new GuiNpcLabel(102, "ability.knockback", labelX, y + 5)); - addTextField(createFloatField(102, fieldX, y, 50, slam.getKnockbackStrength())); - - addLabel(new GuiNpcLabel(103, "ability.leapSpeed", col2LabelX, y + 5)); - addTextField(createFloatField(103, col2FieldX, y, 50, slam.getLeapSpeed())); - - y += 24; - - // Row 3: Leap Height - addLabel(new GuiNpcLabel(104, "ability.leapHeight", labelX, y + 5)); - addTextField(createFloatField(104, fieldX, y, 50, slam.getLeapHeight())); - } - - @Override - protected void handleTypeTextField(int id, GuiNpcTextField field) { - switch (id) { - case 100: - slam.setDamage(parseFloat(field, slam.getDamage())); - break; - case 101: - slam.setRadius(parseFloat(field, slam.getRadius())); - break; - case 102: - slam.setKnockbackStrength(parseFloat(field, slam.getKnockbackStrength())); - break; - case 103: - slam.setLeapSpeed(parseFloat(field, slam.getLeapSpeed())); - break; - case 104: - slam.setLeapHeight(parseFloat(field, slam.getLeapHeight())); - break; - } - } -} diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityTeleport.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityTeleport.java deleted file mode 100644 index 6173f9ded..000000000 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityTeleport.java +++ /dev/null @@ -1,133 +0,0 @@ -package noppes.npcs.client.gui.advanced.ability; - -import kamkeel.npcs.controllers.data.ability.type.AbilityTeleport; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -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.IAbilityConfigCallback; - -/** - * GUI for configuring Teleport ability type-specific settings. - */ -public class SubGuiAbilityTeleport extends SubGuiAbilityConfig { - - private final AbilityTeleport teleport; - - public SubGuiAbilityTeleport(AbilityTeleport ability, IAbilityConfigCallback callback) { - super(ability, callback); - this.teleport = ability; - } - - @Override - protected void initTypeTab(int startY) { - int y = startY; - int labelX = guiLeft + 8; - int fieldX = guiLeft + 85; - int col2LabelX = guiLeft + 145; - int col2FieldX = guiLeft + 205; - - // Row 1: Mode - addLabel(new GuiNpcLabel(100, "ability.mode", labelX, y + 5)); - String[] modes = {"Blink", "Behind", "Single"}; - addButton(new GuiNpcButton(100, fieldX, y, 80, 20, modes, teleport.getMode().ordinal())); - - y += 24; - - switch (teleport.getMode()) { - case BLINK: - // Row 2: Blink Radius + Blink Count - addLabel(new GuiNpcLabel(101, "ability.blinkRadius", labelX, y + 5)); - addTextField(createFloatField(101, fieldX, y, 50, teleport.getBlinkRadius())); - - addLabel(new GuiNpcLabel(102, "ability.blinkCount", col2LabelX, y + 5)); - addTextField(createIntField(102, col2FieldX, y, 50, teleport.getBlinkCount())); - - y += 24; - - // Row 3: Blink Delay + Line of Sight - addLabel(new GuiNpcLabel(103, "ability.blinkDelay", labelX, y + 5)); - addTextField(createIntField(103, fieldX, y, 50, teleport.getBlinkDelayTicks())); - - addLabel(new GuiNpcLabel(105, "ability.lineOfSight", col2LabelX, y + 5)); - addButton(new GuiNpcButton(105, col2FieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, teleport.isRequireLineOfSight() ? 1 : 0)); - y += 24; - break; - case SINGLE: - // Row 2: Blink Radius + Line of Sight - addLabel(new GuiNpcLabel(101, "ability.blinkRadius", labelX, y + 5)); - addTextField(createFloatField(101, fieldX, y, 50, teleport.getBlinkRadius())); - - addLabel(new GuiNpcLabel(105, "ability.lineOfSight", col2LabelX, y + 5)); - addButton(new GuiNpcButton(105, col2FieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, teleport.isRequireLineOfSight() ? 1 : 0)); - y += 24; - break; - case BEHIND: - // Row 2: Behind Distance - addLabel(new GuiNpcLabel(104, "ability.behindDistance", labelX, y + 5)); - addTextField(createFloatField(104, fieldX, y, 50, teleport.getBehindDistance())); - y += 24; - break; - } - - // Damage + Damage Radius - addLabel(new GuiNpcLabel(106, "enchantment.damage", labelX, y + 5)); - addTextField(createFloatField(106, fieldX, y, 50, teleport.getDamage())); - - addLabel(new GuiNpcLabel(107, "ability.dmgRadius", col2LabelX, y + 5)); - addTextField(createFloatField(107, col2FieldX, y, 50, teleport.getDamageRadius())); - - y += 24; - - // Damage at Start/End - addLabel(new GuiNpcLabel(108, "ability.dmgAtStart", labelX, y + 5)); - addButton(new GuiNpcButton(108, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, teleport.isDamageAtStart() ? 1 : 0)); - - addLabel(new GuiNpcLabel(109, "ability.dmgAtEnd", col2LabelX, y + 5)); - addButton(new GuiNpcButton(109, col2FieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, teleport.isDamageAtEnd() ? 1 : 0)); - } - - @Override - protected void handleTypeButton(int id, GuiNpcButton button) { - int value = button.getValue(); - switch (id) { - case 100: - teleport.setMode(AbilityTeleport.TeleportMode.values()[value]); - initGui(); - break; - case 105: - teleport.setRequireLineOfSight(value == 1); - break; - case 108: - teleport.setDamageAtStart(value == 1); - break; - case 109: - teleport.setDamageAtEnd(value == 1); - break; - } - } - - @Override - protected void handleTypeTextField(int id, GuiNpcTextField field) { - switch (id) { - case 101: - teleport.setBlinkRadius(parseFloat(field, teleport.getBlinkRadius())); - break; - case 102: - teleport.setBlinkCount(field.getInteger()); - break; - case 103: - teleport.setBlinkDelayTicks(field.getInteger()); - break; - case 104: - teleport.setBehindDistance(parseFloat(field, teleport.getBehindDistance())); - break; - case 106: - teleport.setDamage(parseFloat(field, teleport.getDamage())); - break; - case 107: - teleport.setDamageRadius(parseFloat(field, teleport.getDamageRadius())); - break; - } - } -} diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityTrap.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityTrap.java deleted file mode 100644 index 781d0665a..000000000 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityTrap.java +++ /dev/null @@ -1,108 +0,0 @@ -package noppes.npcs.client.gui.advanced.ability; - -import kamkeel.npcs.controllers.data.ability.type.AbilityTrap; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -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.IAbilityConfigCallback; - -/** - * GUI for configuring Trap ability type-specific settings. - */ -public class SubGuiAbilityTrap extends SubGuiAbilityConfig { - - private final AbilityTrap trap; - - public SubGuiAbilityTrap(AbilityTrap ability, IAbilityConfigCallback callback) { - super(ability, callback); - this.trap = ability; - } - - @Override - protected void initTypeTab(int startY) { - int y = startY; - int labelX = guiLeft + 8; - int fieldX = guiLeft + 85; - int col2LabelX = guiLeft + 145; - int col2FieldX = guiLeft + 205; - - // Row 1: Placement + Trigger Radius - addLabel(new GuiNpcLabel(100, "ability.placement", labelX, y + 5)); - String[] placements = {"Caster", "Target", "Ahead"}; - addButton(new GuiNpcButton(100, fieldX, y, 55, 20, placements, trap.getPlacement().ordinal())); - - addLabel(new GuiNpcLabel(101, "ability.triggerRad", col2LabelX, y + 5)); - addTextField(createFloatField(101, col2FieldX, y, 50, trap.getTriggerRadius())); - - y += 24; - - // Row 2: Damage + Damage Radius - addLabel(new GuiNpcLabel(102, "enchantment.damage", labelX, y + 5)); - addTextField(createFloatField(102, fieldX, y, 50, trap.getDamage())); - - addLabel(new GuiNpcLabel(103, "ability.dmgRadius", col2LabelX, y + 5)); - addTextField(createFloatField(103, col2FieldX, y, 50, trap.getDamageRadius())); - - y += 24; - - // Row 3: Arm Time + Max Triggers - addLabel(new GuiNpcLabel(104, "ability.armTime", labelX, y + 5)); - addTextField(createIntField(104, fieldX, y, 50, trap.getArmTime())); - - addLabel(new GuiNpcLabel(105, "ability.maxTriggers", col2LabelX, y + 5)); - addTextField(createIntField(105, col2FieldX, y, 50, trap.getMaxTriggers())); - - y += 24; - - // Row 4: Root Duration + Stun Duration - addLabel(new GuiNpcLabel(106, "ability.rootDuration", labelX, y + 5)); - addTextField(createIntField(106, fieldX, y, 50, trap.getRootDuration())); - - addLabel(new GuiNpcLabel(107, "ability.stunDuration", col2LabelX, y + 5)); - addTextField(createIntField(107, col2FieldX, y, 50, trap.getStunDuration())); - - y += 24; - - // Row 5: Knockback - addLabel(new GuiNpcLabel(108, "ability.knockback", labelX, y + 5)); - addTextField(createFloatField(108, fieldX, y, 50, trap.getKnockback())); - } - - @Override - protected void handleTypeButton(int id, GuiNpcButton button) { - if (id == 100) { - trap.setPlacement(AbilityTrap.TrapPlacement.values()[button.getValue()]); - } - } - - @Override - protected void handleTypeTextField(int id, GuiNpcTextField field) { - switch (id) { - case 101: - trap.setTriggerRadius(parseFloat(field, trap.getTriggerRadius())); - break; - case 102: - trap.setDamage(parseFloat(field, trap.getDamage())); - break; - case 103: - trap.setDamageRadius(parseFloat(field, trap.getDamageRadius())); - break; - case 104: - trap.setArmTime(field.getInteger()); - break; - case 105: - trap.setMaxTriggers(field.getInteger()); - break; - case 106: - trap.setRootDuration(field.getInteger()); - break; - case 107: - trap.setStunDuration(field.getInteger()); - break; - case 108: - trap.setKnockback(parseFloat(field, trap.getKnockback())); - break; - } - } -} diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityVortex.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityVortex.java deleted file mode 100644 index 003cef5c6..000000000 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityVortex.java +++ /dev/null @@ -1,116 +0,0 @@ -package noppes.npcs.client.gui.advanced.ability; - -import kamkeel.npcs.controllers.data.ability.type.AbilityVortex; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -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.IAbilityConfigCallback; - -/** - * GUI for configuring Vortex ability type-specific settings. - */ -public class SubGuiAbilityVortex extends SubGuiAbilityConfig { - - private final AbilityVortex vortex; - - public SubGuiAbilityVortex(AbilityVortex ability, IAbilityConfigCallback callback) { - super(ability, callback); - this.vortex = ability; - } - - @Override - protected void initTypeTab(int startY) { - int y = startY; - int labelX = guiLeft + 8; - int fieldX = guiLeft + 85; - int col2LabelX = guiLeft + 145; - int col2FieldX = guiLeft + 205; - - // Row 1: Pull Radius + Pull Strength - addLabel(new GuiNpcLabel(100, "ability.pullRadius", labelX, y + 5)); - addTextField(createFloatField(100, fieldX, y, 50, vortex.getPullRadius())); - - addLabel(new GuiNpcLabel(101, "ability.pullStrength", col2LabelX, y + 5)); - addTextField(createFloatField(101, col2FieldX, y, 50, vortex.getPullStrength())); - - y += 24; - - // Row 2: Damage + Knockback - addLabel(new GuiNpcLabel(102, "enchantment.damage", labelX, y + 5)); - addTextField(createFloatField(102, fieldX, y, 50, vortex.getDamage())); - - addLabel(new GuiNpcLabel(103, "ability.knockback", col2LabelX, y + 5)); - addTextField(createFloatField(103, col2FieldX, y, 50, vortex.getKnockback())); - - y += 24; - - // Row 3: AOE + Max Targets - addLabel(new GuiNpcLabel(104, "ability.aoe", labelX, y + 5)); - addButton(new GuiNpcButton(104, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, vortex.isAoe() ? 1 : 0)); - - addLabel(new GuiNpcLabel(105, "ability.maxTargets", col2LabelX, y + 5)); - addTextField(createIntField(105, col2FieldX, y, 50, vortex.getMaxTargets())); - - y += 24; - - // Row 4: Stun Duration + Root Duration - addLabel(new GuiNpcLabel(106, "ability.stunDuration", labelX, y + 5)); - addTextField(createIntField(106, fieldX, y, 50, vortex.getStunDuration())); - - addLabel(new GuiNpcLabel(107, "ability.rootDuration", col2LabelX, y + 5)); - addTextField(createIntField(107, col2FieldX, y, 50, vortex.getRootDuration())); - - y += 24; - - // Row 5: Damage on Pull + Pull Damage - addLabel(new GuiNpcLabel(108, "ability.dmgOnPull", labelX, y + 5)); - addButton(new GuiNpcButton(108, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, vortex.isDamageOnPull() ? 1 : 0)); - - addLabel(new GuiNpcLabel(109, "ability.pullDamage", col2LabelX, y + 5)); - addTextField(createFloatField(109, col2FieldX, y, 50, vortex.getPullDamage())); - } - - @Override - protected void handleTypeButton(int id, GuiNpcButton button) { - int value = button.getValue(); - switch (id) { - case 104: - vortex.setAoe(value == 1); - break; - case 108: - vortex.setDamageOnPull(value == 1); - break; - } - } - - @Override - protected void handleTypeTextField(int id, GuiNpcTextField field) { - switch (id) { - case 100: - vortex.setPullRadius(parseFloat(field, vortex.getPullRadius())); - break; - case 101: - vortex.setPullStrength(parseFloat(field, vortex.getPullStrength())); - break; - case 102: - vortex.setDamage(parseFloat(field, vortex.getDamage())); - break; - case 103: - vortex.setKnockback(parseFloat(field, vortex.getKnockback())); - break; - case 105: - vortex.setMaxTargets(field.getInteger()); - break; - case 106: - vortex.setStunDuration(field.getInteger()); - break; - case 107: - vortex.setRootDuration(field.getInteger()); - break; - case 109: - vortex.setPullDamage(parseFloat(field, vortex.getPullDamage())); - break; - } - } -} diff --git a/src/main/java/noppes/npcs/client/gui/builder/ColumnHint.java b/src/main/java/noppes/npcs/client/gui/builder/ColumnHint.java new file mode 100644 index 000000000..c076b6e6d --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/builder/ColumnHint.java @@ -0,0 +1,11 @@ +package noppes.npcs.client.gui.builder; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; + +@SideOnly(Side.CLIENT) +public enum ColumnHint { + FULL, + LEFT, + RIGHT +} diff --git a/src/main/java/noppes/npcs/client/gui/builder/FieldDef.java b/src/main/java/noppes/npcs/client/gui/builder/FieldDef.java new file mode 100644 index 000000000..c632f85a2 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/builder/FieldDef.java @@ -0,0 +1,296 @@ +package noppes.npcs.client.gui.builder; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import noppes.npcs.controllers.AnimationController; +import noppes.npcs.controllers.data.Animation; +import noppes.npcs.client.gui.SubGuiColorSelector; +import noppes.npcs.client.gui.select.GuiAnimationSelection; +import noppes.npcs.client.gui.select.GuiSoundSelection; +import noppes.npcs.client.gui.util.SubGuiInterface; + +import java.util.function.BooleanSupplier; +import java.util.function.Consumer; +import java.util.function.Supplier; + +/** + * Declarative field definition for GUI rendering. + * Built via static factories and chaining methods. + * Client-side only — never instantiate or reference on the server. + */ +@SideOnly(Side.CLIENT) +public class FieldDef { + + private final String label; + private final FieldType type; + private String tab = null; + private ColumnHint column = ColumnHint.FULL; + private Supplier getter; + private Consumer setter; + private BooleanSupplier visibleWhen = () -> true; + private BooleanSupplier enabledWhen = () -> true; + private String tooltip = null; + private String hoverText = null; + + // Numeric range + private float min = Float.MIN_VALUE; + private float max = Float.MAX_VALUE; + + // Enum support + private Class> enumClass; + private String[] stringEnumValues; + private Supplier stringEnumGetter; + private Consumer stringEnumSetter; + + // Sub-gui support + private Supplier subGuiFactory; + private Consumer subGuiResultHandler; + + // Button display + private Supplier buttonLabelSupplier; + private Supplier buttonTextColorSupplier; + private Runnable clearAction; + + private FieldDef(String label, FieldType type) { + this.label = label; + this.type = type; + } + + // ═══════════════════════════════════════════════════════════════════ + // STATIC FACTORIES + // ═══════════════════════════════════════════════════════════════════ + + public static FieldDef section(String label) { + return new FieldDef(label, FieldType.SECTION_HEADER); + } + + public static FieldDef floatField(String label, Supplier getter, Consumer setter) { + FieldDef def = new FieldDef(label, FieldType.FLOAT); + def.getter = () -> getter.get(); + def.setter = v -> setter.accept(((Number) v).floatValue()); + return def; + } + + public static FieldDef intField(String label, Supplier getter, Consumer setter) { + FieldDef def = new FieldDef(label, FieldType.INT); + def.getter = () -> getter.get(); + def.setter = v -> setter.accept(((Number) v).intValue()); + return def; + } + + public static FieldDef boolField(String label, Supplier getter, Consumer setter) { + FieldDef def = new FieldDef(label, FieldType.BOOLEAN); + def.getter = () -> getter.get(); + def.setter = v -> setter.accept((Boolean) v); + return def; + } + + @SuppressWarnings("unchecked") + public static > FieldDef enumField(String label, Class enumClass, Supplier getter, Consumer setter) { + FieldDef def = new FieldDef(label, FieldType.ENUM); + def.enumClass = enumClass; + def.getter = () -> getter.get(); + def.setter = v -> setter.accept((E) v); + return def; + } + + public static FieldDef stringEnumField(String label, String[] values, Supplier getter, Consumer setter) { + FieldDef def = new FieldDef(label, FieldType.STRING_ENUM); + def.stringEnumValues = values; + def.stringEnumGetter = getter; + def.stringEnumSetter = setter; + return def; + } + + public static FieldDef stringField(String label, Supplier getter, Consumer setter) { + FieldDef def = new FieldDef(label, FieldType.STRING); + def.getter = () -> getter.get(); + def.setter = v -> setter.accept((String) v); + return def; + } + + public static FieldDef labelField(String label, Supplier textSupplier) { + FieldDef def = new FieldDef(label, FieldType.LABEL); + def.getter = () -> textSupplier.get(); + return def; + } + + public static FieldDef subGuiField(String label, Supplier factory, Consumer resultHandler) { + FieldDef def = new FieldDef(label, FieldType.SUB_GUI); + def.subGuiFactory = factory; + def.subGuiResultHandler = resultHandler; + return def; + } + + /** + * Creates a FieldDef with a custom FieldType. Used by extension code + * (e.g. AbilityFieldDefs) to create fields with types like EFFECTS_LIST + * that the base GuiFieldBuilder does not handle. + */ + public static FieldDef custom(String label, FieldType type, Supplier getter, Consumer setter) { + FieldDef def = new FieldDef(label, type); + def.getter = getter; + def.setter = setter; + return def; + } + + // ═══════════════════════════════════════════════════════════════════ + // CONVENIENCE FACTORIES (SUB_GUI wrappers) + // ═══════════════════════════════════════════════════════════════════ + + public static FieldDef colorSubGui(String label, Supplier getter, Consumer setter) { + return subGuiField(label, + () -> new SubGuiColorSelector(getter.get()), + gui -> setter.accept(((SubGuiColorSelector) gui).color & 0x00FFFFFF)) + .buttonLabel(() -> String.format("%06X", getter.get() & 0xFFFFFF)) + .buttonTextColor(() -> getter.get() & 0xFFFFFF); + } + + public static FieldDef soundSubGui(String label, Supplier getter, Consumer setter) { + return subGuiField(label, + () -> new GuiSoundSelection(getter.get()), + gui -> { + GuiSoundSelection s = (GuiSoundSelection) gui; + if (s.selectedResource != null) setter.accept(s.selectedResource.toString()); + }) + .buttonLabel(() -> { + String s = getter.get(); + return s == null || s.isEmpty() ? "gui.none" : s; + }) + .clearable(() -> setter.accept("")); + } + + public static FieldDef animSubGui(String label, + Supplier idGetter, Consumer idSetter, + Supplier nameGetter, Consumer nameSetter) { + return subGuiField(label, + () -> new GuiAnimationSelection(idGetter.get(), nameGetter.get()), + gui -> { + GuiAnimationSelection sel = (GuiAnimationSelection) gui; + if (sel.isBuiltInSelected()) { + nameSetter.accept(sel.selectedBuiltInName); + idSetter.accept(-1); + } else { + idSetter.accept(sel.selectedAnimationId); + nameSetter.accept(""); + } + }) + .buttonLabel(() -> { + String name = nameGetter.get(); + if (name != null && !name.isEmpty()) return name; + int id = idGetter.get(); + if (id >= 0) { + Animation anim = AnimationController.Instance != null + ? (Animation) AnimationController.Instance.get(id) : null; + String animName = anim != null ? anim.getName() : ""; + return animName != null && !animName.isEmpty() + ? "(ID: " + id + ") " + animName : "ID: " + id; + } + return "gui.none"; + }) + .clearable(() -> { idSetter.accept(-1); nameSetter.accept(""); }); + } + + // ═══════════════════════════════════════════════════════════════════ + // CHAINING METHODS + // ═══════════════════════════════════════════════════════════════════ + + public FieldDef tab(String tabName) { + this.tab = tabName; + return this; + } + + public FieldDef range(float min, float max) { + this.min = min; + this.max = max; + return this; + } + + public FieldDef column(ColumnHint column) { + this.column = column; + return this; + } + + public FieldDef visibleWhen(BooleanSupplier condition) { + this.visibleWhen = condition; + return this; + } + + public FieldDef enabledWhen(BooleanSupplier condition) { + this.enabledWhen = condition; + return this; + } + + public FieldDef tooltip(String tooltip) { + this.tooltip = tooltip; + return this; + } + + public FieldDef hover(String hoverText) { + this.hoverText = hoverText; + return this; + } + + public FieldDef buttonLabel(Supplier supplier) { + this.buttonLabelSupplier = supplier; + return this; + } + + public FieldDef buttonTextColor(Supplier supplier) { + this.buttonTextColorSupplier = supplier; + return this; + } + + public FieldDef clearable(Runnable action) { + this.clearAction = action; + return this; + } + + // ═══════════════════════════════════════════════════════════════════ + // GETTERS + // ═══════════════════════════════════════════════════════════════════ + + public String getLabel() { return label; } + public FieldType getType() { return type; } + public String getTab() { return tab; } + public ColumnHint getColumn() { return column; } + public boolean isVisible() { return visibleWhen.getAsBoolean(); } + public boolean isEnabled() { return enabledWhen.getAsBoolean(); } + public String getTooltip() { return tooltip; } + public String getHoverText() { return hoverText; } + public float getMin() { return min; } + public float getMax() { return max; } + public boolean hasRange() { return min != Float.MIN_VALUE || max != Float.MAX_VALUE; } + public Class> getEnumClass() { return enumClass; } + public String[] getStringEnumValues() { return stringEnumValues; } + public Supplier getSubGuiFactory() { return subGuiFactory; } + public Consumer getSubGuiResultHandler() { return subGuiResultHandler; } + public boolean hasClearAction() { return clearAction != null; } + public Runnable getClearAction() { return clearAction; } + + public Object getValue() { + return getter != null ? getter.get() : null; + } + + public void setValue(Object value) { + if (setter != null) setter.accept(value); + } + + public String getStringEnumValue() { + return stringEnumGetter != null ? stringEnumGetter.get() : null; + } + + public void setStringEnumValue(String value) { + if (stringEnumSetter != null) stringEnumSetter.accept(value); + } + + public String getButtonLabel() { + if (buttonLabelSupplier != null) return buttonLabelSupplier.get(); + return label; + } + + public Integer getButtonTextColor() { + return buttonTextColorSupplier != null ? buttonTextColorSupplier.get() : null; + } + +} diff --git a/src/main/java/noppes/npcs/client/gui/builder/FieldType.java b/src/main/java/noppes/npcs/client/gui/builder/FieldType.java new file mode 100644 index 000000000..e348f6f0b --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/builder/FieldType.java @@ -0,0 +1,18 @@ +package noppes.npcs.client.gui.builder; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; + +@SideOnly(Side.CLIENT) +public enum FieldType { + FLOAT, + INT, + BOOLEAN, + ENUM, + STRING_ENUM, + SUB_GUI, + STRING, + SECTION_HEADER, + LABEL, + EFFECTS_LIST +} diff --git a/src/main/java/noppes/npcs/client/gui/builder/GuiFieldBuilder.java b/src/main/java/noppes/npcs/client/gui/builder/GuiFieldBuilder.java new file mode 100644 index 000000000..02af4c3da --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/builder/GuiFieldBuilder.java @@ -0,0 +1,334 @@ +package noppes.npcs.client.gui.builder; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import net.minecraft.client.gui.FontRenderer; +import net.minecraft.util.StatCollector; +import noppes.npcs.client.gui.util.GuiNPCInterface; +import noppes.npcs.client.gui.util.GuiNpcButton; +import noppes.npcs.client.gui.util.GuiNpcButtonYesNo; +import noppes.npcs.client.gui.util.GuiNpcLabel; +import noppes.npcs.client.gui.util.GuiNpcTextField; +import noppes.npcs.client.gui.util.GuiScrollWindow; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * General-purpose builder that converts a list of {@link FieldDef} into positioned + * GUI widgets inside a {@link GuiScrollWindow}. + *

+ * Supports 1-column and 2-column layouts. Subclasses can override {@link #buildField} + * to handle custom field types. + */ +@SideOnly(Side.CLIENT) +public class GuiFieldBuilder { + + // Layout defaults (2-column mode) + protected int contentRight = 330; + protected int colLLabel = 5; + protected int colLField = 75; + protected int colLWidth = 70; + protected int colRLabel = 178; + protected int colRField = 248; + protected int colRWidth = 70; + protected int rowHeight = 24; + protected int labelPadding = 8; + + // Column mode + protected int columnCount = 2; + + // Dependencies + protected final GuiNPCInterface parent; + protected GuiScrollWindow sw; + protected final FontRenderer fontRenderer; + + // Scroll window ID + protected int scrollWindowId = 0; + + // ID counters + protected int widgetId; + protected int clearId; + protected int labelId; + + // Starting Y + protected int startY = 5; + protected int lastBuildY = 0; + + // Output maps + protected final Map buttonFieldMap = new HashMap<>(); + protected final Map textFieldMap = new HashMap<>(); + protected final Map clearFieldMap = new HashMap<>(); + + public GuiFieldBuilder(GuiNPCInterface parent, FontRenderer fontRenderer) { + this.parent = parent; + this.fontRenderer = fontRenderer; + } + + // ═══════════════════════════════════════════════════════════════════ + // CONFIG CHAINING + // ═══════════════════════════════════════════════════════════════════ + + public GuiFieldBuilder scrollWindowId(int id) { this.scrollWindowId = id; return this; } + public GuiFieldBuilder columns(int count) { this.columnCount = Math.max(1, Math.min(2, count)); return this; } + public GuiFieldBuilder contentRight(int v) { this.contentRight = v; return this; } + public GuiFieldBuilder startIds(int widget, int clear, int label) { + this.widgetId = widget; this.clearId = clear; this.labelId = label; return this; + } + public GuiFieldBuilder startY(int y) { this.startY = y; return this; } + public GuiFieldBuilder rowHeight(int h) { this.rowHeight = h; return this; } + + // ═══════════════════════════════════════════════════════════════════ + // BUILD + // ═══════════════════════════════════════════════════════════════════ + + /** + * Creates a {@link GuiScrollWindow} at the given position/size, builds all fields into it, + * and sets maxScrollY automatically. The returned window is ready to use. + */ + public GuiScrollWindow buildScrollWindow(List fields, int x, int y, int width, int height) { + this.sw = new GuiScrollWindow(parent, x, y, width, height, 0); + sw.backgroundColor = 0x88000000; + // Register with parent first — this calls initGui() which clears widget lists, + // so we must add widgets AFTER this point + parent.addScrollableGui(scrollWindowId, sw); + int finalY = build(fields); + sw.maxScrollY = Math.max(finalY - height, 0); + return sw; + } + + /** + * Processes all fields and adds widgets to the scroll window. + * @return the final Y position after all fields + */ + protected int build(List fields) { + buttonFieldMap.clear(); + textFieldMap.clear(); + clearFieldMap.clear(); + + int y = startY; + + for (int i = 0; i < fields.size(); i++) { + FieldDef def = fields.get(i); + + // Let subclasses handle custom types; returns -1 if not handled + int customResult = buildField(def, y, fields, i); + if (customResult >= 0) { + // Subclass handled it, check if index was advanced + int newIndex = getLastHandledIndex(); + if (newIndex > i) i = newIndex; + y = customResult; + continue; + } + + // Section header + if (def.getType() == FieldType.SECTION_HEADER) { + y += 3; + String sectionText = StatCollector.translateToLocal(def.getLabel()); + if (def.getTooltip() != null && !def.getTooltip().isEmpty()) { + sectionText += " (" + StatCollector.translateToLocal(def.getTooltip()) + ")"; + } + sw.addLabel(new GuiNpcLabel(labelId++, sectionText, colLLabel, y + 2, 0xFFFF55)); + y += 15; + continue; + } + + // Two-column pair: LEFT followed by RIGHT (only in 2-column mode) + if (columnCount == 2 + && def.getColumn() == ColumnHint.LEFT + && i + 1 < fields.size() + && fields.get(i + 1).getColumn() == ColumnHint.RIGHT + && fields.get(i + 1).getType() != FieldType.SECTION_HEADER) { + renderFieldAt(def, colLLabel, colLField, colLWidth, y); + + i++; + FieldDef rightDef = fields.get(i); + renderFieldAt(rightDef, colRLabel, colRField, colRWidth, y); + + y += rowHeight; + } else { + // Full-width: compute fieldX from label pixel width + int fieldW = getFullFieldWidth(def); + String translated = StatCollector.translateToLocal(def.getLabel()); + int labelW = fontRenderer.getStringWidth(translated); + int fieldX = colLLabel + labelW + labelPadding; + + renderFieldAt(def, colLLabel, fieldX, fieldW, y); + y += rowHeight; + } + } + + lastBuildY = y; + return y; + } + + /** + * Hook for subclasses to handle custom field types (e.g. EFFECTS_LIST). + * Return the new Y position if handled, or -1 if not handled (base class will process it). + * If handling advances the field index, call {@link #setLastHandledIndex(int)}. + */ + protected int buildField(FieldDef def, int y, List fields, int index) { + return -1; + } + + private int lastHandledIndex = -1; + protected void setLastHandledIndex(int index) { this.lastHandledIndex = index; } + protected int getLastHandledIndex() { return lastHandledIndex; } + + // ═══════════════════════════════════════════════════════════════════ + // FIELD RENDERING + // ═══════════════════════════════════════════════════════════════════ + + @SuppressWarnings({"unchecked", "rawtypes"}) + protected void renderFieldAt(FieldDef def, int labelX, int fieldX, int fieldW, int y) { + sw.addLabel(new GuiNpcLabel(labelId++, def.getLabel(), labelX, y + 5, 0xFFFFFF)); + + String hover = def.getHoverText() != null ? def.getHoverText() : def.getTooltip(); + + switch (def.getType()) { + case FLOAT: { + float fVal = def.getValue() instanceof Number ? ((Number) def.getValue()).floatValue() : 0f; + GuiNpcTextField tf = new GuiNpcTextField(widgetId, parent, fontRenderer, fieldX, y, fieldW, 20, String.valueOf(fVal)); + tf.setFloatsOnly(); + if (def.hasRange()) tf.setMinMaxDefaultFloat(def.getMin(), def.getMax(), fVal); + if (!def.isEnabled()) tf.setEnabled(false); + if (hover != null) tf.setHoverText(hover); + sw.addTextField(tf); + textFieldMap.put(widgetId, def); + widgetId++; + clearId++; + break; + } + case INT: { + int iVal = def.getValue() instanceof Number ? ((Number) def.getValue()).intValue() : 0; + GuiNpcTextField tf = new GuiNpcTextField(widgetId, parent, fontRenderer, fieldX, y, fieldW, 20, String.valueOf(iVal)); + tf.setIntegersOnly(); + if (def.hasRange()) tf.setMinMaxDefault((int) def.getMin(), (int) def.getMax(), iVal); + if (!def.isEnabled()) tf.setEnabled(false); + if (hover != null) tf.setHoverText(hover); + sw.addTextField(tf); + textFieldMap.put(widgetId, def); + widgetId++; + clearId++; + break; + } + case STRING: { + String sVal = def.getValue() != null ? def.getValue().toString() : ""; + GuiNpcTextField tf = new GuiNpcTextField(widgetId, parent, fontRenderer, fieldX, y, fieldW, 20, sVal); + if (!def.isEnabled()) tf.setEnabled(false); + if (hover != null) tf.setHoverText(hover); + sw.addTextField(tf); + textFieldMap.put(widgetId, def); + widgetId++; + clearId++; + break; + } + case LABEL: { + String text = def.getValue() != null ? def.getValue().toString() : ""; + sw.addLabel(new GuiNpcLabel(labelId++, text, fieldX, y + 5, 0xAAAAAA)); + widgetId++; + clearId++; + break; + } + case BOOLEAN: { + boolean bVal = def.getValue() instanceof Boolean ? (Boolean) def.getValue() : false; + GuiNpcButtonYesNo btn = new GuiNpcButtonYesNo(widgetId, fieldX, y, fieldW, 20, bVal); + if (!def.isEnabled()) btn.setEnabled(false); + if (hover != null) btn.setHoverText(hover); + sw.addButton(btn); + buttonFieldMap.put(widgetId, def); + widgetId++; + clearId++; + break; + } + case ENUM: { + Class> enumClass = def.getEnumClass(); + if (enumClass != null) { + Enum[] constants = enumClass.getEnumConstants(); + String[] names = new String[constants.length]; + for (int i = 0; i < constants.length; i++) names[i] = constants[i].toString(); + int selected = def.getValue() instanceof Enum ? ((Enum) def.getValue()).ordinal() : 0; + GuiNpcButton btn = new GuiNpcButton(widgetId, fieldX, y, fieldW, 20, names, selected); + if (!def.isEnabled()) btn.setEnabled(false); + if (hover != null) btn.setHoverText(hover); + sw.addButton(btn); + buttonFieldMap.put(widgetId, def); + } + widgetId++; + clearId++; + break; + } + case STRING_ENUM: { + String[] values = def.getStringEnumValues(); + if (values != null && values.length > 0) { + String curVal = def.getStringEnumValue(); + int selected = 0; + for (int i = 0; i < values.length; i++) { + if (values[i].equals(curVal)) { selected = i; break; } + } + GuiNpcButton btn = new GuiNpcButton(widgetId, fieldX, y, fieldW, 20, values, selected); + if (!def.isEnabled()) btn.setEnabled(false); + if (hover != null) btn.setHoverText(hover); + sw.addButton(btn); + buttonFieldMap.put(widgetId, def); + } + widgetId++; + clearId++; + break; + } + case SUB_GUI: { + String btnText = def.getButtonLabel(); + int btnW; + if (def.hasClearAction()) { + btnW = contentRight - 22 - fieldX; + GuiNpcButton clearBtn = new GuiNpcButton(clearId, contentRight - 20, y, 20, 20, "X"); + sw.addButton(clearBtn); + clearFieldMap.put(clearId, def); + } else { + btnW = contentRight - fieldX; + } + GuiNpcButton btn = new GuiNpcButton(widgetId, fieldX, y, btnW, 20, btnText); + Integer textColor = def.getButtonTextColor(); + if (textColor != null) btn.setTextColor(textColor); + if (!def.isEnabled()) btn.setEnabled(false); + if (hover != null) btn.setHoverText(hover); + sw.addButton(btn); + buttonFieldMap.put(widgetId, def); + widgetId++; + clearId++; + break; + } + default: + // Unknown type — skip, incrementing IDs to stay in sync + widgetId++; + clearId++; + break; + } + } + + protected int getFullFieldWidth(FieldDef def) { + switch (def.getType()) { + case FLOAT: + case INT: return 60; + case STRING: return 200; + case BOOLEAN: return 50; + case LABEL: return 60; + case ENUM: + case STRING_ENUM: return 120; + case SUB_GUI: return def.hasClearAction() ? 175 : 200; + default: return 80; + } + } + + // ═══════════════════════════════════════════════════════════════════ + // GETTERS + // ═══════════════════════════════════════════════════════════════════ + + public Map getButtonFieldMap() { return buttonFieldMap; } + public Map getTextFieldMap() { return textFieldMap; } + public Map getClearFieldMap() { return clearFieldMap; } + public GuiScrollWindow getScrollWindow() { return sw; } + public int getLastBuildY() { return lastBuildY; } + public int getNextLabelId() { return labelId; } +} diff --git a/src/main/java/noppes/npcs/client/gui/global/GuiNPCManageAnimations.java b/src/main/java/noppes/npcs/client/gui/global/GuiNPCManageAnimations.java index 7d1fa84be..0141e931a 100644 --- a/src/main/java/noppes/npcs/client/gui/global/GuiNPCManageAnimations.java +++ b/src/main/java/noppes/npcs/client/gui/global/GuiNPCManageAnimations.java @@ -5,6 +5,7 @@ import kamkeel.npcs.network.packets.request.animation.AnimationGetPacket; import kamkeel.npcs.network.packets.request.animation.AnimationRemovePacket; import kamkeel.npcs.network.packets.request.animation.AnimationSavePacket; import kamkeel.npcs.network.packets.request.animation.AnimationsGetPacket; +import kamkeel.npcs.network.packets.request.animation.BuiltInAnimationGetPacket; import net.minecraft.client.gui.GuiButton; import net.minecraft.client.gui.GuiYesNo; import net.minecraft.client.gui.GuiYesNoCallback; @@ -37,13 +38,19 @@ import java.util.Vector; public class GuiNPCManageAnimations extends GuiModelInterface2 implements IScrollData, ICustomScrollListener, ITextfieldListener, IGuiData, ISubGuiListener, GuiYesNoCallback { private GuiCustomScroll scrollAnimations; - private HashMap data = new HashMap(); + private HashMap customData = new HashMap(); + private HashMap builtInData = new HashMap(); private Animation animation = new Animation(); public boolean playingAnimation = false; private long prevTick; private String selected = null; private String search = ""; + // Toggle between Custom (false) and Built-in (true) animations + private boolean showingBuiltIn = false; + // Track if current animation is built-in + private boolean currentIsBuiltIn = false; + public GuiNPCManageAnimations(EntityNPCInterface npc, boolean save) { super(npc); this.setSave(save); @@ -58,8 +65,15 @@ public class GuiNPCManageAnimations extends GuiModelInterface2 implements IScrol public void initGui() { super.initGui(); - this.addButton(new GuiNpcButton(0, guiLeft + 368, guiTop + 8, 45, 20, "gui.add")); - this.addButton(new GuiNpcButton(1, guiLeft + 368, guiTop + 32, 45, 20, "gui.remove")); + // Add/Remove buttons - only show for custom animations + if (!showingBuiltIn) { + this.addButton(new GuiNpcButton(0, guiLeft + 368, guiTop + 8, 45, 20, "gui.add")); + this.addButton(new GuiNpcButton(1, guiLeft + 368, guiTop + 32, 45, 20, "gui.remove")); + } + + // Toggle button for Custom/Built-in - under Remove button + String toggleLabel = showingBuiltIn ? "gui.builtin" : "gui.custom"; + this.addButton(new GuiNpcButton(10, guiLeft + 368, guiTop + 56, 45, 20, toggleLabel)); if (scrollAnimations == null) { scrollAnimations = new GuiCustomScroll(this, 0, 0); @@ -68,13 +82,23 @@ public class GuiNPCManageAnimations extends GuiModelInterface2 implements IScrol scrollAnimations.guiLeft = guiLeft + 220; scrollAnimations.guiTop = guiTop + 4; addScroll(scrollAnimations); + + // Search field addTextField(new GuiNpcTextField(55, this, fontRendererObj, guiLeft + 220, guiTop + 192, 143, 20, search)); - if (animation.id == -1) + // Update scroll list based on current view + scrollAnimations.setList(getSearchList()); + + if (animation.id == -1 && !currentIsBuiltIn) return; - addLabel(new GuiNpcLabel(10, "ID", guiLeft + 368, guiTop + 192)); - addLabel(new GuiNpcLabel(11, animation.id + "", guiLeft + 368, guiTop + 192 + 10)); + // ID label - only for custom animations + if (!currentIsBuiltIn) { + addLabel(new GuiNpcLabel(10, "ID", guiLeft + 368, guiTop + 192)); + addLabel(new GuiNpcLabel(11, animation.id + "", guiLeft + 368, guiTop + 192 + 10)); + } else { + addLabel(new GuiNpcLabel(10, "gui.builtin", guiLeft + 368, guiTop + 192)); + } AnimationData data = npc.display.animationData; if (!playingAnimation) { @@ -90,12 +114,13 @@ public class GuiNPCManageAnimations extends GuiModelInterface2 implements IScrol } } - if (animation != null) { + // Edit button - only for custom animations + if (animation != null && !currentIsBuiltIn) { this.addButton(new GuiNpcButton(100, guiLeft + 10, guiTop + 192, 45, 20, "gui.edit")); } String animTexture = "customnpcs:textures/gui/animation.png"; - int playButtonOffsetX = 140; + int playButtonOffsetX = currentIsBuiltIn ? 80 : 140; if (animation != null && !animation.frames.isEmpty()) { if (!this.playingAnimation || data.animation.paused) {//Play this.addLabel(new GuiNpcLabel(90, data.animation.paused ? "animation.paused" : "animation.stopped", guiLeft + playButtonOffsetX - 15, guiTop + 198)); @@ -151,12 +176,17 @@ public class GuiNPCManageAnimations extends GuiModelInterface2 implements IScrol } } + private HashMap getCurrentData() { + return showingBuiltIn ? builtInData : customData; + } + private List getSearchList() { + HashMap data = getCurrentData(); if (search.isEmpty()) { - return new ArrayList(this.data.keySet()); + return new ArrayList(data.keySet()); } List list = new ArrayList(); - for (String name : this.data.keySet()) { + for (String name : data.keySet()) { if (name.toLowerCase().contains(search)) list.add(name); } @@ -167,16 +197,31 @@ public class GuiNPCManageAnimations extends GuiModelInterface2 implements IScrol protected void actionPerformed(GuiButton guibutton) { GuiNpcButton button = (GuiNpcButton) guibutton; - if (button.id == 0) { + // Toggle button + if (button.id == 10) { + showingBuiltIn = !showingBuiltIn; + // Clear selection when switching views + selected = null; + animation = new Animation(); + currentIsBuiltIn = false; + scrollAnimations.clear(); + initGui(); + return; + } + + // Add button - only for custom + if (button.id == 0 && !showingBuiltIn) { save(); String name = "New"; - while (data.containsKey(name)) + while (customData.containsKey(name)) name += "_"; Animation animation = new Animation(-1, name); PacketClient.sendClient(new AnimationSavePacket(animation.writeToNBT())); } - if (button.id == 1) { - if (data.containsKey(scrollAnimations.getSelected())) { + + // Remove button - only for custom + if (button.id == 1 && !showingBuiltIn) { + if (customData.containsKey(scrollAnimations.getSelected())) { GuiYesNo guiyesno = new GuiYesNo(this, scrollAnimations.getSelected(), StatCollector.translateToLocal("gui.delete"), 1); displayGuiScreen(guiyesno); } @@ -204,7 +249,8 @@ public class GuiNPCManageAnimations extends GuiModelInterface2 implements IScrol data.animation.paused = false; } - if (guibutton.id == 100) { + // Edit button - only for custom + if (guibutton.id == 100 && !currentIsBuiltIn) { NoppesUtil.openGUI(player, new GuiNPCEditAnimation(this, this.animation, npc)); } @@ -215,6 +261,10 @@ public class GuiNPCManageAnimations extends GuiModelInterface2 implements IScrol public void setGuiData(NBTTagCompound compound) { this.animation = new Animation(); animation.readFromNBT(compound); + + // Check if this is a built-in animation + currentIsBuiltIn = compound.hasKey("BuiltIn") && compound.getBoolean("BuiltIn"); + setSelected(animation.name); npc.display.animationData.setAnimation(this.animation); this.playingAnimation = false; @@ -225,7 +275,13 @@ public class GuiNPCManageAnimations extends GuiModelInterface2 implements IScrol @Override public void setData(Vector list, HashMap data, EnumScrollData type) { String name = scrollAnimations.getSelected(); - this.data = data; + + if (type == EnumScrollData.ANIMATIONS) { + this.customData = data; + } else if (type == EnumScrollData.BUILTIN_ANIMATIONS) { + this.builtInData = data; + } + scrollAnimations.setList(getSearchList()); if (name != null) @@ -242,28 +298,40 @@ public class GuiNPCManageAnimations extends GuiModelInterface2 implements IScrol public void customScrollClicked(int i, int j, int k, GuiCustomScroll guiCustomScroll) { if (guiCustomScroll.id == 0) { selected = scrollAnimations.getSelected(); - PacketClient.sendClient(new AnimationGetPacket(data.get(selected))); + HashMap data = getCurrentData(); + + if (showingBuiltIn) { + // Fetch built-in animation by name + PacketClient.sendClient(new BuiltInAnimationGetPacket(selected)); + } else { + // Fetch custom animation by ID + if (data.containsKey(selected)) { + PacketClient.sendClient(new AnimationGetPacket(data.get(selected))); + } + } } } public void save() { - if (selected != null && data.containsKey(selected) && animation != null) { + // Only save custom animations + if (selected != null && customData.containsKey(selected) && animation != null && !currentIsBuiltIn) { PacketClient.sendClient(new AnimationSavePacket(animation.writeToNBT())); } } @Override public void unFocused(GuiNpcTextField guiNpcTextField) { - if (animation.id == -1) + // Don't allow renaming built-in animations + if (animation.id == -1 || currentIsBuiltIn) return; if (guiNpcTextField.id == 0) { String name = guiNpcTextField.getText(); - if (!name.isEmpty() && !data.containsKey(name)) { + if (!name.isEmpty() && !customData.containsKey(name)) { String old = animation.name; - data.remove(animation.name); + customData.remove(animation.name); animation.name = name; - data.put(animation.name, animation.id); + customData.put(animation.name, animation.id); selected = name; scrollAnimations.replace(old, animation.name); } @@ -280,10 +348,12 @@ public class GuiNPCManageAnimations extends GuiModelInterface2 implements IScrol if (!result) return; if (id == 1) { - if (data.containsKey(scrollAnimations.getSelected())) { - PacketClient.sendClient(new AnimationRemovePacket(data.get(selected))); + // Only delete custom animations + if (customData.containsKey(scrollAnimations.getSelected()) && !showingBuiltIn) { + PacketClient.sendClient(new AnimationRemovePacket(customData.get(selected))); scrollAnimations.clear(); animation = new Animation(); + currentIsBuiltIn = false; initGui(); } } diff --git a/src/main/java/noppes/npcs/client/gui/global/GuiNPCManageLinked.java b/src/main/java/noppes/npcs/client/gui/global/GuiNPCManageLinked.java index e07d2583c..52e78f7d6 100644 --- a/src/main/java/noppes/npcs/client/gui/global/GuiNPCManageLinked.java +++ b/src/main/java/noppes/npcs/client/gui/global/GuiNPCManageLinked.java @@ -338,21 +338,21 @@ public class GuiNPCManageLinked extends GuiNPCInterface2 implements IScrollData, // Health label = StatCollector.translateToLocal("stats.health") + ": "; - value = String.valueOf(npc.stats.maxHealth); + value = "" + npc.stats.maxHealth; fontRendererObj.drawString(label, xLabel, y, 0x29d6b9, false); fontRendererObj.drawString(value, xValue, y, valueColor, false); y += 15; // Damage (using getAttackStrength) label = StatCollector.translateToLocal("stats.meleestrength") + ": "; - value = String.valueOf(npc.stats.getAttackStrength()); + value = "" + npc.stats.getAttackStrength(); fontRendererObj.drawString(label, xLabel, y, 0xff5714, false); fontRendererObj.drawString(value, xValue, y, valueColor, false); y += 15; // Attack Speed label = StatCollector.translateToLocal("stats.meleespeed") + ": "; - value = String.valueOf(npc.stats.attackSpeed); + value = "" + npc.stats.attackSpeed; fontRendererObj.drawString(label, xLabel, y, 0xf7ca28, false); fontRendererObj.drawString(value, xValue, y, valueColor, false); y += 15; @@ -381,7 +381,7 @@ public class GuiNPCManageLinked extends GuiNPCInterface2 implements IScrollData, // Walk Speed label = StatCollector.translateToLocal("stats.speed") + ": "; - value = String.valueOf(npc.ais.getWalkingSpeed()); + value = "" + npc.ais.getWalkingSpeed(); fontRendererObj.drawString(label, xLabel, y, 0xffae0d, false); fontRendererObj.drawString(value, xValue, y, valueColor, false); y += 15; @@ -415,7 +415,7 @@ public class GuiNPCManageLinked extends GuiNPCInterface2 implements IScrollData, // Version label = StatCollector.translateToLocal("display.version") + ": "; - value = String.valueOf(linkedItem.version); + value = "" + linkedItem.version; fontRendererObj.drawString(label, xLabel, y, labelColor, false); fontRendererObj.drawString(value, xValue, y, valueColor, false); y += 15; @@ -423,7 +423,7 @@ public class GuiNPCManageLinked extends GuiNPCInterface2 implements IScrollData, // Max Stack Size labelColor = 0xff5714; label = StatCollector.translateToLocal("display.maxStack") + ": "; - value = String.valueOf(linkedItem.stackSize); + value = "" + linkedItem.stackSize; fontRendererObj.drawString(label, xLabel, y, labelColor, false); fontRendererObj.drawString(value, xValue, y, valueColor, false); y += 15; @@ -431,7 +431,7 @@ public class GuiNPCManageLinked extends GuiNPCInterface2 implements IScrollData, // Dig Speed labelColor = 0xf7ca28; label = StatCollector.translateToLocal("display.digSpeed") + ": "; - value = String.valueOf(linkedItem.digSpeed); + value = "" + linkedItem.digSpeed; fontRendererObj.drawString(label, xLabel, y, labelColor, false); fontRendererObj.drawString(value, xValue, y, valueColor, false); y += 15; @@ -498,14 +498,14 @@ public class GuiNPCManageLinked extends GuiNPCInterface2 implements IScrollData, labelColor = 0x7cff54; // isTool label = StatCollector.translateToLocal("display.isTool") + ": "; - value = String.valueOf(linkedItem.isTool).toUpperCase(); + value = ("" + linkedItem.isTool).toUpperCase(); fontRendererObj.drawString(label, xLabel, y, labelColor, false); fontRendererObj.drawString(value, xValue, y, valueColor, false); y += 15; // isNormalItem label = StatCollector.translateToLocal("display.isNormalItem") + ": "; - value = String.valueOf(linkedItem.isNormalItem).toUpperCase(); + value = ("" + linkedItem.isNormalItem).toUpperCase(); fontRendererObj.drawString(label, xLabel, y, labelColor, false); fontRendererObj.drawString(value, xValue, y, valueColor, false); y += 15; 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..8fa2e4e41 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,269 @@ 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.CustomAbilitiesGetPacket; +import kamkeel.npcs.network.packets.request.ability.CustomAbilityGetPacket; +import kamkeel.npcs.network.packets.request.ability.CustomAbilityRemovePacket; +import kamkeel.npcs.network.packets.request.ability.CustomAbilitySavePacket; 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<>(); + /** Maps display name -> UUID for custom abilities */ + private final HashMap displayToUuid = new HashMap<>(); + private String selected = null; + private String selectedUuid = 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 CustomAbilitiesGetPacket()); + + // 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() && displayToUuid.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 && displayToUuid.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<>(displayToUuid.keySet()); } - List filtered = new ArrayList<>(); - for (String name : abilityNames) { + List list = new ArrayList<>(); + for (String name : displayToUuid.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 && selectedUuid != null) { + 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)) { + previewExecutor.stop(); + + selected = newSelection; + selectedUuid = displayToUuid.get(selected); + if (selectedUuid != null) { + PacketClient.sendClient(new CustomAbilityGetPacket(selectedUuid)); + } + } } } @@ -124,48 +271,101 @@ 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.CUSTOM_ABILITIES) { + String prevSelected = scroll != null ? scroll.getSelected() : null; + this.abilityData = data; + displayToUuid.clear(); + for (String key : data.keySet()) { + // key format: "displayName\tUUID" + int tabIndex = key.indexOf('\t'); + if (tabIndex > 0) { + String displayName = key.substring(0, tabIndex); + String uuid = key.substring(tabIndex + 1); + displayToUuid.put(displayName, uuid); + } else { + displayToUuid.put(key, key); + } } + if (scroll != null) { + scroll.setList(getSearchList()); + if (prevSelected != null && displayToUuid.containsKey(prevSelected)) { + scroll.setSelected(prevSelected); + } + } + 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 && selectedUuid != null) { + PacketClient.sendClient(new CustomAbilityRemovePacket(selectedUuid)); + scroll.clear(); + selected = null; + selectedUuid = null; + selectedAbility = null; + initGui(); + } + } + @Override public void onAbilitySaved(Ability ability) { - // Save the edited ability - AbilityController.Instance.saveAbility(ability); - loadAbilityNames(); - initGui(); + PacketClient.sendClient(new CustomAbilitySavePacket(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/global/GuiNpcNaturalSpawns.java b/src/main/java/noppes/npcs/client/gui/global/GuiNpcNaturalSpawns.java index 41e105a38..ecdec2990 100644 --- a/src/main/java/noppes/npcs/client/gui/global/GuiNpcNaturalSpawns.java +++ b/src/main/java/noppes/npcs/client/gui/global/GuiNpcNaturalSpawns.java @@ -90,7 +90,7 @@ public class GuiNpcNaturalSpawns extends GuiNPCInterface2 implements IGuiData, I ArrayList list = new ArrayList<>(); Set keySet = this.spawn.spawnCompounds.keySet(); for (int i : keySet) { - list.add(String.valueOf(i)); + list.add("" + i); } this.spawnEntryScroll.setList(list); addScroll(this.spawnEntryScroll); @@ -99,7 +99,7 @@ public class GuiNpcNaturalSpawns extends GuiNPCInterface2 implements IGuiData, I if (this.spawnEntryScroll.hasSelected()) { int selected = Integer.parseInt(this.spawnEntryScroll.getSelected()); addButton(new GuiNpcButton(22, guiLeft + 32, y, 20, 20, "-")); - GuiNpcTextField num = new GuiNpcTextField(25, this, guiLeft + 60, y += 10, 30, 20, String.valueOf(selected)); + GuiNpcTextField num = new GuiNpcTextField(25, this, guiLeft + 60, y += 10, 30, 20, "" + selected); num.integersOnly = true; addTextField(num); addButton(new GuiNpcButton(26, guiLeft + 92, y, 100, 20, this.getTitle(this.spawn.spawnCompounds.get(selected)))); @@ -215,8 +215,8 @@ public class GuiNpcNaturalSpawns extends GuiNPCInterface2 implements IGuiData, I } if (textfield.id == 25 && this.spawnEntryScroll.hasSelected()) { int selected = Integer.parseInt(spawnEntryScroll.getSelected()); - if (spawnEntryScroll.getList().contains(String.valueOf(textfield.getInteger()))) { - textfield.setText(String.valueOf(selected)); + if (spawnEntryScroll.getList().contains("" + textfield.getInteger())) { + textfield.setText("" + selected); return; } NBTTagCompound compound = spawn.spawnCompounds.get(selected); 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/player/AuctionTooltipHandler.java b/src/main/java/noppes/npcs/client/gui/player/AuctionTooltipHandler.java index 33966d1e2..a50622529 100644 --- a/src/main/java/noppes/npcs/client/gui/player/AuctionTooltipHandler.java +++ b/src/main/java/noppes/npcs/client/gui/player/AuctionTooltipHandler.java @@ -192,7 +192,7 @@ public class AuctionTooltipHandler { tooltip.add(EnumChatFormatting.RED + StatCollector.translateToLocal("auction.claim.expiresSoon")); } else { tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.claim.expires") - .replace("%s", String.valueOf(daysLeft))); + .replace("%s", "" + daysLeft)); } tooltip.add(""); @@ -238,7 +238,7 @@ public class AuctionTooltipHandler { return amount + " " + AuctionClientConfig.getCurrencyName(); } StringBuilder sb = new StringBuilder(); - String str = String.valueOf(amount); + String str = "" + amount; int count = 0; for (int i = str.length() - 1; i >= 0; i--) { if (count > 0 && count % 3 == 0) { diff --git a/src/main/java/noppes/npcs/client/gui/player/GuiAnvilRecipes.java b/src/main/java/noppes/npcs/client/gui/player/GuiAnvilRecipes.java index 683e21798..00551911a 100644 --- a/src/main/java/noppes/npcs/client/gui/player/GuiAnvilRecipes.java +++ b/src/main/java/noppes/npcs/client/gui/player/GuiAnvilRecipes.java @@ -237,7 +237,7 @@ public class GuiAnvilRecipes extends GuiNPCInterface { fontRendererObj.drawString(percentText, percentTextX, percentTextY, CustomNpcResourceListener.DefaultTextColor); // Add text overlay for percentage with the full unrounded value. if (func_146978_c(percentTextX - guiLeft, percentTextY - guiTop, percentWidth, fontRendererObj.FONT_HEIGHT, mouseX, mouseY)) { - textOverlays.add(new TextOverlayData(percentTextX, percentTextY, percentWidth, fontRendererObj.FONT_HEIGHT, String.valueOf(recipe.getRepairPercentage()))); + textOverlays.add(new TextOverlayData(percentTextX, percentTextY, percentWidth, fontRendererObj.FONT_HEIGHT, "" + recipe.getRepairPercentage())); } if (!recipe.availability.isDefault()) { @@ -303,7 +303,7 @@ public class GuiAnvilRecipes extends GuiNPCInterface { int xpBoxWidth = scaledTextWidth; int xpBoxHeight = fontRendererObj.FONT_HEIGHT; if (func_146978_c(xpTextX - guiLeft, xpTextY - guiTop, xpBoxWidth, xpBoxHeight, mouseX, mouseY)) { - textOverlays.add(new TextOverlayData(xpTextX, xpTextY, xpBoxWidth, xpBoxHeight, String.valueOf(recipe.getXpCost()))); + textOverlays.add(new TextOverlayData(xpTextX, xpTextY, xpBoxWidth, xpBoxHeight, "" + recipe.getXpCost())); } } @@ -326,7 +326,7 @@ public class GuiAnvilRecipes extends GuiNPCInterface { private String formatXpCost(int xp) { if (xp < 1000) { - return String.valueOf(xp); + return "" + xp; } double xpK = xp / 1000.0; if (xpK == (int) xpK) { diff --git a/src/main/java/noppes/npcs/client/gui/player/GuiAuctionBidding.java b/src/main/java/noppes/npcs/client/gui/player/GuiAuctionBidding.java index cc5e5d804..afe47fb44 100644 --- a/src/main/java/noppes/npcs/client/gui/player/GuiAuctionBidding.java +++ b/src/main/java/noppes/npcs/client/gui/player/GuiAuctionBidding.java @@ -255,7 +255,7 @@ public class GuiAuctionBidding extends GuiAuctionInterface implements ISubGuiLis String bidCountLabel = StatCollector.translateToLocal("auction.info.bidCount"); fontRendererObj.drawString(EnumChatFormatting.GRAY + bidCountLabel, x, y, 0xFFFFFF); - label = String.valueOf(listing.bidCount); + label = "" + listing.bidCount; length = fontRendererObj.getStringWidth(label); fontRendererObj.drawString(EnumChatFormatting.WHITE + label, xRight - length, y, 0xFFFFFF); } diff --git a/src/main/java/noppes/npcs/client/gui/player/GuiAuctionInterface.java b/src/main/java/noppes/npcs/client/gui/player/GuiAuctionInterface.java index 851c92263..7efdbe9c9 100644 --- a/src/main/java/noppes/npcs/client/gui/player/GuiAuctionInterface.java +++ b/src/main/java/noppes/npcs/client/gui/player/GuiAuctionInterface.java @@ -196,9 +196,9 @@ public abstract class GuiAuctionInterface extends GuiContainerNPCInterface { /** Format currency with commas (e.g., 1000 -> "1,000") */ protected String formatCurrency(long amount) { - if (amount < 1000) return String.valueOf(amount); + if (amount < 1000) return "" + amount; StringBuilder sb = new StringBuilder(); - String str = String.valueOf(amount); + String str = "" + amount; int count = 0; for (int i = str.length() - 1; i >= 0; i--) { if (count > 0 && count % 3 == 0) sb.insert(0, ','); diff --git a/src/main/java/noppes/npcs/client/gui/player/GuiAuctionSell.java b/src/main/java/noppes/npcs/client/gui/player/GuiAuctionSell.java index 8b288c83f..a8784b3b0 100644 --- a/src/main/java/noppes/npcs/client/gui/player/GuiAuctionSell.java +++ b/src/main/java/noppes/npcs/client/gui/player/GuiAuctionSell.java @@ -67,7 +67,7 @@ public class GuiAuctionSell extends GuiAuctionInterface implements ITextfieldLis // Starting Price label and field addLabel(new GuiNpcLabel(1, "auction.sell.startingPrice", guiLeft + contentX, y + 5, 0xFFFFFF)); GuiNpcTextField startingField = new GuiNpcTextField(txtStartingPriceId, this, fontRendererObj, - guiLeft + contentX + 80, y, 80, 18, startingPrice > 0 ? String.valueOf(startingPrice) : ""); + guiLeft + contentX + 80, y, 80, 18, startingPrice > 0 ? "" + startingPrice : ""); startingField.setIntegersOnly(); addTextField(startingField); @@ -78,7 +78,7 @@ public class GuiAuctionSell extends GuiAuctionInterface implements ITextfieldLis addLabel(lblBuyout); txtBuyout = new GuiNpcTextField(txtBuyoutPriceId, this, fontRendererObj, - guiLeft + contentX + 80, y, 80, 18, buyoutPrice > 0 ? String.valueOf(buyoutPrice) : ""); + guiLeft + contentX + 80, y, 80, 18, buyoutPrice > 0 ? "" + buyoutPrice : ""); txtBuyout.setIntegersOnly(); addTextField(txtBuyout); diff --git a/src/main/java/noppes/npcs/client/gui/player/GuiNPCTrader.java b/src/main/java/noppes/npcs/client/gui/player/GuiNPCTrader.java index f11bbb2e6..b0a1c6c32 100644 --- a/src/main/java/noppes/npcs/client/gui/player/GuiNPCTrader.java +++ b/src/main/java/noppes/npcs/client/gui/player/GuiNPCTrader.java @@ -172,7 +172,7 @@ public class GuiNPCTrader extends GuiContainerNPCInterface implements IGuiData { // Draw stock count to the RIGHT of the trade (after output slot) if (stockEnabled && availableStock[slotIdx] < Integer.MAX_VALUE) { - String stockText = String.valueOf(availableStock[slotIdx]); + String stockText = "" + availableStock[slotIdx]; int stockColor = availableStock[slotIdx] > 0 ? 0x00AA00 : 0xAA0000; fontRendererObj.drawString(stockText, x + OUTPUT_OFFSET + 20, y + 5, stockColor); } @@ -317,10 +317,10 @@ public class GuiNPCTrader extends GuiContainerNPCInterface implements IGuiData { */ private String formatCurrency(long amount) { if (amount < 1000) { - return String.valueOf(amount); + return "" + amount; } StringBuilder sb = new StringBuilder(); - String str = String.valueOf(amount); + String str = "" + amount; int count = 0; for (int i = str.length() - 1; i >= 0; i--) { if (count > 0 && count % 3 == 0) { diff --git a/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBid.java b/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBid.java index de7ab50e4..a1793c545 100644 --- a/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBid.java +++ b/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBid.java @@ -62,7 +62,7 @@ public class SubGuiAuctionBid extends SubGuiInterface implements ITextfieldListe // Bid amount field addLabel(new GuiNpcLabel(3, "auction.bid.yourBid", guiLeft + 15, y + 4)); GuiNpcTextField bidField = new GuiNpcTextField(txtBidId, this, fontRendererObj, - guiLeft + 80, y, 100, 18, String.valueOf(minimumBid)); + guiLeft + 80, y, 100, 18, "" + minimumBid); bidField.setIntegersOnly(); addTextField(bidField); y += 28; @@ -146,9 +146,9 @@ public class SubGuiAuctionBid extends SubGuiInterface implements ITextfieldListe } private String formatCurrency(long amount) { - if (amount < 1000) return String.valueOf(amount); + if (amount < 1000) return "" + amount; StringBuilder sb = new StringBuilder(); - String str = String.valueOf(amount); + String str = "" + amount; int count = 0; for (int i = str.length() - 1; i >= 0; i--) { if (count > 0 && count % 3 == 0) sb.insert(0, ','); diff --git a/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBuyNow.java b/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBuyNow.java index f4061299b..b63471047 100644 --- a/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBuyNow.java +++ b/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBuyNow.java @@ -107,9 +107,9 @@ public class SubGuiAuctionBuyNow extends SubGuiInterface { } private String formatCurrency(long amount) { - if (amount < 1000) return String.valueOf(amount); + if (amount < 1000) return "" + amount; StringBuilder sb = new StringBuilder(); - String str = String.valueOf(amount); + String str = "" + amount; int count = 0; for (int i = str.length() - 1; i >= 0; i--) { if (count > 0 && count % 3 == 0) sb.insert(0, ','); diff --git a/src/main/java/noppes/npcs/client/gui/player/inventory/GuiProfiles.java b/src/main/java/noppes/npcs/client/gui/player/inventory/GuiProfiles.java index 7ffb7aff2..f9e627fe7 100644 --- a/src/main/java/noppes/npcs/client/gui/player/inventory/GuiProfiles.java +++ b/src/main/java/noppes/npcs/client/gui/player/inventory/GuiProfiles.java @@ -180,7 +180,7 @@ public class GuiProfiles extends GuiCNPCInventory implements ISubGuiListener, IC if (slot.isTemporary()) { label = StatCollector.translateToLocal("profile.temporary"); - result = (String.valueOf(slot.isTemporary())).toUpperCase(); + result = ("" + slot.isTemporary()).toUpperCase(); labelColor = 0xfc60dd; resultColor = 0x35fc81; fontRendererObj.drawString(label, xLabel, y, labelColor, false); diff --git a/src/main/java/noppes/npcs/client/gui/roles/GuiNpcTraderSetup.java b/src/main/java/noppes/npcs/client/gui/roles/GuiNpcTraderSetup.java index 6c4745d42..7b54518e3 100644 --- a/src/main/java/noppes/npcs/client/gui/roles/GuiNpcTraderSetup.java +++ b/src/main/java/noppes/npcs/client/gui/roles/GuiNpcTraderSetup.java @@ -56,7 +56,7 @@ public class GuiNpcTraderSetup extends GuiContainerNPCInterface2 implements ITex int x = guiLeft + i % 3 * 130 + 15; // First position after "$" label int y = guiTop + i / 3 * 22 + 14; long cost = role.getCurrencyCost(i); - GuiNpcTextField field = new GuiNpcTextField(CURRENCY_FIELD_ID_START + i, this, x, y, 36, 16, cost > 0 ? String.valueOf(cost) : ""); + GuiNpcTextField field = new GuiNpcTextField(CURRENCY_FIELD_ID_START + i, this, x, y, 36, 16, cost > 0 ? "" + cost : ""); field.setMaxStringLength(8); field.setIntegersOnly(); addTextField(field); diff --git a/src/main/java/noppes/npcs/client/gui/roles/SubGuiNpcTraderStock.java b/src/main/java/noppes/npcs/client/gui/roles/SubGuiNpcTraderStock.java index 2e369b7e6..4e88c9b21 100644 --- a/src/main/java/noppes/npcs/client/gui/roles/SubGuiNpcTraderStock.java +++ b/src/main/java/noppes/npcs/client/gui/roles/SubGuiNpcTraderStock.java @@ -95,7 +95,7 @@ public class SubGuiNpcTraderStock extends SubGuiInterface implements ISubGuiList return "-1"; // Mixed values, return unlimited } } - return String.valueOf(first); + return "" + first; } @Override diff --git a/src/main/java/noppes/npcs/client/gui/roles/SubGuiNpcTraderStockSlots.java b/src/main/java/noppes/npcs/client/gui/roles/SubGuiNpcTraderStockSlots.java index b4a76ba48..c9d40c5df 100644 --- a/src/main/java/noppes/npcs/client/gui/roles/SubGuiNpcTraderStockSlots.java +++ b/src/main/java/noppes/npcs/client/gui/roles/SubGuiNpcTraderStockSlots.java @@ -47,7 +47,7 @@ public class SubGuiNpcTraderStockSlots extends SubGuiInterface implements ITextf // Max stock textfield int maxStock = role.stock.maxStock[i]; - String value = maxStock < 0 ? "-1" : String.valueOf(maxStock); + String value = maxStock < 0 ? "-1" : "" + maxStock; GuiNpcTextField field = new GuiNpcTextField(i, this, fontRendererObj, x + 42, y, 32, 18, value); field.setIntegersOnly(); field.setMinMaxDefault(-1, Integer.MAX_VALUE, -1); diff --git a/src/main/java/noppes/npcs/client/gui/select/GuiAnimationSelection.java b/src/main/java/noppes/npcs/client/gui/select/GuiAnimationSelection.java index 882f5817b..7161722d5 100644 --- a/src/main/java/noppes/npcs/client/gui/select/GuiAnimationSelection.java +++ b/src/main/java/noppes/npcs/client/gui/select/GuiAnimationSelection.java @@ -19,28 +19,54 @@ import java.util.Vector; /** * Animation selection dialog for choosing global animations. - * Requests animation data from the server via packets. + * Toggle between Built-in animations (by name) and Custom animations (by ID). */ public class GuiAnimationSelection extends SubGuiInterface implements ICustomScrollListener, IScrollData { private GuiCustomScroll scrollAnimations; - private HashMap animationData = new HashMap(); + // Built-in animations (name only, no ID) + private HashMap builtInData = new HashMap<>(); + // Custom/user animations (name -> ID) + private HashMap customData = new HashMap<>(); + + // View mode: true = built-in, false = custom + private boolean showingBuiltIn = true; + + // Initial values passed in private int initialAnimationId; - public int selectedAnimationId = -1; - private String selectedName = null; + private String initialAnimationName; + // Selected values to return + public int selectedAnimationId = -1; + public String selectedBuiltInName = ""; + + private String selectedCustomName = null; private String search = ""; + /** + * Constructor for selecting by ID (user animations). + */ public GuiAnimationSelection(int animationId) { - drawDefaultBackground = false; + this(animationId, ""); + } + + /** + * Constructor for selecting by name (built-in) or ID (user). + */ + public GuiAnimationSelection(int animationId, String animationName) { title = ""; setBackground("menubg.png"); xSize = 220; ySize = 226; this.initialAnimationId = animationId; + this.initialAnimationName = animationName != null ? animationName : ""; this.selectedAnimationId = animationId; + this.selectedBuiltInName = this.initialAnimationName; + + // Start showing built-in if we have a built-in name, otherwise custom + this.showingBuiltIn = !this.initialAnimationName.isEmpty(); // Request animation data from server PacketClient.sendClient(new AnimationsGetPacket()); @@ -50,55 +76,87 @@ public class GuiAnimationSelection extends SubGuiInterface implements ICustomScr public void initGui() { super.initGui(); - this.addLabel(new GuiNpcLabel(0, "menu.animations", guiLeft + 8, guiTop + 4)); + int y = guiTop + 4; - this.addButton(new GuiNpcButton(1, guiLeft + 4, guiTop + ySize - 35, 50, 20, "gui.clear")); - this.addButton(new GuiNpcButton(2, guiLeft + xSize - 56, guiTop + ySize - 35, 50, 20, "gui.done")); - this.addButton(new GuiNpcButton(3, guiLeft + xSize - 108, guiTop + ySize - 35, 50, 20, "gui.cancel")); + // Toggle button for switching views + String viewLabel = showingBuiltIn ? "gui.builtinAnimations" : "gui.customAnimations"; + this.addButton(new GuiNpcButton(10, guiLeft + 4, y, 212, 20, viewLabel)); + y += 24; + // Animation scroll list if (scrollAnimations == null) { scrollAnimations = new GuiCustomScroll(this, 0, 0); - scrollAnimations.setSize(212, 163); + scrollAnimations.setSize(212, 130); } scrollAnimations.setList(getSearchList()); - if (selectedName != null) { - scrollAnimations.setSelected(selectedName); - } - scrollAnimations.guiLeft = guiLeft + 4; - scrollAnimations.guiTop = guiTop + 14; - this.addScroll(scrollAnimations); - addTextField(new GuiNpcTextField(55, this, fontRendererObj, guiLeft + 4, guiTop + 179, 212, 20, search)); + // Set selection based on current mode + if (showingBuiltIn && !selectedBuiltInName.isEmpty()) { + scrollAnimations.setSelected(selectedBuiltInName); + } else if (!showingBuiltIn && selectedCustomName != null) { + scrollAnimations.setSelected(selectedCustomName); + } + + scrollAnimations.guiLeft = guiLeft + 4; + scrollAnimations.guiTop = y; + this.addScroll(scrollAnimations); + y += 134; + + // Search textbox + addTextField(new GuiNpcTextField(55, this, fontRendererObj, guiLeft + 4, y, 212, 20, search)); + y += 24; + + // Buttons + this.addButton(new GuiNpcButton(1, guiLeft + 4, y, 50, 20, "gui.clear")); + this.addButton(new GuiNpcButton(3, guiLeft + xSize - 108, y, 50, 20, "gui.cancel")); + this.addButton(new GuiNpcButton(2, guiLeft + xSize - 56, y, 50, 20, "gui.done")); } @Override public void setData(Vector list, HashMap data, EnumScrollData type) { - this.animationData = data; + if (type == EnumScrollData.BUILTIN_ANIMATIONS) { + this.builtInData = data; - // Find the name for our initial animation ID - if (initialAnimationId >= 0) { - for (String name : data.keySet()) { - if (data.get(name) == initialAnimationId) { - selectedName = name; - break; + // Check if initial name matches a built-in animation + if (!initialAnimationName.isEmpty()) { + for (String name : builtInData.keySet()) { + if (name.equalsIgnoreCase(initialAnimationName)) { + selectedBuiltInName = name; + selectedAnimationId = -1; + selectedCustomName = null; + showingBuiltIn = true; + break; + } + } + } + } else if (type == EnumScrollData.ANIMATIONS) { + this.customData = data; + + // Find the name for our initial animation ID (if not using built-in) + if (selectedBuiltInName.isEmpty() && initialAnimationId >= 0) { + for (String name : data.keySet()) { + if (data.get(name) == initialAnimationId) { + selectedCustomName = name; + showingBuiltIn = false; + break; + } } } } if (scrollAnimations != null) { scrollAnimations.setList(getSearchList()); - if (selectedName != null) { - scrollAnimations.setSelected(selectedName); + if (showingBuiltIn && !selectedBuiltInName.isEmpty()) { + scrollAnimations.setSelected(selectedBuiltInName); + } else if (!showingBuiltIn && selectedCustomName != null) { + scrollAnimations.setSelected(selectedCustomName); } } } @Override public void setSelected(String selected) { - this.selectedName = selected; - if (scrollAnimations != null) { - scrollAnimations.setSelected(selected); - } + // Not used in this implementation } @Override @@ -109,18 +167,22 @@ public class GuiAnimationSelection extends SubGuiInterface implements ICustomScr if (search.equals(getTextField(55).getText())) return; search = getTextField(55).getText().toLowerCase(); - scrollAnimations.resetScroll(); - scrollAnimations.setList(getSearchList()); + if (scrollAnimations != null) { + scrollAnimations.resetScroll(); + scrollAnimations.setList(getSearchList()); + } } } } private List getSearchList() { + HashMap sourceData = showingBuiltIn ? builtInData : customData; + if (search.isEmpty()) { - return new ArrayList(animationData.keySet()); + return new ArrayList<>(sourceData.keySet()); } - List list = new ArrayList(); - for (String name : animationData.keySet()) { + List list = new ArrayList<>(); + for (String name : sourceData.keySet()) { if (name.toLowerCase().contains(search)) list.add(name); } @@ -129,11 +191,20 @@ public class GuiAnimationSelection extends SubGuiInterface implements ICustomScr @Override public void customScrollClicked(int i, int j, int k, GuiCustomScroll guiCustomScroll) { - if (guiCustomScroll.id == 0) { - String selected = scrollAnimations.getSelected(); - if (selected != null && animationData.containsKey(selected)) { - selectedName = selected; - selectedAnimationId = animationData.get(selected); + String selected = scrollAnimations.getSelected(); + if (selected == null) return; + + if (showingBuiltIn) { + if (builtInData.containsKey(selected)) { + selectedBuiltInName = selected; + selectedAnimationId = -1; + selectedCustomName = null; + } + } else { + if (customData.containsKey(selected)) { + selectedCustomName = selected; + selectedAnimationId = customData.get(selected); + selectedBuiltInName = ""; } } initGui(); @@ -141,27 +212,40 @@ public class GuiAnimationSelection extends SubGuiInterface implements ICustomScr @Override public void customScrollDoubleClicked(String selection, GuiCustomScroll scroll) { - if (selectedAnimationId < 0) - return; - close(); + // Double-click to confirm selection + if (!selectedBuiltInName.isEmpty() || selectedAnimationId >= 0) { + close(); + } } @Override protected void actionPerformed(GuiButton guibutton) { int id = guibutton.id; - if (id == 1) { - // Clear selection - selectedName = null; + + // Toggle view button + if (id == 10) { + showingBuiltIn = !showingBuiltIn; + search = ""; + if (scrollAnimations != null) { + scrollAnimations.resetScroll(); + } + initGui(); + } + // Clear + else if (id == 1) { + selectedBuiltInName = ""; selectedAnimationId = -1; + selectedCustomName = null; close(); } - if (id == 2) { - // Done - keep selection + // Done + else if (id == 2) { close(); } - if (id == 3) { - // Cancel - restore initial value + // Cancel + else if (id == 3) { selectedAnimationId = initialAnimationId; + selectedBuiltInName = initialAnimationName; close(); } } @@ -170,6 +254,16 @@ public class GuiAnimationSelection extends SubGuiInterface implements ICustomScr * Get the name of the selected animation (for display purposes). */ public String getSelectedName() { - return selectedName; + if (!selectedBuiltInName.isEmpty()) { + return selectedBuiltInName; + } + return selectedCustomName; + } + + /** + * Check if a built-in animation was selected. + */ + public boolean isBuiltInSelected() { + return !selectedBuiltInName.isEmpty(); } } diff --git a/src/main/java/noppes/npcs/client/gui/select/GuiSoundSelection.java b/src/main/java/noppes/npcs/client/gui/select/GuiSoundSelection.java index 341b3cc48..aae3043ce 100644 --- a/src/main/java/noppes/npcs/client/gui/select/GuiSoundSelection.java +++ b/src/main/java/noppes/npcs/client/gui/select/GuiSoundSelection.java @@ -39,7 +39,6 @@ public class GuiSoundSelection extends SubGuiInterface implements ICustomScrollL public static HashMap> cachedDomains = new HashMap<>(); public GuiSoundSelection(String sound) { - drawDefaultBackground = false; title = ""; setBackground("menubg.png"); xSize = 366; 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..ea8dcc0b5 --- /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, 300F); + + // 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/GuiCustomScroll.java b/src/main/java/noppes/npcs/client/gui/util/GuiCustomScroll.java index 0bf481ffb..3f3ca9223 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiCustomScroll.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiCustomScroll.java @@ -104,8 +104,10 @@ public class GuiCustomScroll extends GuiScreen { GL11.glTranslatef(guiLeft, guiTop, 0.0F); GL11.glColor4f(1.0F, 1.0F, 1.0F, 1.0F); - if (selectable) + if (selectable && !hasSubGUI) hover = getMouseOver(i, j); + else if (hasSubGUI) + hover = -1; drawItems(); 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/client/gui/util/GuiNPCInterface.java b/src/main/java/noppes/npcs/client/gui/util/GuiNPCInterface.java index 9cefa6816..0e53ba30d 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiNPCInterface.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiNPCInterface.java @@ -57,6 +57,7 @@ public abstract class GuiNPCInterface extends GuiScreen { public float bgScaleX = 1; public float bgScaleY = 1; public float bgScaleZ = 1; + public int bgTextureHeight = 256; // Actual content height in texture (for bottom border stitching) public GuiNPCInterface(EntityNPCInterface npc) { this.player = Minecraft.getMinecraft().thePlayer; @@ -74,6 +75,11 @@ public abstract class GuiNPCInterface extends GuiScreen { background = new ResourceLocation("customnpcs", "textures/gui/" + texture); } + public void setBackground(String texture, int textureHeight) { + background = new ResourceLocation("customnpcs", "textures/gui/" + texture); + bgTextureHeight = textureHeight; + } + public ResourceLocation getResource(String texture) { return new ResourceLocation("customnpcs", "textures/gui/" + texture); } @@ -404,6 +410,8 @@ public abstract class GuiNPCInterface extends GuiScreen { } for (GuiNpcTextField tf : textfields.values()) { tf.drawTextBox(i, j); + if (textField.hasHoverText()) + textField.drawHover(i, j, subGui); } for (GuiScreen gui : extra.values()) @@ -423,23 +431,44 @@ public abstract class GuiNPCInterface extends GuiScreen { GL11.glTranslatef(guiLeft, guiTop, 0); GL11.glScalef(bgScale * bgScaleX, bgScale * bgScaleY, bgScale * bgScaleZ); mc.renderEngine.bindTexture(background); + + // Handle Y dimension: if ySize < texture content height, stitch bottom border + int topHeight = ySize; + int bottomHeight = 0; + int bottomTextureV = 0; + if (ySize < bgTextureHeight) { + // Keep bottom border from texture bottom (last 6 pixels of actual content) + bottomHeight = 6; + topHeight = ySize - bottomHeight; + bottomTextureV = bgTextureHeight - bottomHeight; + } + if (xSize > 256) { // GUI wider than texture: draw left portion, then right portion from texture edge - drawTexturedModalRect(0, 0, 0, 0, 250, ySize); - drawTexturedModalRect(250, 0, 256 - (xSize - 250), 0, xSize - 250, ySize); + drawTexturedModalRect(0, 0, 0, 0, 250, topHeight); + drawTexturedModalRect(250, 0, 256 - (xSize - 250), 0, xSize - 250, topHeight); + if (bottomHeight > 0) { + drawTexturedModalRect(0, topHeight, 0, bottomTextureV, 250, bottomHeight); + drawTexturedModalRect(250, topHeight, 256 - (xSize - 250), bottomTextureV, xSize - 250, bottomHeight); + } } else if (xSize < 256) { // GUI narrower than texture: stitch left and right edges together - // Left portion (first half of GUI) int leftWidth = xSize / 2; - // Right portion width (remaining) int rightWidth = xSize - leftWidth; - // Draw left side from texture start - drawTexturedModalRect(0, 0, 0, 0, leftWidth, ySize); - // Draw right side from texture end (256 - rightWidth) - drawTexturedModalRect(leftWidth, 0, 256 - rightWidth, 0, rightWidth, ySize); + // Draw top portion + drawTexturedModalRect(0, 0, 0, 0, leftWidth, topHeight); + drawTexturedModalRect(leftWidth, 0, 256 - rightWidth, 0, rightWidth, topHeight); + // Draw bottom border strip + if (bottomHeight > 0) { + drawTexturedModalRect(0, topHeight, 0, bottomTextureV, leftWidth, bottomHeight); + drawTexturedModalRect(leftWidth, topHeight, 256 - rightWidth, bottomTextureV, rightWidth, bottomHeight); + } } else { - // GUI exactly 256px: draw full texture - drawTexturedModalRect(0, 0, 0, 0, xSize, ySize); + // GUI exactly 256px wide + drawTexturedModalRect(0, 0, 0, 0, xSize, topHeight); + if (bottomHeight > 0) { + drawTexturedModalRect(0, topHeight, 0, bottomTextureV, xSize, bottomHeight); + } } GL11.glPopMatrix(); } diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiNpcButton.java b/src/main/java/noppes/npcs/client/gui/util/GuiNpcButton.java index c14d0da83..4ebafd54f 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiNpcButton.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiNpcButton.java @@ -2,7 +2,9 @@ package noppes.npcs.client.gui.util; import kamkeel.npcs.util.TextSplitter; import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.FontRenderer; import net.minecraft.client.gui.GuiButton; +import net.minecraft.client.gui.ScaledResolution; import net.minecraft.client.renderer.RenderHelper; import net.minecraft.util.ResourceLocation; import net.minecraft.util.StatCollector; @@ -150,8 +152,23 @@ public class GuiNpcButton extends GuiButton { public void drawButton(Minecraft mc, int mouseX, int mouseY) { if (!this.visible) return; + // Auto-truncate display text if wider than button + String original = this.displayString; + FontRenderer fr = mc.fontRenderer; + if (fr != null) { + int maxTextWidth = this.width - 6; + if (fr.getStringWidth(original) > maxTextWidth) { + String ellipsis = "..."; + int targetWidth = maxTextWidth - fr.getStringWidth(ellipsis); + if (targetWidth > 0) { + this.displayString = fr.trimStringToWidth(original, targetWidth) + ellipsis; + } + } + } // First, draw the button normally using the superclass method. super.drawButton(mc, mouseX, mouseY); + // Restore original display string + this.displayString = original; // Then, if an icon texture is set, draw it. if (iconTexture != null) { mc.getTextureManager().bindTexture(iconTexture); @@ -217,17 +234,27 @@ public class GuiNpcButton extends GuiButton { maxHeight += 2 + (textLines.size() - 1) * 10; } - int gameWidth = mc.displayWidth; - int maxTooltipX = gameWidth - maxWidth - 4; + // Get scaled screen dimensions + ScaledResolution scaledRes = new ScaledResolution(mc, mc.displayWidth, mc.displayHeight); + int screenWidth = scaledRes.getScaledWidth(); + int screenHeight = scaledRes.getScaledHeight(); - if (j2 > maxTooltipX) { - int diff = j2 - maxTooltipX; - j2 -= diff; - GL11.glTranslatef(-300, 0, 0); + // If tooltip would go off right edge, draw on left side of mouse + if (j2 + maxWidth + 6 > screenWidth) { + j2 = x - maxWidth - 16; } - if (k2 + maxHeight + 6 > mc.displayHeight) { - k2 = mc.displayHeight - maxHeight - 6; + // Keep tooltip on screen horizontally + if (j2 < 4) { + j2 = 4; + } + + // Keep tooltip on screen vertically + if (k2 + maxHeight + 6 > screenHeight) { + k2 = screenHeight - maxHeight - 6; + } + if (k2 < 4) { + k2 = 4; } this.zLevel = 300.0F; diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiNpcTextField.java b/src/main/java/noppes/npcs/client/gui/util/GuiNpcTextField.java index 91f3ca142..436b47bb5 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiNpcTextField.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiNpcTextField.java @@ -4,6 +4,14 @@ import net.minecraft.client.Minecraft; import net.minecraft.client.gui.FontRenderer; import net.minecraft.client.gui.GuiScreen; import net.minecraft.client.gui.GuiTextField; +import net.minecraft.util.StatCollector; +import net.minecraft.client.gui.ScaledResolution; +import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GL12; +import net.minecraft.client.renderer.RenderHelper; +import kamkeel.npcs.util.TextSplitter; +import java.util.List; +import java.util.Iterator; public class GuiNpcTextField extends GuiTextField { public boolean enabled = true; @@ -18,6 +26,9 @@ public class GuiNpcTextField extends GuiTextField { public float minFloat = 0, maxFloat = Float.MAX_VALUE, defFloat = 0; protected static GuiNpcTextField activeTextfield = null; public boolean canEdit = true; + public String hoverableText = ""; + private boolean wasHovered = false; + private int hoverCount = 0; private final int[] allowedSpecialChars = {14, 211, 203, 205}; @@ -203,5 +214,151 @@ public class GuiNpcTextField extends GuiTextField { return this; } + public void setHoverText(String text) { + this.hoverableText = StatCollector.translateToLocal(text); + } + public boolean hasHoverText() { + return hoverableText != null && !hoverableText.isEmpty(); + } + + public boolean isMouseOver(int mouseX, int mouseY) { + return mouseX >= xPosition && mouseX < xPosition + width && + mouseY >= yPosition && mouseY < yPosition + height; + } + + public void drawHover(int mouseX, int mouseY, boolean hasSubGui) { + if (hasSubGui || !enabled || hoverableText.isEmpty()) + return; + + boolean isHovered = isMouseOver(mouseX, mouseY); + if (!isHovered) { + wasHovered = false; + hoverCount = 0; + return; + } + + if (!wasHovered) { + wasHovered = true; + hoverCount = 0; + } else if (hoverCount < 65) { + hoverCount++; + } + + if (hoverCount > 60) { + GL11.glPushMatrix(); + Minecraft mc = Minecraft.getMinecraft(); + GL11.glColor4f(1, 1, 1, 1); + List lines = TextSplitter.splitText(hoverableText, 30); + drawHoveringText(lines, mouseX, mouseY, mc); + GL11.glDisable(GL11.GL_LIGHTING); + GL11.glPopMatrix(); + } + } + + protected void drawHoveringText(List textLines, int x, int y, Minecraft mc) { + if (mc.fontRenderer == null || textLines.isEmpty()) + return; + GL11.glDisable(GL12.GL_RESCALE_NORMAL); + RenderHelper.disableStandardItemLighting(); + GL11.glDisable(GL11.GL_LIGHTING); + GL11.glDisable(GL11.GL_DEPTH_TEST); + + int maxWidth = 0; + for (String s : textLines) { + int lineWidth = mc.fontRenderer.getStringWidth(s); + if (lineWidth > maxWidth) { + maxWidth = lineWidth; + } + } + + int j2 = x + 12; + int k2 = y - 12; + int maxHeight = 8; + + if (textLines.size() > 1) { + maxHeight += 2 + (textLines.size() - 1) * 10; + } + + // Get scaled screen dimensions + ScaledResolution scaledRes = new ScaledResolution(mc, mc.displayWidth, mc.displayHeight); + int screenWidth = scaledRes.getScaledWidth(); + int screenHeight = scaledRes.getScaledHeight(); + + // If tooltip would go off right edge, draw on left side of mouse + if (j2 + maxWidth + 6 > screenWidth) { + j2 = x - maxWidth - 16; + } + + // Keep tooltip on screen horizontally + if (j2 < 4) { + j2 = 4; + } + + // Keep tooltip on screen vertically + if (k2 + maxHeight + 6 > screenHeight) { + k2 = screenHeight - maxHeight - 6; + } + if (k2 < 4) { + k2 = 4; + } + + int zLevel = 300; + int bgColor = 0xF0100010; + int borderColor1 = 0x505000FF; + int borderColor2 = (borderColor1 & 0xFEFEFE) >> 1 | borderColor1 & 0xFF000000; + + drawGradientRect(j2 - 3, k2 - 4, j2 + maxWidth + 3, k2 - 3, bgColor, bgColor, zLevel); + drawGradientRect(j2 - 3, k2 + maxHeight + 3, j2 + maxWidth + 3, k2 + maxHeight + 4, bgColor, bgColor, zLevel); + drawGradientRect(j2 - 3, k2 - 3, j2 + maxWidth + 3, k2 + maxHeight + 3, bgColor, bgColor, zLevel); + drawGradientRect(j2 - 4, k2 - 3, j2 - 3, k2 + maxHeight + 3, bgColor, bgColor, zLevel); + drawGradientRect(j2 + maxWidth + 3, k2 - 3, j2 + maxWidth + 4, k2 + maxHeight + 3, bgColor, bgColor, zLevel); + drawGradientRect(j2 - 3, k2 - 3 + 1, j2 - 3 + 1, k2 + maxHeight + 3 - 1, borderColor1, borderColor2, zLevel); + drawGradientRect(j2 + maxWidth + 2, k2 - 3 + 1, j2 + maxWidth + 3, k2 + maxHeight + 3 - 1, borderColor1, borderColor2, zLevel); + drawGradientRect(j2 - 3, k2 - 3, j2 + maxWidth + 3, k2 - 3 + 1, borderColor1, borderColor1, zLevel); + drawGradientRect(j2 - 3, k2 + maxHeight + 2, j2 + maxWidth + 3, k2 + maxHeight + 3, borderColor2, borderColor2, zLevel); + + for (int l = 0; l < textLines.size(); ++l) { + String s1 = textLines.get(l); + mc.fontRenderer.drawStringWithShadow(s1, j2, k2, -1); + if (l == 0) { + k2 += 2; + } + k2 += 10; + } + + GL11.glEnable(GL11.GL_LIGHTING); + GL11.glEnable(GL11.GL_DEPTH_TEST); + RenderHelper.enableStandardItemLighting(); + GL11.glEnable(GL12.GL_RESCALE_NORMAL); + } + + protected void drawGradientRect(int left, int top, int right, int bottom, int startColor, int endColor, int zLevel) { + float f = (float)(startColor >> 24 & 255) / 255.0F; + float f1 = (float)(startColor >> 16 & 255) / 255.0F; + float f2 = (float)(startColor >> 8 & 255) / 255.0F; + float f3 = (float)(startColor & 255) / 255.0F; + float f4 = (float)(endColor >> 24 & 255) / 255.0F; + float f5 = (float)(endColor >> 16 & 255) / 255.0F; + float f6 = (float)(endColor >> 8 & 255) / 255.0F; + float f7 = (float)(endColor & 255) / 255.0F; + GL11.glDisable(GL11.GL_TEXTURE_2D); + GL11.glEnable(GL11.GL_BLEND); + GL11.glDisable(GL11.GL_ALPHA_TEST); + GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); + GL11.glShadeModel(GL11.GL_SMOOTH); + net.minecraft.client.renderer.Tessellator tessellator = net.minecraft.client.renderer.Tessellator.instance; + tessellator.startDrawingQuads(); + tessellator.setColorRGBA_F(f1, f2, f3, f); + tessellator.addVertex((double)right, (double)top, (double)zLevel); + tessellator.addVertex((double)left, (double)top, (double)zLevel); + tessellator.setColorRGBA_F(f5, f6, f7, f4); + tessellator.addVertex((double)left, (double)bottom, (double)zLevel); + tessellator.addVertex((double)right, (double)bottom, (double)zLevel); + tessellator.draw(); + GL11.glShadeModel(GL11.GL_FLAT); + GL11.glDisable(GL11.GL_BLEND); + GL11.glEnable(GL11.GL_ALPHA_TEST); + GL11.glEnable(GL11.GL_TEXTURE_2D); + } } diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index 3c1f40850..1ed86e4d4 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -587,7 +587,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { // Dynamically calculate gutter width based on line count digits + icon space if (container != null && container.linesCount > 0) { int maxLineNum = container.linesCount; - String maxLineStr = String.valueOf(maxLineNum); + String maxLineStr = "" + maxLineNum; int digitWidth = ClientProxy.Font.width(maxLineStr); LINE_NUMBER_GUTTER_WIDTH = digitWidth + 10 + GutterIconRenderer.ICON_GUTTER_WIDTH; // 10px padding + icon space } @@ -761,7 +761,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { // Render LINE GUTTER numbers for (int i = renderStart; i <= renderEnd; i++) { int posY = y + (i - scroll.getScrolledLine()) * container.lineHeight + stringYOffset; - String lineNum = String.valueOf(i + 1); + String lineNum = "" + (i + 1); int lineNumWidth = ClientProxy.Font.width(lineNum); int lineNumX = x + LINE_NUMBER_GUTTER_WIDTH - lineNumWidth - 5 - GutterIconRenderer.ICON_GUTTER_WIDTH; // right-align before icon space int lineNumY = posY + 1; diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScrollWindow.java b/src/main/java/noppes/npcs/client/gui/util/GuiScrollWindow.java index 3273f1f6c..fa33a9a09 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScrollWindow.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScrollWindow.java @@ -97,6 +97,11 @@ public class GuiScrollWindow extends GuiScreen implements ITextfieldListener, IC } public void mouseClicked(int i, int j, int k) { + // Ignore clicks outside the visible scroll window area + if (i < xPos || i > xPos + clipWidth || j < yPos || j > yPos + clipHeight) { + return; + } + i = i - xPos; j = (int) (j - yPos + scrollY); @@ -246,6 +251,12 @@ public class GuiScrollWindow extends GuiScreen implements ITextfieldListener, IC } GL11.glPopMatrix(); + + // Draw hover texts in screen space (after scissor/translation are undone) + // so tooltips are not clipped and are positioned correctly + if (isMouseOver(mouseX, mouseY)) { + drawHoverTexts(mouseX, mouseY); + } } public void drawScreen(int mouseX, int mouseY, float partialTicks, int mouseScroll) { @@ -275,8 +286,18 @@ public class GuiScrollWindow extends GuiScreen implements ITextfieldListener, IC } for (GuiNpcButton button : buttons.values()) { button.updateSubGUI(subGui); + } + } + + /** + * Draws hover texts for buttons in screen space (called after GL scissor/translate are undone). + * Hover state was already computed during drawComponents with scroll-local coords. + */ + private void drawHoverTexts(int mouseX, int mouseY) { + boolean subGui = parent.hasSubGui(); + for (GuiNpcButton button : buttons.values()) { if (!button.hoverableText.isEmpty()) { - button.drawHover(i, j, subGui); + button.drawHover(mouseX, mouseY, subGui); } } } diff --git a/src/main/java/noppes/npcs/client/gui/util/key/OverlayKeyPresetViewer.java b/src/main/java/noppes/npcs/client/gui/util/key/OverlayKeyPresetViewer.java index 7ebc098be..5d9cfdaa0 100644 --- a/src/main/java/noppes/npcs/client/gui/util/key/OverlayKeyPresetViewer.java +++ b/src/main/java/noppes/npcs/client/gui/util/key/OverlayKeyPresetViewer.java @@ -361,7 +361,7 @@ public class OverlayKeyPresetViewer { GL11.glPushMatrix(); GL11.glScalef(scale, scale, 1); GL11.glTranslatef(letterX / scale, nameY / scale, 1); - font.drawStringWithShadow(String.valueOf(letter), 0, 0, isDefault ? 0xff8a8a8a : 0xffffffff); + font.drawStringWithShadow("" + letter, 0, 0, isDefault ? 0xff8a8a8a : 0xffffffff); GL11.glPopMatrix(); //////////////////////////////////////////// diff --git a/src/main/java/noppes/npcs/client/gui/util/script/GoToLineDialog.java b/src/main/java/noppes/npcs/client/gui/util/script/GoToLineDialog.java index f054f32a7..e3e023dee 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/GoToLineDialog.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/GoToLineDialog.java @@ -470,7 +470,7 @@ public class GoToLineDialog { // Type character - only allow digits and colon if (ChatAllowedCharacters.isAllowedCharacter(c)) { if (Character.isDigit(c) || c == ':') { - insertText(String.valueOf(c)); + insertText("" + c); markActivity(); return true; } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/RenameRefactorHandler.java b/src/main/java/noppes/npcs/client/gui/util/script/RenameRefactorHandler.java index 2193c20d5..cf6adb9e2 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/RenameRefactorHandler.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/RenameRefactorHandler.java @@ -410,7 +410,7 @@ public class RenameRefactorHandler { if (isValidIdentifierChar(c, isFirstChar)) { if (hasSelection) { // Replace entire selection with this character - currentWord = String.valueOf(c); + currentWord = "" + c; applyLiveRename(); callback.setCursorPosition(primaryOccurrenceStart + 1); sel.setSelection(0, 0); // Clear selection diff --git a/src/main/java/noppes/npcs/client/renderer/blocks/BlockBeamRenderer.java b/src/main/java/noppes/npcs/client/renderer/blocks/BlockBeamRenderer.java index 64b37340f..7f9fdd19b 100644 --- a/src/main/java/noppes/npcs/client/renderer/blocks/BlockBeamRenderer.java +++ b/src/main/java/noppes/npcs/client/renderer/blocks/BlockBeamRenderer.java @@ -26,8 +26,8 @@ public class BlockBeamRenderer extends BlockRendererInterface { public void renderTileEntityAt(TileEntity var1, double var2, double var4, double var6, float var8) { TileVariant tile = (TileVariant) var1; - GL11.glDisable(GL12.GL_RESCALE_NORMAL); GL11.glPushMatrix(); + GL11.glDisable(GL12.GL_RESCALE_NORMAL); GL11.glTranslatef((float) var2 + 0.5f, (float) var4 + 1.5f, (float) var6 + 0.5f); //GL11.glScalef(1.2f, 1.1f, 1.2f); GL11.glRotatef(180, 0, 0, 1); diff --git a/src/main/java/noppes/npcs/constants/EnumScrollData.java b/src/main/java/noppes/npcs/constants/EnumScrollData.java index abf55a84e..06d5a6ec3 100644 --- a/src/main/java/noppes/npcs/constants/EnumScrollData.java +++ b/src/main/java/noppes/npcs/constants/EnumScrollData.java @@ -6,5 +6,8 @@ public enum EnumScrollData { MAGIC_CYCLES, DIALOG_GROUP, QUEST_GROUP, - ABILITY_TYPES + ABILITY_TYPES, + CUSTOM_ABILITIES, + ANIMATIONS, + BUILTIN_ANIMATIONS } diff --git a/src/main/java/noppes/npcs/controllers/AnimationController.java b/src/main/java/noppes/npcs/controllers/AnimationController.java index e3caddce5..b4203b452 100644 --- a/src/main/java/noppes/npcs/controllers/AnimationController.java +++ b/src/main/java/noppes/npcs/controllers/AnimationController.java @@ -8,23 +8,43 @@ import noppes.npcs.LogWriter; import noppes.npcs.api.handler.IAnimationHandler; import noppes.npcs.api.handler.data.IAnimation; import noppes.npcs.controllers.data.Animation; +import noppes.npcs.controllers.data.BuiltInAnimation; import noppes.npcs.util.NBTJsonUtil; import java.io.BufferedInputStream; +import java.io.BufferedReader; import java.io.DataInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.FileSystem; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; +import java.util.Iterator; import java.util.Map; +import java.util.stream.Stream; import java.util.zip.GZIPInputStream; public class AnimationController implements IAnimationHandler { public HashMap animations; private HashMap bootOrder; + // Built-in animations loaded from assets folder (read-only, keyed by name) + public HashMap builtInAnimations = new HashMap<>(); + private static final String BUILTIN_ANIMATIONS_PATH = "/assets/customnpcs/animations"; + private static final String BUILTIN_ANIMATIONS_RESOURCE = "assets/customnpcs/animations"; + public static AnimationController Instance; private int lastUsedID = 0; @@ -42,12 +62,98 @@ public class AnimationController implements IAnimationHandler { public void load() { bootOrder = new HashMap<>(); animations = new HashMap<>(); + builtInAnimations = new HashMap<>(); LogWriter.info("Loading animations..."); + loadBuiltInAnimations(); readAnimationMap(); loadAnimations(); LogWriter.info("Done loading animations."); } + /** + * Load built-in animations from assets/customnpcs/animations/ folder. + * These animations are read-only and accessed by name only (no IDs). + * Scans the folder directly instead of using a manifest. + */ + private void loadBuiltInAnimations() { + try { + URL resourceUrl = CustomNpcs.class.getResource(BUILTIN_ANIMATIONS_PATH); + if (resourceUrl == null) { + LogWriter.info("Built-in animations folder not found: " + BUILTIN_ANIMATIONS_PATH); + return; + } + + URI uri = resourceUrl.toURI(); + Path animationsPath; + + if (uri.getScheme().equals("jar")) { + // Running from JAR - need to create a FileSystem to access it + FileSystem fileSystem = null; + try { + fileSystem = FileSystems.getFileSystem(uri); + } catch (Exception e) { + fileSystem = FileSystems.newFileSystem(uri, Collections.emptyMap()); + } + animationsPath = fileSystem.getPath(BUILTIN_ANIMATIONS_PATH); + } else { + // Running from file system (dev environment) + animationsPath = Paths.get(uri); + } + + // Scan the directory for .json files + try (Stream paths = Files.walk(animationsPath, 1)) { + paths.filter(path -> path.toString().endsWith(".json")) + .forEach(path -> { + String fileName = path.getFileName().toString(); + String animName = fileName.substring(0, fileName.length() - 5); // Remove .json + try { + loadBuiltInAnimation(animName); + } catch (Exception e) { + LogWriter.error("Error loading built-in animation: " + animName, e); + } + }); + } + + LogWriter.info("Loaded " + builtInAnimations.size() + " built-in animations."); + } catch (Exception e) { + LogWriter.error("Error scanning built-in animations folder", e); + } + } + + public void loadBuiltInAnimation(Class modClass, String path, String animName) throws Exception { + String resourcePath = path + "/" + animName + ".json"; + try (InputStream stream = modClass.getResourceAsStream(resourcePath)) { + if (stream == null) { + return; + } + + // Read JSON content + StringBuilder content = new StringBuilder(); + BufferedReader reader = new BufferedReader(new InputStreamReader(stream)); + String line; + while ((line = reader.readLine()) != null) { + content.append(line); + } + reader.close(); + + // Parse NBT from JSON + NBTTagCompound nbt = NBTJsonUtil.Convert(content.toString()); + + BuiltInAnimation animation = new BuiltInAnimation(animName); + animation.readFromNBT(nbt); + + // Store with lowercase key for case-insensitive lookup + builtInAnimations.put(animName.toLowerCase(), animation); + } + } + + /** + * Load a single built-in animation by name from assets folder. + */ + private void loadBuiltInAnimation(String animName) throws Exception { + loadBuiltInAnimation(CustomNpcs.class, BUILTIN_ANIMATIONS_PATH, animName); + } + private void loadAnimations() { animations.clear(); @@ -110,6 +216,12 @@ public class AnimationController implements IAnimationHandler { } public IAnimation saveAnimation(IAnimation animation) { + // Reject saving built-in animations + if (animation instanceof BuiltInAnimation) { + LogWriter.info("Cannot save built-in animation: " + animation.getName()); + return animation; + } + if (animation.getID() < 0) { animation.setID(getUnusedId()); while (hasName(animation.getName())) @@ -157,6 +269,12 @@ public class AnimationController implements IAnimationHandler { public void delete(String name) { Animation delete = getAnimationFromName(name); if (delete != null) { + // Reject deleting built-in animations + if (delete instanceof BuiltInAnimation) { + LogWriter.info("Cannot delete built-in animation: " + name); + return; + } + Animation foundAnimation = this.animations.remove(delete.getID()); if (foundAnimation != null && foundAnimation.name != null) { File dir = this.getDir(); @@ -175,7 +293,8 @@ public class AnimationController implements IAnimationHandler { } public void delete(int id) { - if (!this.animations.containsKey(id)) + // Only user animations have valid IDs (>= 0) + if (id < 0 || !this.animations.containsKey(id)) return; Animation foundAnimation = this.animations.remove(id); @@ -199,27 +318,109 @@ public class AnimationController implements IAnimationHandler { } public IAnimation get(String name) { - return getAnimationFromName(name); + return get(name, false); } + public IAnimation get(String name, boolean builtIn) { + // Check user/custom animations first (allows overriding built-in) + for (Map.Entry entry : animations.entrySet()) { + if (entry.getValue().name.equalsIgnoreCase(name)) { + return entry.getValue(); + } + } + // Then check built-in animations + if (builtIn) { + BuiltInAnimation builtInAnim = builtInAnimations.get(name.toLowerCase()); + if (builtInAnim != null) { + return builtInAnim; + } + } + + return null; + } + + /** + * Get a user animation by ID. + * Built-in animations have no IDs and cannot be retrieved this way. + */ public IAnimation get(int id) { + // Only user animations have valid IDs + if (id < 0) { + return null; + } return this.animations.get(id); } + /** + * Get a built-in animation by name. + * @param name The animation name (case-insensitive) + * @return The built-in animation, or null if not found + */ + public BuiltInAnimation getBuiltInAnimation(String name) { + return builtInAnimations.get(name.toLowerCase()); + } + public IAnimation[] getAnimations() { ArrayList animations = new ArrayList<>(this.animations.values()); return animations.toArray(new IAnimation[0]); } + /** + * Get all built-in animations. + */ + public IAnimation[] getBuiltInAnimations() { + return builtInAnimations.values().toArray(new IAnimation[0]); + } + + /** + * Get all animations (both built-in and user-created). + */ + public IAnimation[] getAllAnimations() { + ArrayList all = new ArrayList<>(); + all.addAll(builtInAnimations.values()); + all.addAll(animations.values()); + return all.toArray(new IAnimation[0]); + } + + /** + * Check if an animation name is a built-in animation. + * @param name The animation name to check + * @return true if this is a built-in animation + */ + public boolean isBuiltIn(String name) { + return builtInAnimations.containsKey(name.toLowerCase()); + } + + /** + * @deprecated Built-in animations no longer use IDs. Use isBuiltIn(String name) instead. + */ + @Deprecated + public boolean isBuiltIn(int id) { + return false; // Built-in animations have no IDs + } + public Animation getAnimationFromName(String animation) { - for (Map.Entry entryAnimation : AnimationController.getInstance().animations.entrySet()) { + // Check user/custom animations first (allows overriding built-in) + for (Map.Entry entryAnimation : animations.entrySet()) { if (entryAnimation.getValue().name.equalsIgnoreCase(animation)) { return entryAnimation.getValue(); } } + // Then check built-in animations + BuiltInAnimation builtIn = builtInAnimations.get(animation.toLowerCase()); + if (builtIn != null) { + return builtIn; + } return null; } + /** + * Get names of all built-in animations. + */ + public String[] getBuiltInAnimationNames() { + return builtInAnimations.keySet().toArray(new String[0]); + } + public String[] getNames() { String[] names = new String[animations.size()]; int i = 0; diff --git a/src/main/java/noppes/npcs/controllers/ScriptContainer.java b/src/main/java/noppes/npcs/controllers/ScriptContainer.java index 381d075d2..ad2ca2f1f 100644 --- a/src/main/java/noppes/npcs/controllers/ScriptContainer.java +++ b/src/main/java/noppes/npcs/controllers/ScriptContainer.java @@ -342,5 +342,10 @@ public class ScriptContainer implements IScriptUnit { public void setErrored(boolean errored) { this.errored = errored; } + + @Override + public boolean isJanino() { + return false; + } } diff --git a/src/main/java/noppes/npcs/controllers/data/BuiltInAnimation.java b/src/main/java/noppes/npcs/controllers/data/BuiltInAnimation.java new file mode 100644 index 000000000..a30923eac --- /dev/null +++ b/src/main/java/noppes/npcs/controllers/data/BuiltInAnimation.java @@ -0,0 +1,94 @@ +package noppes.npcs.controllers.data; + +import net.minecraft.nbt.NBTTagCompound; +import noppes.npcs.api.handler.data.IAnimation; + +/** + * A read-only animation loaded from mod assets. + * Built-in animations are identified by name only, never by ID. + * They cannot be saved, deleted, or modified. + */ +public class BuiltInAnimation extends Animation { + + public BuiltInAnimation() { + super(); + this.id = -1; // No valid ID for built-in animations + } + + public BuiltInAnimation(String name) { + super(); + this.name = name; + this.id = -1; + } + + /** + * Built-in animations cannot be saved. + * @return this animation unchanged + */ + @Override + public IAnimation save() { + // Do nothing - built-in animations are read-only + return this; + } + + /** + * Built-in animations have no valid ID. + * @return always -1 + */ + @Override + public int getID() { + return -1; + } + + /** + * Built-in animations cannot have their ID changed. + */ + @Override + public void setID(int newID) { + // Ignore - built-in animations don't use IDs + } + + /** + * Check if this is a built-in animation. + * @return always true for BuiltInAnimation + */ + public boolean isBuiltIn() { + return true; + } + + /** + * Read animation data from NBT. + * Preserves the name set in constructor (from filename) rather than reading from NBT. + */ + @Override + public void readFromNBT(NBTTagCompound compound) { + // Preserve the name set in constructor - don't read from NBT + // (filename is the source of truth for built-in animations) + String preservedName = this.name; + + speed = compound.getFloat("Speed"); + smooth = compound.getByte("Smooth"); + loop = compound.getInteger("Loop"); + + frames.clear(); + net.minecraft.nbt.NBTTagList list = compound.getTagList("Frames", 10); + for (int i = 0; i < list.tagCount(); i++) { + NBTTagCompound item = list.getCompoundTagAt(i); + Frame frame = new Frame(); + frame.parent = this; + frame.readFromNBT(item); + frames.add(frame); + } + + this.whileStanding = compound.getBoolean("WhileStanding"); + this.whileMoving = compound.getBoolean("WhileWalking"); + this.whileAttacking = compound.getBoolean("WhileAttacking"); + + this.currentFrame = compound.getInteger("CurrentFrame"); + this.currentFrameTime = compound.getInteger("CurrentFrameTime"); + + // Ensure name and ID are preserved + this.name = preservedName; + this.id = -1; + } +} diff --git a/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java b/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java new file mode 100644 index 000000000..45f9dfe44 --- /dev/null +++ b/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java @@ -0,0 +1,503 @@ +package noppes.npcs.controllers.data; + +import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.AbilityController; +import kamkeel.npcs.controllers.data.ability.AbilityPhase; +import kamkeel.npcs.controllers.data.ability.UserType; +import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; +import kamkeel.npcs.network.packets.data.telegraph.TelegraphRemovePacket; +import kamkeel.npcs.network.packets.data.telegraph.TelegraphSpawnPacket; +import net.minecraft.entity.EntityLivingBase; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.entity.player.EntityPlayerMP; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.nbt.NBTTagList; +import net.minecraft.nbt.NBTTagString; +import net.minecraft.util.DamageSource; +import noppes.npcs.api.ability.IPlayerAbilityData; +import noppes.npcs.controllers.AnimationController; +import noppes.npcs.controllers.data.Animation; + +import kamkeel.npcs.network.packets.data.ability.PlayerAbilitySyncPacket; + +import java.util.ArrayList; +import java.util.List; + +/** + * Manages player ability data - unlocked abilities, selection, cooldowns, and execution. + * Players reference abilities by key (built-in keys or preset names), never customized copies. + *

+ * Key differences from NPC DataAbilities: + * - Universal cooldown (shared across all abilities) + * - Player manually selects ability (no weighted random) + * - Conditions requiring a target are skipped + * - Uses ability's base cooldownTicks only + */ +public class PlayerAbilityData implements IPlayerAbilityData { + + private final PlayerData playerData; + + // ═══════════════════════════════════════════════════════════════════ + // CONFIGURATION (saved to NBT) + // ═══════════════════════════════════════════════════════════════════ + + /** + * List of unlocked ability keys (preset names or built-in keys). + * Order matters - determines selection index. + */ + private List unlockedAbilities = new ArrayList<>(); + + /** + * Currently selected ability index in the unlocked list. + */ + private int selectedIndex = 0; + + // ═══════════════════════════════════════════════════════════════════ + // RUNTIME STATE (not saved) + // ═══════════════════════════════════════════════════════════════════ + + /** + * Universal cooldown end time (shared across all abilities). + */ + private transient long cooldownEndTime = 0; + + /** + * Currently executing ability instance (null if none). + */ + private transient Ability currentAbility; + + /** + * Current ability's key (for cooldown tracking after completion). + */ + private transient String currentAbilityKey; + + /** + * Target for the current ability (can be null for self-targeting). + */ + private transient EntityLivingBase currentTarget; + + // ═══════════════════════════════════════════════════════════════════ + // CONSTRUCTOR + // ═══════════════════════════════════════════════════════════════════ + + public PlayerAbilityData(PlayerData playerData) { + this.playerData = playerData; + } + + // ═══════════════════════════════════════════════════════════════════ + // TICK - Called from player tick handler + // ═══════════════════════════════════════════════════════════════════ + + /** + * Main tick method - handles ability execution. + * Should be called every server tick. + */ + public void tick(EntityPlayer player) { + if (player.worldObj.isRemote || player.isDead) { + return; + } + + if (currentAbility != null && currentAbility.isExecuting()) { + tickCurrentAbility(player); + } + } + + /** + * Tick the currently executing ability. + */ + private void tickCurrentAbility(EntityPlayer player) { + AbilityPhase oldPhase = currentAbility.getPhase(); + + // Tick advances time and possibly changes phase + boolean phaseChanged = currentAbility.tick(); + + switch (currentAbility.getPhase()) { + case WINDUP: + currentAbility.onWindUpTick(player, currentTarget, player.worldObj, currentAbility.getCurrentTick()); + break; + + case ACTIVE: + if (phaseChanged && oldPhase == AbilityPhase.WINDUP) { + // Just entered ACTIVE phase + TelegraphInstance telegraph = currentAbility.getTelegraphInstance(); + if (telegraph != null) { + telegraph.lockPosition(); + } + removeTelegraph(currentAbility, player); + + // Play active sound and animation + playAbilitySound(player, currentAbility.getActiveSound()); + playAbilityAnimation(currentAbility.getActiveAnimation()); + + // Call onExecute + currentAbility.onExecute(player, currentTarget, player.worldObj); + + if (currentAbility.getPhase() == AbilityPhase.IDLE) { + handleAbilityCompletion(player); + return; + } + } + currentAbility.onActiveTick(player, currentTarget, player.worldObj, currentAbility.getCurrentTick()); + + if (currentAbility.getPhase() == AbilityPhase.IDLE) { + handleAbilityCompletion(player); + return; + } + break; + + case DAZED: + // Player is dazed - just wait + break; + + case IDLE: + handleAbilityCompletion(player); + break; + } + } + + /** + * Handle ability completion. + */ + private void handleAbilityCompletion(EntityPlayer player) { + if (currentAbility == null) return; + + currentAbility.onComplete(player, currentTarget); + + // Apply universal cooldown (using base cooldownTicks only, not min/max random) + if (!currentAbility.isIgnoreCooldown()) { + cooldownEndTime = player.worldObj.getTotalWorldTime() + currentAbility.getCooldownTicks(); + } + + // Stop animation + stopAbilityAnimation(); + + currentAbility = null; + currentAbilityKey = null; + currentTarget = null; + } + + // ═══════════════════════════════════════════════════════════════════ + // ABILITY ACTIVATION - Called on Special Key press + // ═══════════════════════════════════════════════════════════════════ + + /** + * Activate the currently selected ability. + * Called when the player presses the Special Key. + * + * @param player The player + * @return true if the ability was started + */ + public boolean activateAbility(EntityPlayer player) { + if (player.worldObj.isRemote) return false; + if (unlockedAbilities.isEmpty()) return false; + if (selectedIndex < 0 || selectedIndex >= unlockedAbilities.size()) return false; + + String key = unlockedAbilities.get(selectedIndex); + return activateAbility(player, key); + } + + /** + * Activate a specific ability by key. + * + * @param player The player + * @param key The ability key (built-in or preset name) + * @return true if the ability was started + */ + public boolean activateAbility(EntityPlayer player, String key) { + if (player.worldObj.isRemote || key == null || key.isEmpty()) return false; + + // Can't activate if already executing + if (currentAbility != null && currentAbility.isExecuting()) return false; + + // Resolve ability from controller + if (AbilityController.Instance == null) return false; + Ability ability = AbilityController.Instance.resolveAbility(key); + if (ability == null) return false; + + // Check user type allows player + if (!ability.getAllowedBy().allowsPlayer()) return false; + + // Check universal cooldown + if (!ability.isIgnoreCooldown() && isOnCooldown(player)) return false; + + // Check conditions (skip target-requiring ones) + if (!ability.checkConditionsForPlayer(player)) return false; + + // Start the ability + currentAbility = ability; + currentAbilityKey = key; + currentTarget = null; // Players don't have auto-targets + ability.start(null); + + // Spawn telegraph + spawnTelegraph(ability, player, null); + + // Play wind up sound and animation + playAbilitySound(player, ability.getWindUpSound()); + playAbilityAnimation(ability.getWindUpAnimation()); + + return true; + } + + // ═══════════════════════════════════════════════════════════════════ + // CLIENT SYNC + // ═══════════════════════════════════════════════════════════════════ + + /** + * Sync ability data to the client. + * Called when abilities change (unlock/lock/select) and on login. + */ + public void syncToClient() { + EntityPlayer player = playerData.player; + if (player instanceof EntityPlayerMP) { + PlayerAbilitySyncPacket.sendToPlayer((EntityPlayerMP) player); + } + } + + // ═══════════════════════════════════════════════════════════════════ + // SELECTION + // ═══════════════════════════════════════════════════════════════════ + + public int getSelectedIndex() { + return selectedIndex; + } + + public void setSelectedIndex(int index) { + if (index >= 0 && index < unlockedAbilities.size()) { + this.selectedIndex = index; + syncToClient(); + } + } + + public void selectNext() { + if (unlockedAbilities.isEmpty()) return; + selectedIndex = (selectedIndex + 1) % unlockedAbilities.size(); + syncToClient(); + } + + public void selectPrevious() { + if (unlockedAbilities.isEmpty()) return; + selectedIndex = (selectedIndex - 1 + unlockedAbilities.size()) % unlockedAbilities.size(); + syncToClient(); + } + + public String getSelectedAbilityKey() { + if (unlockedAbilities.isEmpty() || selectedIndex < 0 || selectedIndex >= unlockedAbilities.size()) { + return null; + } + return unlockedAbilities.get(selectedIndex); + } + + // ═══════════════════════════════════════════════════════════════════ + // UNLOCKED ABILITIES + // ═══════════════════════════════════════════════════════════════════ + + public List getUnlockedAbilityList() { + return unlockedAbilities; + } + + @Override + public String[] getUnlockedAbilities() { + return unlockedAbilities.toArray(new String[0]); + } + + public void setUnlockedAbilities(List abilities) { + this.unlockedAbilities = abilities != null ? new ArrayList<>(abilities) : new ArrayList<>(); + // Clamp selected index + if (selectedIndex >= unlockedAbilities.size()) { + selectedIndex = Math.max(0, unlockedAbilities.size() - 1); + } + syncToClient(); + } + + public void unlockAbility(String key) { + if (key != null && !key.isEmpty() && !unlockedAbilities.contains(key)) { + unlockedAbilities.add(key); + syncToClient(); + } + } + + public void lockAbility(String key) { + unlockedAbilities.remove(key); + if (selectedIndex >= unlockedAbilities.size()) { + selectedIndex = Math.max(0, unlockedAbilities.size() - 1); + } + syncToClient(); + } + + public boolean hasUnlockedAbility(String key) { + return unlockedAbilities.contains(key); + } + + // ═══════════════════════════════════════════════════════════════════ + // COOLDOWNS + // ═══════════════════════════════════════════════════════════════════ + + public boolean isOnCooldown(EntityPlayer player) { + return player.worldObj.getTotalWorldTime() < cooldownEndTime; + } + + /** + * Check if a specific ability key is on cooldown. + * Currently uses universal cooldown (same for all abilities). + */ + public boolean isOnCooldown(String key, EntityPlayer player) { + return isOnCooldown(player); + } + + public long getRemainingCooldown(EntityPlayer player) { + long remaining = cooldownEndTime - player.worldObj.getTotalWorldTime(); + return remaining > 0 ? remaining : 0; + } + + /** + * Reset cooldown for a specific ability key. + * Currently resets the universal cooldown. + */ + public void resetCooldown(String key) { + cooldownEndTime = 0; + } + + /** + * Reset all cooldowns. + */ + public void resetAllCooldowns() { + cooldownEndTime = 0; + } + + @Override + public boolean activateAbility() { + return activateAbility(playerData.player); + } + + @Override + public boolean activateAbility(String key) { + return activateAbility(playerData.player, key); + } + + // ═══════════════════════════════════════════════════════════════════ + // STATE QUERIES + // ═══════════════════════════════════════════════════════════════════ + + public boolean isExecutingAbility() { + return currentAbility != null && currentAbility.isExecuting(); + } + + public Ability getCurrentAbility() { + return currentAbility; + } + + public void interruptCurrentAbility() { + if (currentAbility != null && currentAbility.isExecuting()) { + currentAbility.interrupt(); + stopAbilityAnimation(); + currentAbility = null; + currentAbilityKey = null; + currentTarget = null; + } + } + + @Override + public boolean isOnCooldown() { + EntityPlayer player = playerData.player; + if (player == null) return false; + return isOnCooldown(player); + } + + @Override + public boolean isOnCooldown(String key) { + EntityPlayer player = playerData.player; + if (player == null) return false; + return isOnCooldown(key, player); + } + + @Override + public void resetCooldown() { + cooldownEndTime = 0; + } + + /** + * Check if the player's movement should be locked due to an executing ability. + * Used by the tick handler to restrict player movement during ability phases. + */ + public boolean isMovementLocked() { + return currentAbility != null && currentAbility.isExecuting() + && currentAbility.isMovementLockedForCurrentPhase(); + } + + // ═══════════════════════════════════════════════════════════════════ + // TELEGRAPH + // ═══════════════════════════════════════════════════════════════════ + + private void spawnTelegraph(Ability ability, EntityPlayer player, EntityLivingBase target) { + TelegraphInstance telegraph = ability.createTelegraph(player, target); + if (telegraph != null) { + ability.setTelegraphInstance(telegraph); + if (player instanceof EntityPlayerMP) { + TelegraphSpawnPacket.sendToTracking(telegraph, player); + } + } + } + + private void removeTelegraph(Ability ability, EntityPlayer player) { + TelegraphInstance telegraph = ability.getTelegraphInstance(); + if (telegraph != null) { + if (player instanceof EntityPlayerMP) { + TelegraphRemovePacket.sendToTracking(telegraph.getInstanceId(), player); + } + ability.setTelegraphInstance(null); + } + } + + // ═══════════════════════════════════════════════════════════════════ + // ANIMATION + // ═══════════════════════════════════════════════════════════════════ + + private void playAbilityAnimation(Animation animation) { + if (animation == null || AnimationController.Instance == null) return; + playerData.animationData.setEnabled(true); + playerData.animationData.setAnimation(animation); + playerData.animationData.updateClient(); + } + + private void stopAbilityAnimation() { + playerData.animationData.setAnimation(null); + playerData.animationData.updateClient(); + } + + private void playAbilitySound(EntityPlayer player, String sound) { + if (sound != null && !sound.isEmpty()) { + player.worldObj.playSoundAtEntity(player, sound, 1.0f, 1.0f); + } + } + + // ═══════════════════════════════════════════════════════════════════ + // NBT SERIALIZATION + // ═══════════════════════════════════════════════════════════════════ + + public void writeToNBT(NBTTagCompound compound) { + NBTTagList list = new NBTTagList(); + for (String key : unlockedAbilities) { + list.appendTag(new NBTTagString(key)); + } + compound.setTag("PlayerAbilities", list); + compound.setInteger("PlayerAbilitySelected", selectedIndex); + } + + public void readFromNBT(NBTTagCompound compound) { + unlockedAbilities.clear(); + if (compound.hasKey("PlayerAbilities")) { + NBTTagList list = compound.getTagList("PlayerAbilities", 8); // 8 = string tag + for (int i = 0; i < list.tagCount(); i++) { + String key = list.getStringTagAt(i); + if (key != null && !key.isEmpty()) { + unlockedAbilities.add(key); + } + } + } + selectedIndex = compound.getInteger("PlayerAbilitySelected"); + if (selectedIndex >= unlockedAbilities.size()) { + selectedIndex = Math.max(0, unlockedAbilities.size() - 1); + } + } +} diff --git a/src/main/java/noppes/npcs/controllers/data/PlayerData.java b/src/main/java/noppes/npcs/controllers/data/PlayerData.java index 04e06a41e..fa4e7d961 100644 --- a/src/main/java/noppes/npcs/controllers/data/PlayerData.java +++ b/src/main/java/noppes/npcs/controllers/data/PlayerData.java @@ -15,6 +15,7 @@ import net.minecraft.world.World; import net.minecraftforge.common.IExtendedEntityProperties; import noppes.npcs.CustomNpcs; import noppes.npcs.LogWriter; +import noppes.npcs.api.ability.IPlayerAbilityData; import noppes.npcs.api.entity.ICustomNpc; import noppes.npcs.api.handler.IPlayerBankData; import noppes.npcs.api.handler.IPlayerTradeData; @@ -65,6 +66,7 @@ public class PlayerData implements IExtendedEntityProperties, IPlayerData { // Trade data (currency + auction claims) - shared across all profile slots public PlayerTradeData tradeData = new PlayerTradeData(this); + public PlayerAbilityData abilityData = new PlayerAbilityData(this); public ActionManager actionManager = new ActionManager(); public PlayerDataScript scriptData; @@ -108,6 +110,9 @@ public class PlayerData implements IExtendedEntityProperties, IPlayerData { if (!controller.playerEffects.containsKey(playerID)) { controller.playerEffects.put(playerID, effectData.getEffects()); } + + // Sync player ability data to client + abilityData.syncToClient(); } public void onLogout() { @@ -137,6 +142,7 @@ public class PlayerData implements IExtendedEntityProperties, IPlayerData { effectData.readFromNBT(data); magicData.readToNBT(data); tradeData.readFromNBT(data); + abilityData.readFromNBT(data); if (player != null) { playername = player.getCommandSenderName(); @@ -180,6 +186,7 @@ public class PlayerData implements IExtendedEntityProperties, IPlayerData { effectData.writeToNBT(compound); magicData.writeToNBT(compound); tradeData.writeToNBT(compound); + abilityData.writeToNBT(compound); compound.setString("PlayerName", playername); compound.setString("UUID", uuid); @@ -411,6 +418,10 @@ public class PlayerData implements IExtendedEntityProperties, IPlayerData { return tradeData; } + public IPlayerAbilityData getAbilityData() { + return abilityData; + } + public synchronized void save() { // Don't Save this is a Modification of a Profile's PlayerData Profile profile = ProfileController.Instance.getProfile(UUID.fromString(uuid)); diff --git a/src/main/java/noppes/npcs/entity/EntityNPCInterface.java b/src/main/java/noppes/npcs/entity/EntityNPCInterface.java index 20d2f6d5c..16389f74d 100644 --- a/src/main/java/noppes/npcs/entity/EntityNPCInterface.java +++ b/src/main/java/noppes/npcs/entity/EntityNPCInterface.java @@ -481,6 +481,9 @@ public abstract class EntityNPCInterface extends EntityCreature implements IEnti super.onLivingUpdate(); + // Apply locked rotation after super.onLivingUpdate() to override look helper changes + abilities.applyLockedRotation(); + handleMountRiderState(); if (worldObj.isRemote) { @@ -1931,6 +1934,13 @@ public abstract class EntityNPCInterface extends EntityCreature implements IEnti if (NPCMountUtil.isFlyingMountWithFlightEnabled(this)) { return; } + // Skip fall damage during abilities that control NPC movement (e.g., Slam leap) + if (this.abilities != null && this.abilities.isExecutingAbility() + && this.abilities.getCurrentAbility() != null + && this.abilities.getCurrentAbility().hasAbilityMovement()) { + this.fallDistance = 0; + return; + } if (!this.stats.noFallDamage) super.fall(distance); } diff --git a/src/main/java/noppes/npcs/scripted/entity/ScriptNpc.java b/src/main/java/noppes/npcs/scripted/entity/ScriptNpc.java index 8b632f7ce..f534f9dbc 100644 --- a/src/main/java/noppes/npcs/scripted/entity/ScriptNpc.java +++ b/src/main/java/noppes/npcs/scripted/entity/ScriptNpc.java @@ -5,6 +5,7 @@ import net.minecraft.item.ItemStack; import noppes.npcs.NoppesUtilServer; import noppes.npcs.api.IPos; import noppes.npcs.api.ITimers; +import noppes.npcs.api.ability.IDataAbilities; import noppes.npcs.api.entity.ICustomNpc; import noppes.npcs.api.entity.IEntityLivingBase; import noppes.npcs.api.entity.IPlayer; @@ -51,6 +52,7 @@ import noppes.npcs.scripted.roles.ScriptRoleInterface; import noppes.npcs.scripted.roles.ScriptRoleMailman; import noppes.npcs.scripted.roles.ScriptRoleTrader; import noppes.npcs.scripted.roles.ScriptRoleTransporter; +import noppes.npcs.scripted.wrapper.ScriptDataAbilities; import noppes.npcs.util.ValueUtil; import java.util.ArrayList; @@ -1457,4 +1459,8 @@ public class ScriptNpc extends ScriptLiving imp public IMagicData getMagicData() { return npc.stats.magicData; } + + public IDataAbilities getAbilityData() { + return new ScriptDataAbilities(npc); + } } diff --git a/src/main/java/noppes/npcs/scripted/event/player/DialogEvent.java b/src/main/java/noppes/npcs/scripted/event/player/DialogEvent.java index efc8caeab..a6291cac0 100644 --- a/src/main/java/noppes/npcs/scripted/event/player/DialogEvent.java +++ b/src/main/java/noppes/npcs/scripted/event/player/DialogEvent.java @@ -8,15 +8,13 @@ import noppes.npcs.constants.EnumScriptType; public class DialogEvent extends PlayerEvent implements IDialogEvent { public final IDialog dialog; - private final int optionId; + public final int dialogId; + public final int optionId; - public DialogEvent(IPlayer player, IDialog dialog) { - this(player, dialog, -1); - } - - public DialogEvent(IPlayer player, IDialog dialog, int optionId) { + public DialogEvent(IPlayer player, int id, int optionId, IDialog dialog) { super(player); this.dialog = dialog; + this.dialogId = id; this.optionId = optionId; } @@ -24,12 +22,10 @@ public class DialogEvent extends PlayerEvent implements IDialogEvent { return dialog; } - @Override public int getDialogId() { - return dialog != null ? dialog.getId() : -1; + return dialogId; } - @Override public int getOptionId() { return optionId; } @@ -40,8 +36,8 @@ public class DialogEvent extends PlayerEvent implements IDialogEvent { @Cancelable public static class DialogOpen extends DialogEvent implements IDialogEvent.DialogOpen { - public DialogOpen(IPlayer player, IDialog dialog) { - super(player, dialog); + public DialogOpen(IPlayer player, int id, int optionId, IDialog dialog) { + super(player, id, optionId, dialog); } public String getHookName() { @@ -51,12 +47,8 @@ public class DialogEvent extends PlayerEvent implements IDialogEvent { @Cancelable public static class DialogOption extends DialogEvent implements IDialogEvent.DialogOption { - public DialogOption(IPlayer player, IDialog dialog) { - super(player, dialog); - } - - public DialogOption(IPlayer player, IDialog dialog, int optionId) { - super(player, dialog, optionId); + public DialogOption(IPlayer player, int id, int optionId, IDialog dialog) { + super(player, id, optionId, dialog); } public String getHookName() { @@ -66,13 +58,12 @@ public class DialogEvent extends PlayerEvent implements IDialogEvent { } public static class DialogClosed extends DialogEvent implements IDialogEvent.DialogClosed { - public DialogClosed(IPlayer player, IDialog dialog) { - super(player, dialog); + public DialogClosed(IPlayer player, int id, int optionId, IDialog dialog) { + super(player, id, optionId, dialog); } public String getHookName() { return EnumScriptType.DIALOG_CLOSE.function; } - } } diff --git a/src/main/java/noppes/npcs/scripted/event/player/PlayerAbilityEvent.java b/src/main/java/noppes/npcs/scripted/event/player/PlayerAbilityEvent.java new file mode 100644 index 000000000..cb7def405 --- /dev/null +++ b/src/main/java/noppes/npcs/scripted/event/player/PlayerAbilityEvent.java @@ -0,0 +1,101 @@ +package noppes.npcs.scripted.event.player; + +import cpw.mods.fml.common.eventhandler.Cancelable; +import kamkeel.npcs.controllers.data.ability.Ability; +import net.minecraft.entity.EntityLivingBase; +import noppes.npcs.api.ability.IAbility; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.event.IPlayerAbilityEvent; +import noppes.npcs.constants.EnumScriptType; +import noppes.npcs.scripted.NpcAPI; + +/** + * Events fired during player ability execution lifecycle. + */ +public class PlayerAbilityEvent extends PlayerEvent implements IPlayerAbilityEvent { + protected final Ability ability; + protected final IEntityLivingBase target; + + public PlayerAbilityEvent(IPlayer player, Ability ability, EntityLivingBase target) { + super(player); + this.ability = ability; + this.target = target != null ? (IEntityLivingBase) NpcAPI.Instance().getIEntity(target) : null; + } + + @Override + public IAbility getAbility() { + return ability; + } + + @Override + public IEntityLivingBase getTarget() { + return target; + } + + @Override + public String getHookName() { + return "playerAbilityEvent"; + } + + /** + * Fired when a player's ability hits an entity with damage. + * Canceling prevents the damage from being applied. + */ + @Cancelable + public static class HitEvent extends PlayerAbilityEvent implements IPlayerAbilityEvent.HitEvent { + private final IEntityLivingBase hitEntity; + private float damage; + private float knockback; + private float knockbackUp; + + public HitEvent(IPlayer player, Ability ability, EntityLivingBase target, + EntityLivingBase hitEntity, float damage, float knockback, float knockbackUp) { + super(player, ability, target); + this.hitEntity = hitEntity != null ? (IEntityLivingBase) NpcAPI.Instance().getIEntity(hitEntity) : null; + this.damage = damage; + this.knockback = knockback; + this.knockbackUp = knockbackUp; + } + + @Override + public IEntityLivingBase getHitEntity() { + return hitEntity; + } + + @Override + public float getDamage() { + return damage; + } + + @Override + public void setDamage(float damage) { + this.damage = damage; + } + + @Override + public float getKnockback() { + return knockback; + } + + @Override + public void setKnockback(float knockback) { + this.knockback = knockback; + } + + @Override + public float getKnockbackUp() { + return knockbackUp; + } + + @Override + public void setKnockbackUp(float knockbackUp) { + this.knockbackUp = knockbackUp; + } + + @Override + public String getHookName() { + return EnumScriptType.ABILITY_HIT.function; + } + } +} diff --git a/src/main/java/noppes/npcs/scripted/wrapper/ScriptDataAbilities.java b/src/main/java/noppes/npcs/scripted/wrapper/ScriptDataAbilities.java new file mode 100644 index 000000000..d0b5259ea --- /dev/null +++ b/src/main/java/noppes/npcs/scripted/wrapper/ScriptDataAbilities.java @@ -0,0 +1,195 @@ +package noppes.npcs.scripted.wrapper; + +import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.AbilityController; +import kamkeel.npcs.controllers.data.ability.AbilitySlot; +import net.minecraft.entity.EntityLivingBase; +import noppes.npcs.DataAbilities; +import noppes.npcs.api.ability.IAbility; +import noppes.npcs.api.ability.IDataAbilities; +import noppes.npcs.entity.EntityNPCInterface; + +import java.util.List; + +/** + * Script wrapper for DataAbilities providing API access to NPC abilities. + */ +public class ScriptDataAbilities implements IDataAbilities { + + private final DataAbilities data; + private final EntityNPCInterface npc; + + public ScriptDataAbilities(EntityNPCInterface npc) { + this.npc = npc; + this.data = npc.abilities; + } + + @Override + public boolean isEnabled() { + return data.enabled; + } + + @Override + public void setEnabled(boolean enabled) { + data.enabled = enabled; + } + + @Override + public IAbility[] getAbilities() { + return data.getAbilities().toArray(new IAbility[0]); + } + + @Override + public void addAbility(IAbility ability) { + if (ability instanceof Ability) { + data.addAbility((Ability) ability); + } + } + + @Override + public void addAbilityReference(String key) { + data.addAbilityReference(key); + } + + @Override + public void removeAbility(String abilityId) { + data.removeAbility(abilityId); + } + + @Override + public IAbility getAbility(String abilityId) { + return data.getAbility(abilityId); + } + + @Override + public boolean hasAbility(String abilityId) { + return data.getAbility(abilityId) != null; + } + + @Override + public boolean isAbilityReference(String abilityId) { + List slots = data.getAbilitySlots(); + for (int i = 0; i < slots.size(); i++) { + kamkeel.npcs.controllers.data.ability.AbilitySlot slot = slots.get(i); + if (slot.isReference()) { + if (slot.getReferenceId().equals(abilityId)) return true; + } else { + Ability a = slot.getAbility(); + if (a != null && a.getId().equals(abilityId)) return false; + } + } + return false; + } + + @Override + public boolean convertToInline(String abilityId) { + List slots = data.getAbilitySlots(); + for (int i = 0; i < slots.size(); i++) { + kamkeel.npcs.controllers.data.ability.AbilitySlot slot = slots.get(i); + if (slot.isReference() && slot.getReferenceId().equals(abilityId)) { + return data.convertToInline(i); + } + } + return false; + } + + @Override + public void clearAbilities() { + data.clearAbilities(); + } + + @Override + public IAbility getCurrentAbility() { + return data.getCurrentAbility(); + } + + @Override + public boolean isExecutingAbility() { + return data.isExecutingAbility(); + } + + @Override + public void interruptCurrentAbility() { + data.interruptCurrentAbility(null, 0); + } + + @Override + public int getGlobalCooldown() { + return (int) data.getRemainingCooldown(); + } + + @Override + public void setGlobalCooldown(int ticks) { + // Set the cooldown by manipulating the internal cooldown end time + // Not directly exposed, but we can reset and let it tick down + data.resetCooldown(); + // This is a best-effort implementation + } + + @Override + public void resetCooldowns() { + data.resetCooldown(); + } + + @Override + public boolean forceStartAbility(String abilityId) { + return forceStartAbility(abilityId, null); + } + + @Override + public boolean forceStartAbility(String abilityId, Object target) { + // Find the ability + Ability ability = data.getAbility(abilityId); + if (ability == null) { + return false; + } + + // Resolve target + EntityLivingBase targetEntity = resolveTarget(target); + + // Use the direct force start method + return data.forceStartAbility(ability, targetEntity); + } + + @Override + public boolean executeAbility(String key) { + return executeAbility(key, null); + } + + @Override + public boolean executeAbility(String key, Object target) { + EntityLivingBase targetEntity = resolveTarget(target); + return data.executeAbility(key, targetEntity); + } + + @Override + public IAbility createAbility(String typeId) { + if (AbilityController.Instance == null) { + return null; + } + return AbilityController.Instance.create(typeId); + } + + /** + * Resolve a target object to an EntityLivingBase. + */ + private EntityLivingBase resolveTarget(Object target) { + if (target == null) { + // Try to use the NPC's current attack target + return npc.getAttackTarget(); + } + + if (target instanceof EntityLivingBase) { + return (EntityLivingBase) target; + } + + if (target instanceof noppes.npcs.api.entity.IEntityLivingBase) { + Object mcEntity = ((noppes.npcs.api.entity.IEntityLivingBase) target).getMCEntity(); + if (mcEntity instanceof EntityLivingBase) { + return (EntityLivingBase) mcEntity; + } + } + + return null; + } +} diff --git a/src/main/java/noppes/npcs/scripted/wrapper/ScriptPlayerAbilityData.java b/src/main/java/noppes/npcs/scripted/wrapper/ScriptPlayerAbilityData.java new file mode 100644 index 000000000..a36c65d51 --- /dev/null +++ b/src/main/java/noppes/npcs/scripted/wrapper/ScriptPlayerAbilityData.java @@ -0,0 +1,122 @@ +package noppes.npcs.scripted.wrapper; + +import net.minecraft.entity.player.EntityPlayer; +import noppes.npcs.api.ability.IAbility; +import noppes.npcs.api.ability.IPlayerAbilityData; +import noppes.npcs.controllers.data.PlayerAbilityData; +import noppes.npcs.controllers.data.PlayerData; + +/** + * Script wrapper for PlayerAbilityData providing API access to player abilities. + */ +public class ScriptPlayerAbilityData implements IPlayerAbilityData { + + private final PlayerAbilityData data; + private final PlayerData playerData; + + public ScriptPlayerAbilityData(PlayerData playerData) { + this.playerData = playerData; + this.data = playerData.abilityData; + } + + @Override + public String[] getUnlockedAbilities() { + return data.getUnlockedAbilities(); + } + + @Override + public void unlockAbility(String key) { + data.unlockAbility(key); + } + + @Override + public void lockAbility(String key) { + data.lockAbility(key); + } + + @Override + public boolean hasUnlockedAbility(String key) { + return data.hasUnlockedAbility(key); + } + + @Override + public int getSelectedIndex() { + return data.getSelectedIndex(); + } + + @Override + public void setSelectedIndex(int index) { + data.setSelectedIndex(index); + } + + @Override + public String getSelectedAbilityKey() { + return data.getSelectedAbilityKey(); + } + + @Override + public void selectNext() { + data.selectNext(); + } + + @Override + public void selectPrevious() { + data.selectPrevious(); + } + + @Override + public boolean isExecutingAbility() { + return data.isExecutingAbility(); + } + + @Override + public IAbility getCurrentAbility() { + return data.getCurrentAbility(); + } + + @Override + public void interruptCurrentAbility() { + data.interruptCurrentAbility(); + } + + @Override + public boolean isOnCooldown() { + return data.isOnCooldown(); + } + + @Override + public boolean isOnCooldown(String key) { + EntityPlayer player = playerData.player; + if (player == null) return false; + return data.isOnCooldown(key, player); + } + + @Override + public void resetCooldown() { + data.resetCooldown(); + } + + @Override + public void resetCooldown(String key) { + data.resetCooldown(key); + } + + @Override + public void resetAllCooldowns() { + data.resetAllCooldowns(); + } + + @Override + public boolean activateAbility() { + EntityPlayer player = playerData.player; + if (player == null) return false; + return data.activateAbility(player); + } + + @Override + public boolean activateAbility(String key) { + EntityPlayer player = playerData.player; + if (player == null) return false; + return data.activateAbility(player, key); + } +} diff --git a/src/main/java/somehussar/gui/AnnotatedGui.java b/src/main/java/somehussar/gui/AnnotatedGui.java new file mode 100644 index 000000000..8cdf224d5 --- /dev/null +++ b/src/main/java/somehussar/gui/AnnotatedGui.java @@ -0,0 +1,44 @@ +package somehussar.gui; + +import net.minecraft.client.gui.GuiScreen; +import noppes.npcs.client.gui.util.GuiNpcLabel; +import noppes.npcs.client.gui.util.SubGuiInterface; +import somehussar.gui.annotationHandling.GuiFieldHandler; +import somehussar.gui.annotationHandling.field.EditableField; + +public class AnnotatedGui extends SubGuiInterface { + private final GuiFieldHandler.ClassMetadata metadata; + + public AnnotatedGui(T object, String[] groups, GuiScreen parent) { + this(object, groups, parent, 256, 216); + } + + public AnnotatedGui(T object, String[] groups, GuiScreen parent, int xSize, int ySize) { + if (object != null) + metadata = GuiFieldHandler.getMetadata(object.getClass()); + else + metadata = null; + setBackground("menubg.png"); + this.xSize = xSize; + this.ySize = ySize; + closeOnEsc = true; + this.parent = parent; + } + + @Override + public void initGui() { + super.initGui(); + if (metadata == null) + return; + + int i = 0; + for (EditableField field : metadata.getParent().getDeclaredFields()) { + addLabel(new GuiNpcLabel(i++, field.getName(), guiLeft+8, guiTop+i*15+5)); + } + } + + @Override + public void save() { + + } +} diff --git a/src/main/java/somehussar/gui/ImmediateTextField.java b/src/main/java/somehussar/gui/ImmediateTextField.java new file mode 100644 index 000000000..5f49a9d84 --- /dev/null +++ b/src/main/java/somehussar/gui/ImmediateTextField.java @@ -0,0 +1,32 @@ +package somehussar.gui; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.FontRenderer; +import noppes.npcs.client.gui.util.GuiNpcTextField; +import somehussar.gui.annotationHandling.field.EditableField; + +public class ImmediateTextField extends GuiNpcTextField{ + private final EditableField editableField; +// +// private final GuiNpcTextField encapsulatedField; + + public ImmediateTextField(int id, EditableField field, int xPos, int yPos) { + this(id, field, Minecraft.getMinecraft().fontRenderer, xPos, yPos); + } + public ImmediateTextField(int id, EditableField field, FontRenderer fontRenderer, int xPos, int yPos) { + super(id, null, fontRenderer, xPos, yPos, 0, 0, null); + + this.editableField = field; + } + + @Override + public void drawTextBox(int mousX, int mousY) { + + } + + @Override + public void mouseClicked(int xPos, int yPos, int k) { + xPos -= this.xPosition; + yPos -= this.yPosition; + } +} diff --git a/src/main/java/somehussar/gui/annotationHandling/FieldConstraint.java b/src/main/java/somehussar/gui/annotationHandling/FieldConstraint.java new file mode 100644 index 000000000..7a9b124e4 --- /dev/null +++ b/src/main/java/somehussar/gui/annotationHandling/FieldConstraint.java @@ -0,0 +1,5 @@ +package somehussar.gui.annotationHandling; + +public interface FieldConstraint { + void validate(Object value); +} diff --git a/src/main/java/somehussar/gui/annotationHandling/FieldConstraints.java b/src/main/java/somehussar/gui/annotationHandling/FieldConstraints.java new file mode 100644 index 000000000..9a48fd1e3 --- /dev/null +++ b/src/main/java/somehussar/gui/annotationHandling/FieldConstraints.java @@ -0,0 +1,27 @@ +package somehussar.gui.annotationHandling; + +public final class FieldConstraints { + + private static final FieldConstraints EMPTY = + new FieldConstraints(new FieldConstraint[0]); + + private final FieldConstraint[] constraints; + + public FieldConstraints(FieldConstraint[] constraints) { + this.constraints = constraints; + } + + public static FieldConstraints empty() { + return EMPTY; + } + + public void validate(Object value) { + for (FieldConstraint c : constraints) { + c.validate(value); + } + } + + public boolean isEmpty() { + return constraints.length == 0; + } +} diff --git a/src/main/java/somehussar/gui/annotationHandling/GuiEditable.java b/src/main/java/somehussar/gui/annotationHandling/GuiEditable.java new file mode 100644 index 000000000..596f3ab2f --- /dev/null +++ b/src/main/java/somehussar/gui/annotationHandling/GuiEditable.java @@ -0,0 +1,46 @@ +package somehussar.gui.annotationHandling; + +import java.lang.annotation.Annotation; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Retention(RetentionPolicy.RUNTIME) +public @interface GuiEditable { + + @Retention(RetentionPolicy.RUNTIME) + @interface Field { + String value(); + + /** + * Display order + * Higher value = higher priority. + */ + int order() default 0; + + } + + @Retention(RetentionPolicy.RUNTIME) + @interface Group { + String value(); + } + + @Retention(RetentionPolicy.RUNTIME) + @interface Size { + int labelWidth() default 60; + int fieldWidth() default 50; + + GuiEditable.Size DEFAULT_SIZE = new GuiEditable.Size() { + @Override + public int labelWidth() { + return 60; + } + + @Override + public int fieldWidth() { + return 50; + } + @Override public Class annotationType() { return GuiEditable.Size.class; } + }; + + } +} diff --git a/src/main/java/somehussar/gui/annotationHandling/GuiFieldHandler.java b/src/main/java/somehussar/gui/annotationHandling/GuiFieldHandler.java new file mode 100644 index 000000000..c6b7f3625 --- /dev/null +++ b/src/main/java/somehussar/gui/annotationHandling/GuiFieldHandler.java @@ -0,0 +1,140 @@ +package somehussar.gui.annotationHandling; + +import somehussar.gui.annotationHandling.constraints.ConstraintFactory; +import somehussar.gui.annotationHandling.constraints.NumericConstraint; +import somehussar.gui.annotationHandling.constraints.NumericConstraintFactory; +import somehussar.gui.annotationHandling.field.EditableField; + +import java.lang.annotation.Annotation; +import java.lang.invoke.MethodHandles; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public final class GuiFieldHandler { + + private static final Map, ClassMetadata> CACHE = new HashMap<>(); + private static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup(); + + public static final Map, ConstraintFactory> registry = new HashMap<>(); + + static { + registry.put(NumericConstraint.class, new NumericConstraintFactory()); + } + + + private GuiFieldHandler() {} + + public static ClassMetadata getMetadata(Class type) { + return CACHE.computeIfAbsent(type, GuiFieldHandler::scanClass); + } + + @SuppressWarnings({"RawUseOfParameterized"}) + private static ClassMetadata scanClass(Class type) { + if (!type.isAnnotationPresent(GuiEditable.class)) { + return null; + } + + ClassMetadata parent = null; + + Class superCls = type.getSuperclass(); + if (superCls != null && superCls != Object.class) { + parent = getMetadata(superCls); + } + + List list = new ArrayList<>(); + for (Field field : type.getDeclaredFields()) { + EditableField editableField = createEditableField(field); + + if (editableField != null) { + list.add(editableField); + } + } + + return new ClassMetadata( + type, + list.toArray(list.toArray(new EditableField[0])), + parent + ); + } + + @SuppressWarnings({"RawUseOfParameterized", "unchecked"}) + private static EditableField createEditableField(Field field) { + try { + GuiEditable.Field fieldConfig = field.getAnnotation(GuiEditable.Field.class); + if (fieldConfig == null) return null; + if (Modifier.isFinal(field.getModifiers())) return null; + + GuiEditable.Size size = field.getAnnotation(GuiEditable.Size.class); + if (size == null) size = GuiEditable.Size.DEFAULT_SIZE;; + + field.setAccessible(true); + EditableField.Builder builder = new EditableField.Builder() + .name(fieldConfig.value()) + .type(field.getType()) + .getter(LOOKUP.unreflectGetter(field)) + .setter(LOOKUP.unreflectSetter(field)) + .constraints(buildConstraints(field)) + .order(fieldConfig.order()) + .size(size); + + + GuiEditable.Group group = field.getAnnotation(GuiEditable.Group.class); + if (group != null) + builder.group(group.value()); + + + return builder.build(); + } catch (IllegalAccessException e) { + throw new RuntimeException( + "Failed to bind field: " + field, e); + } + } + + @SuppressWarnings({"RawUseOfParameterized", "unchecked"}) + private static FieldConstraints buildConstraints(Field field) { + List list = new ArrayList<>(); + + for (Annotation ann : field.getAnnotations()) { + ConstraintFactory factory = registry.get(ann.annotationType()); + if (factory != null) { + FieldConstraint c = factory.create(field, ann); + list.add(c); + } + } + + if (list.isEmpty()) return FieldConstraints.empty(); + return new FieldConstraints(list.toArray(new FieldConstraint[0])); + } + + + @SuppressWarnings({"RawUseOfParameterized"}) + public static final class ClassMetadata { + final Class type; + final EditableField[] fields; // only declared fields + final ClassMetadata parent; // superclass metadata (nullable) + + ClassMetadata(Class type, EditableField[] fields, ClassMetadata parent) { + this.type = type; + this.fields = fields; + this.parent = parent; + } + + public ClassMetadata getParent() { + return parent; + } + + public EditableField[] getDeclaredFields() { + return fields; + } + + public Class getType() { + return type; + } + } + + +} diff --git a/src/main/java/somehussar/gui/annotationHandling/constraints/ConstraintFactory.java b/src/main/java/somehussar/gui/annotationHandling/constraints/ConstraintFactory.java new file mode 100644 index 000000000..d62de94b4 --- /dev/null +++ b/src/main/java/somehussar/gui/annotationHandling/constraints/ConstraintFactory.java @@ -0,0 +1,10 @@ +package somehussar.gui.annotationHandling.constraints; + +import somehussar.gui.annotationHandling.FieldConstraint; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; + +public interface ConstraintFactory { + FieldConstraint create(Field field, A annotation); +} diff --git a/src/main/java/somehussar/gui/annotationHandling/constraints/NumericConstraint.java b/src/main/java/somehussar/gui/annotationHandling/constraints/NumericConstraint.java new file mode 100644 index 000000000..3b9268a30 --- /dev/null +++ b/src/main/java/somehussar/gui/annotationHandling/constraints/NumericConstraint.java @@ -0,0 +1,13 @@ +package somehussar.gui.annotationHandling.constraints; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(value = RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface NumericConstraint { + double min() default Double.NEGATIVE_INFINITY; + double max() default Double.POSITIVE_INFINITY; +} diff --git a/src/main/java/somehussar/gui/annotationHandling/constraints/NumericConstraintFactory.java b/src/main/java/somehussar/gui/annotationHandling/constraints/NumericConstraintFactory.java new file mode 100644 index 000000000..24b08ceb9 --- /dev/null +++ b/src/main/java/somehussar/gui/annotationHandling/constraints/NumericConstraintFactory.java @@ -0,0 +1,13 @@ +package somehussar.gui.annotationHandling.constraints; + +import somehussar.gui.annotationHandling.FieldConstraint; + +import java.lang.reflect.Field; + +public class NumericConstraintFactory implements ConstraintFactory { + + @Override + public FieldConstraint create(Field field, NumericConstraint ann) { + return new NumericRangeConstraint(ann.min(), ann.max()); + } +} diff --git a/src/main/java/somehussar/gui/annotationHandling/constraints/NumericRangeConstraint.java b/src/main/java/somehussar/gui/annotationHandling/constraints/NumericRangeConstraint.java new file mode 100644 index 000000000..198888d25 --- /dev/null +++ b/src/main/java/somehussar/gui/annotationHandling/constraints/NumericRangeConstraint.java @@ -0,0 +1,35 @@ +package somehussar.gui.annotationHandling.constraints; + +import somehussar.gui.annotationHandling.FieldConstraint; + +public final class NumericRangeConstraint implements FieldConstraint { + + private final double min; + private final double max; + + public NumericRangeConstraint(double min, double max) { + this.min = min; + this.max = max; + } + + @Override + public void validate(Object value) { + if (!(value instanceof Number)) { + throw new ConstraintViolationException("Expected number, got " + value); + } + + double v = ((Number) value).doubleValue(); + if (v < min || v > max) { + throw new ConstraintViolationException( + "Value " + v + " must be between " + min + " and " + max + ); + } + } + + public static final class ConstraintViolationException extends RuntimeException { + public ConstraintViolationException(String message) { + super(message); + } + } + +} diff --git a/src/main/java/somehussar/gui/annotationHandling/field/EditableField.java b/src/main/java/somehussar/gui/annotationHandling/field/EditableField.java new file mode 100644 index 000000000..73172ef42 --- /dev/null +++ b/src/main/java/somehussar/gui/annotationHandling/field/EditableField.java @@ -0,0 +1,85 @@ +package somehussar.gui.annotationHandling.field; + +import somehussar.gui.annotationHandling.FieldConstraints; +import somehussar.gui.annotationHandling.GuiEditable; + +import java.lang.invoke.MethodHandle; + +public final class EditableField { + + private final String name; + private final Class type; + private final MethodHandle getter; + private final MethodHandle setter; + private final FieldConstraints constraints; + private final int order; + private final String group; + private final GuiEditable.Size size; + + public String getName() { + return name; + } + + public Class getType() { + return type; + } + + @SuppressWarnings({"unchecked"}) + public T read(T instance) { + try { + return (T) getter.invoke(instance); + } catch (Throwable t) { + throw new RuntimeException("Failed to read field " + name, t); + } + } + + public void write(T instance, T value) { + constraints.validate(value); + try { + setter.invoke(instance, value); + } catch (Throwable t) { + throw new RuntimeException("Failed to write field " + name, t); + } + } + + public FieldConstraints getConstraints() { + return this.constraints; + } + + public int getOrder() { return this.order; } + + EditableField(Builder b) { + this.name = b.name; + this.type = b.type; + this.getter = b.getter; + this.setter = b.setter; + this.constraints = b.constraints; + this.order = b.order; + this.group = b.group; + this.size = b.size; + } + + public static class Builder { + private String name; + private Class type; + private MethodHandle getter; + private MethodHandle setter; + private FieldConstraints constraints; + private int order; + private String group = null; + private GuiEditable.Size size; + + public Builder name(String v) { this.name = v; return this; } + public Builder type(Class v) { this.type = v; return this; } + public Builder getter(MethodHandle v) { this.getter = v; return this; } + public Builder setter(MethodHandle v) { this.setter = v; return this; } + public Builder constraints(FieldConstraints v) { this.constraints = v; return this; } + public Builder order(int order) { this.order = order; return this; } + public Builder group(String value) { this.group = value; return this; } + public Builder size(GuiEditable.Size size) { this.size = size; return this; } + + public EditableField build() { + return new EditableField<>(this); + } + } +} diff --git a/src/main/resources/assets/customnpcs/animations/Ability_BeamDual_Active.json b/src/main/resources/assets/customnpcs/animations/Ability_BeamDual_Active.json new file mode 100644 index 000000000..c98f89236 --- /dev/null +++ b/src/main/resources/assets/customnpcs/animations/Ability_BeamDual_Active.json @@ -0,0 +1,203 @@ +{ + "Loop": 0, + "Speed": 1.0f, + "Frames": [ + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 3.0f, + "FrameParts": [ + { + "Pivot2": -1.0f, + "Part": "RIGHT_LEG", + "Rotation1": 31.0f, + "Rotation0": 18.0f, + "Pivot0": -3.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 18.0f, + "Pivot0": 3.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -3.0f, + "Part": "RIGHT_ARM", + "Rotation1": -25.0f, + "Rotation0": -90.0f, + "Pivot0": 1.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -3.0f, + "Part": "LEFT_ARM", + "Rotation1": 25.0f, + "Rotation0": -90.0f, + "Pivot0": -1.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": 6.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + } + ], + "Smooth": 0b, + "Duration": 2 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 3.0f, + "FrameParts": [ + { + "Pivot2": -1.0f, + "Part": "RIGHT_LEG", + "Rotation1": 31.0f, + "Rotation0": 18.0f, + "Pivot0": -3.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 18.0f, + "Pivot0": 3.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -3.0f, + "Part": "RIGHT_ARM", + "Rotation1": -25.0f, + "Rotation0": -85.0f, + "Pivot0": 1.0f, + "Pivot1": 1.0f, + "Rotation2": 20.0f + }, + { + "Pivot2": -3.0f, + "Part": "LEFT_ARM", + "Rotation1": 25.0f, + "Rotation0": -85.0f, + "Pivot0": -1.0f, + "Pivot1": 1.0f, + "Rotation2": -20.0f + }, + { + "Pivot2": -1.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": 6.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + } + ], + "Smooth": 0b, + "Duration": 2 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 3.0f, + "FrameParts": [ + { + "Pivot2": -1.0f, + "Part": "RIGHT_LEG", + "Rotation1": 31.0f, + "Rotation0": 18.0f, + "Pivot0": -3.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 18.0f, + "Pivot0": 3.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -3.0f, + "Part": "RIGHT_ARM", + "Rotation1": -25.0f, + "Rotation0": -85.0f, + "Pivot0": 1.0f, + "Pivot1": 1.0f, + "Rotation2": -20.0f + }, + { + "Pivot2": -3.0f, + "Part": "LEFT_ARM", + "Rotation1": 25.0f, + "Rotation0": -85.0f, + "Pivot0": -1.0f, + "Pivot1": 1.0f, + "Rotation2": 20.0f + }, + { + "Pivot2": -1.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": 6.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + } + ], + "Smooth": 0b, + "Duration": 2 + } + ], + "CurrentFrameTime": 0, + "CurrentFrame": 3, + "Smooth": 0b, + "ID": 12, + "WhileAttacking": 1b, + "WhileWalking": 1b, + "Name": "Ability_BeamDual_Active", + "WhileStanding": 1b +} diff --git a/src/main/resources/assets/customnpcs/animations/Ability_BeamDual_Windup.json b/src/main/resources/assets/customnpcs/animations/Ability_BeamDual_Windup.json new file mode 100644 index 000000000..d793e3f22 --- /dev/null +++ b/src/main/resources/assets/customnpcs/animations/Ability_BeamDual_Windup.json @@ -0,0 +1,201 @@ +{ + "Loop": -1, + "Speed": 1.0f, + "Frames": [ + { + "ColorMarker": 16777215, + "Comment": "", + "FrameParts": [ + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": 0.0f, + "Rotation0": -6.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 18.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_LEG", + "Rotation1": 0.0f, + "Rotation0": -6.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": -18.0f + }, + { + "Pivot2": -5.0f, + "Part": "RIGHT_ARM", + "Rotation1": 90.0f, + "Rotation0": -90.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -5.0f, + "Part": "LEFT_ARM", + "Rotation1": -90.0f, + "Rotation0": -90.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -5.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": 25.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -5.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + } + ], + "Duration": 60 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 2.0f, + "FrameParts": [ + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": 0.0f, + "Rotation0": 12.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 18.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_LEG", + "Rotation1": 0.0f, + "Rotation0": 12.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": -18.0f + }, + { + "Pivot2": 4.0f, + "Part": "RIGHT_ARM", + "Rotation1": 75.0f, + "Rotation0": -90.0f, + "Pivot0": 0.0f, + "Pivot1": 2.0f, + "Rotation2": -25.0f + }, + { + "Pivot2": 4.0f, + "Part": "LEFT_ARM", + "Rotation1": -75.0f, + "Rotation0": -90.0f, + "Pivot0": 0.0f, + "Pivot1": 2.0f, + "Rotation2": 25.0f + }, + { + "Pivot2": 3.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": -18.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 3.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": -31.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + } + ], + "Smooth": 0b, + "Duration": 5 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 3.0f, + "FrameParts": [ + { + "Pivot2": -1.0f, + "Part": "RIGHT_LEG", + "Rotation1": 31.0f, + "Rotation0": 18.0f, + "Pivot0": -3.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 18.0f, + "Pivot0": 3.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -3.0f, + "Part": "RIGHT_ARM", + "Rotation1": -25.0f, + "Rotation0": -90.0f, + "Pivot0": 1.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -3.0f, + "Part": "LEFT_ARM", + "Rotation1": 25.0f, + "Rotation0": -90.0f, + "Pivot0": -1.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": 6.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + } + ], + "Smooth": 0b, + "Duration": 15 + } + ], + "CurrentFrameTime": 0, + "CurrentFrame": 2, + "Smooth": 0b, + "ID": 11, + "WhileAttacking": 1b, + "WhileWalking": 1b, + "Name": "Ability_BeamDual_Windup", + "WhileStanding": 1b +} diff --git a/src/main/resources/assets/customnpcs/animations/Ability_Beam_Active.json b/src/main/resources/assets/customnpcs/animations/Ability_Beam_Active.json new file mode 100644 index 000000000..e285bc679 --- /dev/null +++ b/src/main/resources/assets/customnpcs/animations/Ability_Beam_Active.json @@ -0,0 +1,86 @@ +{ + "Loop": 0, + "Speed": 1.0f, + "Frames": [ + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 1.0f, + "FrameParts": [ + { + "Pivot2": -5.0f, + "Part": "LEFT_ARM", + "Rotation1": 12.0f, + "Rotation0": -75.0f, + "Pivot0": -2.0f, + "Pivot1": 3.0f, + "Rotation2": 50.0f + }, + { + "Pivot2": -4.0f, + "Part": "RIGHT_LEG", + "Rotation1": 56.0f, + "Rotation0": 18.0f, + "Pivot0": -4.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -6.0f, + "Part": "LEFT_LEG", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": -3.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": -1.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -3.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": -1.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -5.0f, + "Part": "RIGHT_ARM", + "Rotation1": -18.0f, + "Rotation0": -107.0f, + "Pivot0": -1.0f, + "Pivot1": 2.0f, + "Rotation2": 50.0f + }, + { + "Pivot2": 0.0f, + "Part": "FULL_MODEL", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + } + ], + "Smooth": 0b, + "Duration": 30 + } + ], + "CurrentFrameTime": 0, + "CurrentFrame": 0, + "Smooth": 0b, + "ID": 2, + "WhileAttacking": 1b, + "WhileWalking": 1b, + "Name": "Ability_Beam_Active", + "WhileStanding": 1b +} diff --git a/src/main/resources/assets/customnpcs/animations/Ability_Beam_Windup.json b/src/main/resources/assets/customnpcs/animations/Ability_Beam_Windup.json new file mode 100644 index 000000000..62e54167a --- /dev/null +++ b/src/main/resources/assets/customnpcs/animations/Ability_Beam_Windup.json @@ -0,0 +1,230 @@ +{ + "Loop": -1, + "Speed": 1.0f, + "Frames": [ + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.5f, + "FrameParts": [ + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": 50.0f, + "Rotation0": 6.0f, + "Pivot0": -2.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -3.0f, + "Part": "BODY", + "Rotation1": 18.0f, + "Rotation0": 18.0f, + "Pivot0": -1.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -5.0f, + "Part": "LEFT_ARM", + "Rotation1": 94.0f, + "Rotation0": -94.0f, + "Pivot0": -4.0f, + "Pivot1": 3.0f, + "Rotation2": -31.0f + }, + { + "Pivot2": -3.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 18.0f, + "Pivot0": -1.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": 0.0f, + "Part": "FULL_MODEL", + "Rotation1": 25.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -5.0f, + "Part": "LEFT_LEG", + "Rotation1": -25.0f, + "Rotation0": 12.0f, + "Pivot0": 2.0f, + "Pivot1": 0.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_ARM", + "Rotation1": 75.0f, + "Rotation0": -56.0f, + "Pivot0": 1.0f, + "Pivot1": 0.0f, + "Rotation2": -18.0f + } + ], + "Smooth": 0b, + "Duration": 5 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.25f, + "FrameParts": [ + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": 56.0f, + "Rotation0": 6.0f, + "Pivot0": -2.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -3.0f, + "Part": "BODY", + "Rotation1": 25.0f, + "Rotation0": 18.0f, + "Pivot0": -1.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -5.0f, + "Part": "LEFT_ARM", + "Rotation1": 107.0f, + "Rotation0": -94.0f, + "Pivot0": -5.0f, + "Pivot1": 3.0f, + "Rotation2": -31.0f + }, + { + "Pivot2": -3.0f, + "Part": "HEAD", + "Rotation1": 12.0f, + "Rotation0": 18.0f, + "Pivot0": -1.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": 0.0f, + "Part": "FULL_MODEL", + "Rotation1": 25.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -5.0f, + "Part": "LEFT_LEG", + "Rotation1": -25.0f, + "Rotation0": 12.0f, + "Pivot0": 2.0f, + "Pivot1": 0.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_ARM", + "Rotation1": 88.0f, + "Rotation0": -56.0f, + "Pivot0": 1.0f, + "Pivot1": 0.0f, + "Rotation2": -18.0f + } + ], + "Smooth": 0b, + "Duration": 30 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 2.0f, + "FrameParts": [ + { + "Pivot2": -4.0f, + "Part": "RIGHT_LEG", + "Rotation1": 56.0f, + "Rotation0": 18.0f, + "Pivot0": -4.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -3.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": -1.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -5.0f, + "Part": "LEFT_ARM", + "Rotation1": 12.0f, + "Rotation0": -75.0f, + "Pivot0": -2.0f, + "Pivot1": 3.0f, + "Rotation2": 50.0f + }, + { + "Pivot2": -3.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": -1.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "FULL_MODEL", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -6.0f, + "Part": "LEFT_LEG", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": -5.0f, + "Part": "RIGHT_ARM", + "Rotation1": -18.0f, + "Rotation0": -107.0f, + "Pivot0": -1.0f, + "Pivot1": 2.0f, + "Rotation2": 50.0f + } + ], + "Smooth": 0b, + "Duration": 5 + } + ], + "CurrentFrameTime": 0, + "CurrentFrame": 1, + "Smooth": 0b, + "ID": 1, + "WhileAttacking": 1b, + "WhileWalking": 1b, + "Name": "Ability_Beam_Windup", + "WhileStanding": 1b +} diff --git a/src/main/resources/assets/customnpcs/animations/Ability_Cutter_Active.json b/src/main/resources/assets/customnpcs/animations/Ability_Cutter_Active.json new file mode 100644 index 000000000..1fdf06c24 --- /dev/null +++ b/src/main/resources/assets/customnpcs/animations/Ability_Cutter_Active.json @@ -0,0 +1,86 @@ +{ + "Loop": 0, + "Speed": 1.0f, + "Frames": [ + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 2.0f, + "FrameParts": [ + { + "Pivot2": 2.0f, + "Part": "LEFT_ARM", + "Rotation1": 18.0f, + "Rotation0": 88.0f, + "Pivot0": -10.0f, + "Pivot1": 0.0f, + "Rotation2": 90.0f + }, + { + "Pivot2": 0.0f, + "Part": "HEAD", + "Rotation1": 107.0f, + "Rotation0": -25.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "BODY", + "Rotation1": 107.0f, + "Rotation0": -25.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "FULL_MODEL", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 4.0f, + "Part": "RIGHT_ARM", + "Rotation1": 6.0f, + "Rotation0": 82.0f, + "Pivot0": 3.0f, + "Pivot1": 3.0f, + "Rotation2": 88.0f + }, + { + "Pivot2": 5.0f, + "Part": "RIGHT_LEG", + "Rotation1": 107.0f, + "Rotation0": 6.0f, + "Pivot0": -2.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -2.0f, + "Part": "LEFT_LEG", + "Rotation1": 56.0f, + "Rotation0": 0.0f, + "Pivot0": -6.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + } + ], + "Smooth": 0b, + "Duration": 20 + } + ], + "CurrentFrameTime": 0, + "CurrentFrame": 0, + "Smooth": 0b, + "ID": 14, + "WhileAttacking": 1b, + "WhileWalking": 1b, + "Name": "Ability_Cutter_Active", + "WhileStanding": 1b +} diff --git a/src/main/resources/assets/customnpcs/animations/Ability_Cutter_Windup.json b/src/main/resources/assets/customnpcs/animations/Ability_Cutter_Windup.json new file mode 100644 index 000000000..153d90f12 --- /dev/null +++ b/src/main/resources/assets/customnpcs/animations/Ability_Cutter_Windup.json @@ -0,0 +1,156 @@ +{ + "Loop": -1, + "Speed": 1.0f, + "Frames": [ + { + "ColorMarker": 16777215, + "Comment": "", + "FrameParts": [ + { + "Pivot2": 1.0f, + "Part": "LEFT_ARM", + "Rotation1": -31.0f, + "Rotation0": -94.0f, + "Pivot0": -1.0f, + "Pivot1": 1.0f, + "Rotation2": 90.0f + }, + { + "Pivot2": -3.0f, + "Part": "HEAD", + "Rotation1": -31.0f, + "Rotation0": 25.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -3.0f, + "Part": "BODY", + "Rotation1": -31.0f, + "Rotation0": 25.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "FULL_MODEL", + "Rotation1": -75.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -5.0f, + "Part": "RIGHT_ARM", + "Rotation1": -50.0f, + "Rotation0": -145.0f, + "Pivot0": 2.0f, + "Pivot1": 3.0f, + "Rotation2": 88.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": 18.0f, + "Rotation0": 12.0f, + "Pivot0": -3.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_LEG", + "Rotation1": -25.0f, + "Rotation0": 6.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + } + ], + "Duration": 20 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 2.0f, + "FrameParts": [ + { + "Pivot2": 2.0f, + "Part": "LEFT_ARM", + "Rotation1": 18.0f, + "Rotation0": 88.0f, + "Pivot0": -10.0f, + "Pivot1": 0.0f, + "Rotation2": 90.0f + }, + { + "Pivot2": 0.0f, + "Part": "HEAD", + "Rotation1": 107.0f, + "Rotation0": -25.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "BODY", + "Rotation1": 107.0f, + "Rotation0": -25.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "FULL_MODEL", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 4.0f, + "Part": "RIGHT_ARM", + "Rotation1": 6.0f, + "Rotation0": 82.0f, + "Pivot0": 3.0f, + "Pivot1": 3.0f, + "Rotation2": 88.0f + }, + { + "Pivot2": 5.0f, + "Part": "RIGHT_LEG", + "Rotation1": 107.0f, + "Rotation0": 6.0f, + "Pivot0": -2.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -2.0f, + "Part": "LEFT_LEG", + "Rotation1": 56.0f, + "Rotation0": 0.0f, + "Pivot0": -6.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + } + ], + "Smooth": 0b, + "Duration": 2 + } + ], + "CurrentFrameTime": 0, + "CurrentFrame": 0, + "Smooth": 0b, + "ID": 13, + "WhileAttacking": 1b, + "WhileWalking": 1b, + "Name": "Ability_Cutter_Windup", + "WhileStanding": 1b +} diff --git a/src/main/resources/assets/customnpcs/animations/Ability_DiscDual_Active.json b/src/main/resources/assets/customnpcs/animations/Ability_DiscDual_Active.json new file mode 100644 index 000000000..8fbaa73da --- /dev/null +++ b/src/main/resources/assets/customnpcs/animations/Ability_DiscDual_Active.json @@ -0,0 +1,392 @@ +{ + "Loop": 5, + "Speed": 1.0f, + "Frames": [ + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 2.0f, + "FrameParts": [ + { + "Pivot2": -5.0f, + "Part": "BODY", + "Rotation1": -37.0f, + "Rotation0": 37.0f, + "Pivot0": 3.0f, + "Pivot1": 2.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -7.0f, + "Part": "RIGHT_ARM", + "Rotation1": 0.0f, + "Rotation0": 25.0f, + "Pivot0": 3.0f, + "Pivot1": 2.0f, + "Rotation2": 56.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_LEG", + "Rotation1": -37.0f, + "Rotation0": 31.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": -25.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_ARM", + "Rotation1": 0.0f, + "Rotation0": 120.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "RIGHT_LEG", + "Rotation1": -18.0f, + "Rotation0": 12.0f, + "Pivot0": -1.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -6.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 18.0f, + "Pivot0": 3.0f, + "Pivot1": 2.0f, + "Rotation2": 0.0f + } + ], + "Smooth": 0b, + "Duration": 2 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 1.5f, + "FrameParts": [ + { + "Pivot2": -5.0f, + "Part": "BODY", + "Rotation1": -37.0f, + "Rotation0": 37.0f, + "Pivot0": 3.0f, + "Pivot1": 2.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -7.0f, + "Part": "RIGHT_ARM", + "Rotation1": 0.0f, + "Rotation0": 25.0f, + "Pivot0": 3.0f, + "Pivot1": 2.0f, + "Rotation2": 56.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_LEG", + "Rotation1": -37.0f, + "Rotation0": 31.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": -25.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_ARM", + "Rotation1": 0.0f, + "Rotation0": 120.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "RIGHT_LEG", + "Rotation1": -18.0f, + "Rotation0": 12.0f, + "Pivot0": -1.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -6.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 18.0f, + "Pivot0": 3.0f, + "Pivot1": 2.0f, + "Rotation2": 0.0f + } + ], + "Smooth": 0b, + "Duration": 4 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.5f, + "FrameParts": [ + { + "Pivot2": -5.0f, + "Part": "BODY", + "Rotation1": -37.0f, + "Rotation0": 37.0f, + "Pivot0": 3.0f, + "Pivot1": 2.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -7.0f, + "Part": "RIGHT_ARM", + "Rotation1": 0.0f, + "Rotation0": 25.0f, + "Pivot0": 3.0f, + "Pivot1": 2.0f, + "Rotation2": 56.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_LEG", + "Rotation1": -37.0f, + "Rotation0": 31.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": -25.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_ARM", + "Rotation1": 0.0f, + "Rotation0": 120.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "RIGHT_LEG", + "Rotation1": -18.0f, + "Rotation0": 12.0f, + "Pivot0": -1.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -6.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 18.0f, + "Pivot0": 3.0f, + "Pivot1": 2.0f, + "Rotation2": 0.0f + } + ], + "Smooth": 0b, + "Duration": 4 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 2.0f, + "FrameParts": [ + { + "Pivot2": 2.0f, + "Part": "BODY", + "Rotation1": -25.0f, + "Rotation0": -12.0f, + "Pivot0": -1.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -2.0f, + "Part": "RIGHT_ARM", + "Rotation1": 0.0f, + "Rotation0": -145.0f, + "Pivot0": 2.0f, + "Pivot1": 0.0f, + "Rotation2": 75.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_LEG", + "Rotation1": -25.0f, + "Rotation0": 12.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": -12.0f + }, + { + "Pivot2": 2.0f, + "Part": "LEFT_ARM", + "Rotation1": 37.0f, + "Rotation0": -90.0f, + "Pivot0": -1.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": -12.0f, + "Rotation0": 25.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 6.0f + }, + { + "Pivot2": 2.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": -12.0f, + "Pivot0": -1.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + } + ], + "Smooth": 0b, + "Duration": 2 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 1.0f, + "FrameParts": [ + { + "Pivot2": 2.0f, + "Part": "BODY", + "Rotation1": -25.0f, + "Rotation0": -12.0f, + "Pivot0": -1.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -2.0f, + "Part": "RIGHT_ARM", + "Rotation1": 0.0f, + "Rotation0": -145.0f, + "Pivot0": 2.0f, + "Pivot1": 0.0f, + "Rotation2": 75.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_LEG", + "Rotation1": -25.0f, + "Rotation0": 12.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": -12.0f + }, + { + "Pivot2": 2.0f, + "Part": "LEFT_ARM", + "Rotation1": 37.0f, + "Rotation0": -90.0f, + "Pivot0": -1.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": -12.0f, + "Rotation0": 25.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 6.0f + }, + { + "Pivot2": 2.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": -12.0f, + "Pivot0": -1.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + } + ], + "Smooth": 0b, + "Duration": 4 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.5f, + "FrameParts": [ + { + "Pivot2": 2.0f, + "Part": "BODY", + "Rotation1": -25.0f, + "Rotation0": -12.0f, + "Pivot0": -1.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -2.0f, + "Part": "RIGHT_ARM", + "Rotation1": 0.0f, + "Rotation0": -145.0f, + "Pivot0": 2.0f, + "Pivot1": 0.0f, + "Rotation2": 75.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_LEG", + "Rotation1": -25.0f, + "Rotation0": 12.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": -12.0f + }, + { + "Pivot2": 2.0f, + "Part": "LEFT_ARM", + "Rotation1": 37.0f, + "Rotation0": -90.0f, + "Pivot0": -1.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": -12.0f, + "Rotation0": 25.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 6.0f + }, + { + "Pivot2": 2.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": -12.0f, + "Pivot0": -1.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + } + ], + "Smooth": 0b, + "Duration": 4 + } + ], + "CurrentFrameTime": 0, + "CurrentFrame": 0, + "Smooth": 0b, + "ID": 18, + "WhileAttacking": 1b, + "WhileWalking": 1b, + "Name": "Ability_DiscDual_Active", + "WhileStanding": 1b +} diff --git a/src/main/resources/assets/customnpcs/animations/Ability_DiscDual_Windup.json b/src/main/resources/assets/customnpcs/animations/Ability_DiscDual_Windup.json new file mode 100644 index 000000000..080306c95 --- /dev/null +++ b/src/main/resources/assets/customnpcs/animations/Ability_DiscDual_Windup.json @@ -0,0 +1,293 @@ +{ + "Loop": -1, + "Speed": 1.0f, + "Frames": [ + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.5f, + "FrameParts": [ + { + "Pivot2": -8.0f, + "Part": "BODY", + "Rotation1": 25.0f, + "Rotation0": 31.0f, + "Pivot0": -2.0f, + "Pivot1": 2.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -5.0f, + "Part": "RIGHT_ARM", + "Rotation1": 0.0f, + "Rotation0": 50.0f, + "Pivot0": 0.0f, + "Pivot1": 2.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -2.0f, + "Part": "LEFT_LEG", + "Rotation1": 0.0f, + "Rotation0": 25.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": -31.0f + }, + { + "Pivot2": -8.0f, + "Part": "LEFT_ARM", + "Rotation1": 31.0f, + "Rotation0": -56.0f, + "Pivot0": -2.0f, + "Pivot1": 2.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -3.0f, + "Part": "RIGHT_LEG", + "Rotation1": 25.0f, + "Rotation0": 18.0f, + "Pivot0": -1.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "FULL_MODEL", + "Rotation1": 360.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -8.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": -2.0f, + "Pivot1": 2.0f, + "Rotation2": -12.0f + } + ], + "Smooth": 0b, + "Duration": 10 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.35f, + "FrameParts": [ + { + "Pivot2": 0.0f, + "Part": "BODY", + "Rotation1": 50.0f, + "Rotation0": -6.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 5.0f, + "Part": "RIGHT_ARM", + "Rotation1": 0.0f, + "Rotation0": 50.0f, + "Pivot0": 2.0f, + "Pivot1": 0.0f, + "Rotation2": 170.0f + }, + { + "Pivot2": -3.0f, + "Part": "LEFT_ARM", + "Rotation1": 75.0f, + "Rotation0": -107.0f, + "Pivot0": -6.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -4.0f, + "Part": "LEFT_LEG", + "Rotation1": -6.0f, + "Rotation0": 12.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": 50.0f, + "Rotation0": 18.0f, + "Pivot0": -3.0f, + "Pivot1": -4.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "FULL_MODEL", + "Rotation1": 360.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + } + ], + "Smooth": 0b, + "Duration": 46 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 1.0f, + "FrameParts": [ + { + "Pivot2": 0.0f, + "Part": "BODY", + "Rotation1": 50.0f, + "Rotation0": -6.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 5.0f, + "Part": "RIGHT_ARM", + "Rotation1": 0.0f, + "Rotation0": 50.0f, + "Pivot0": 2.0f, + "Pivot1": 0.0f, + "Rotation2": 170.0f + }, + { + "Pivot2": -3.0f, + "Part": "LEFT_ARM", + "Rotation1": 75.0f, + "Rotation0": -107.0f, + "Pivot0": -6.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -4.0f, + "Part": "LEFT_LEG", + "Rotation1": -6.0f, + "Rotation0": 12.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": 50.0f, + "Rotation0": 18.0f, + "Pivot0": -3.0f, + "Pivot1": -4.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "FULL_MODEL", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + } + ], + "Smooth": 2b, + "Duration": 1 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 2.0f, + "FrameParts": [ + { + "Pivot2": -5.0f, + "Part": "BODY", + "Rotation1": -37.0f, + "Rotation0": 37.0f, + "Pivot0": 3.0f, + "Pivot1": 2.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -7.0f, + "Part": "RIGHT_ARM", + "Rotation1": 0.0f, + "Rotation0": 25.0f, + "Pivot0": 3.0f, + "Pivot1": 2.0f, + "Rotation2": 56.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_LEG", + "Rotation1": -37.0f, + "Rotation0": 31.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": -25.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_ARM", + "Rotation1": 0.0f, + "Rotation0": 120.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "RIGHT_LEG", + "Rotation1": -18.0f, + "Rotation0": 12.0f, + "Pivot0": -1.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -6.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 18.0f, + "Pivot0": 3.0f, + "Pivot1": 2.0f, + "Rotation2": 0.0f + } + ], + "Smooth": 0b, + "Duration": 3 + } + ], + "CurrentFrameTime": 0, + "CurrentFrame": 1, + "Smooth": 0b, + "ID": 17, + "WhileAttacking": 1b, + "WhileWalking": 1b, + "Name": "Ability_DiscDual_Windup", + "WhileStanding": 1b +} diff --git a/src/main/resources/assets/customnpcs/animations/Ability_Disc_Active.json b/src/main/resources/assets/customnpcs/animations/Ability_Disc_Active.json new file mode 100644 index 000000000..13fe07485 --- /dev/null +++ b/src/main/resources/assets/customnpcs/animations/Ability_Disc_Active.json @@ -0,0 +1,77 @@ +{ + "Loop": 0, + "Speed": 1.0f, + "Frames": [ + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 2.0f, + "FrameParts": [ + { + "Pivot2": -5.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 25.0f, + "Pivot0": 2.0f, + "Pivot1": 3.0f, + "Rotation2": 6.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": 0.0f, + "Rotation0": 31.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 31.0f + }, + { + "Pivot2": -4.0f, + "Part": "LEFT_LEG", + "Rotation1": 0.0f, + "Rotation0": 31.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -7.0f, + "Part": "RIGHT_ARM", + "Rotation1": -44.0f, + "Rotation0": -90.0f, + "Pivot0": 1.0f, + "Pivot1": 4.0f, + "Rotation2": 90.0f + }, + { + "Pivot2": -2.0f, + "Part": "LEFT_ARM", + "Rotation1": 31.0f, + "Rotation0": 44.0f, + "Pivot0": 2.0f, + "Pivot1": 3.0f, + "Rotation2": -82.0f + }, + { + "Pivot2": -5.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": 25.0f, + "Pivot0": 2.0f, + "Pivot1": 3.0f, + "Rotation2": 12.0f + } + ], + "Smooth": 0b, + "Duration": 10 + } + ], + "CurrentFrameTime": 0, + "CurrentFrame": 0, + "Smooth": 0b, + "ID": 9, + "WhileAttacking": 1b, + "WhileWalking": 1b, + "Name": "Ability_Disc_Active", + "WhileStanding": 1b +} diff --git a/src/main/resources/assets/customnpcs/animations/Ability_Disc_Windup.json b/src/main/resources/assets/customnpcs/animations/Ability_Disc_Windup.json new file mode 100644 index 000000000..a8231ff50 --- /dev/null +++ b/src/main/resources/assets/customnpcs/animations/Ability_Disc_Windup.json @@ -0,0 +1,896 @@ +{ + "Loop": -1, + "Speed": 1.0f, + "Frames": [ + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.8f, + "FrameParts": [ + { + "Pivot2": 0.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 2.0f, + "Pivot1": 2.0f, + "Rotation2": 18.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": -2.0f, + "Pivot1": 0.0f, + "Rotation2": 31.0f + }, + { + "Pivot2": -3.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 12.0f, + "Pivot0": 1.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "RIGHT_ARM", + "Rotation1": 90.0f, + "Rotation0": 0.0f, + "Pivot0": 1.0f, + "Pivot1": -2.0f, + "Rotation2": 180.0f + }, + { + "Pivot2": 3.0f, + "Part": "LEFT_ARM", + "Rotation1": -18.0f, + "Rotation0": -94.0f, + "Pivot0": -1.0f, + "Pivot1": 7.0f, + "Rotation2": 37.0f + }, + { + "Pivot2": 0.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 2.0f, + "Pivot1": 2.0f, + "Rotation2": 18.0f + } + ], + "Smooth": 0b, + "Duration": 12 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.2f, + "FrameParts": [ + { + "Pivot2": 0.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 6.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": -2.0f, + "Pivot1": 0.0f, + "Rotation2": 31.0f + }, + { + "Pivot2": -3.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 12.0f, + "Pivot0": 1.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "RIGHT_ARM", + "Rotation1": 90.0f, + "Rotation0": 25.0f, + "Pivot0": -1.0f, + "Pivot1": -2.0f, + "Rotation2": 180.0f + }, + { + "Pivot2": 3.0f, + "Part": "LEFT_ARM", + "Rotation1": -18.0f, + "Rotation0": -94.0f, + "Pivot0": -2.0f, + "Pivot1": 7.0f, + "Rotation2": 37.0f + }, + { + "Pivot2": 0.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 6.0f + } + ], + "Smooth": 0b, + "Duration": 6 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.25f, + "FrameParts": [ + { + "Pivot2": 2.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": -12.0f, + "Pivot0": 0.0f, + "Pivot1": 2.0f, + "Rotation2": 6.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": 0.0f, + "Rotation0": 6.0f, + "Pivot0": -2.0f, + "Pivot1": 0.0f, + "Rotation2": 31.0f + }, + { + "Pivot2": -3.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 6.0f, + "Pivot0": 1.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 1.0f, + "Part": "RIGHT_ARM", + "Rotation1": 110.0f, + "Rotation0": -90.0f, + "Pivot0": -1.0f, + "Pivot1": -2.0f, + "Rotation2": 90.0f + }, + { + "Pivot2": 4.0f, + "Part": "LEFT_ARM", + "Rotation1": -12.0f, + "Rotation0": -94.0f, + "Pivot0": -1.0f, + "Pivot1": 7.0f, + "Rotation2": 37.0f + }, + { + "Pivot2": 2.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": -12.0f, + "Pivot0": 0.0f, + "Pivot1": 2.0f, + "Rotation2": 6.0f + } + ], + "Smooth": 0b, + "Duration": 6 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.4f, + "FrameParts": [ + { + "Pivot2": 2.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": -12.0f, + "Pivot0": 2.0f, + "Pivot1": 2.0f, + "Rotation2": 18.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": 0.0f, + "Rotation0": 6.0f, + "Pivot0": -2.0f, + "Pivot1": 0.0f, + "Rotation2": 31.0f + }, + { + "Pivot2": -3.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 6.0f, + "Pivot0": 1.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 2.0f, + "Part": "RIGHT_ARM", + "Rotation1": 120.0f, + "Rotation0": -90.0f, + "Pivot0": 1.0f, + "Pivot1": -2.0f, + "Rotation2": 90.0f + }, + { + "Pivot2": 4.0f, + "Part": "LEFT_ARM", + "Rotation1": -18.0f, + "Rotation0": -94.0f, + "Pivot0": -1.0f, + "Pivot1": 7.0f, + "Rotation2": 37.0f + }, + { + "Pivot2": 2.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": -12.0f, + "Pivot0": 2.0f, + "Pivot1": 2.0f, + "Rotation2": 18.0f + } + ], + "Smooth": 0b, + "Duration": 6 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.5f, + "FrameParts": [ + { + "Pivot2": 0.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 4.0f, + "Pivot1": 3.0f, + "Rotation2": 31.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": -2.0f, + "Pivot1": 0.0f, + "Rotation2": 31.0f + }, + { + "Pivot2": -3.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 18.0f, + "Pivot0": 1.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "RIGHT_ARM", + "Rotation1": 90.0f, + "Rotation0": -18.0f, + "Pivot0": 3.0f, + "Pivot1": -1.0f, + "Rotation2": 180.0f + }, + { + "Pivot2": 3.0f, + "Part": "LEFT_ARM", + "Rotation1": -18.0f, + "Rotation0": -94.0f, + "Pivot0": 0.0f, + "Pivot1": 8.0f, + "Rotation2": 37.0f + }, + { + "Pivot2": 0.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 4.0f, + "Pivot1": 3.0f, + "Rotation2": 31.0f + } + ], + "Smooth": 0b, + "Duration": 4 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.5f, + "FrameParts": [ + { + "Pivot2": -2.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 12.0f, + "Pivot0": 4.0f, + "Pivot1": 3.0f, + "Rotation2": 31.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": 0.0f, + "Rotation0": 12.0f, + "Pivot0": -2.0f, + "Pivot1": 0.0f, + "Rotation2": 31.0f + }, + { + "Pivot2": -3.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 18.0f, + "Pivot0": 1.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -3.0f, + "Part": "RIGHT_ARM", + "Rotation1": 90.0f, + "Rotation0": -18.0f, + "Pivot0": 3.0f, + "Pivot1": -1.0f, + "Rotation2": 180.0f + }, + { + "Pivot2": 1.0f, + "Part": "LEFT_ARM", + "Rotation1": -25.0f, + "Rotation0": -94.0f, + "Pivot0": 0.0f, + "Pivot1": 8.0f, + "Rotation2": 37.0f + }, + { + "Pivot2": -2.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": 12.0f, + "Pivot0": 4.0f, + "Pivot1": 3.0f, + "Rotation2": 31.0f + } + ], + "Smooth": 0b, + "Duration": 4 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.6f, + "FrameParts": [ + { + "Pivot2": -2.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 12.0f, + "Pivot0": 2.0f, + "Pivot1": 2.0f, + "Rotation2": 18.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": 0.0f, + "Rotation0": 18.0f, + "Pivot0": -2.0f, + "Pivot1": 0.0f, + "Rotation2": 31.0f + }, + { + "Pivot2": -3.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 12.0f, + "Pivot0": 1.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -2.0f, + "Part": "RIGHT_ARM", + "Rotation1": 90.0f, + "Rotation0": 0.0f, + "Pivot0": 1.0f, + "Pivot1": -2.0f, + "Rotation2": 180.0f + }, + { + "Pivot2": 2.0f, + "Part": "LEFT_ARM", + "Rotation1": -18.0f, + "Rotation0": -94.0f, + "Pivot0": -1.0f, + "Pivot1": 7.0f, + "Rotation2": 37.0f + }, + { + "Pivot2": -2.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": 12.0f, + "Pivot0": 2.0f, + "Pivot1": 2.0f, + "Rotation2": 18.0f + } + ], + "Smooth": 0b, + "Duration": 3 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 1.0f, + "FrameParts": [ + { + "Pivot2": 0.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 2.0f, + "Pivot1": 2.0f, + "Rotation2": 18.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": -2.0f, + "Pivot1": 0.0f, + "Rotation2": 31.0f + }, + { + "Pivot2": -3.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 12.0f, + "Pivot0": 1.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "RIGHT_ARM", + "Rotation1": 90.0f, + "Rotation0": 0.0f, + "Pivot0": 1.0f, + "Pivot1": -2.0f, + "Rotation2": 180.0f + }, + { + "Pivot2": 3.0f, + "Part": "LEFT_ARM", + "Rotation1": -18.0f, + "Rotation0": -94.0f, + "Pivot0": -1.0f, + "Pivot1": 7.0f, + "Rotation2": 37.0f + }, + { + "Pivot2": 0.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 2.0f, + "Pivot1": 2.0f, + "Rotation2": 18.0f + } + ], + "Smooth": 0b, + "Duration": 3 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 1.0f, + "FrameParts": [ + { + "Pivot2": 0.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 6.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": -2.0f, + "Pivot1": 0.0f, + "Rotation2": 31.0f + }, + { + "Pivot2": -3.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 12.0f, + "Pivot0": 1.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "RIGHT_ARM", + "Rotation1": 90.0f, + "Rotation0": 25.0f, + "Pivot0": -1.0f, + "Pivot1": -2.0f, + "Rotation2": 180.0f + }, + { + "Pivot2": 3.0f, + "Part": "LEFT_ARM", + "Rotation1": -18.0f, + "Rotation0": -94.0f, + "Pivot0": -2.0f, + "Pivot1": 7.0f, + "Rotation2": 37.0f + }, + { + "Pivot2": 0.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 6.0f + } + ], + "Smooth": 0b, + "Duration": 3 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 1.0f, + "FrameParts": [ + { + "Pivot2": 2.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": -12.0f, + "Pivot0": 0.0f, + "Pivot1": 2.0f, + "Rotation2": 6.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": 0.0f, + "Rotation0": 6.0f, + "Pivot0": -2.0f, + "Pivot1": 0.0f, + "Rotation2": 31.0f + }, + { + "Pivot2": -3.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 6.0f, + "Pivot0": 1.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 1.0f, + "Part": "RIGHT_ARM", + "Rotation1": 110.0f, + "Rotation0": -90.0f, + "Pivot0": -1.0f, + "Pivot1": -2.0f, + "Rotation2": 90.0f + }, + { + "Pivot2": 4.0f, + "Part": "LEFT_ARM", + "Rotation1": -12.0f, + "Rotation0": -94.0f, + "Pivot0": -1.0f, + "Pivot1": 7.0f, + "Rotation2": 37.0f + }, + { + "Pivot2": 2.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": -12.0f, + "Pivot0": 0.0f, + "Pivot1": 2.0f, + "Rotation2": 6.0f + } + ], + "Smooth": 0b, + "Duration": 3 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 1.2f, + "FrameParts": [ + { + "Pivot2": 2.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": -12.0f, + "Pivot0": 2.0f, + "Pivot1": 2.0f, + "Rotation2": 18.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": 0.0f, + "Rotation0": 6.0f, + "Pivot0": -2.0f, + "Pivot1": 0.0f, + "Rotation2": 31.0f + }, + { + "Pivot2": -3.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 6.0f, + "Pivot0": 1.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 2.0f, + "Part": "RIGHT_ARM", + "Rotation1": 120.0f, + "Rotation0": -90.0f, + "Pivot0": 1.0f, + "Pivot1": -2.0f, + "Rotation2": 90.0f + }, + { + "Pivot2": 4.0f, + "Part": "LEFT_ARM", + "Rotation1": -18.0f, + "Rotation0": -94.0f, + "Pivot0": -1.0f, + "Pivot1": 7.0f, + "Rotation2": 37.0f + }, + { + "Pivot2": 2.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": -12.0f, + "Pivot0": 2.0f, + "Pivot1": 2.0f, + "Rotation2": 18.0f + } + ], + "Smooth": 0b, + "Duration": 2 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 1.25f, + "FrameParts": [ + { + "Pivot2": 0.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 4.0f, + "Pivot1": 3.0f, + "Rotation2": 31.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": -2.0f, + "Pivot1": 0.0f, + "Rotation2": 31.0f + }, + { + "Pivot2": -3.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 18.0f, + "Pivot0": 1.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "RIGHT_ARM", + "Rotation1": 90.0f, + "Rotation0": -18.0f, + "Pivot0": 3.0f, + "Pivot1": -1.0f, + "Rotation2": 180.0f + }, + { + "Pivot2": 3.0f, + "Part": "LEFT_ARM", + "Rotation1": -18.0f, + "Rotation0": -94.0f, + "Pivot0": 0.0f, + "Pivot1": 8.0f, + "Rotation2": 37.0f + }, + { + "Pivot2": 0.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 4.0f, + "Pivot1": 3.0f, + "Rotation2": 31.0f + } + ], + "Smooth": 0b, + "Duration": 2 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 1.5f, + "FrameParts": [ + { + "Pivot2": -2.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 12.0f, + "Pivot0": 4.0f, + "Pivot1": 3.0f, + "Rotation2": 31.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": 0.0f, + "Rotation0": 12.0f, + "Pivot0": -2.0f, + "Pivot1": 0.0f, + "Rotation2": 31.0f + }, + { + "Pivot2": -3.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 18.0f, + "Pivot0": 1.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -3.0f, + "Part": "RIGHT_ARM", + "Rotation1": 90.0f, + "Rotation0": -18.0f, + "Pivot0": 3.0f, + "Pivot1": -1.0f, + "Rotation2": 180.0f + }, + { + "Pivot2": 1.0f, + "Part": "LEFT_ARM", + "Rotation1": -25.0f, + "Rotation0": -94.0f, + "Pivot0": 0.0f, + "Pivot1": 8.0f, + "Rotation2": 37.0f + }, + { + "Pivot2": -2.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": 12.0f, + "Pivot0": 4.0f, + "Pivot1": 3.0f, + "Rotation2": 31.0f + } + ], + "Smooth": 0b, + "Duration": 2 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 1.5f, + "FrameParts": [ + { + "Pivot2": -5.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 25.0f, + "Pivot0": 2.0f, + "Pivot1": 3.0f, + "Rotation2": 6.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": 0.0f, + "Rotation0": 31.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 31.0f + }, + { + "Pivot2": -4.0f, + "Part": "LEFT_LEG", + "Rotation1": 0.0f, + "Rotation0": 31.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -7.0f, + "Part": "RIGHT_ARM", + "Rotation1": -44.0f, + "Rotation0": -90.0f, + "Pivot0": 1.0f, + "Pivot1": 4.0f, + "Rotation2": 90.0f + }, + { + "Pivot2": -2.0f, + "Part": "LEFT_ARM", + "Rotation1": 31.0f, + "Rotation0": 44.0f, + "Pivot0": 2.0f, + "Pivot1": 3.0f, + "Rotation2": -82.0f + }, + { + "Pivot2": -5.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": 25.0f, + "Pivot0": 2.0f, + "Pivot1": 3.0f, + "Rotation2": 12.0f + } + ], + "Smooth": 0b, + "Duration": 4 + } + ], + "CurrentFrameTime": 0, + "CurrentFrame": 0, + "Smooth": 0b, + "ID": 11, + "WhileAttacking": 1b, + "WhileWalking": 1b, + "Name": "Ability_Disc_Windup", + "WhileStanding": 1b +} diff --git a/src/main/resources/assets/customnpcs/animations/Ability_HeavyHit_Active.json b/src/main/resources/assets/customnpcs/animations/Ability_HeavyHit_Active.json new file mode 100644 index 000000000..fa6b44543 --- /dev/null +++ b/src/main/resources/assets/customnpcs/animations/Ability_HeavyHit_Active.json @@ -0,0 +1,77 @@ +{ + "Loop": 0, + "Speed": 1.0f, + "Frames": [ + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 2.0f, + "FrameParts": [ + { + "Pivot2": -5.0f, + "Part": "RIGHT_ARM", + "Rotation1": 0.0f, + "Rotation0": -90.0f, + "Pivot0": 0.0f, + "Pivot1": 2.0f, + "Rotation2": 90.0f + }, + { + "Pivot2": 2.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 31.0f, + "Pivot0": 1.0f, + "Pivot1": 0.0f, + "Rotation2": -18.0f + }, + { + "Pivot2": 0.0f, + "Part": "HEAD", + "Rotation1": -25.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -2.0f, + "Part": "RIGHT_LEG", + "Rotation1": 6.0f, + "Rotation0": 12.0f, + "Pivot0": -1.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "BODY", + "Rotation1": -25.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 4.0f, + "Part": "LEFT_ARM", + "Rotation1": -63.0f, + "Rotation0": -31.0f, + "Pivot0": -2.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + } + ], + "Smooth": 0b, + "Duration": 20 + } + ], + "CurrentFrameTime": 0, + "CurrentFrame": 19, + "Smooth": 0b, + "ID": 16, + "WhileAttacking": 1b, + "WhileWalking": 1b, + "Name": "Ability_HeavyHit_Active", + "WhileStanding": 1b +} diff --git a/src/main/resources/assets/customnpcs/animations/Ability_HeavyHit_Windup.json b/src/main/resources/assets/customnpcs/animations/Ability_HeavyHit_Windup.json new file mode 100644 index 000000000..15d49bce6 --- /dev/null +++ b/src/main/resources/assets/customnpcs/animations/Ability_HeavyHit_Windup.json @@ -0,0 +1,1274 @@ +{ + "Loop": -1, + "Speed": 1.0f, + "Frames": [ + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.8f, + "FrameParts": [ + { + "Pivot2": 3.0f, + "Part": "RIGHT_ARM", + "Rotation1": 31.0f, + "Rotation0": -107.0f, + "Pivot0": 2.0f, + "Pivot1": 1.0f, + "Rotation2": -37.0f + }, + { + "Pivot2": -4.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 31.0f, + "Pivot0": 2.0f, + "Pivot1": 0.0f, + "Rotation2": -12.0f + }, + { + "Pivot2": -1.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -2.0f, + "Part": "RIGHT_LEG", + "Rotation1": 37.0f, + "Rotation0": 25.0f, + "Pivot0": -2.0f, + "Pivot1": -1.0f, + "Rotation2": 6.0f + }, + { + "Pivot2": -1.0f, + "Part": "BODY", + "Rotation1": 18.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_ARM", + "Rotation1": -37.0f, + "Rotation0": -37.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 6.0f + } + ], + "Smooth": 0b, + "Duration": 10 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.1f, + "FrameParts": [ + { + "Pivot2": 3.0f, + "Part": "RIGHT_ARM", + "Rotation1": 40.0f, + "Rotation0": -107.0f, + "Pivot0": 2.0f, + "Pivot1": 1.0f, + "Rotation2": -37.0f + }, + { + "Pivot2": -4.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 31.0f, + "Pivot0": 2.0f, + "Pivot1": 0.0f, + "Rotation2": -18.0f + }, + { + "Pivot2": -1.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": -2.0f, + "Part": "RIGHT_LEG", + "Rotation1": 37.0f, + "Rotation0": 25.0f, + "Pivot0": -2.0f, + "Pivot1": -1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "BODY", + "Rotation1": 18.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_ARM", + "Rotation1": -40.0f, + "Rotation0": -40.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + } + ], + "Smooth": 0b, + "Duration": 1 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.1f, + "FrameParts": [ + { + "Pivot2": 3.0f, + "Part": "RIGHT_ARM", + "Rotation1": 40.0f, + "Rotation0": -107.0f, + "Pivot0": 2.0f, + "Pivot1": 1.0f, + "Rotation2": -37.0f + }, + { + "Pivot2": -4.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 31.0f, + "Pivot0": 2.0f, + "Pivot1": 0.0f, + "Rotation2": -18.0f + }, + { + "Pivot2": -1.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": -2.0f, + "Part": "RIGHT_LEG", + "Rotation1": 37.0f, + "Rotation0": 25.0f, + "Pivot0": -2.0f, + "Pivot1": -1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "BODY", + "Rotation1": 18.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_ARM", + "Rotation1": -40.0f, + "Rotation0": -40.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + } + ], + "Smooth": 0b, + "Duration": 1 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.1f, + "FrameParts": [ + { + "Pivot2": 3.0f, + "Part": "RIGHT_ARM", + "Rotation1": 40.0f, + "Rotation0": -107.0f, + "Pivot0": 2.0f, + "Pivot1": 1.0f, + "Rotation2": -37.0f + }, + { + "Pivot2": -4.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 31.0f, + "Pivot0": 2.0f, + "Pivot1": 0.0f, + "Rotation2": -18.0f + }, + { + "Pivot2": -1.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": -2.0f, + "Part": "RIGHT_LEG", + "Rotation1": 37.0f, + "Rotation0": 25.0f, + "Pivot0": -2.0f, + "Pivot1": -1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "BODY", + "Rotation1": 18.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_ARM", + "Rotation1": -40.0f, + "Rotation0": -40.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + } + ], + "Smooth": 0b, + "Duration": 1 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.1f, + "FrameParts": [ + { + "Pivot2": 3.0f, + "Part": "RIGHT_ARM", + "Rotation1": 40.0f, + "Rotation0": -107.0f, + "Pivot0": 2.0f, + "Pivot1": 1.0f, + "Rotation2": -37.0f + }, + { + "Pivot2": -4.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 31.0f, + "Pivot0": 2.0f, + "Pivot1": 0.0f, + "Rotation2": -18.0f + }, + { + "Pivot2": -1.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": -2.0f, + "Part": "RIGHT_LEG", + "Rotation1": 37.0f, + "Rotation0": 25.0f, + "Pivot0": -2.0f, + "Pivot1": -1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "BODY", + "Rotation1": 18.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_ARM", + "Rotation1": -40.0f, + "Rotation0": -40.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + } + ], + "Smooth": 0b, + "Duration": 1 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.1f, + "FrameParts": [ + { + "Pivot2": 3.0f, + "Part": "RIGHT_ARM", + "Rotation1": 40.0f, + "Rotation0": -107.0f, + "Pivot0": 2.0f, + "Pivot1": 1.0f, + "Rotation2": -37.0f + }, + { + "Pivot2": -4.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 31.0f, + "Pivot0": 2.0f, + "Pivot1": 0.0f, + "Rotation2": -18.0f + }, + { + "Pivot2": -1.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": -2.0f, + "Part": "RIGHT_LEG", + "Rotation1": 37.0f, + "Rotation0": 25.0f, + "Pivot0": -2.0f, + "Pivot1": -1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "BODY", + "Rotation1": 18.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_ARM", + "Rotation1": -40.0f, + "Rotation0": -40.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + } + ], + "Smooth": 0b, + "Duration": 1 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.1f, + "FrameParts": [ + { + "Pivot2": 3.0f, + "Part": "RIGHT_ARM", + "Rotation1": 40.0f, + "Rotation0": -107.0f, + "Pivot0": 2.0f, + "Pivot1": 1.0f, + "Rotation2": -37.0f + }, + { + "Pivot2": -4.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 31.0f, + "Pivot0": 2.0f, + "Pivot1": 0.0f, + "Rotation2": -18.0f + }, + { + "Pivot2": -1.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": -2.0f, + "Part": "RIGHT_LEG", + "Rotation1": 37.0f, + "Rotation0": 25.0f, + "Pivot0": -2.0f, + "Pivot1": -1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "BODY", + "Rotation1": 18.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_ARM", + "Rotation1": -40.0f, + "Rotation0": -40.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + } + ], + "Smooth": 0b, + "Duration": 1 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.1f, + "FrameParts": [ + { + "Pivot2": 3.0f, + "Part": "RIGHT_ARM", + "Rotation1": 40.0f, + "Rotation0": -107.0f, + "Pivot0": 2.0f, + "Pivot1": 1.0f, + "Rotation2": -37.0f + }, + { + "Pivot2": -4.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 31.0f, + "Pivot0": 2.0f, + "Pivot1": 0.0f, + "Rotation2": -18.0f + }, + { + "Pivot2": -1.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": -2.0f, + "Part": "RIGHT_LEG", + "Rotation1": 37.0f, + "Rotation0": 25.0f, + "Pivot0": -2.0f, + "Pivot1": -1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "BODY", + "Rotation1": 18.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_ARM", + "Rotation1": -40.0f, + "Rotation0": -40.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + } + ], + "Smooth": 0b, + "Duration": 1 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.1f, + "FrameParts": [ + { + "Pivot2": 3.0f, + "Part": "RIGHT_ARM", + "Rotation1": 40.0f, + "Rotation0": -107.0f, + "Pivot0": 2.0f, + "Pivot1": 1.0f, + "Rotation2": -37.0f + }, + { + "Pivot2": -4.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 31.0f, + "Pivot0": 2.0f, + "Pivot1": 0.0f, + "Rotation2": -18.0f + }, + { + "Pivot2": -1.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": -2.0f, + "Part": "RIGHT_LEG", + "Rotation1": 37.0f, + "Rotation0": 25.0f, + "Pivot0": -2.0f, + "Pivot1": -1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "BODY", + "Rotation1": 18.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_ARM", + "Rotation1": -40.0f, + "Rotation0": -40.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + } + ], + "Smooth": 0b, + "Duration": 1 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.1f, + "FrameParts": [ + { + "Pivot2": 3.0f, + "Part": "RIGHT_ARM", + "Rotation1": 40.0f, + "Rotation0": -107.0f, + "Pivot0": 2.0f, + "Pivot1": 1.0f, + "Rotation2": -37.0f + }, + { + "Pivot2": -4.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 31.0f, + "Pivot0": 2.0f, + "Pivot1": 0.0f, + "Rotation2": -18.0f + }, + { + "Pivot2": -1.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": -2.0f, + "Part": "RIGHT_LEG", + "Rotation1": 37.0f, + "Rotation0": 25.0f, + "Pivot0": -2.0f, + "Pivot1": -1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "BODY", + "Rotation1": 18.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_ARM", + "Rotation1": -40.0f, + "Rotation0": -40.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + } + ], + "Smooth": 0b, + "Duration": 1 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.1f, + "FrameParts": [ + { + "Pivot2": 3.0f, + "Part": "RIGHT_ARM", + "Rotation1": 40.0f, + "Rotation0": -107.0f, + "Pivot0": 2.0f, + "Pivot1": 1.0f, + "Rotation2": -37.0f + }, + { + "Pivot2": -4.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 31.0f, + "Pivot0": 2.0f, + "Pivot1": 0.0f, + "Rotation2": -18.0f + }, + { + "Pivot2": -1.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": -2.0f, + "Part": "RIGHT_LEG", + "Rotation1": 37.0f, + "Rotation0": 25.0f, + "Pivot0": -2.0f, + "Pivot1": -1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "BODY", + "Rotation1": 18.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_ARM", + "Rotation1": -40.0f, + "Rotation0": -40.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + } + ], + "Smooth": 0b, + "Duration": 1 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.1f, + "FrameParts": [ + { + "Pivot2": 3.0f, + "Part": "RIGHT_ARM", + "Rotation1": 40.0f, + "Rotation0": -107.0f, + "Pivot0": 2.0f, + "Pivot1": 1.0f, + "Rotation2": -37.0f + }, + { + "Pivot2": -4.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 31.0f, + "Pivot0": 2.0f, + "Pivot1": 0.0f, + "Rotation2": -18.0f + }, + { + "Pivot2": -1.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": -2.0f, + "Part": "RIGHT_LEG", + "Rotation1": 37.0f, + "Rotation0": 25.0f, + "Pivot0": -2.0f, + "Pivot1": -1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "BODY", + "Rotation1": 18.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_ARM", + "Rotation1": -40.0f, + "Rotation0": -40.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + } + ], + "Smooth": 0b, + "Duration": 1 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.1f, + "FrameParts": [ + { + "Pivot2": 3.0f, + "Part": "RIGHT_ARM", + "Rotation1": 40.0f, + "Rotation0": -107.0f, + "Pivot0": 2.0f, + "Pivot1": 1.0f, + "Rotation2": -37.0f + }, + { + "Pivot2": -4.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 31.0f, + "Pivot0": 2.0f, + "Pivot1": 0.0f, + "Rotation2": -18.0f + }, + { + "Pivot2": -1.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": -2.0f, + "Part": "RIGHT_LEG", + "Rotation1": 37.0f, + "Rotation0": 25.0f, + "Pivot0": -2.0f, + "Pivot1": -1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "BODY", + "Rotation1": 18.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_ARM", + "Rotation1": -40.0f, + "Rotation0": -40.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + } + ], + "Smooth": 0b, + "Duration": 1 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.1f, + "FrameParts": [ + { + "Pivot2": 3.0f, + "Part": "RIGHT_ARM", + "Rotation1": 40.0f, + "Rotation0": -107.0f, + "Pivot0": 2.0f, + "Pivot1": 1.0f, + "Rotation2": -37.0f + }, + { + "Pivot2": -4.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 31.0f, + "Pivot0": 2.0f, + "Pivot1": 0.0f, + "Rotation2": -18.0f + }, + { + "Pivot2": -1.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": -2.0f, + "Part": "RIGHT_LEG", + "Rotation1": 37.0f, + "Rotation0": 25.0f, + "Pivot0": -2.0f, + "Pivot1": -1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "BODY", + "Rotation1": 18.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_ARM", + "Rotation1": -40.0f, + "Rotation0": -40.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + } + ], + "Smooth": 0b, + "Duration": 1 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.1f, + "FrameParts": [ + { + "Pivot2": 3.0f, + "Part": "RIGHT_ARM", + "Rotation1": 40.0f, + "Rotation0": -107.0f, + "Pivot0": 2.0f, + "Pivot1": 1.0f, + "Rotation2": -37.0f + }, + { + "Pivot2": -4.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 31.0f, + "Pivot0": 2.0f, + "Pivot1": 0.0f, + "Rotation2": -18.0f + }, + { + "Pivot2": -1.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": -2.0f, + "Part": "RIGHT_LEG", + "Rotation1": 37.0f, + "Rotation0": 25.0f, + "Pivot0": -2.0f, + "Pivot1": -1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "BODY", + "Rotation1": 18.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_ARM", + "Rotation1": -40.0f, + "Rotation0": -40.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + } + ], + "Smooth": 0b, + "Duration": 1 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.1f, + "FrameParts": [ + { + "Pivot2": 3.0f, + "Part": "RIGHT_ARM", + "Rotation1": 40.0f, + "Rotation0": -107.0f, + "Pivot0": 2.0f, + "Pivot1": 1.0f, + "Rotation2": -37.0f + }, + { + "Pivot2": -4.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 31.0f, + "Pivot0": 2.0f, + "Pivot1": 0.0f, + "Rotation2": -18.0f + }, + { + "Pivot2": -1.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": -2.0f, + "Part": "RIGHT_LEG", + "Rotation1": 37.0f, + "Rotation0": 25.0f, + "Pivot0": -2.0f, + "Pivot1": -1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "BODY", + "Rotation1": 18.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_ARM", + "Rotation1": -40.0f, + "Rotation0": -40.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + } + ], + "Smooth": 0b, + "Duration": 1 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.1f, + "FrameParts": [ + { + "Pivot2": 3.0f, + "Part": "RIGHT_ARM", + "Rotation1": 40.0f, + "Rotation0": -107.0f, + "Pivot0": 2.0f, + "Pivot1": 1.0f, + "Rotation2": -37.0f + }, + { + "Pivot2": -4.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 31.0f, + "Pivot0": 2.0f, + "Pivot1": 0.0f, + "Rotation2": -18.0f + }, + { + "Pivot2": -1.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": -2.0f, + "Part": "RIGHT_LEG", + "Rotation1": 37.0f, + "Rotation0": 25.0f, + "Pivot0": -2.0f, + "Pivot1": -1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "BODY", + "Rotation1": 18.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_ARM", + "Rotation1": -40.0f, + "Rotation0": -40.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + } + ], + "Smooth": 0b, + "Duration": 1 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.1f, + "FrameParts": [ + { + "Pivot2": 3.0f, + "Part": "RIGHT_ARM", + "Rotation1": 40.0f, + "Rotation0": -107.0f, + "Pivot0": 2.0f, + "Pivot1": 1.0f, + "Rotation2": -37.0f + }, + { + "Pivot2": -4.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 31.0f, + "Pivot0": 2.0f, + "Pivot1": 0.0f, + "Rotation2": -18.0f + }, + { + "Pivot2": -1.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": -2.0f, + "Part": "RIGHT_LEG", + "Rotation1": 37.0f, + "Rotation0": 25.0f, + "Pivot0": -2.0f, + "Pivot1": -1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "BODY", + "Rotation1": 18.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_ARM", + "Rotation1": -40.0f, + "Rotation0": -40.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + } + ], + "Smooth": 0b, + "Duration": 1 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.1f, + "FrameParts": [ + { + "Pivot2": 3.0f, + "Part": "RIGHT_ARM", + "Rotation1": 40.0f, + "Rotation0": -107.0f, + "Pivot0": 2.0f, + "Pivot1": 1.0f, + "Rotation2": -37.0f + }, + { + "Pivot2": -4.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 31.0f, + "Pivot0": 2.0f, + "Pivot1": 0.0f, + "Rotation2": -18.0f + }, + { + "Pivot2": -1.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": -2.0f, + "Part": "RIGHT_LEG", + "Rotation1": 37.0f, + "Rotation0": 25.0f, + "Pivot0": -2.0f, + "Pivot1": -1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "BODY", + "Rotation1": 18.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_ARM", + "Rotation1": -40.0f, + "Rotation0": -40.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + } + ], + "Smooth": 0b, + "Duration": 1 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 2.0f, + "FrameParts": [ + { + "Pivot2": -5.0f, + "Part": "RIGHT_ARM", + "Rotation1": 0.0f, + "Rotation0": -90.0f, + "Pivot0": 0.0f, + "Pivot1": 2.0f, + "Rotation2": 90.0f + }, + { + "Pivot2": 2.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 31.0f, + "Pivot0": 1.0f, + "Pivot1": 0.0f, + "Rotation2": -18.0f + }, + { + "Pivot2": 0.0f, + "Part": "HEAD", + "Rotation1": -25.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -2.0f, + "Part": "RIGHT_LEG", + "Rotation1": 6.0f, + "Rotation0": 12.0f, + "Pivot0": -1.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "BODY", + "Rotation1": -25.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 4.0f, + "Part": "LEFT_ARM", + "Rotation1": -63.0f, + "Rotation0": -31.0f, + "Pivot0": -2.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + } + ], + "Smooth": 0b, + "Duration": 2 + } + ], + "CurrentFrameTime": 0, + "CurrentFrame": 19, + "Smooth": 0b, + "ID": 15, + "WhileAttacking": 1b, + "WhileWalking": 1b, + "Name": "Ability_HeavyHit_Windup", + "WhileStanding": 1b +} diff --git a/src/main/resources/assets/customnpcs/animations/Ability_Laser_Active.json b/src/main/resources/assets/customnpcs/animations/Ability_Laser_Active.json new file mode 100644 index 000000000..5cb4fc3be --- /dev/null +++ b/src/main/resources/assets/customnpcs/animations/Ability_Laser_Active.json @@ -0,0 +1,77 @@ +{ + "Loop": 0, + "Speed": 1.0f, + "Frames": [ + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 3.0f, + "FrameParts": [ + { + "Pivot2": 0.0f, + "Part": "LEFT_LEG", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": -18.0f + }, + { + "Pivot2": 5.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": -25.0f, + "Pivot0": 0.0f, + "Pivot1": 2.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 18.0f + }, + { + "Pivot2": 2.0f, + "Part": "LEFT_ARM", + "Rotation1": 0.0f, + "Rotation0": -195.0f, + "Pivot0": -1.0f, + "Pivot1": -1.0f, + "Rotation2": -56.0f + }, + { + "Pivot2": 4.0f, + "Part": "RIGHT_ARM", + "Rotation1": 90.0f, + "Rotation0": -90.0f, + "Pivot0": 0.0f, + "Pivot1": -1.0f, + "Rotation2": 90.0f + }, + { + "Pivot2": 5.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": -37.0f, + "Pivot0": 0.0f, + "Pivot1": 2.0f, + "Rotation2": 0.0f + } + ], + "Smooth": 0b, + "Duration": 3 + } + ], + "CurrentFrameTime": 0, + "CurrentFrame": 3, + "Smooth": 0b, + "ID": 10, + "WhileAttacking": 1b, + "WhileWalking": 1b, + "Name": "Ability_Laser_Active", + "WhileStanding": 1b +} diff --git a/src/main/resources/assets/customnpcs/animations/Ability_Laser_Windup.json b/src/main/resources/assets/customnpcs/animations/Ability_Laser_Windup.json new file mode 100644 index 000000000..c445faca9 --- /dev/null +++ b/src/main/resources/assets/customnpcs/animations/Ability_Laser_Windup.json @@ -0,0 +1,329 @@ +{ + "Loop": -1, + "Speed": 1.0f, + "Frames": [ + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 3.0f, + "FrameParts": [ + { + "Pivot2": 3.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_LEG", + "Rotation1": 0.0f, + "Rotation0": 12.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": -18.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": 0.0f, + "Rotation0": 12.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 18.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_ARM", + "Rotation1": 50.0f, + "Rotation0": -82.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_ARM", + "Rotation1": 0.0f, + "Rotation0": -90.0f, + "Pivot0": 1.0f, + "Pivot1": 2.0f, + "Rotation2": 90.0f + }, + { + "Pivot2": 3.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": -12.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + } + ], + "Smooth": 0b, + "Duration": 3 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 3.0f, + "FrameParts": [ + { + "Pivot2": 3.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_LEG", + "Rotation1": 0.0f, + "Rotation0": 12.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": -18.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": 0.0f, + "Rotation0": 12.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 18.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_ARM", + "Rotation1": 50.0f, + "Rotation0": -82.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_ARM", + "Rotation1": 2.0f, + "Rotation0": -88.0f, + "Pivot0": 1.0f, + "Pivot1": 2.0f, + "Rotation2": 86.0f + }, + { + "Pivot2": 3.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": -12.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + } + ], + "Smooth": 0b, + "Duration": 3 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 3.0f, + "FrameParts": [ + { + "Pivot2": 3.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_LEG", + "Rotation1": 0.0f, + "Rotation0": 12.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": -18.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": 0.0f, + "Rotation0": 12.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 18.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_ARM", + "Rotation1": 50.0f, + "Rotation0": -82.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_ARM", + "Rotation1": 0.0f, + "Rotation0": -92.0f, + "Pivot0": 1.0f, + "Pivot1": 2.0f, + "Rotation2": 94.0f + }, + { + "Pivot2": 3.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": -12.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + } + ], + "Smooth": 0b, + "Duration": 3 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 3.0f, + "FrameParts": [ + { + "Pivot2": 3.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_LEG", + "Rotation1": 0.0f, + "Rotation0": 12.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": -18.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": 0.0f, + "Rotation0": 12.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 18.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_ARM", + "Rotation1": 50.0f, + "Rotation0": -82.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_ARM", + "Rotation1": -2.0f, + "Rotation0": -90.0f, + "Pivot0": 1.0f, + "Pivot1": 2.0f, + "Rotation2": 80.0f + }, + { + "Pivot2": 3.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": -12.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + } + ], + "Smooth": 0b, + "Duration": 3 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 3.0f, + "FrameParts": [ + { + "Pivot2": 5.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": -37.0f, + "Pivot0": 0.0f, + "Pivot1": 2.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_LEG", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": -18.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 18.0f + }, + { + "Pivot2": 2.0f, + "Part": "LEFT_ARM", + "Rotation1": 0.0f, + "Rotation0": -195.0f, + "Pivot0": -1.0f, + "Pivot1": -1.0f, + "Rotation2": -56.0f + }, + { + "Pivot2": 4.0f, + "Part": "RIGHT_ARM", + "Rotation1": 90.0f, + "Rotation0": -90.0f, + "Pivot0": 0.0f, + "Pivot1": -1.0f, + "Rotation2": 90.0f + }, + { + "Pivot2": 5.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": -25.0f, + "Pivot0": 0.0f, + "Pivot1": 2.0f, + "Rotation2": 0.0f + } + ], + "Smooth": 0b, + "Duration": 3 + } + ], + "CurrentFrameTime": 0, + "CurrentFrame": 3, + "Smooth": 0b, + "ID": 12, + "WhileAttacking": 1b, + "WhileWalking": 1b, + "Name": "Ability_Laser_Windup", + "WhileStanding": 1b +} diff --git a/src/main/resources/assets/customnpcs/animations/Ability_Orb_Active.json b/src/main/resources/assets/customnpcs/animations/Ability_Orb_Active.json new file mode 100644 index 000000000..994e2190c --- /dev/null +++ b/src/main/resources/assets/customnpcs/animations/Ability_Orb_Active.json @@ -0,0 +1,77 @@ +{ + "Loop": 0, + "Speed": 1.0f, + "Frames": [ + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.5f, + "FrameParts": [ + { + "Pivot2": 0.0f, + "Part": "HEAD", + "Rotation1": -37.0f, + "Rotation0": 25.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 4.0f, + "Part": "LEFT_LEG", + "Rotation1": -75.0f, + "Rotation0": 12.0f, + "Pivot0": -1.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -3.0f, + "Part": "RIGHT_LEG", + "Rotation1": -18.0f, + "Rotation0": 12.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 4.0f, + "Part": "LEFT_ARM", + "Rotation1": -50.0f, + "Rotation0": 50.0f, + "Pivot0": -3.0f, + "Pivot1": 0.0f, + "Rotation2": -56.0f + }, + { + "Pivot2": -6.0f, + "Part": "RIGHT_ARM", + "Rotation1": 0.0f, + "Rotation0": -90.0f, + "Pivot0": 2.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "BODY", + "Rotation1": -44.0f, + "Rotation0": 12.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + } + ], + "Smooth": 0b, + "Duration": 30 + } + ], + "CurrentFrameTime": 0, + "CurrentFrame": 0, + "Smooth": 0b, + "ID": 13, + "WhileAttacking": 1b, + "WhileWalking": 1b, + "Name": "Ability_Orb_Active", + "WhileStanding": 1b +} diff --git a/src/main/resources/assets/customnpcs/animations/Ability_Orb_Windup.json b/src/main/resources/assets/customnpcs/animations/Ability_Orb_Windup.json new file mode 100644 index 000000000..1be8ef71c --- /dev/null +++ b/src/main/resources/assets/customnpcs/animations/Ability_Orb_Windup.json @@ -0,0 +1,129 @@ +{ + "Loop": -1, + "Speed": 1.0f, + "Frames": [ + { + "ColorMarker": 16777215, + "Comment": "", + "FrameParts": [ + { + "Pivot2": -3.0f, + "Part": "LEFT_LEG", + "Rotation1": -18.0f, + "Rotation0": 12.0f, + "Pivot0": 1.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 3.0f, + "Part": "RIGHT_LEG", + "Rotation1": 69.0f, + "Rotation0": 12.0f, + "Pivot0": -2.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_ARM", + "Rotation1": 0.0f, + "Rotation0": -88.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": -90.0f + }, + { + "Pivot2": 4.0f, + "Part": "RIGHT_ARM", + "Rotation1": 70.0f, + "Rotation0": -125.0f, + "Pivot0": 2.0f, + "Pivot1": 1.0f, + "Rotation2": -50.0f + }, + { + "Pivot2": 0.0f, + "Part": "BODY", + "Rotation1": 31.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + } + ], + "Duration": 26 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 2.0f, + "FrameParts": [ + { + "Pivot2": 0.0f, + "Part": "HEAD", + "Rotation1": -37.0f, + "Rotation0": 25.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 4.0f, + "Part": "LEFT_LEG", + "Rotation1": -75.0f, + "Rotation0": 12.0f, + "Pivot0": -1.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -3.0f, + "Part": "RIGHT_LEG", + "Rotation1": -18.0f, + "Rotation0": 12.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 4.0f, + "Part": "LEFT_ARM", + "Rotation1": -50.0f, + "Rotation0": 50.0f, + "Pivot0": -3.0f, + "Pivot1": 0.0f, + "Rotation2": -56.0f + }, + { + "Pivot2": -6.0f, + "Part": "RIGHT_ARM", + "Rotation1": 0.0f, + "Rotation0": -90.0f, + "Pivot0": 2.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "BODY", + "Rotation1": -44.0f, + "Rotation0": 12.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + } + ], + "Smooth": 0b, + "Duration": 4 + } + ], + "CurrentFrameTime": 0, + "CurrentFrame": 0, + "Smooth": 0b, + "ID": 3, + "WhileAttacking": 1b, + "WhileWalking": 1b, + "Name": "Ability_Orb_Windup", + "WhileStanding": 1b +} diff --git a/src/main/resources/assets/customnpcs/animations/Ability_Slam_Active.json b/src/main/resources/assets/customnpcs/animations/Ability_Slam_Active.json new file mode 100644 index 000000000..da3e8d61f --- /dev/null +++ b/src/main/resources/assets/customnpcs/animations/Ability_Slam_Active.json @@ -0,0 +1,84 @@ +{ + "Loop": 0, + "Speed": 1.0f, + "Frames": [ + { + "ColorMarker": 16777215, + "Comment": "", + "FrameParts": [ + { + "Pivot2": 0.0f, + "Part": "FULL_MODEL", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 31.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -2.0f, + "Part": "RIGHT_LEG", + "Rotation1": 0.0f, + "Rotation0": -18.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -2.0f, + "Part": "LEFT_LEG", + "Rotation1": 0.0f, + "Rotation0": 107.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_ARM", + "Rotation1": 0.0f, + "Rotation0": -208.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 12.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_ARM", + "Rotation1": 0.0f, + "Rotation0": -208.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": -12.0f + }, + { + "Pivot2": 0.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": -6.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + } + ], + "Duration": 3 + } + ], + "CurrentFrameTime": 0, + "CurrentFrame": 0, + "Smooth": 0b, + "ID": 14, + "WhileAttacking": 1b, + "WhileWalking": 1b, + "Name": "Ability_Slam_Active", + "WhileStanding": 1b +} diff --git a/src/main/resources/assets/customnpcs/animations/Ability_Slam_Windup.json b/src/main/resources/assets/customnpcs/animations/Ability_Slam_Windup.json new file mode 100644 index 000000000..0c22ac017 --- /dev/null +++ b/src/main/resources/assets/customnpcs/animations/Ability_Slam_Windup.json @@ -0,0 +1,147 @@ +{ + "Loop": -1, + "Speed": 1.0f, + "Frames": [ + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.25f, + "FrameParts": [ + { + "Pivot2": 2.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 18.0f, + "Pivot0": 0.0f, + "Pivot1": 6.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 4.0f, + "Part": "RIGHT_LEG", + "Rotation1": 0.0f, + "Rotation0": 82.0f, + "Pivot0": 0.0f, + "Pivot1": 7.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_LEG", + "Rotation1": 0.0f, + "Rotation0": 18.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 2.0f, + "Part": "LEFT_ARM", + "Rotation1": 0.0f, + "Rotation0": 12.0f, + "Pivot0": 0.0f, + "Pivot1": 6.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": 2.0f, + "Part": "RIGHT_ARM", + "Rotation1": 0.0f, + "Rotation0": 12.0f, + "Pivot0": 0.0f, + "Pivot1": 6.0f, + "Rotation2": 6.0f + }, + { + "Pivot2": 2.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": 12.0f, + "Pivot0": 0.0f, + "Pivot1": 6.0f, + "Rotation2": 0.0f + } + ], + "Smooth": 0b, + "Duration": 27 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "FrameParts": [ + { + "Pivot2": 0.0f, + "Part": "FULL_MODEL", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": -31.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -2.0f, + "Part": "RIGHT_LEG", + "Rotation1": 0.0f, + "Rotation0": 25.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -2.0f, + "Part": "LEFT_LEG", + "Rotation1": 0.0f, + "Rotation0": 31.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_ARM", + "Rotation1": 0.0f, + "Rotation0": -82.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_ARM", + "Rotation1": 0.0f, + "Rotation0": -88.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": -6.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + } + ], + "Duration": 3 + } + ], + "CurrentFrameTime": 0, + "CurrentFrame": 0, + "Smooth": 0b, + "ID": 15, + "WhileAttacking": 1b, + "WhileWalking": 1b, + "Name": "Ability_Slam_Windup", + "WhileStanding": 1b +} diff --git a/src/main/resources/assets/customnpcs/lang/en_US.lang b/src/main/resources/assets/customnpcs/lang/en_US.lang index 7f0618786..8f5af1340 100644 --- a/src/main/resources/assets/customnpcs/lang/en_US.lang +++ b/src/main/resources/assets/customnpcs/lang/en_US.lang @@ -115,10 +115,15 @@ gui.play=Play gui.selectSound=Select Sound gui.selectnpc=Select NPC gui.select=Select +gui.sync=Sync gui.range=Range gui.partly=Partially gui.copy=Copy gui.clear=Clear +gui.builtin=Built-in +gui.custom=Custom +gui.builtinAnimations=Built-in Animations +gui.customAnimations=Custom Animations gui.done=Done gui.paste=Paste gui.test=Test @@ -684,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 @@ -1504,6 +1511,8 @@ menu.abilities=Abilities ability.availableTypes=Available Types ability.npcAbilities=NPC Abilities ability.globalCooldown=Global Cooldown +ability.minCooldown=Min CD +ability.maxCooldown=Max CD ability.edit=Edit Ability ability.weight=Weight ability.targeting=Targeting @@ -1512,11 +1521,15 @@ ability.maxRange=Max Range ability.timing=Timing (ticks) ability.cooldown=Cooldown ability.windup=Wind Up -ability.active=Active -ability.recovery=Recovery +ability.syncOn=Sync: On +ability.syncOff=Sync: Off +ability.dazed=Dazed Ticks ability.interruptible=Interruptible ability.threshold=Threshold ability.lockMove=Lock Move +ability.lockMove.windup=Windup +ability.lockMove.active=Active +ability.lockMove.both=Both ability.telegraph=Telegraph ability.telegraphType=Telegraph Type @@ -1524,6 +1537,48 @@ ability.target.aggro_target=Target ability.target.self=Self ability.target.aoe_self=AOE Self ability.target.aoe_target=AOE Target +ability.syncWindup=Sync Windup + +# Anchor Point +ability.anchor.front=Front +ability.anchor.center=Center +ability.anchor.rightHand=Right Hand +ability.anchor.leftHand=Left Hand +ability.anchor.aboveHead=Above Head +ability.anchor.chest=Chest + +# Sweep Mode +ability.sweep.swipe=Swipe +ability.sweep.spin=Spin + +# Dash Mode +ability.dash.aggressive=Aggressive +ability.dash.defensive=Defensive + +# Counter Type +ability.counter.flat=Flat +ability.counter.percent=Percent + +# Hazard Shape +ability.shape.circle=Circle +ability.shape.ring=Ring +ability.shape.cone=Cone + +# Placement Mode +ability.placement.atCaster=At Caster +ability.placement.atTarget=At Target +ability.placement.followCaster=Follow Caster +ability.placement.followTarget=Follow Target + +# Teleport Mode +ability.teleport.blink=Blink +ability.teleport.behind=Behind +ability.teleport.single=Single + +# Trap Placement +ability.trapPlace.atCaster=At Caster +ability.trapPlace.atTarget=At Target +ability.trapPlace.aheadOfCaster=Ahead of Caster telegraph.circle=Circle telegraph.ring=Ring @@ -1534,6 +1589,7 @@ telegraph.point=Point ability.tab.timing=Timing ability.tab.effects=Effects ability.tab.telegraph=Telegraph +ability.tab.visual=Visual ability.showTelegraph=Show Telegraph ability.telegraphColor=Color ability.telegraphSizeNote=Size is based on ability parameters @@ -1548,8 +1604,10 @@ ability.cnpc.slam=Slam ability.cnpc.heavy_hit=Heavy Hit ability.cnpc.cutter=Cutter ability.cnpc.beam=Beam +ability.cnpc.beam_dual=Beam Dual ability.cnpc.projectile=Projectile ability.cnpc.orb=Orb +ability.cnpc.orb_dual=Orb Dual ability.cnpc.charge=Charge ability.cnpc.dash=Dash ability.cnpc.teleport=Teleport @@ -1559,113 +1617,198 @@ ability.cnpc.guard=Guard ability.cnpc.heal=Heal ability.cnpc.hazard=Hazard ability.cnpc.trap=Trap +ability.cnpc.disc=Disc +ability.cnpc.disc_dual=Disc Dual +ability.cnpc.laser_shot=Laser Shot +ability.cnpc.sweeper=Sweeper -# Ability GUI - Type Tab +# ═══════════════════════════════════════════════════════════════ +# Ability Config GUI +# ═══════════════════════════════════════════════════════════════ + +# General Tab +ability.weight=Weight +ability.windUpTicks=Wind Up +ability.cooldownTicks=Cooldown +ability.lockMovement=Lock Movement +ability.interruptible=Interruptible +ability.dazedTicks=Dazed Ticks +ability.targetingMode=Targeting + +# Target Tab +ability.minRange=Min Range +ability.maxRange=Max Range +ability.conditions=Conditions + +# Effects Tab +ability.windUpSound=Wind Up Sound +ability.activeSound=Active Sound +ability.windUpAnimation=Wind Up Animation +ability.activeAnimation=Active Animation +ability.showTelegraph=Show Telegraph +ability.windUpColor=Wind Up Color +ability.activeColor=Active Color + +# Section Headers +ability.section.timing=Timing +ability.section.movement=Movement +ability.section.sounds=Sounds +ability.section.animations=Animations +ability.section.telegraph=Telegraph +ability.section.beam=Beam +ability.section.sweep=Sweep +ability.section.push=Push +ability.section.leap=Leap +ability.section.disc=Disc +ability.section.homing=Homing +ability.section.boomerang=Boomerang +ability.section.explosive=Explosive +ability.section.colors=Colors +ability.section.effects=Effects +ability.section.dual=Dual +ability.section.beam1=Beam 1 +ability.section.beam2=Beam 2 +ability.section.disc1=Disc 1 +ability.section.disc2=Disc 2 +ability.section.orb1=Orb 1 +ability.section.orb2=Orb 2 +ability.section.counter=Counter +ability.section.area=Area +ability.section.damage=Damage +ability.section.debuffs=Debuffs +ability.section.healing=Healing +ability.section.trigger=Trigger +ability.section.aoe=AOE +ability.section.pullDamage=Pull Damage + +# Shared Sub-Section Labels (used under section headers) +gui.radius=Radius +gui.thickness=Thickness +gui.rotationSpeed=Rotation Speed +gui.strength=Strength +gui.range=Range +gui.delay=Delay +gui.density=Density +gui.length=Length +gui.width=Width +gui.height=Height +gui.value=Value +gui.chance=Chance +gui.sound=Sound +gui.animation=Animation +gui.duration=Duration +gui.amp=Amp # Common Type Settings ability.radius=Radius ability.knockback=Knockback -ability.knockbackUp=KB Up -ability.maxDist=Max Dist -ability.minDist=Min Dist +ability.knockbackUp=Knockback Up ability.distance=Distance +ability.maxDistance=Max Distance ability.lifetime=Lifetime -ability.stunTicks=Stun -ability.stunDur=Stun Dur -ability.rootDur=Root Dur -ability.rootDuration=Root Duration -ability.stunDuration=Stun Duration -ability.leapHeight=Leap Height +ability.stunTicks=Stun Ticks ability.hitWidth=Hit Width -ability.potionTime=Potion Time (s) -ability.weaknessLvl=Weakness Lvl -ability.poisonTime=Poison Time (s) -ability.slownessLvl=Slowness Lvl -ability.poisonLvl=Poison Lvl +ability.effects=Effects +ability.mode=Mode +ability.duration=Duration +ability.anchorPoint=Anchor Point +ability.rotationSpeed=Rotation Speed -# Slam Settings +# Slam ability.leapSpeed=Leap Speed +ability.leapHeight=Leap Height -# Charge Settings -ability.hitRadius=Hit Radius - -# Cutter Settings +# Cutter +ability.range=Range +ability.sweepMode=Sweep Mode +ability.spinDuration=Spin Duration ability.arcAngle=Arc Angle -ability.sweepWaves=Waves -ability.rotSpeed=Rot Speed -ability.piercing=Piercing +ability.innerRadius=Inner Radius + +# Projectile +ability.projectileType=Projectile Type # Orb Settings ability.homing=Homing ability.explosive=Explosive +ability.firingSpeed=Firing Speed +ability.amount=Amount +ability.dualCharge=Dual Charging -# Vortex Settings -ability.pullRadius=Radius -ability.pullStrength=Strength -ability.pullTo=Pull To -ability.aoe=AOE +# Charge (no unique keys) + +# Dash (no unique keys) + +# Teleport +ability.blinkRadius=Blink Radius +ability.blinkCount=Blink Count +ability.blinkDelay=Blink Delay +ability.behindDistance=Behind Distance +ability.lineOfSight=Line of Sight +ability.damageAtStart=Damage At Start +ability.damageAtEnd=Damage At End + +# Vortex +ability.pullRadius=Pull Radius +ability.pullStrength=Pull Strength ability.maxTargets=Max Targets -# Dash Settings -ability.dash.aggressive=Aggressive -ability.dash.defensive=Defensive -ability.mode=Mode -ability.behindDistance=Behind Dist +# Shockwave +ability.pushStrength=Push Strength -# Teleport Settings -ability.blinkCount=Blinks -ability.blinkDelay=Delay -ability.blinkRadius=Radius -ability.minRadius=Min Rad -ability.dmgRadius=Dmg Radius -ability.lineOfSight=Line Sight -ability.dmgAtStart=Dmg Start +# Guard +ability.damageReduction=Damage Reduction -# Projectile Settings -ability.explRadius=Expl Rad - -# Beam Settings -ability.beamLength=Length -ability.beamWidth=Width -ability.dmgInterval=Interval -ability.sweepAngle=Sweep Ang -ability.sweepSpeed=Sweep Spd -ability.lockTarget=Lock Tgt - -# Guard Settings -ability.dmgReduce=Dmg Reduce -ability.counterDmg=Counter Dmg -ability.canCounter=Counter -ability.counterChance=Counter % -ability.counterType=Counter Type -ability.counterValue=Counter Value -ability.counterSound=Counter Sound -ability.counterAnim=Counter Anim - -# Heal Settings -ability.healAmount=Amount -ability.healPercent=Percent -ability.healRadius=Radius +# Heal +ability.instantHeal=Instant Heal +ability.healAmount=Heal Amount +ability.healPercent=Heal Percent +ability.healRadius=Heal Radius ability.healSelf=Heal Self ability.healAllies=Heal Allies -# Hazard Settings -ability.dmgPerSec=Dmg/Sec -ability.debuffDur=Debuff Dur +# Hazard ability.shape=Shape ability.placement=Placement -ability.affectsCaster=Hit Caster -ability.ignoreInvuln=Ign Invuln -ability.place.caster=At Caster -ability.place.target=At Target -ability.place.followCaster=Follow Caster -ability.place.followTarget=Follow Target -ability.place.ahead=Ahead +ability.damagePerSecond=Damage/Sec +ability.damageInterval=Damage Interval +ability.slownessLevel=Slowness Level +ability.debuffDuration=Debuff Duration +ability.poisonLevel=Poison Level +ability.affectsCaster=Affects Caster -# Trap Settings -ability.triggerRadius=Trig Rad +# Trap ability.armTime=Arm Time -ability.maxTriggers=Max Trig +ability.maxTriggers=Max Triggers +ability.triggerCooldown=Trigger Cooldown + +# Sweeper +ability.sweepSpeed=Sweep Speed +ability.rotations=Rotations +ability.piercing=Piercing +ability.lockTarget=Lock Target + +# Beam / Laser +ability.beamWidth=Beam Width +ability.headSize=Head Size +ability.attachedToOwner=Attached to Owner +ability.laserWidth=Laser Width +ability.expansionSpeed=Expansion Speed +ability.lingerTicks=Linger Ticks + +# Visual Settings +ability.innerColor=Inner Color +ability.outerColor=Outer Color +ability.outerEnabled=Outer Glow +ability.outerWidth=Outer Width +ability.outerAlpha=Outer Alpha +ability.lightning=Lightning +ability.lightningDensity=Density +ability.lightningRadius=Radius + +# Dual Energy Projectile Settings +ability.dualFire=Dual Fire +ability.dualFireDelay=Fire Delay # Ability Save/Load ability.save.confirm=Save ability preset @@ -1675,29 +1818,67 @@ ability.delete.confirm=Delete ability preset gui.load=Load # Global Abilities Manager -global.abilities=Abilities ability.manager.title=Ability Manager # Explosive Hover Text ability.explosive.noBlockDamage=Explosions do not destroy blocks -# Additional Orb Settings -ability.explRadius=Expl Radius - -# Telegraph Colors (for abilities that support separate colors) -ability.windUpColor=Wind Up Color -ability.activeColor=Active Color - -# Shockwave Settings -ability.pushRadius=Push Radius -ability.pushStrength=Push Str +# Shockwave Settings (dev) ability.shockwaveFalloff=Damage/knockback drop off with distance -# Ability Config GUI +# Ability Config GUI (dev) ability.typeId=Type ID -ability.conditions=Conditions ability.noTypeSettings=No type-specific settings +# ═══════════════════════════════════════════════════════════════ +# Ability Hover Tooltips +# ═══════════════════════════════════════════════════════════════ + +ability.hover.weight=Higher weight = more likely to be chosen when multiple abilities available +ability.hover.lockMove=Prevents NPC from moving while ability is charging or active +ability.hover.interruptible=If enabled, ability can be interrupted by taking damage +ability.hover.dazed=Stun duration in ticks if ability is interrupted (20 ticks = 1 second) +ability.hover.windup=Charge-up time before ability activates (20 ticks = 1 second) +ability.hover.cooldown=Time before ability can be used again (20 ticks = 1 second) +ability.hover.sync=Set windup ticks to match animation duration +ability.hover.minRange=Target must be at least this far away +ability.hover.maxRange=Target must be within this distance +ability.hover.targeting=How the ability selects its target +ability.hover.conditions=Requirements that must be met for ability to activate +ability.hover.showTelegraph=Show visual warning indicator during windup +ability.hover.windUpColor=Color of telegraph during charge-up phase +ability.hover.activeColor=Color of telegraph when ability is active +ability.hover.homing=Projectile tracks toward target +ability.hover.homingStr=How aggressively the projectile tracks (higher = tighter turns) +ability.hover.explosive=Creates explosion on impact +ability.hover.explosionRad=Size of explosion radius +ability.hover.piercing=Hits multiple targets instead of stopping on first hit +ability.hover.lightning=Add electrical effect to visuals +ability.hover.outerGlow=Enable outer glow layer around projectile +ability.hover.anchorPoint=Where on the NPC the ability spawns from +ability.hover.dualCharge=Whether or not should create two anchor points +ability.hover.damage=Base damage dealt to targets +ability.hover.speed=Movement speed of projectile +ability.hover.lifetime=Maximum ticks before despawning +ability.hover.knockback=Push force applied to targets +ability.hover.boomerang=Projectile returns to caster after reaching max distance +ability.hover.dashMode=Aggressive dashes toward target, Defensive dashes away +ability.hover.aoe=Affects all valid targets in range instead of just one +ability.hover.dmgOnPull=Deal damage continuously while pulling targets +ability.hover.lineOfSight=Requires clear line of sight to the target +ability.hover.dmgAtStart=Deal damage at the teleport origin location +ability.hover.dmgAtEnd=Deal damage at the teleport destination +ability.hover.instant=Heals instantly instead of over time +ability.hover.healSelf=Ability can heal the casting NPC +ability.hover.healAllies=Ability can heal nearby allied NPCs +ability.hover.placement=Where the ability effect is placed (Caster, Target, etc.) +ability.hover.lockTarget=Sweeper beam tracks the target's position +ability.hover.canCounter=Enable counterattack when blocking damage +ability.hover.counterType=Flat = fixed damage, Percent = % of blocked damage +ability.hover.shape=Shape of the hazard zone (Circle, Ring, Cone) +ability.hover.affectsCaster=Hazard can damage the NPC that created it +ability.hover.sweepMode=Swipe = single arc attack, Spin = continuous rotation + # Conditions condition.edit=Edit Condition condition.hp_above=NPC HP Above @@ -1709,6 +1890,22 @@ condition.percent=Threshold condition.hits=Required Hits condition.within=Within (ticks) +# Condition Hover Tooltips +condition.hover.type=Condition type that must be met for ability to activate +condition.hover.hpAbove=Activates when NPC health is above this percentage +condition.hover.hpBelow=Activates when NPC health is below this percentage +condition.hover.targetHpAbove=Activates when target's health is above this percentage +condition.hover.targetHpBelow=Activates when target's health is below this percentage +condition.hover.hitCount=Activates after receiving X hits within the time window +condition.hover.hits=Number of hits required to trigger condition +condition.hover.within=Time window in ticks (20 ticks = 1 second) +condition.hover.threshold=Health percentage threshold (1-99%) + +# Effect Hover Tooltips +effect.hover.type=Potion effect type to apply to targets +effect.hover.duration=Effect duration in ticks (20 ticks = 1 second) +effect.hover.amplifier=Effect strength level (higher = stronger) + # ========================================= # Auction House # ========================================= diff --git a/src/main/resources/assets/customnpcs/textures/entity/white.png b/src/main/resources/assets/customnpcs/textures/entity/white.png new file mode 100644 index 000000000..32a20fb7c Binary files /dev/null and b/src/main/resources/assets/customnpcs/textures/entity/white.png differ diff --git a/src/main/resources/assets/customnpcs/textures/entity/whiteSmall.png b/src/main/resources/assets/customnpcs/textures/entity/whiteSmall.png new file mode 100644 index 000000000..0cdc3313a Binary files /dev/null and b/src/main/resources/assets/customnpcs/textures/entity/whiteSmall.png differ 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 diff --git a/tools/bukkit-api/Vault-1.4.1.jar b/tools/bukkit-api/Vault-1.4.1.jar new file mode 100644 index 000000000..d4d4ecc76 Binary files /dev/null and b/tools/bukkit-api/Vault-1.4.1.jar differ diff --git a/tools/bukkit-api/spigot-api-ONLY.jar b/tools/bukkit-api/spigot-api-ONLY.jar new file mode 100644 index 000000000..a8028aeab Binary files /dev/null and b/tools/bukkit-api/spigot-api-ONLY.jar differ