diff --git a/.gitignore b/.gitignore index f24532312..d24cc7cf7 100644 --- a/.gitignore +++ b/.gitignore @@ -41,4 +41,5 @@ 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/ 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/src/api b/src/api index 2e5d86993..b84763a8e 160000 --- a/src/api +++ b/src/api @@ -1 +1 @@ -Subproject commit 2e5d86993cd47e6df4572323514d4fe2166c570c +Subproject commit b84763a8eefdc0be7fb909888ac5ad6d22e3f987 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..49cb4fb6c --- /dev/null +++ b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityBeam.java @@ -0,0 +1,347 @@ +package kamkeel.npcs.client.renderer; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +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(); + + // 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(); + + // Render everything relative to origin position + GL11.glPushMatrix(); + GL11.glTranslated(renderOriginX, renderOriginY, renderOriginZ); + + // Render tail orb at origin (0,0,0) - static, no rotation + renderTailOrb(headSize * 0.8f, innerColor, outerColor); + + // Render trail segments with smooth connections + renderSmoothTrail(trail, beamWidth, innerColor, outerColor); + + // Render head at interpolated offset position + renderHead(beam, headOffsetX, headOffsetY, headOffsetZ, headSize, innerColor, outerColor, partialTicks); + + GL11.glPopMatrix(); + + restoreRenderState(); + } + + /** + * 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) { + GL11.glPushMatrix(); + // Already at origin (0,0,0) + + // Render outer glow + GL11.glDepthMask(false); + GL11.glPushMatrix(); + GL11.glScalef(size, size, size); + renderCube(outerColor, 0.4f, 0.5f); + GL11.glPopMatrix(); + GL11.glDepthMask(true); + + // Render inner core (smaller, solid) + GL11.glPushMatrix(); + GL11.glScalef(size * 0.6f, size * 0.6f, size * 0.6f); + 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) { + 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); + + // Render outer glow trail (wider, translucent) + GL11.glDepthMask(false); + renderTrailTube(trail, perpFrames, width * 1.8f, outerColor, 0.3f, true); + GL11.glDepthMask(true); + + // Render inner core trail + renderTrailTube(trail, perpFrames, width * 0.6f, innerColor, 0.9f, false); + } + + /** + * 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 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); + + // 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); + + // Render outer glow + GL11.glDepthMask(false); + renderCube(outerColor, 0.5f, 0.5f); + GL11.glDepthMask(true); + + // Render inner core + float innerScale = 0.6f; + GL11.glScalef(innerScale, innerScale, innerScale); + renderCube(innerColor, 1.0f, 0.5f); + + GL11.glPopMatrix(); + GL11.glPopMatrix(); + } +} 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..93049fad5 --- /dev/null +++ b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityDisc.java @@ -0,0 +1,136 @@ +package kamkeel.npcs.client.renderer; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +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.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; + + 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); + + // Render outer square (translucent) + GL11.glDepthMask(false); + renderFlatSquare(disc.getOuterColor(), 0.5f, radius, thickness); + GL11.glDepthMask(true); + + // Render inner square (solid, smaller) + float innerScale = 0.6f; + renderFlatSquare(disc.getInnerColor(), 1.0f, radius * innerScale, thickness * 1.2f); + + GL11.glPopMatrix(); + + restoreRenderState(); + } + + /** + * 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..0c1d4a1e8 --- /dev/null +++ b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityLaser.java @@ -0,0 +1,200 @@ +package kamkeel.npcs.client.renderer; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +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; + + // Don't render if length is 0 + if (laser.getCurrentLength() <= 0) return; + + setupRenderState(); + + // 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; + + // Render outer rectangle (larger, outer color, translucent) + GL11.glDepthMask(false); + renderBeamRectangle(renderStartX, renderStartY, renderStartZ, + renderEndX, renderEndY, renderEndZ, + horzX, horzY, horzZ, vertX, vertY, vertZ, + width * 1.8f, laser.getOuterColor(), alpha * 0.4f); + + // Render middle layer + renderBeamRectangle(renderStartX, renderStartY, renderStartZ, + renderEndX, renderEndY, renderEndZ, + horzX, horzY, horzZ, vertX, vertY, vertZ, + width * 1.3f, laser.getOuterColor(), alpha * 0.7f); + GL11.glDepthMask(true); + + // Render inner core (smaller, inner color, solid) + renderBeamRectangle(renderStartX, renderStartY, renderStartZ, + renderEndX, renderEndY, renderEndZ, + horzX, horzY, horzZ, vertX, vertY, vertZ, + width * 0.6f, laser.getInnerColor(), alpha); + + restoreRenderState(); + } + + /** + * 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..3a09e7b6a --- /dev/null +++ b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityOrb.java @@ -0,0 +1,105 @@ +package kamkeel.npcs.client.renderer; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import kamkeel.npcs.client.renderer.quadric.SphereRenderer; +import kamkeel.npcs.entity.EntityAbilityOrb; +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; + +/** + * Renders the AbilityOrb entity as a glowing sphere with inner and outer layers. + * + * Design inspired by LouisXIV's energy rendering system. + */ +@SideOnly(Side.CLIENT) +public class RenderAbilityOrb extends Render { + + // White texture for solid color rendering + private static final ResourceLocation WHITE_TEXTURE = new ResourceLocation("customnpcs", "textures/entity/white.png"); + + // Static sphere renderer - shared across all orb renders + private static final SphereRenderer sphere = new SphereRenderer(0.5f, 16); + + public RenderAbilityOrb() { + this.shadowSize = 0.0f; + } + + @Override + public void doRender(Entity entity, double x, double y, double z, float yaw, float partialTicks) { + EntityAbilityOrb orb = (EntityAbilityOrb) entity; + + GL11.glPushMatrix(); + + // Disable culling, enable rescale normal + GL11.glDisable(GL11.GL_CULL_FACE); + GL11.glEnable(GL12.GL_RESCALE_NORMAL); + GL11.glEnable(GL11.GL_ALPHA_TEST); + + // Enable blending for transparency + GL11.glEnable(GL11.GL_BLEND); + GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); + GL11.glAlphaFunc(GL11.GL_GREATER, 0.003921569F); + + // Disable lighting for self-illumination + GL11.glDisable(GL11.GL_LIGHTING); + + // Translate to render position + GL11.glTranslated(x, y, z); + + // Bind texture + this.bindTexture(WHITE_TEXTURE); + + // 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); + + GL11.glPushMatrix(); + GL11.glScalef(scale, scale, 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); + + // Render outer sphere (translucent) + GL11.glDepthMask(false); + this.bindTexture(WHITE_TEXTURE); + renderSphere(orb.getOuterColor(), 0.6f); + GL11.glDepthMask(true); + + // Render inner sphere (more opaque) + float innerScale = 0.75f; + GL11.glPushMatrix(); + GL11.glScalef(innerScale, innerScale, innerScale); + this.bindTexture(WHITE_TEXTURE); + renderSphere(orb.getInnerColor(), 1.0f); + GL11.glPopMatrix(); + + GL11.glPopMatrix(); + + // Restore GL state + GL11.glEnable(GL11.GL_LIGHTING); + GL11.glPopMatrix(); + } + + private void renderSphere(int color, float alpha) { + // Set sphere properties and render (rotation already applied to matrix) + sphere.setScale(1.0f); + sphere.setAlpha(alpha); + sphere.setColor(color); + sphere.render(0); + } + + @Override + protected ResourceLocation getEntityTexture(Entity entity) { + return WHITE_TEXTURE; + } +} diff --git a/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityOrbCube.java b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityOrbCube.java new file mode 100644 index 000000000..29656e597 --- /dev/null +++ b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityOrbCube.java @@ -0,0 +1,56 @@ +package kamkeel.npcs.client.renderer; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +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. + * + * Design inspired by LouisXIV's energy rendering system. + */ +@SideOnly(Side.CLIENT) +public class RenderAbilityOrbCube extends RenderAbilityProjectile { + + @Override + public void doRender(Entity entity, double x, double y, double z, float yaw, float partialTicks) { + EntityAbilityOrb orb = (EntityAbilityOrb) entity; + + setupRenderState(); + + 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); + + // 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); + + // Render outer cube (translucent) + GL11.glDepthMask(false); + renderCube(orb.getOuterColor(), 0.5f, 0.5f); + GL11.glDepthMask(true); + + // Render inner cube (solid, smaller) + float innerScale = 0.6f; + GL11.glScalef(innerScale, innerScale, innerScale); + renderCube(orb.getInnerColor(), 1.0f, 0.5f); + + GL11.glPopMatrix(); + + restoreRenderState(); + } +} 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..4180783bf --- /dev/null +++ b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityProjectile.java @@ -0,0 +1,255 @@ +package kamkeel.npcs.client.renderer; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +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. + */ + protected void setupRenderState() { + GL11.glPushMatrix(); + 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); + } + + /** + * Restore GL state after rendering. + */ + protected void restoreRenderState() { + GL11.glEnable(GL11.GL_TEXTURE_2D); + GL11.glEnable(GL11.GL_LIGHTING); + GL11.glEnable(GL11.GL_CULL_FACE); + 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..735c1e183 --- /dev/null +++ b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilitySweeper.java @@ -0,0 +1,210 @@ +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.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(); + 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); + + 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); + } + + 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) { + // 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 + GL11.glDepthMask(false); + renderBeamSegment(0, 0, 0, 0, 0, length, width * 1.8f, 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/quadric/QuadricRenderer.java b/src/main/java/kamkeel/npcs/client/renderer/quadric/QuadricRenderer.java new file mode 100644 index 000000000..13c994adc --- /dev/null +++ b/src/main/java/kamkeel/npcs/client/renderer/quadric/QuadricRenderer.java @@ -0,0 +1,138 @@ +package kamkeel.npcs.client.renderer.quadric; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import net.minecraft.client.renderer.GLAllocation; +import org.lwjgl.opengl.GL11; + +/** + * Base class for rendering GLU quadric shapes (spheres, cylinders, etc.) + * Uses display lists for efficient rendering. + * + * Design inspired by LouisXIV's quadric rendering system. + */ +@SideOnly(Side.CLIENT) +public abstract class QuadricRenderer { + protected int displayList; + private boolean compiled; + + public final float radius; + public final int slices; + public final int stacks; + + // Current render state + protected float scaleX = 1.0f; + protected float scaleY = 1.0f; + protected float scaleZ = 1.0f; + + protected float rotationX = 0.0f; + protected float rotationY = 0.0f; + protected float rotationZ = 0.0f; + + protected int color = 0xFFFFFF; + protected float alpha = 1.0f; + + public QuadricRenderer(float radius, int slices, int stacks) { + this.radius = radius; + this.slices = slices; + this.stacks = stacks; + } + + public QuadricRenderer(float radius, int resolution) { + this(radius, resolution, resolution); + } + + public QuadricRenderer() { + this(0.5f, 16); + } + + /** + * Render the quadric shape with current transform and color settings. + */ + public void render(float partialTicks) { + if (!compiled) { + displayList = GLAllocation.generateDisplayLists(1); + GL11.glNewList(displayList, GL11.GL_COMPILE); + compile(); + GL11.glEndList(); + compiled = true; + } + + GL11.glPushMatrix(); + + // Apply scale + GL11.glScalef(scaleX, scaleY, scaleZ); + + // Apply rotation + GL11.glRotatef(rotationX, 1.0f, 0.0f, 0.0f); + GL11.glRotatef(rotationY, 0.0f, 1.0f, 0.0f); + GL11.glRotatef(rotationZ, 0.0f, 0.0f, 1.0f); + + // Apply color + float r = ((color >> 16) & 0xFF) / 255.0f; + float g = ((color >> 8) & 0xFF) / 255.0f; + float b = (color & 0xFF) / 255.0f; + GL11.glColor4f(r, g, b, alpha); + + GL11.glCallList(displayList); + + GL11.glPopMatrix(); + } + + /** + * Compile the quadric geometry into the display list. + * Called once, then cached. + */ + protected abstract void compile(); + + // Setters for render state + public void setScale(float scale) { + this.scaleX = this.scaleY = this.scaleZ = scale; + } + + public void setScale(float x, float y, float z) { + this.scaleX = x; + this.scaleY = y; + this.scaleZ = z; + } + + public void setRotation(float x, float y, float z) { + this.rotationX = x; + this.rotationY = y; + this.rotationZ = z; + } + + public void setColor(int color) { + this.color = color; + } + + public void setAlpha(float alpha) { + this.alpha = alpha; + } + + /** + * Linear interpolation helper. + */ + public static float lerp(float current, float target, float factor) { + return current + (target - current) * factor; + } + + /** + * Color interpolation helper. + */ + public static int lerpColor(int from, int to, float factor) { + int r1 = (from >> 16) & 0xFF; + int g1 = (from >> 8) & 0xFF; + int b1 = from & 0xFF; + + int r2 = (to >> 16) & 0xFF; + int g2 = (to >> 8) & 0xFF; + int b2 = to & 0xFF; + + int r = (int) (r1 + (r2 - r1) * factor); + int g = (int) (g1 + (g2 - g1) * factor); + int b = (int) (b1 + (b2 - b1) * factor); + + return (r << 16) | (g << 8) | b; + } +} diff --git a/src/main/java/kamkeel/npcs/client/renderer/quadric/SphereRenderer.java b/src/main/java/kamkeel/npcs/client/renderer/quadric/SphereRenderer.java new file mode 100644 index 000000000..dc72779a8 --- /dev/null +++ b/src/main/java/kamkeel/npcs/client/renderer/quadric/SphereRenderer.java @@ -0,0 +1,37 @@ +package kamkeel.npcs.client.renderer.quadric; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import org.lwjgl.util.glu.GLU; +import org.lwjgl.util.glu.Sphere; + +/** + * Renders a sphere using GLU quadrics. + * + * Design inspired by LouisXIV's quadric rendering system. + */ +@SideOnly(Side.CLIENT) +public class SphereRenderer extends QuadricRenderer { + + public SphereRenderer(float radius, int slices, int stacks) { + super(radius, slices, stacks); + } + + public SphereRenderer(float radius, int resolution) { + super(radius, resolution); + } + + public SphereRenderer() { + super(); + } + + @Override + protected void compile() { + Sphere sphere = new Sphere(); + sphere.setTextureFlag(true); // Enable texture coordinates + sphere.setDrawStyle(GLU.GLU_FILL); + sphere.setNormals(GLU.GLU_NONE); // No normals for self-illuminated rendering + sphere.setOrientation(GLU.GLU_OUTSIDE); + sphere.draw(radius, slices, stacks); + } +} 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 65a4e8ac5..18406c313 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java @@ -80,7 +80,6 @@ public abstract class Ability implements IAbility { 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; @@ -138,7 +137,15 @@ public abstract class Ability implements IAbility { } /** - * 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 npc The NPC executing the ability + * @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, float damage, float knockback, float knockbackUp) { @@ -153,6 +160,7 @@ public abstract class Ability implements IAbility { // Apply damage with potentially modified values float finalDamage = event.getDamage(); float finalKnockback = event.getKnockback(); + float finalKnockbackUp = event.getKnockbackUp(); // Apply damage if (finalDamage > 0) { @@ -160,10 +168,10 @@ public abstract class Ability implements IAbility { } // Apply knockback if any - if (finalKnockback > 0) { + if (finalKnockback > 0 || finalKnockbackUp > 0) { double dx = hitEntity.posX - npc.posX; double dz = hitEntity.posZ - npc.posZ; - applyNpcKnockback(hitEntity, dx, dz, finalKnockback); + applyNpcKnockback(hitEntity, dx, dz, finalKnockback, finalKnockbackUp); } return true; @@ -195,28 +203,31 @@ public abstract class Ability implements IAbility { // Apply damage with potentially modified values float finalDamage = event.getDamage(); float finalKnockback = event.getKnockback(); + float finalKnockbackUp = event.getKnockbackUp(); - // Apply damage + // Apply damage - use NpcDamageSource for consistency if (finalDamage > 0) { - hitEntity.attackEntityFrom(DamageSource.causeMobDamage(npc), finalDamage); + hitEntity.attackEntityFrom(new NpcDamageSource("mob", npc), finalDamage); } // Apply knockback in specified direction - if (finalKnockback > 0) { - applyNpcKnockback(hitEntity, knockbackDirX, knockbackDirZ, finalKnockback); + if (finalKnockback > 0 || finalKnockbackUp > 0) { + applyNpcKnockback(hitEntity, knockbackDirX, knockbackDirZ, finalKnockback, finalKnockbackUp); } return true; } - private void applyNpcKnockback(EntityLivingBase hitEntity, double dirX, double dirZ, float knockback) { + private void applyNpcKnockback(EntityLivingBase hitEntity, double dirX, double dirZ, float knockback, float knockbackUp) { double len = Math.sqrt(dirX * dirX + dirZ * dirZ); - if (len <= 0) { - return; + double x = 0; + double z = 0; + if (len > 0) { + x = (dirX / len) * knockback * 0.5; + z = (dirZ / len) * knockback * 0.5; } - double x = (dirX / len) * knockback * 0.5; - double z = (dirZ / len) * knockback * 0.5; - hitEntity.addVelocity(x, 0.1D, z); + double y = knockbackUp > 0 ? knockbackUp : 0.1D; + hitEntity.addVelocity(x, y, z); hitEntity.velocityChanged = true; } @@ -390,7 +401,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); @@ -410,6 +421,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,12 +488,12 @@ public abstract class Ability implements IAbility { /** * Tick the ability. Returns true if phase changed. + * Note: Cooldown is managed by DataAbilities when the ability completes. */ public boolean tick() { if (phase == AbilityPhase.IDLE) return false; currentTick++; - AbilityPhase oldPhase = phase; switch (phase) { case WINDUP: @@ -465,7 +513,7 @@ public abstract class Ability implements IAbility { case RECOVERY: if (currentTick >= recoveryTicks) { phase = AbilityPhase.IDLE; - startCooldown(); + // Cooldown is managed by DataAbilities.onAbilityComplete() return true; } break; @@ -483,20 +531,6 @@ public abstract class Ability implements IAbility { 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 */ @@ -543,7 +577,6 @@ public abstract class Ability implements IAbility { phase = AbilityPhase.IDLE; currentTick = 0; currentTarget = null; - cooldownEndTime = 0; telegraphInstance = null; } @@ -558,19 +591,6 @@ public abstract class Ability implements IAbility { return true; } - /** - * Full eligibility check - */ - 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); - } // ═══════════════════════════════════════════════════════════════════ // NBT (config only, execution state is transient) @@ -811,22 +831,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; } @@ -859,38 +863,6 @@ public abstract class Ability implements IAbility { this.activeAnimationId = activeAnimationId; } - /** - * @deprecated Use getWindUpSound() instead - */ - @Deprecated - public String getCastSound() { - return windUpSound; - } - - /** - * @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 boolean isShowTelegraph() { return showTelegraph; } 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..41c20a085 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java @@ -1,6 +1,6 @@ package kamkeel.npcs.controllers.data.ability; -import kamkeel.npcs.controllers.data.ability.type.AbilityBeam; +import kamkeel.npcs.controllers.data.ability.type.AbilitySweeper; import kamkeel.npcs.controllers.data.ability.type.AbilityCharge; import kamkeel.npcs.controllers.data.ability.type.AbilityCutter; import kamkeel.npcs.controllers.data.ability.type.AbilityDash; @@ -9,6 +9,9 @@ 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.AbilityDisc; +import kamkeel.npcs.controllers.data.ability.type.AbilityLaserShot; +import kamkeel.npcs.controllers.data.ability.type.AbilityEnergyBeam; import kamkeel.npcs.controllers.data.ability.type.AbilityProjectile; import kamkeel.npcs.controllers.data.ability.type.AbilityShockwave; import kamkeel.npcs.controllers.data.ability.type.AbilitySlam; @@ -296,9 +299,12 @@ public class AbilityController implements IAbilityHandler { 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:disc", AbilityDisc::new); + registerType("cnpc:laser_shot", AbilityLaserShot::new); + registerType("cnpc:beam", AbilityEnergyBeam::new); // Movement abilities registerType("cnpc:charge", AbilityCharge::new); 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..bd1bb6a47 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,5 @@ public enum TargetingMode { /** * AOE centered on target */ - AOE_TARGET, - - /** - * Random nearby enemy - */ - RANDOM_ENEMY, - - /** - * Lowest HP enemy in range - */ - LOWEST_HP, - - /** - * Highest threat enemy - */ - HIGHEST_THREAT + AOE_TARGET } 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/AbilityCharge.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java index 6c43ea1cb..696367ce8 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 @@ -49,7 +49,7 @@ public class AbilityCharge extends Ability { this.cooldownTicks = 80; this.windUpTicks = 20; this.activeTicks = 40; - this.recoveryTicks = 20; + this.recoveryTicks = 10; // LINE telegraph showing charge path this.telegraphType = TelegraphType.LINE; this.showTelegraph = true; 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..97d69e19c --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java @@ -0,0 +1,237 @@ +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 kamkeel.npcs.entity.EntityAbilityDisc; +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.SubGuiAbilityDisc; +import noppes.npcs.client.gui.util.IAbilityConfigCallback; +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 { + + // Movement properties + private float speed = 0.6f; + private float discRadius = 1.0f; + private float discThickness = 0.2f; + private float maxDistance = 35.0f; + private int maxLifetime = 250; + + // Combat properties + private float damage = 8.0f; + private float knockback = 1.2f; + private float knockbackUp = 0.15f; + + // Homing properties + private boolean homing = true; + private float homingStrength = 0.12f; + private float homingRange = 18.0f; + + // Boomerang properties + private boolean boomerang = false; + private int boomerangDelay = 40; + + // Explosion properties + private boolean explosive = false; + private float explosionRadius = 3.0f; + private float explosionDamageFalloff = 0.5f; + + // Effect properties + private int stunDuration = 0; + private int slowDuration = 0; + private int slowLevel = 0; + + // Visual properties + private int innerColor = 0xFFFFFF; + private int outerColor = 0xFF8800; + private float outerColorWidth = 1.8f; + private boolean outerColorEnabled = true; + private float rotationSpeed = 5.0f; + + 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 = 80; + this.windUpTicks = 20; + this.activeTicks = 1; + 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 SubGuiAbilityDisc(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) { + if (world.isRemote) return; + + double spawnX = npc.posX; + double spawnY = npc.posY + npc.getEyeHeight() * 0.8; + double spawnZ = npc.posZ; + + EntityAbilityDisc disc = new EntityAbilityDisc( + world, npc, target, + spawnX, spawnY, spawnZ, + discRadius, discThickness, innerColor, outerColor, rotationSpeed, + damage, knockback, knockbackUp, + speed, homing, homingStrength, homingRange, + boomerang, boomerangDelay, + explosive, explosionRadius, explosionDamageFalloff, + stunDuration, slowDuration, slowLevel, + maxDistance, maxLifetime + ); + + world.spawnEntityInWorld(disc); + } + + @Override + public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { + } + + @Override + public void onComplete(EntityNPCInterface npc, EntityLivingBase target) { + } + + @Override + public void onInterrupt(EntityNPCInterface npc, DamageSource source, float damage) { + } + + @Override + public void writeTypeNBT(NBTTagCompound nbt) { + nbt.setFloat("speed", speed); + nbt.setFloat("discRadius", discRadius); + nbt.setFloat("discThickness", discThickness); + nbt.setFloat("maxDistance", maxDistance); + nbt.setInteger("maxLifetime", maxLifetime); + nbt.setFloat("damage", damage); + nbt.setFloat("knockback", knockback); + nbt.setFloat("knockbackUp", knockbackUp); + nbt.setBoolean("homing", homing); + nbt.setFloat("homingStrength", homingStrength); + nbt.setFloat("homingRange", homingRange); + nbt.setBoolean("boomerang", boomerang); + nbt.setInteger("boomerangDelay", boomerangDelay); + nbt.setBoolean("explosive", explosive); + nbt.setFloat("explosionRadius", explosionRadius); + nbt.setFloat("explosionDamageFalloff", explosionDamageFalloff); + nbt.setInteger("stunDuration", stunDuration); + nbt.setInteger("slowDuration", slowDuration); + nbt.setInteger("slowLevel", slowLevel); + nbt.setInteger("innerColor", innerColor); + nbt.setInteger("outerColor", outerColor); + nbt.setFloat("outerColorWidth", outerColorWidth); + nbt.setBoolean("outerColorEnabled", outerColorEnabled); + nbt.setFloat("rotationSpeed", rotationSpeed); + } + + @Override + public void readTypeNBT(NBTTagCompound nbt) { + this.speed = nbt.hasKey("speed") ? nbt.getFloat("speed") : 0.6f; + this.discRadius = nbt.hasKey("discRadius") ? nbt.getFloat("discRadius") : 1.0f; + this.discThickness = nbt.hasKey("discThickness") ? nbt.getFloat("discThickness") : 0.2f; + this.maxDistance = nbt.hasKey("maxDistance") ? nbt.getFloat("maxDistance") : 35.0f; + this.maxLifetime = nbt.hasKey("maxLifetime") ? nbt.getInteger("maxLifetime") : 250; + this.damage = nbt.hasKey("damage") ? nbt.getFloat("damage") : 8.0f; + this.knockback = nbt.hasKey("knockback") ? nbt.getFloat("knockback") : 1.2f; + this.knockbackUp = nbt.hasKey("knockbackUp") ? nbt.getFloat("knockbackUp") : 0.15f; + this.homing = !nbt.hasKey("homing") || nbt.getBoolean("homing"); + this.homingStrength = nbt.hasKey("homingStrength") ? nbt.getFloat("homingStrength") : 0.12f; + this.homingRange = nbt.hasKey("homingRange") ? nbt.getFloat("homingRange") : 18.0f; + this.boomerang = nbt.hasKey("boomerang") && nbt.getBoolean("boomerang"); + this.boomerangDelay = nbt.hasKey("boomerangDelay") ? nbt.getInteger("boomerangDelay") : 40; + 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; + this.innerColor = nbt.hasKey("innerColor") ? nbt.getInteger("innerColor") : 0xFFFFFF; + this.outerColor = nbt.hasKey("outerColor") ? nbt.getInteger("outerColor") : 0xFF8800; + this.outerColorWidth = nbt.hasKey("outerColorWidth") ? nbt.getFloat("outerColorWidth") : 1.8f; + this.outerColorEnabled = !nbt.hasKey("outerColorEnabled") || nbt.getBoolean("outerColorEnabled"); + this.rotationSpeed = nbt.hasKey("rotationSpeed") ? nbt.getFloat("rotationSpeed") : 8.0f; + } + + // Getters & Setters + public float getSpeed() { return speed; } + public void setSpeed(float speed) { this.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 maxDistance; } + public void setMaxDistance(float maxDistance) { this.maxDistance = maxDistance; } + public int getMaxLifetime() { return maxLifetime; } + public void setMaxLifetime(int maxLifetime) { this.maxLifetime = maxLifetime; } + 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 getKnockbackUp() { return knockbackUp; } + public void setKnockbackUp(float knockbackUp) { this.knockbackUp = knockbackUp; } + public boolean isHoming() { return homing; } + public void setHoming(boolean homing) { this.homing = homing; } + public float getHomingStrength() { return homingStrength; } + public void setHomingStrength(float homingStrength) { this.homingStrength = homingStrength; } + public float getHomingRange() { return homingRange; } + public void setHomingRange(float homingRange) { this.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 explosive; } + public void setExplosive(boolean explosive) { this.explosive = explosive; } + public float getExplosionRadius() { return explosionRadius; } + public void setExplosionRadius(float explosionRadius) { this.explosionRadius = explosionRadius; } + public float getExplosionDamageFalloff() { return explosionDamageFalloff; } + public void setExplosionDamageFalloff(float explosionDamageFalloff) { this.explosionDamageFalloff = explosionDamageFalloff; } + public int getStunDuration() { return stunDuration; } + public void setStunDuration(int stunDuration) { this.stunDuration = stunDuration; } + 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 getInnerColor() { return innerColor; } + public void setInnerColor(int innerColor) { this.innerColor = innerColor; } + public int getOuterColor() { return outerColor; } + public void setOuterColor(int outerColor) { this.outerColor = outerColor; } + public float getOuterColorWidth() { return outerColorWidth; } + public void setOuterColorWidth(float outerColorWidth) { this.outerColorWidth = outerColorWidth; } + public boolean isOuterColorEnabled() { return outerColorEnabled; } + public void setOuterColorEnabled(boolean outerColorEnabled) { this.outerColorEnabled = outerColorEnabled; } + public float getRotationSpeed() { return rotationSpeed; } + public void setRotationSpeed(float rotationSpeed) { this.rotationSpeed = rotationSpeed; } +} 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..2790eafd8 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergyBeam.java @@ -0,0 +1,227 @@ +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 kamkeel.npcs.entity.EntityAbilityBeam; +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.SubGuiAbilityEnergyBeam; +import noppes.npcs.client.gui.util.IAbilityConfigCallback; +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 { + + // Movement properties + private float speed = 0.4f; + private float maxDistance = 25.0f; + private int maxLifetime = 200; + + // Shape properties + private float beamWidth = 0.4f; + private float headSize = 0.6f; + + // Combat properties + private float damage = 10.0f; + private float knockback = 1.5f; + private float knockbackUp = 0.2f; + + // Homing properties + private boolean homing = true; + private float homingStrength = 0.1f; + private float homingRange = 15.0f; + + // Explosion properties + private boolean explosive = false; + private float explosionRadius = 4.0f; + private float explosionDamageFalloff = 0.5f; + + // Effect properties + private int stunDuration = 0; + private int slowDuration = 0; + private int slowLevel = 0; + + // Visual properties + private int innerColor = 0xFFFFFF; + private int outerColor = 0x00AAFF; + private float outerColorWidth = 1.8f; + private boolean outerColorEnabled = true; + private float rotationSpeed = 6.0f; + + 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 = 150; + this.windUpTicks = 40; + this.activeTicks = 200; // Match maxLifetime to keep NPC locked while beam is active + this.recoveryTicks = 5; + this.telegraphType = TelegraphType.POINT; + this.showTelegraph = false; + } + + @Override + public boolean hasTypeSettings() { + return true; + } + + @Override + @SideOnly(Side.CLIENT) + public SubGuiAbilityConfig createConfigGui(IAbilityConfigCallback callback) { + return new SubGuiAbilityEnergyBeam(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) { + if (world.isRemote) return; + + double spawnX = npc.posX; + double spawnY = npc.posY + npc.getEyeHeight() * 0.7; + double spawnZ = npc.posZ; + + EntityAbilityBeam beam = new EntityAbilityBeam( + world, npc, target, + spawnX, spawnY, spawnZ, + beamWidth, headSize, innerColor, outerColor, rotationSpeed, + damage, knockback, knockbackUp, + speed, homing, homingStrength, homingRange, + explosive, explosionRadius, explosionDamageFalloff, + stunDuration, slowDuration, slowLevel, + maxDistance, maxLifetime + ); + + world.spawnEntityInWorld(beam); + } + + @Override + public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { + } + + @Override + public void onComplete(EntityNPCInterface npc, EntityLivingBase target) { + } + + @Override + public void onInterrupt(EntityNPCInterface npc, DamageSource source, float damage) { + } + + @Override + public void writeTypeNBT(NBTTagCompound nbt) { + nbt.setFloat("speed", speed); + nbt.setFloat("maxDistance", maxDistance); + nbt.setInteger("maxLifetime", maxLifetime); + nbt.setFloat("beamWidth", beamWidth); + nbt.setFloat("headSize", headSize); + nbt.setFloat("damage", damage); + nbt.setFloat("knockback", knockback); + nbt.setFloat("knockbackUp", knockbackUp); + 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); + nbt.setInteger("innerColor", innerColor); + nbt.setInteger("outerColor", outerColor); + nbt.setFloat("outerColorWidth", outerColorWidth); + nbt.setBoolean("outerColorEnabled", outerColorEnabled); + nbt.setFloat("rotationSpeed", rotationSpeed); + } + + @Override + public void readTypeNBT(NBTTagCompound nbt) { + this.speed = nbt.hasKey("speed") ? nbt.getFloat("speed") : 0.4f; + this.maxDistance = nbt.hasKey("maxDistance") ? nbt.getFloat("maxDistance") : 25.0f; + this.maxLifetime = nbt.hasKey("maxLifetime") ? nbt.getInteger("maxLifetime") : 200; + this.beamWidth = nbt.hasKey("beamWidth") ? nbt.getFloat("beamWidth") : 0.4f; + this.headSize = nbt.hasKey("headSize") ? nbt.getFloat("headSize") : 0.6f; + this.damage = nbt.hasKey("damage") ? nbt.getFloat("damage") : 10.0f; + this.knockback = nbt.hasKey("knockback") ? nbt.getFloat("knockback") : 1.5f; + this.knockbackUp = nbt.hasKey("knockbackUp") ? nbt.getFloat("knockbackUp") : 0.2f; + this.homing = !nbt.hasKey("homing") || nbt.getBoolean("homing"); + this.homingStrength = nbt.hasKey("homingStrength") ? nbt.getFloat("homingStrength") : 0.1f; + this.homingRange = nbt.hasKey("homingRange") ? nbt.getFloat("homingRange") : 15.0f; + this.explosive = nbt.hasKey("explosive") && nbt.getBoolean("explosive"); + this.explosionRadius = nbt.hasKey("explosionRadius") ? nbt.getFloat("explosionRadius") : 4.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; + this.innerColor = nbt.hasKey("innerColor") ? nbt.getInteger("innerColor") : 0xFFFFFF; + this.outerColor = nbt.hasKey("outerColor") ? nbt.getInteger("outerColor") : 0x00AAFF; + this.outerColorWidth = nbt.hasKey("outerColorWidth") ? nbt.getFloat("outerColorWidth") : 1.8f; + this.outerColorEnabled = !nbt.hasKey("outerColorEnabled") || nbt.getBoolean("outerColorEnabled"); + this.rotationSpeed = nbt.hasKey("rotationSpeed") ? nbt.getFloat("rotationSpeed") : 6.0f; + } + + // Getters & Setters + public float getSpeed() { return speed; } + public void setSpeed(float speed) { this.speed = speed; } + public float getMaxDistance() { return maxDistance; } + public void setMaxDistance(float maxDistance) { this.maxDistance = maxDistance; } + public int getMaxLifetime() { return maxLifetime; } + public void setMaxLifetime(int maxLifetime) { this.maxLifetime = maxLifetime; } + 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 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 getKnockbackUp() { return knockbackUp; } + public void setKnockbackUp(float knockbackUp) { this.knockbackUp = knockbackUp; } + public boolean isHoming() { return homing; } + public void setHoming(boolean homing) { this.homing = homing; } + public float getHomingStrength() { return homingStrength; } + public void setHomingStrength(float homingStrength) { this.homingStrength = homingStrength; } + public float getHomingRange() { return homingRange; } + public void setHomingRange(float homingRange) { this.homingRange = homingRange; } + public boolean isExplosive() { return explosive; } + public void setExplosive(boolean explosive) { this.explosive = explosive; } + public float getExplosionRadius() { return explosionRadius; } + public void setExplosionRadius(float explosionRadius) { this.explosionRadius = explosionRadius; } + public float getExplosionDamageFalloff() { return explosionDamageFalloff; } + public void setExplosionDamageFalloff(float explosionDamageFalloff) { this.explosionDamageFalloff = explosionDamageFalloff; } + public int getStunDuration() { return stunDuration; } + public void setStunDuration(int stunDuration) { this.stunDuration = stunDuration; } + 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 getInnerColor() { return innerColor; } + public void setInnerColor(int innerColor) { this.innerColor = innerColor; } + public int getOuterColor() { return outerColor; } + public void setOuterColor(int outerColor) { this.outerColor = outerColor; } + public float getOuterColorWidth() { return outerColorWidth; } + public void setOuterColorWidth(float outerColorWidth) { this.outerColorWidth = outerColorWidth; } + public boolean isOuterColorEnabled() { return outerColorEnabled; } + public void setOuterColorEnabled(boolean outerColorEnabled) { this.outerColorEnabled = outerColorEnabled; } + public float getRotationSpeed() { return rotationSpeed; } + public void setRotationSpeed(float rotationSpeed) { this.rotationSpeed = rotationSpeed; } +} 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..6a98b643a 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 @@ -51,7 +51,7 @@ public class AbilityGuard extends Ability { this.cooldownTicks = 120; this.windUpTicks = 10; this.activeTicks = 60; - this.recoveryTicks = 20; + this.recoveryTicks = 10; this.interruptible = false; // Hard to interrupt while guarding // No telegraph for guard - it's a defensive stance this.telegraphType = TelegraphType.NONE; 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..3161c5dbe 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 @@ -146,34 +146,6 @@ 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) { // AT_CASTER locks immediately, AT_TARGET follows during windup and locks on execute @@ -214,12 +186,18 @@ public class AbilityHazard extends Ability { 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]; @@ -474,18 +452,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; } 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..2e23a1e56 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 @@ -76,16 +76,24 @@ public class AbilityHeal extends Ability { healedAllies.clear(); + // Always find allies if we're healing them (needed for both instant and HoT) + if (healAllies && healRadius > 0) { + findAlliesInRadius(npc, world); + } + if (instantHeal) { + // Instant heal - apply all healing now if (healSelf) { healEntity(npc); spawnHealParticles(world, npc); } - if (healAllies && healRadius > 0) { - healAlliesInRadius(npc, world); + for (EntityLivingBase ally : healedAllies) { + healEntity(ally); + spawnHealParticles(world, ally); } } + // If not instant, HoT healing is applied in onActiveTick } @Override @@ -104,6 +112,9 @@ public class AbilityHeal extends Ability { selfTickHeal += (npc.getMaxHealth() * healPercent / (float) activeTicks) * 10; } npc.heal(selfTickHeal); + if (tick % 20 == 0) { + spawnHealParticles(world, npc); + } } if (healAllies && healRadius > 0) { @@ -115,21 +126,19 @@ public class AbilityHeal extends Ability { allyTickHeal += (ally.getMaxHealth() * healPercent / (float) activeTicks) * 10; } ally.heal(allyTickHeal); + if (tick % 20 == 0) { + spawnHealParticles(world, ally); + } } } } } } - private void healEntity(EntityLivingBase entity) { - float totalHeal = healAmount; - if (healPercent > 0) { - totalHeal += entity.getMaxHealth() * healPercent; - } - entity.heal(totalHeal); - } - - private void healAlliesInRadius(EntityNPCInterface npc, World world) { + /** + * Find all allies in radius and add them to healedAllies list. + */ + private void findAlliesInRadius(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 @@ -148,14 +157,20 @@ public class AbilityHeal extends Ability { if (living instanceof EntityNPCInterface) { float dist = npc.getDistanceToEntity(living); if (dist <= healRadius) { - healEntity(living); healedAllies.add(living); - spawnHealParticles(world, living); } } } } + private void healEntity(EntityLivingBase entity) { + float totalHeal = healAmount; + if (healPercent > 0) { + totalHeal += entity.getMaxHealth() * healPercent; + } + entity.heal(totalHeal); + } + private void spawnHealParticles(World world, EntityLivingBase entity) { for (int i = 0; i < 10; i++) { double offsetX = (world.rand.nextDouble() - 0.5) * entity.width; 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..abdcc6f86 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 @@ -37,7 +37,7 @@ public class AbilityHeavyHit extends Ability { this.cooldownTicks = 80; this.windUpTicks = 30; this.activeTicks = 5; - this.recoveryTicks = 20; + this.recoveryTicks = 10; this.telegraphType = TelegraphType.POINT; this.showTelegraph = false; this.windUpSound = "random.anvil_use"; 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..2b224e400 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityLaserShot.java @@ -0,0 +1,202 @@ +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 kamkeel.npcs.entity.EntityAbilityLaser; +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.SubGuiAbilityLaserShot; +import noppes.npcs.client.gui.util.IAbilityConfigCallback; +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 { + + // Laser properties + private float laserWidth = 0.3f; + private float expansionSpeed = 3.0f; + private int lingerTicks = 8; + private float maxDistance = 40.0f; + private int maxLifetime = 100; + + // Combat properties + private float damage = 6.0f; + private float knockback = 0.5f; + private float knockbackUp = 0.05f; + + // Explosion properties + private boolean explosive = false; + private float explosionRadius = 2.0f; + private float explosionDamageFalloff = 0.5f; + + // Effect properties + private int stunDuration = 0; + private int slowDuration = 0; + private int slowLevel = 0; + + // Visual properties + private int innerColor = 0xFFFFFF; + private int outerColor = 0xFF0000; + + 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 = 60; + this.windUpTicks = 15; + this.activeTicks = 1; + this.recoveryTicks = 5; + this.telegraphType = TelegraphType.LINE; + this.showTelegraph = true; + } + + @Override + public boolean hasTypeSettings() { + return true; + } + + @Override + @SideOnly(Side.CLIENT) + public SubGuiAbilityConfig createConfigGui(IAbilityConfigCallback callback) { + return new SubGuiAbilityLaserShot(this, callback); + } + + @Override + public boolean isTargetingModeLocked() { + return false; + } + + @Override + public TargetingMode[] getAllowedTargetingModes() { + return new TargetingMode[]{TargetingMode.AGGRO_TARGET}; + } + + @Override + public float getTelegraphLength() { + return maxDistance; + } + + @Override + public float getTelegraphWidth() { + return laserWidth * 2.0f; // Make telegraph slightly wider for visibility + } + + @Override + public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) { + if (world.isRemote) return; + + double spawnX = npc.posX; + double spawnY = npc.posY + npc.getEyeHeight() * 0.8; + double spawnZ = npc.posZ; + + EntityAbilityLaser laser = new EntityAbilityLaser( + world, npc, target, + spawnX, spawnY, spawnZ, + laserWidth, innerColor, outerColor, + damage, knockback, knockbackUp, + expansionSpeed, lingerTicks, + explosive, explosionRadius, explosionDamageFalloff, + stunDuration, slowDuration, slowLevel, + maxDistance, maxLifetime + ); + + world.spawnEntityInWorld(laser); + } + + @Override + public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { + } + + @Override + public void onComplete(EntityNPCInterface npc, EntityLivingBase target) { + } + + @Override + public void onInterrupt(EntityNPCInterface npc, DamageSource source, float damage) { + } + + @Override + public void writeTypeNBT(NBTTagCompound nbt) { + nbt.setFloat("laserWidth", laserWidth); + nbt.setFloat("expansionSpeed", expansionSpeed); + nbt.setInteger("lingerTicks", lingerTicks); + nbt.setFloat("maxDistance", maxDistance); + nbt.setInteger("maxLifetime", maxLifetime); + nbt.setFloat("damage", damage); + nbt.setFloat("knockback", knockback); + nbt.setFloat("knockbackUp", knockbackUp); + nbt.setBoolean("explosive", explosive); + nbt.setFloat("explosionRadius", explosionRadius); + nbt.setFloat("explosionDamageFalloff", explosionDamageFalloff); + nbt.setInteger("stunDuration", stunDuration); + nbt.setInteger("slowDuration", slowDuration); + nbt.setInteger("slowLevel", slowLevel); + nbt.setInteger("innerColor", innerColor); + nbt.setInteger("outerColor", outerColor); + } + + @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; + this.maxDistance = nbt.hasKey("maxDistance") ? nbt.getFloat("maxDistance") : 40.0f; + this.maxLifetime = nbt.hasKey("maxLifetime") ? nbt.getInteger("maxLifetime") : 100; + this.damage = nbt.hasKey("damage") ? nbt.getFloat("damage") : 6.0f; + this.knockback = nbt.hasKey("knockback") ? nbt.getFloat("knockback") : 0.5f; + this.knockbackUp = nbt.hasKey("knockbackUp") ? nbt.getFloat("knockbackUp") : 0.05f; + this.explosive = nbt.hasKey("explosive") && nbt.getBoolean("explosive"); + this.explosionRadius = nbt.hasKey("explosionRadius") ? nbt.getFloat("explosionRadius") : 2.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; + this.innerColor = nbt.hasKey("innerColor") ? nbt.getInteger("innerColor") : 0xFFFFFF; + this.outerColor = nbt.hasKey("outerColor") ? nbt.getInteger("outerColor") : 0xFF0000; + } + + // Getters & Setters + 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; } + public float getMaxDistance() { return maxDistance; } + public void setMaxDistance(float maxDistance) { this.maxDistance = maxDistance; } + public int getMaxLifetime() { return maxLifetime; } + public void setMaxLifetime(int maxLifetime) { this.maxLifetime = maxLifetime; } + 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 getKnockbackUp() { return knockbackUp; } + public void setKnockbackUp(float knockbackUp) { this.knockbackUp = knockbackUp; } + public boolean isExplosive() { return explosive; } + public void setExplosive(boolean explosive) { this.explosive = explosive; } + public float getExplosionRadius() { return explosionRadius; } + public void setExplosionRadius(float explosionRadius) { this.explosionRadius = explosionRadius; } + public float getExplosionDamageFalloff() { return explosionDamageFalloff; } + public void setExplosionDamageFalloff(float explosionDamageFalloff) { this.explosionDamageFalloff = explosionDamageFalloff; } + public int getStunDuration() { return stunDuration; } + public void setStunDuration(int stunDuration) { this.stunDuration = stunDuration; } + 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 getInnerColor() { return innerColor; } + public void setInnerColor(int innerColor) { this.innerColor = innerColor; } + public int getOuterColor() { return outerColor; } + public void setOuterColor(int outerColor) { this.outerColor = outerColor; } +} 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..ff03ac39f 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 @@ -5,11 +5,9 @@ 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 kamkeel.npcs.entity.EntityAbilityOrb; 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; @@ -17,39 +15,45 @@ import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityOrb; import noppes.npcs.client.gui.util.IAbilityConfigCallback; 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 { + // Movement properties private float orbSpeed = 0.5f; private float orbSize = 1.0f; + private float maxDistance = 30.0f; // Max distance before orb dies + private int maxLifetime = 200; // Ticks the orb lives + + // Combat properties private float damage = 7.0f; private float knockback = 1.0f; - private float maxDistance = 30.0f; - private int maxLifetime = 200; + private float knockbackUp = 0.1f; + // Homing properties private boolean homing = true; private float homingStrength = 0.15f; private float homingRange = 20.0f; + // Explosion properties private boolean explosive = false; private float explosionRadius = 3.0f; private float explosionDamageFalloff = 0.5f; + // Effect properties 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; + // Visual properties + private int innerColor = 0xFFFFFF; // White core + private int outerColor = 0x8888FF; // Light blue glow + private float outerColorWidth = 1.8f; // Multiplier for outer glow size + private boolean outerColorEnabled = true; // Whether to render outer glow + private float rotationSpeed = 4.0f; // Degrees per tick public AbilityOrb() { this.typeId = "ability.cnpc.orb"; @@ -59,7 +63,7 @@ public class AbilityOrb extends Ability { this.minRange = 5.0f; this.cooldownTicks = 100; this.windUpTicks = 30; - this.activeTicks = 200; + this.activeTicks = 1; // Minimal - just spawn and done this.recoveryTicks = 10; this.telegraphType = TelegraphType.POINT; this.showTelegraph = false; @@ -72,8 +76,7 @@ public class AbilityOrb extends Ability { @Override @SideOnly(Side.CLIENT) - public SubGuiAbilityConfig createConfigGui( - IAbilityConfigCallback callback) { + public SubGuiAbilityConfig createConfigGui(IAbilityConfigCallback callback) { return new SubGuiAbilityOrb(this, callback); } @@ -89,203 +92,52 @@ 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 (world.isRemote) return; - 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; - } + // Spawn position at NPC's eye level + double spawnX = npc.posX; + double spawnY = npc.posY + npc.getEyeHeight() * 0.8; + double spawnZ = npc.posZ; + + // Create and spawn the self-managing orb entity + EntityAbilityOrb orb = new EntityAbilityOrb( + world, npc, target, + spawnX, spawnY, spawnZ, + orbSize, innerColor, outerColor, rotationSpeed, + damage, knockback, knockbackUp, + orbSpeed, homing, homingStrength, homingRange, + explosive, explosionRadius, explosionDamageFalloff, + stunDuration, slowDuration, slowLevel, + maxDistance, maxLifetime + ); + + world.spawnEntityInWorld(orb); } @Override public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { - if (hasHit) return; - - ticksAlive++; - - if (ticksAlive >= maxLifetime) { - hasHit = true; - 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; - } - - 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); - } - } - - private void checkCollision(EntityNPCInterface npc, World world) { - AxisAlignedBB hitBox = AxisAlignedBB.getBoundingBox( - orbX - orbSize, orbY - orbSize, orbZ - orbSize, - orbX + orbSize, orbY + orbSize, orbZ + orbSize - ); - - @SuppressWarnings("unchecked") - List entities = world.getEntitiesWithinAABB(EntityLivingBase.class, hitBox); - - 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)); - } + // Nothing to do - entity manages itself } @Override public void onComplete(EntityNPCInterface npc, EntityLivingBase target) { - hasHit = false; - ticksAlive = 0; + // Nothing to clean up - entity manages itself } @Override public void onInterrupt(EntityNPCInterface npc, DamageSource source, float damage) { - hasHit = false; - ticksAlive = 0; + // Nothing to clean up - entity manages itself } @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.setFloat("damage", damage); + nbt.setFloat("knockback", knockback); + nbt.setFloat("knockbackUp", knockbackUp); nbt.setBoolean("homing", homing); nbt.setFloat("homingStrength", homingStrength); nbt.setFloat("homingRange", homingRange); @@ -295,16 +147,22 @@ public class AbilityOrb extends Ability { nbt.setInteger("stunDuration", stunDuration); nbt.setInteger("slowDuration", slowDuration); nbt.setInteger("slowLevel", slowLevel); + nbt.setInteger("innerColor", innerColor); + nbt.setInteger("outerColor", outerColor); + nbt.setFloat("outerColorWidth", outerColorWidth); + nbt.setBoolean("outerColorEnabled", outerColorEnabled); + nbt.setFloat("rotationSpeed", rotationSpeed); } @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.damage = nbt.hasKey("damage") ? nbt.getFloat("damage") : 7.0f; + this.knockback = nbt.hasKey("knockback") ? nbt.getFloat("knockback") : 1.0f; + this.knockbackUp = nbt.hasKey("knockbackUp") ? nbt.getFloat("knockbackUp") : 0.1f; 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; @@ -314,6 +172,11 @@ public class AbilityOrb extends Ability { 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; + this.innerColor = nbt.hasKey("innerColor") ? nbt.getInteger("innerColor") : 0xFFFFFF; + this.outerColor = nbt.hasKey("outerColor") ? nbt.getInteger("outerColor") : 0x8888FF; + this.outerColorWidth = nbt.hasKey("outerColorWidth") ? nbt.getFloat("outerColorWidth") : 1.8f; + this.outerColorEnabled = !nbt.hasKey("outerColorEnabled") || nbt.getBoolean("outerColorEnabled"); + this.rotationSpeed = nbt.hasKey("rotationSpeed") ? nbt.getFloat("rotationSpeed") : 4.0f; } // Getters & Setters @@ -333,6 +196,22 @@ public class AbilityOrb extends Ability { this.orbSize = orbSize; } + public float getMaxDistance() { + return maxDistance; + } + + public void setMaxDistance(float maxDistance) { + this.maxDistance = maxDistance; + } + + public int getMaxLifetime() { + return maxLifetime; + } + + public void setMaxLifetime(int maxLifetime) { + this.maxLifetime = maxLifetime; + } + public float getDamage() { return damage; } @@ -349,20 +228,12 @@ public class AbilityOrb extends Ability { this.knockback = knockback; } - public float getMaxDistance() { - return maxDistance; + public float getKnockbackUp() { + return knockbackUp; } - public void setMaxDistance(float maxDistance) { - this.maxDistance = maxDistance; - } - - public int getMaxLifetime() { - return maxLifetime; - } - - public void setMaxLifetime(int maxLifetime) { - this.maxLifetime = maxLifetime; + public void setKnockbackUp(float knockbackUp) { + this.knockbackUp = knockbackUp; } public boolean isHoming() { @@ -437,20 +308,43 @@ public class AbilityOrb extends Ability { this.slowLevel = slowLevel; } - // Runtime getters for rendering - public double getOrbX() { - return orbX; + public int getInnerColor() { + return innerColor; } - public double getOrbY() { - return orbY; + public void setInnerColor(int innerColor) { + this.innerColor = innerColor; } - public double getOrbZ() { - return orbZ; + public int getOuterColor() { + return outerColor; } - public boolean hasHit() { - return hasHit; + public void setOuterColor(int outerColor) { + this.outerColor = outerColor; + } + + public float getOuterColorWidth() { + return outerColorWidth; + } + + public void setOuterColorWidth(float outerColorWidth) { + this.outerColorWidth = outerColorWidth; + } + + public boolean isOuterColorEnabled() { + return outerColorEnabled; + } + + public void setOuterColorEnabled(boolean outerColorEnabled) { + this.outerColorEnabled = outerColorEnabled; + } + + public float getRotationSpeed() { + return rotationSpeed; + } + + public void setRotationSpeed(float rotationSpeed) { + this.rotationSpeed = rotationSpeed; } } 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..1708b49bd 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 @@ -43,7 +43,7 @@ public class AbilityShockwave extends Ability { this.cooldownTicks = 100; this.windUpTicks = 25; this.activeTicks = 5; - this.recoveryTicks = 20; + this.recoveryTicks = 10; this.telegraphType = TelegraphType.CIRCLE; this.windUpSound = "random.explode"; this.activeSound = "random.explode"; 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..6068b269f 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 @@ -50,7 +50,7 @@ public class AbilitySlam extends Ability { 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.recoveryTicks = 10; this.cooldownTicks = 100; this.lockMovement = true; this.minRange = 2.0f; 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..d1a67eb3a --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySweeper.java @@ -0,0 +1,217 @@ +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.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.client.gui.advanced.SubGuiAbilityConfig; +import noppes.npcs.client.gui.advanced.ability.SubGuiAbilitySweeper; +import noppes.npcs.client.gui.util.IAbilityConfigCallback; +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 { + + // Type-specific parameters + private float beamLength = 10.0f; + private float beamWidth = 0.3f; // Thin like beam trail + private float beamHeight = 0.8f; // 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 int innerColor = 0xFF6600; + private int outerColor = 0xFF0000; + private float outerColorWidth = 1.8f; + private boolean outerColorEnabled = true; + + // 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 = true; + this.cooldownTicks = 200; + this.windUpTicks = 60; + this.activeTicks = 400; // Long duration - entity controls actual end + this.recoveryTicks = 5; // Short recovery + // Circle telegraph to show range + this.telegraphType = TelegraphType.CIRCLE; + this.showTelegraph = true; + } + + @Override + public boolean hasTypeSettings() { + return true; + } + + @Override + @SideOnly(Side.CLIENT) + public SubGuiAbilityConfig createConfigGui(IAbilityConfigCallback callback) { + return new SubGuiAbilitySweeper(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) { + if (!world.isRemote) { + LogWriter.info("[Sweeper] onExecute called at NPC pos " + npc.posX + ", " + npc.posY + ", " + npc.posZ); + + // Spawn the entity that handles BOTH visuals AND damage + activeEntity = new EntityAbilitySweeper(world, npc, target, + beamLength, beamWidth, beamHeight, + innerColor, outerColor, + sweepSpeed, numberOfRotations, + damage, damageInterval, piercing, + lockOnTarget); + world.spawnEntityInWorld(activeEntity); + + LogWriter.info("[Sweeper] Spawned sweeper entity"); + } + } + + @Override + public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { + // Entity handles everything - nothing to do here + // Check if entity died early (entity manages its own death based on rotations) + if (activeEntity != null && activeEntity.isDead) { + // Entity finished, ability can end early + // The ability system will handle the recovery phase + } + } + + @Override + public void reset() { + super.reset(); + + // 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(EntityNPCInterface npc, EntityLivingBase target) { + if (!showTelegraph || telegraphType == TelegraphType.NONE) { + return null; + } + + // Create circle telegraph centered on NPC + 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 NPC, not target + TelegraphInstance instance = new TelegraphInstance(telegraph, npc.posX, npc.posY, npc.posZ, npc.rotationYaw); + instance.setCasterEntityId(npc.getEntityId()); + instance.setEntityIdToFollow(npc.getEntityId()); // Follow NPC + + 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); + nbt.setInteger("innerColor", innerColor); + nbt.setInteger("outerColor", outerColor); + nbt.setFloat("outerColorWidth", outerColorWidth); + nbt.setBoolean("outerColorEnabled", outerColorEnabled); + } + + @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.8f; + 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"); + this.innerColor = nbt.hasKey("innerColor") ? nbt.getInteger("innerColor") : 0xFF6600; + this.outerColor = nbt.hasKey("outerColor") ? nbt.getInteger("outerColor") : 0xFF0000; + this.outerColorWidth = nbt.hasKey("outerColorWidth") ? nbt.getFloat("outerColorWidth") : 1.8f; + this.outerColorEnabled = !nbt.hasKey("outerColorEnabled") || nbt.getBoolean("outerColorEnabled"); + } + + // 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 innerColor; } + public void setInnerColor(int innerColor) { this.innerColor = innerColor; } + public int getOuterColor() { return outerColor; } + public void setOuterColor(int outerColor) { this.outerColor = outerColor; } + public float getOuterColorWidth() { return outerColorWidth; } + public void setOuterColorWidth(float outerColorWidth) { this.outerColorWidth = outerColorWidth; } + public boolean isOuterColorEnabled() { return outerColorEnabled; } + public void setOuterColorEnabled(boolean outerColorEnabled) { this.outerColorEnabled = outerColorEnabled; } +} 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..fa2fefb4c 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 @@ -137,31 +137,6 @@ 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) { armed = false; @@ -181,12 +156,18 @@ public class AbilityTrap extends Ability { 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]; 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..f0e08dd56 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 @@ -53,7 +53,7 @@ public class AbilityVortex extends Ability { this.cooldownTicks = 120; this.windUpTicks = 30; this.activeTicks = 40; - this.recoveryTicks = 20; + this.recoveryTicks = 5; this.telegraphType = TelegraphType.CIRCLE; this.windUpSound = "mob.ghast.charge"; this.activeSound = "mob.ghast.fireball"; 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..47fdf8cbb 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; @@ -77,7 +77,7 @@ 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); } } @@ -91,34 +91,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). */ 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..586e57ae9 --- /dev/null +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java @@ -0,0 +1,428 @@ +package kamkeel.npcs.entity; + +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 noppes.npcs.entity.EntityNPCInterface; + +import java.util.ArrayList; +import java.util.List; + +/** + * 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; + + // 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; + + // Debug logging + private static final boolean DEBUG_LOGGING = true; + + public EntityAbilityBeam(World world) { + super(world); + } + + /** + * Full constructor with all parameters. + */ + public EntityAbilityBeam(World world, EntityNPCInterface owner, EntityLivingBase target, + double x, double y, double z, + float beamWidth, float headSize, int innerColor, int outerColor, float rotationSpeed, + float damage, float knockback, float knockbackUp, + float speed, boolean homing, float homingStrength, float homingRange, + boolean explosive, float explosionRadius, float explosionDamageFalloff, + int stunDuration, int slowDuration, int slowLevel, + float maxDistance, int maxLifetime) { + super(world); + + // Initialize base properties + initProjectile(owner, target, x, y, z, + headSize, innerColor, outerColor, rotationSpeed, + damage, knockback, knockbackUp, + explosive, explosionRadius, explosionDamageFalloff, + stunDuration, slowDuration, slowLevel, + maxDistance, maxLifetime); + + // Beam-specific properties + this.speed = speed; + this.homing = homing; + this.homingStrength = homingStrength; + this.homingRange = homingRange; + this.beamWidth = beamWidth; + this.headSize = headSize; + + // 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=" + maxDistance); + } + + // Calculate initial velocity toward target + if (target != null) { + double dx = target.posX - x; + double dy = (target.posY + target.height * 0.5) - 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; + } + } + + @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() { + // Store previous head offset for interpolation + prevHeadOffsetX = headOffsetX; + prevHeadOffsetY = headOffsetY; + prevHeadOffsetZ = headOffsetZ; + + if (worldObj.isRemote) { + // Client: interpolate entity position, derive head from offset + handleClientInterpolation(); + + // Client also updates head offset to match motion for smooth movement + headOffsetX += motionX; + headOffsetY += motionY; + headOffsetZ += motionZ; + + // 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.height * 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); + } + } + } + + private void addTrailPoint() { + // Trail points are RELATIVE to origin + if (trailPoints.isEmpty()) { + trailPoints.add(Vec3.createVectorHelper(headOffsetX, headOffsetY, headOffsetZ)); + 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)); + + // Limit trail length + while (trailPoints.size() > MAX_TRAIL_POINTS) { + trailPoints.remove(0); + } + } + } + + 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.height * 0.5) - 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); + MovingObjectPosition blockHit = worldObj.rayTraceBlocks(currentPos, nextPos); + + if (blockHit != null && blockHit.typeOfHit == MovingObjectPosition.MovingObjectType.BLOCK) { + if (DEBUG_LOGGING) { + LogWriter.info("[Beam] DEAD: Block collision at tick " + ticksExisted); + } + 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; + } + } + + // ==================== 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; + } + + // ==================== 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"); + + // 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") + )); + } + } + } + + @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); + + // 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); + } +} 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..e46542d8c --- /dev/null +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java @@ -0,0 +1,329 @@ +package kamkeel.npcs.entity; + +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 noppes.npcs.entity.EntityNPCInterface; + +import java.util.List; + +/** + * 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 + + public EntityAbilityDisc(World world) { + super(world); + } + + /** + * Full constructor with all parameters. + */ + public EntityAbilityDisc(World world, EntityNPCInterface owner, EntityLivingBase target, + double x, double y, double z, + float discRadius, float discThickness, int innerColor, int outerColor, float rotationSpeed, + float damage, float knockback, float knockbackUp, + float speed, boolean homing, float homingStrength, float homingRange, + boolean boomerang, int boomerangDelay, + boolean explosive, float explosionRadius, float explosionDamageFalloff, + int stunDuration, int slowDuration, int slowLevel, + float maxDistance, int maxLifetime) { + super(world); + + // Initialize base properties (use discRadius as size for base) + initProjectile(owner, target, x, y, z, + discRadius, innerColor, outerColor, rotationSpeed, + damage, knockback, knockbackUp, + explosive, explosionRadius, explosionDamageFalloff, + stunDuration, slowDuration, slowLevel, + maxDistance, maxLifetime); + + // Disc-specific properties + this.speed = speed; + this.homing = homing; + this.homingStrength = homingStrength; + this.homingRange = 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.height * 0.5) - 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; + } + } + + @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() { + if (worldObj.isRemote) { + handleClientInterpolation(); + } else { + // Server-side logic + if (returning) { + updateReturnToOwner(); + } else { + updateHoming(); + } + + checkBlockCollision(); + checkEntityCollision(); + this.moveEntity(motionX, motionY, motionZ); + + // Boomerang: if we've traveled far without hitting, start returning + if (boomerang && !returning && !hasHit) { + double distTraveled = Math.sqrt( + (posX - startX) * (posX - startX) + + (posY - startY) * (posY - startY) + + (posZ - startZ) * (posZ - startZ) + ); + // Start returning at 80% of max distance or after delay + if (distTraveled >= maxDistance * 0.8 || ticksSinceMiss >= boomerangDelay) { + returning = true; + } + ticksSinceMiss++; + } + } + + // Check wall collision + if (this.isCollidedHorizontally || this.isCollidedVertically) { + if (!worldObj.isRemote) { + hasHit = true; + if (explosive) { + doExplosion(); + } + } + this.setDead(); + } + } + + @Override + protected boolean checkMaxDistance() { + // If boomerang and returning, don't die from max distance + if (boomerang && returning) { + // Die if we get close to owner + 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.0; + } + } + return super.checkMaxDistance(); + } + + 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.height * 0.5) - 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 == null) { + this.setDead(); + return; + } + + double dx = owner.posX - posX; + double dy = (owner.posY + owner.height * 0.5) - posY; + double dz = owner.posZ - posZ; + double dist = Math.sqrt(dx * dx + dy * dy + dz * dz); + + if (dist > 0) { + // Stronger homing when returning + double returnStrength = homingStrength * 2.0; + 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); + MovingObjectPosition blockHit = worldObj.rayTraceBlocks(currentPos, nextPos); + + if (blockHit != null && blockHit.typeOfHit == MovingObjectPosition.MovingObjectType.BLOCK) { + hasHit = true; + if (explosive) { + 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; + } + } + + // ==================== 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; + } + + // ==================== 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"); + } + + @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); + } +} 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..02b797524 --- /dev/null +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java @@ -0,0 +1,326 @@ +package kamkeel.npcs.entity; + +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 noppes.npcs.entity.EntityNPCInterface; + +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; + + public EntityAbilityLaser(World world) { + super(world); + } + + /** + * Full constructor with all parameters. + */ + public EntityAbilityLaser(World world, EntityNPCInterface owner, EntityLivingBase target, + double x, double y, double z, + float laserWidth, int innerColor, int outerColor, + float damage, float knockback, float knockbackUp, + float expansionSpeed, int lingerTicks, + boolean explosive, float explosionRadius, float explosionDamageFalloff, + int stunDuration, int slowDuration, int slowLevel, + float maxDistance, int maxLifetime) { + super(world); + + // Initialize base properties + initProjectile(owner, target, x, y, z, + laserWidth, innerColor, outerColor, 0.0f, // No rotation for laser + damage, knockback, knockbackUp, + explosive, explosionRadius, explosionDamageFalloff, + stunDuration, slowDuration, slowLevel, + maxDistance, maxLifetime); + + // 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.height * 0.5) - 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; + } + + @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 (!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(); + } + } + } + + private void checkBlockCollision() { + Vec3 start = Vec3.createVectorHelper(startX, startY, startZ); + Vec3 end = Vec3.createVectorHelper(endX, endY, endZ); + MovingObjectPosition blockHit = worldObj.rayTraceBlocks(start, end); + + 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; + } + } + } + + 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"); + } + + @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); + } +} 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..c218007a0 --- /dev/null +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java @@ -0,0 +1,225 @@ +package kamkeel.npcs.entity; + +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 noppes.npcs.entity.EntityNPCInterface; + +import java.util.List; + +/** + * 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; + + public EntityAbilityOrb(World world) { + super(world); + } + + /** + * Full constructor with all parameters. + */ + public EntityAbilityOrb(World world, EntityNPCInterface owner, EntityLivingBase target, + double x, double y, double z, + float orbSize, int innerColor, int outerColor, float rotationSpeed, + float damage, float knockback, float knockbackUp, + float speed, boolean homing, float homingStrength, float homingRange, + boolean explosive, float explosionRadius, float explosionDamageFalloff, + int stunDuration, int slowDuration, int slowLevel, + float maxDistance, int maxLifetime) { + super(world); + + // Initialize base properties + initProjectile(owner, target, x, y, z, + orbSize, innerColor, outerColor, rotationSpeed, + damage, knockback, knockbackUp, + explosive, explosionRadius, explosionDamageFalloff, + stunDuration, slowDuration, slowLevel, + maxDistance, maxLifetime); + + // Orb-specific properties + this.speed = speed; + this.homing = homing; + this.homingStrength = homingStrength; + this.homingRange = homingRange; + + // Calculate initial velocity toward target + if (target != null) { + double dx = target.posX - x; + double dy = (target.posY + target.height * 0.5) - 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 { + // 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) * speed; + this.motionY = -Math.sin(pitch) * speed; + this.motionZ = Math.cos(yaw) * Math.cos(pitch) * speed; + } + } + + @Override + protected void updateProjectile() { + 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(); + } + } + + 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.height * 0.5) - 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); + MovingObjectPosition blockHit = worldObj.rayTraceBlocks(currentPos, nextPos); + + if (blockHit != null && blockHit.typeOfHit == MovingObjectPosition.MovingObjectType.BLOCK) { + hasHit = true; + if (explosive) { + 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; + } + } + + // ==================== 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; + } + + // ==================== 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; + } + + @Override + protected void writeProjectileNBT(NBTTagCompound nbt) { + nbt.setFloat("OrbSpeed", speed); + nbt.setBoolean("Homing", homing); + nbt.setFloat("HomingStrength", homingStrength); + nbt.setFloat("HomingRange", homingRange); + } +} 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..fa84d933f --- /dev/null +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityProjectile.java @@ -0,0 +1,570 @@ +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 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.Vec3; +import net.minecraft.world.World; +import noppes.npcs.NpcDamageSource; +import noppes.npcs.entity.EntityNPCInterface; + +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 float rotationSpeed = 4.0f; + + // ==================== COMBAT PROPERTIES ==================== + protected float damage = 7.0f; + protected float knockback = 1.0f; + protected float knockbackUp = 0.1f; + + // ==================== EFFECT PROPERTIES ==================== + protected int stunDuration = 0; + protected int slowDuration = 0; + protected int slowLevel = 0; + + // ==================== EXPLOSION PROPERTIES ==================== + protected boolean explosive = false; + protected float explosionRadius = 3.0f; + protected float explosionDamageFalloff = 0.5f; + + // ==================== 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; + + // ==================== 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. Call from subclass constructors. + */ + protected void initProjectile(EntityNPCInterface owner, EntityLivingBase target, + double x, double y, double z, + float size, int innerColor, int outerColor, float rotationSpeed, + float damage, float knockback, float knockbackUp, + boolean explosive, float explosionRadius, float explosionDamageFalloff, + int stunDuration, int slowDuration, int slowLevel, + float maxDistance, int maxLifetime) { + 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 = innerColor; + this.outerColor = outerColor; + this.rotationSpeed = rotationSpeed; + + // Combat + this.damage = damage; + this.knockback = knockback; + this.knockbackUp = knockbackUp; + + // Explosion + this.explosive = explosive; + this.explosionRadius = explosionRadius; + this.explosionDamageFalloff = explosionDamageFalloff; + + // Effects + this.stunDuration = stunDuration; + this.slowDuration = slowDuration; + this.slowLevel = slowLevel; + + // Lifespan + this.maxDistance = maxDistance; + this.maxLifetime = maxLifetime; + // deathWorldTime will be set on first tick when world is available + + // 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; + + super.onUpdate(); + + // Update rotation + updateRotation(); + + // Lerp render size toward actual size + this.renderCurrentSize = this.renderCurrentSize + (this.size - this.renderCurrentSize) * 0.15f; + + // Set death time on first tick if not already set (handles chunk load/unload) + if (deathWorldTime < 0 && worldObj != null) { + deathWorldTime = worldObj.getTotalWorldTime() + maxLifetime; + } + + // Check lifespan using world time (survives chunk unload/reload) + if (deathWorldTime > 0 && worldObj.getTotalWorldTime() >= deathWorldTime) { + if (!worldObj.isRemote) { + noppes.npcs.LogWriter.info("[Projectile:" + this.getClass().getSimpleName() + "] DEAD: Lifetime exceeded. worldTime=" + worldObj.getTotalWorldTime() + " deathTime=" + deathWorldTime); + } + this.setDead(); + return; + } + + // Check max distance (subclass can override if needed) + if (checkMaxDistance()) { + this.setDead(); + return; + } + + if (hasHit) { + if (!worldObj.isRemote) { + noppes.npcs.LogWriter.info("[Projectile:" + this.getClass().getSimpleName() + "] DEAD: hasHit=true at tick " + ticksExisted); + } + 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); + } + } + + // ==================== DAMAGE & EFFECTS ==================== + + protected void applyDamage(EntityLivingBase target) { + applyDamage(target, this.damage, this.knockback); + } + + protected void applyDamage(EntityLivingBase target, float dmg, float kb) { + Entity owner = getOwner(); + EntityNPCInterface npc = (owner instanceof EntityNPCInterface) ? (EntityNPCInterface) owner : null; + + if (npc != null) { + target.attackEntityFrom(new NpcDamageSource("npc_ability", npc), 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) { + 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)); + } + } + + protected void doExplosion() { + 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() { + if (ownerEntityId == -1) return null; + return worldObj.getEntityByID(ownerEntityId); + } + + 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 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; + } + + // ==================== 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.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"); + + this.stunDuration = nbt.getInteger("StunDuration"); + this.slowDuration = nbt.getInteger("SlowDuration"); + this.slowLevel = nbt.getInteger("SlowLevel"); + + 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"); + + 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.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); + + nbt.setInteger("StunDuration", stunDuration); + nbt.setInteger("SlowDuration", slowDuration); + nbt.setInteger("SlowLevel", slowLevel); + + 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); + } + + /** + * 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..a5758a2ed --- /dev/null +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilitySweeper.java @@ -0,0 +1,437 @@ +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 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.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; + + // 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, EntityNPCInterface owner, EntityLivingBase target, + float beamLength, float beamWidth, float beamHeight, + int innerColor, int outerColor, + 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 = innerColor; + this.outerColor = outerColor; + 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) { + EntityNPCInterface npc = (owner instanceof EntityNPCInterface) ? (EntityNPCInterface) owner : null; + + if (npc != null) { + target.attackEntityFrom(new NpcDamageSource("npc_ability", npc), 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; + } + + /** + * 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.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.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.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.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/noppes/npcs/CustomNpcs.java b/src/main/java/noppes/npcs/CustomNpcs.java index ed084c578..0005341f8 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; @@ -276,6 +281,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(); diff --git a/src/main/java/noppes/npcs/DataAbilities.java b/src/main/java/noppes/npcs/DataAbilities.java index 8d390f5cc..51d2831ac 100644 --- a/src/main/java/noppes/npcs/DataAbilities.java +++ b/src/main/java/noppes/npcs/DataAbilities.java @@ -3,6 +3,7 @@ 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.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; @@ -444,7 +445,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 +455,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,6 +475,18 @@ 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. */ @@ -553,7 +574,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, @@ -686,16 +707,6 @@ public class DataAbilities { } } - /** - * Check if NPC movement should be blocked due to ability execution. - * - * @deprecated Use isAbilityControllingMovement() instead - */ - @Deprecated - public boolean isMovementBlocked() { - return isAbilityControllingMovement(); - } - /** * Check if NPC should skip normal attacks during ability execution. */ 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 95dceaec2..534963e38 100644 --- a/src/main/java/noppes/npcs/client/ClientProxy.java +++ b/src/main/java/noppes/npcs/client/ClientProxy.java @@ -144,6 +144,16 @@ 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.RenderAbilityOrbCube; +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.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; @@ -251,6 +261,12 @@ 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(EntityAbilityOrb.class, new RenderAbilityOrbCube()); + 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()); 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..5f03cbd9f 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java @@ -42,6 +42,7 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi protected static final int TAB_TYPE = 1; protected static final int TAB_TARGET = 2; protected static final int TAB_EFFECTS = 3; + protected static final int TAB_VISUAL = 4; // Core references protected final Ability ability; @@ -98,6 +99,7 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi protected final boolean supportsTelegraph; protected final boolean targetingModeLocked; protected final boolean hasTypeSettings; + protected final boolean hasVisualSettings; public SubGuiAbilityConfig(Ability ability, IAbilityConfigCallback callback) { this.ability = ability; @@ -138,6 +140,7 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi this.supportsTelegraph = defaultType != null && defaultType != TelegraphType.NONE; this.targetingModeLocked = ability.isTargetingModeLocked(); this.hasTypeSettings = ability.hasTypeSettings(); + this.hasVisualSettings = hasVisualSettings(); setBackground("menubg.png"); xSize = 356; @@ -172,6 +175,13 @@ 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; + + if (hasVisualSettings) { + GuiMenuTopButton visualTab = new GuiMenuTopButton(94, lastTab, "ability.tab.visual"); + visualTab.active = (activeTab == TAB_VISUAL); + addTopButton(visualTab); + } // 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"); @@ -195,15 +205,20 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi case TAB_EFFECTS: initEffectsTab(contentY); break; + case TAB_VISUAL: + initVisualTab(contentY); + break; } // ═══════════════════════════════════════════════════════════════════════ - // BOTTOM ROW - Enabled toggle + // BOTTOM ROW - Enabled toggle (only on General tab) // ═══════════════════════════════════════════════════════════════════════ - 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); + if (activeTab == TAB_GENERAL) { + 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); + } } // ═══════════════════════════════════════════════════════════════════════════ @@ -310,6 +325,45 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi // Default: no-op } + // ═══════════════════════════════════════════════════════════════════════════ + // VISUAL TAB - Override in subclasses for visual settings (colors, etc.) + // ═══════════════════════════════════════════════════════════════════════════ + + /** + * Override to return true if this ability has visual settings (colors, glow, etc.). + * When true, a Visual tab will be shown in the GUI. + */ + protected boolean hasVisualSettings() { + return false; + } + + /** + * Override this method to render visual settings in the Visual tab. + * Use button/text field IDs starting at 200 for visual settings. + * + * @param startY The Y position to start rendering from + */ + protected void initVisualTab(int startY) { + // Default: show message that no visual settings are available + addLabel(new GuiNpcLabel(200, "ability.noVisualSettings", guiLeft + 8, startY + 5)); + } + + /** + * Override this method to handle button clicks for visual-specific buttons. + * Button IDs 200+ are reserved for visual-specific use. + */ + protected void handleVisualButton(int id, GuiNpcButton button) { + // Default: no-op + } + + /** + * Override this method to handle text field changes for visual-specific fields. + * Field IDs 200+ are reserved for visual-specific use. + */ + protected void handleVisualTextField(int id, GuiNpcTextField field) { + // Default: no-op + } + // ═══════════════════════════════════════════════════════════════════════════ // TARGET TAB // ═══════════════════════════════════════════════════════════════════════════ @@ -563,6 +617,11 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi initGui(); return; } + if (id == 94 && hasVisualSettings) { + activeTab = TAB_VISUAL; + initGui(); + return; + } // Enabled button (bottom left) if (id == 2) { @@ -668,7 +727,11 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi initGui(); } - // Type-specific buttons (100+) - delegate to subclass + // Visual-specific buttons (200+) - delegate to subclass + else if (id >= 200) { + handleVisualButton(id, (GuiNpcButton) guibutton); + } + // Type-specific buttons (100-199) - delegate to subclass else if (id >= 100) { handleTypeButton(id, (GuiNpcButton) guibutton); } @@ -707,7 +770,11 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi recoveryTicks = textField.getInteger(); } - // Type-specific fields (100+) - delegate to subclass + // Visual-specific fields (200+) - delegate to subclass + else if (id >= 200) { + handleVisualTextField(id, textField); + } + // Type-specific fields (100-199) - delegate to subclass else if (id >= 100) { handleTypeTextField(id, textField); } 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/SubGuiAbilityDisc.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDisc.java new file mode 100644 index 000000000..ec80ed5d6 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDisc.java @@ -0,0 +1,227 @@ +package noppes.npcs.client.gui.advanced.ability; + +import kamkeel.npcs.controllers.data.ability.type.AbilityDisc; +import noppes.npcs.client.gui.SubGuiColorSelector; +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; +import noppes.npcs.client.gui.util.SubGuiInterface; + +/** + * GUI for configuring Disc ability type-specific settings. + */ +public class SubGuiAbilityDisc extends SubGuiAbilityConfig { + + private final AbilityDisc disc; + private int editingVisualColorId = 0; + + public SubGuiAbilityDisc(AbilityDisc ability, IAbilityConfigCallback callback) { + super(ability, callback); + this.disc = ability; + } + + @Override + protected boolean hasVisualSettings() { + return true; + } + + @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, disc.getDamage())); + + addLabel(new GuiNpcLabel(101, "stats.speed", col2LabelX, y + 5)); + addTextField(createFloatField(101, col2FieldX, y, 50, disc.getSpeed())); + + y += 24; + + // Row 2: Disc Radius + Disc Thickness + addLabel(new GuiNpcLabel(102, "ability.discRadius", labelX, y + 5)); + addTextField(createFloatField(102, fieldX, y, 50, disc.getDiscRadius())); + + addLabel(new GuiNpcLabel(103, "ability.discThickness", col2LabelX, y + 5)); + addTextField(createFloatField(103, col2FieldX, y, 50, disc.getDiscThickness())); + + 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"}, disc.isHoming() ? 1 : 0)); + + addLabel(new GuiNpcLabel(105, "ability.homingStr", col2LabelX, y + 5)); + addTextField(createFloatField(105, col2FieldX, y, 50, disc.getHomingStrength())); + + y += 24; + + // Row 4: Boomerang + Boomerang Delay + addLabel(new GuiNpcLabel(106, "ability.boomerang", labelX, y + 5)); + addButton(new GuiNpcButton(106, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, disc.isBoomerang() ? 1 : 0)); + + addLabel(new GuiNpcLabel(107, "ability.boomerangDelay", col2LabelX, y + 5)); + addTextField(createIntField(107, col2FieldX, y, 50, disc.getBoomerangDelay())); + + y += 24; + + // Row 5: Explosive + Explosion Radius + addLabel(new GuiNpcLabel(108, "ability.explosive", labelX, y + 5)); + addButton(new GuiNpcButton(108, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, disc.isExplosive() ? 1 : 0)); + + addLabel(new GuiNpcLabel(109, "ability.explosionRad", col2LabelX, y + 5)); + addTextField(createFloatField(109, col2FieldX, y, 50, disc.getExplosionRadius())); + + y += 24; + + // Row 6: Knockback + Max Distance + addLabel(new GuiNpcLabel(110, "ability.knockback", labelX, y + 5)); + addTextField(createFloatField(110, fieldX, y, 50, disc.getKnockback())); + + addLabel(new GuiNpcLabel(111, "ability.maxDist", col2LabelX, y + 5)); + addTextField(createFloatField(111, col2FieldX, y, 50, disc.getMaxDistance())); + } + + @Override + protected void initVisualTab(int startY) { + int y = startY; + int labelX = guiLeft + 8; + int fieldX = guiLeft + 85; + int col2LabelX = guiLeft + 180; + int col2FieldX = guiLeft + 260; + + // Row 1: Inner Color + Outer Color + addLabel(new GuiNpcLabel(200, "ability.innerColor", labelX, y + 5)); + String innerHex = String.format("%06X", disc.getInnerColor() & 0xFFFFFF); + GuiNpcButton innerColorBtn = new GuiNpcButton(200, fieldX, y, 55, 20, innerHex); + innerColorBtn.setTextColor(disc.getInnerColor() & 0xFFFFFF); + addButton(innerColorBtn); + + addLabel(new GuiNpcLabel(201, "ability.outerColor", col2LabelX, y + 5)); + String outerHex = String.format("%06X", disc.getOuterColor() & 0xFFFFFF); + GuiNpcButton outerColorBtn = new GuiNpcButton(201, col2FieldX, y, 55, 20, outerHex); + outerColorBtn.setTextColor(disc.getOuterColor() & 0xFFFFFF); + outerColorBtn.setEnabled(disc.isOuterColorEnabled()); + addButton(outerColorBtn); + + y += 24; + + // Row 2: Outer Color Enabled + Outer Color Width + addLabel(new GuiNpcLabel(202, "ability.outerEnabled", labelX, y + 5)); + addButton(new GuiNpcButton(202, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, disc.isOuterColorEnabled() ? 1 : 0)); + + addLabel(new GuiNpcLabel(203, "ability.outerWidth", col2LabelX, y + 5)); + GuiNpcTextField widthField = createFloatField(203, col2FieldX, y, 55, disc.getOuterColorWidth()); + widthField.setEnabled(disc.isOuterColorEnabled()); + addTextField(widthField); + + y += 24; + + // Row 3: Rotation Speed + addLabel(new GuiNpcLabel(204, "ability.rotationSpeed", labelX, y + 5)); + addTextField(createFloatField(204, fieldX, y, 50, disc.getRotationSpeed())); + } + + @Override + protected void handleTypeButton(int id, GuiNpcButton button) { + int value = button.getValue(); + switch (id) { + case 104: + disc.setHoming(value == 1); + break; + case 106: + disc.setBoomerang(value == 1); + break; + case 108: + disc.setExplosive(value == 1); + break; + } + } + + @Override + protected void handleVisualButton(int id, GuiNpcButton button) { + int value = button.getValue(); + switch (id) { + case 200: + editingVisualColorId = 200; + setSubGui(new SubGuiColorSelector(disc.getInnerColor())); + break; + case 201: + editingVisualColorId = 201; + setSubGui(new SubGuiColorSelector(disc.getOuterColor())); + break; + case 202: + disc.setOuterColorEnabled(value == 1); + initGui(); + break; + } + } + + @Override + public void subGuiClosed(SubGuiInterface subgui) { + if (subgui instanceof SubGuiColorSelector && editingVisualColorId != 0) { + SubGuiColorSelector colorSelector = (SubGuiColorSelector) subgui; + int rgb = colorSelector.color & 0x00FFFFFF; + if (editingVisualColorId == 200) { + disc.setInnerColor(rgb); + } else if (editingVisualColorId == 201) { + disc.setOuterColor(rgb); + } + editingVisualColorId = 0; + initGui(); + } else { + super.subGuiClosed(subgui); + } + } + + @Override + protected void handleTypeTextField(int id, GuiNpcTextField field) { + switch (id) { + case 100: + disc.setDamage(parseFloat(field, disc.getDamage())); + break; + case 101: + disc.setSpeed(parseFloat(field, disc.getSpeed())); + break; + case 102: + disc.setDiscRadius(parseFloat(field, disc.getDiscRadius())); + break; + case 103: + disc.setDiscThickness(parseFloat(field, disc.getDiscThickness())); + break; + case 105: + disc.setHomingStrength(parseFloat(field, disc.getHomingStrength())); + break; + case 107: + disc.setBoomerangDelay(field.getInteger()); + break; + case 109: + disc.setExplosionRadius(parseFloat(field, disc.getExplosionRadius())); + break; + case 110: + disc.setKnockback(parseFloat(field, disc.getKnockback())); + break; + case 111: + disc.setMaxDistance(parseFloat(field, disc.getMaxDistance())); + break; + } + } + + @Override + protected void handleVisualTextField(int id, GuiNpcTextField field) { + switch (id) { + case 203: + disc.setOuterColorWidth(parseFloat(field, disc.getOuterColorWidth())); + break; + case 204: + disc.setRotationSpeed(parseFloat(field, disc.getRotationSpeed())); + break; + } + } +} diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityEnergyBeam.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityEnergyBeam.java new file mode 100644 index 000000000..afb89ffed --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityEnergyBeam.java @@ -0,0 +1,228 @@ +package noppes.npcs.client.gui.advanced.ability; + +import kamkeel.npcs.controllers.data.ability.type.AbilityEnergyBeam; +import noppes.npcs.client.gui.SubGuiColorSelector; +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; +import noppes.npcs.client.gui.util.SubGuiInterface; + +/** + * GUI for configuring Energy Beam ability type-specific settings. + * Energy Beam is a homing head with trailing path (DBZ-style). + */ +public class SubGuiAbilityEnergyBeam extends SubGuiAbilityConfig { + + private final AbilityEnergyBeam beam; + private int editingVisualColorId = 0; + + public SubGuiAbilityEnergyBeam(AbilityEnergyBeam ability, IAbilityConfigCallback callback) { + super(ability, callback); + this.beam = ability; + } + + @Override + protected boolean hasVisualSettings() { + return true; + } + + @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, beam.getDamage())); + + addLabel(new GuiNpcLabel(101, "stats.speed", col2LabelX, y + 5)); + addTextField(createFloatField(101, col2FieldX, y, 50, beam.getSpeed())); + + y += 24; + + // Row 2: Beam Width + Head Size + addLabel(new GuiNpcLabel(102, "ability.beamWidth", labelX, y + 5)); + addTextField(createFloatField(102, fieldX, y, 50, beam.getBeamWidth())); + + addLabel(new GuiNpcLabel(103, "ability.headSize", col2LabelX, y + 5)); + addTextField(createFloatField(103, col2FieldX, y, 50, beam.getHeadSize())); + + 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"}, beam.isHoming() ? 1 : 0)); + + addLabel(new GuiNpcLabel(105, "ability.homingStr", col2LabelX, y + 5)); + addTextField(createFloatField(105, col2FieldX, y, 50, beam.getHomingStrength())); + + y += 24; + + // Row 4: Homing Range + Max Distance + addLabel(new GuiNpcLabel(106, "ability.homingRange", labelX, y + 5)); + addTextField(createFloatField(106, fieldX, y, 50, beam.getHomingRange())); + + addLabel(new GuiNpcLabel(107, "ability.maxDist", col2LabelX, y + 5)); + addTextField(createFloatField(107, col2FieldX, y, 50, beam.getMaxDistance())); + + y += 24; + + // Row 5: Explosive + Explosion Radius + addLabel(new GuiNpcLabel(108, "ability.explosive", labelX, y + 5)); + addButton(new GuiNpcButton(108, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, beam.isExplosive() ? 1 : 0)); + + addLabel(new GuiNpcLabel(109, "ability.explosionRad", col2LabelX, y + 5)); + addTextField(createFloatField(109, col2FieldX, y, 50, beam.getExplosionRadius())); + + y += 24; + + // Row 6: Knockback + Max Lifetime + addLabel(new GuiNpcLabel(110, "ability.knockback", labelX, y + 5)); + addTextField(createFloatField(110, fieldX, y, 50, beam.getKnockback())); + + addLabel(new GuiNpcLabel(111, "ability.lifetime", col2LabelX, y + 5)); + addTextField(createIntField(111, col2FieldX, y, 50, beam.getMaxLifetime())); + } + + @Override + protected void initVisualTab(int startY) { + int y = startY; + int labelX = guiLeft + 8; + int fieldX = guiLeft + 85; + int col2LabelX = guiLeft + 180; + int col2FieldX = guiLeft + 260; + + // Row 1: Inner Color + Outer Color + addLabel(new GuiNpcLabel(200, "ability.innerColor", labelX, y + 5)); + String innerHex = String.format("%06X", beam.getInnerColor() & 0xFFFFFF); + GuiNpcButton innerColorBtn = new GuiNpcButton(200, fieldX, y, 55, 20, innerHex); + innerColorBtn.setTextColor(beam.getInnerColor() & 0xFFFFFF); + addButton(innerColorBtn); + + addLabel(new GuiNpcLabel(201, "ability.outerColor", col2LabelX, y + 5)); + String outerHex = String.format("%06X", beam.getOuterColor() & 0xFFFFFF); + GuiNpcButton outerColorBtn = new GuiNpcButton(201, col2FieldX, y, 55, 20, outerHex); + outerColorBtn.setTextColor(beam.getOuterColor() & 0xFFFFFF); + outerColorBtn.setEnabled(beam.isOuterColorEnabled()); + addButton(outerColorBtn); + + y += 24; + + // Row 2: Outer Color Enabled + Outer Color Width + addLabel(new GuiNpcLabel(202, "ability.outerEnabled", labelX, y + 5)); + addButton(new GuiNpcButton(202, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, beam.isOuterColorEnabled() ? 1 : 0)); + + addLabel(new GuiNpcLabel(203, "ability.outerWidth", col2LabelX, y + 5)); + GuiNpcTextField widthField = createFloatField(203, col2FieldX, y, 55, beam.getOuterColorWidth()); + widthField.setEnabled(beam.isOuterColorEnabled()); + addTextField(widthField); + + y += 24; + + // Row 3: Rotation Speed + addLabel(new GuiNpcLabel(204, "ability.rotationSpeed", labelX, y + 5)); + addTextField(createFloatField(204, fieldX, y, 50, beam.getRotationSpeed())); + } + + @Override + protected void handleTypeButton(int id, GuiNpcButton button) { + int value = button.getValue(); + switch (id) { + case 104: + beam.setHoming(value == 1); + break; + case 108: + beam.setExplosive(value == 1); + break; + } + } + + @Override + protected void handleVisualButton(int id, GuiNpcButton button) { + int value = button.getValue(); + switch (id) { + case 200: + editingVisualColorId = 200; + setSubGui(new SubGuiColorSelector(beam.getInnerColor())); + break; + case 201: + editingVisualColorId = 201; + setSubGui(new SubGuiColorSelector(beam.getOuterColor())); + break; + case 202: + beam.setOuterColorEnabled(value == 1); + initGui(); + break; + } + } + + @Override + public void subGuiClosed(SubGuiInterface subgui) { + if (subgui instanceof SubGuiColorSelector && editingVisualColorId != 0) { + SubGuiColorSelector colorSelector = (SubGuiColorSelector) subgui; + int rgb = colorSelector.color & 0x00FFFFFF; + if (editingVisualColorId == 200) { + beam.setInnerColor(rgb); + } else if (editingVisualColorId == 201) { + beam.setOuterColor(rgb); + } + editingVisualColorId = 0; + initGui(); + } else { + super.subGuiClosed(subgui); + } + } + + @Override + protected void handleTypeTextField(int id, GuiNpcTextField field) { + switch (id) { + case 100: + beam.setDamage(parseFloat(field, beam.getDamage())); + break; + case 101: + beam.setSpeed(parseFloat(field, beam.getSpeed())); + break; + case 102: + beam.setBeamWidth(parseFloat(field, beam.getBeamWidth())); + break; + case 103: + beam.setHeadSize(parseFloat(field, beam.getHeadSize())); + break; + case 105: + beam.setHomingStrength(parseFloat(field, beam.getHomingStrength())); + break; + case 106: + beam.setHomingRange(parseFloat(field, beam.getHomingRange())); + break; + case 107: + beam.setMaxDistance(parseFloat(field, beam.getMaxDistance())); + break; + case 109: + beam.setExplosionRadius(parseFloat(field, beam.getExplosionRadius())); + break; + case 110: + beam.setKnockback(parseFloat(field, beam.getKnockback())); + break; + case 111: + beam.setMaxLifetime(field.getInteger()); + break; + } + } + + @Override + protected void handleVisualTextField(int id, GuiNpcTextField field) { + switch (id) { + case 203: + beam.setOuterColorWidth(parseFloat(field, beam.getOuterColorWidth())); + break; + case 204: + beam.setRotationSpeed(parseFloat(field, beam.getRotationSpeed())); + break; + } + } +} diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityLaserShot.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityLaserShot.java new file mode 100644 index 000000000..6f5c8ea57 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityLaserShot.java @@ -0,0 +1,159 @@ +package noppes.npcs.client.gui.advanced.ability; + +import kamkeel.npcs.controllers.data.ability.type.AbilityLaserShot; +import noppes.npcs.client.gui.SubGuiColorSelector; +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; +import noppes.npcs.client.gui.util.SubGuiInterface; + +/** + * GUI for configuring Laser Shot ability type-specific settings. + */ +public class SubGuiAbilityLaserShot extends SubGuiAbilityConfig { + + private final AbilityLaserShot laser; + private int editingTypeColorId = 0; + + public SubGuiAbilityLaserShot(AbilityLaserShot ability, IAbilityConfigCallback callback) { + super(ability, callback); + this.laser = 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 + Laser Width + addLabel(new GuiNpcLabel(100, "enchantment.damage", labelX, y + 5)); + addTextField(createFloatField(100, fieldX, y, 50, laser.getDamage())); + + addLabel(new GuiNpcLabel(101, "ability.laserWidth", col2LabelX, y + 5)); + addTextField(createFloatField(101, col2FieldX, y, 50, laser.getLaserWidth())); + + y += 24; + + // Row 2: Expansion Speed + Linger Ticks + addLabel(new GuiNpcLabel(102, "ability.expansionSpeed", labelX, y + 5)); + addTextField(createFloatField(102, fieldX, y, 50, laser.getExpansionSpeed())); + + addLabel(new GuiNpcLabel(103, "ability.lingerTicks", col2LabelX, y + 5)); + addTextField(createIntField(103, col2FieldX, y, 50, laser.getLingerTicks())); + + y += 24; + + // Row 3: Max Distance + Max Lifetime + addLabel(new GuiNpcLabel(104, "ability.maxDist", labelX, y + 5)); + addTextField(createFloatField(104, fieldX, y, 50, laser.getMaxDistance())); + + addLabel(new GuiNpcLabel(105, "ability.lifetime", col2LabelX, y + 5)); + addTextField(createIntField(105, col2FieldX, y, 50, laser.getMaxLifetime())); + + 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"}, laser.isExplosive() ? 1 : 0)); + + addLabel(new GuiNpcLabel(107, "ability.explosionRad", col2LabelX, y + 5)); + addTextField(createFloatField(107, col2FieldX, y, 50, laser.getExplosionRadius())); + + y += 24; + + // Row 5: Knockback + Knockback Up + addLabel(new GuiNpcLabel(108, "ability.knockback", labelX, y + 5)); + addTextField(createFloatField(108, fieldX, y, 50, laser.getKnockback())); + + addLabel(new GuiNpcLabel(109, "ability.knockbackUp", col2LabelX, y + 5)); + addTextField(createFloatField(109, col2FieldX, y, 50, laser.getKnockbackUp())); + + y += 24; + + // Row 6: Inner Color + Outer Color + addLabel(new GuiNpcLabel(120, "ability.innerColor", labelX, y + 5)); + String innerHex = String.format("%06X", laser.getInnerColor() & 0xFFFFFF); + GuiNpcButton innerColorBtn = new GuiNpcButton(120, fieldX, y, 50, 20, innerHex); + innerColorBtn.setTextColor(laser.getInnerColor() & 0xFFFFFF); + addButton(innerColorBtn); + + addLabel(new GuiNpcLabel(121, "ability.outerColor", col2LabelX, y + 5)); + String outerHex = String.format("%06X", laser.getOuterColor() & 0xFFFFFF); + GuiNpcButton outerColorBtn = new GuiNpcButton(121, col2FieldX, y, 50, 20, outerHex); + outerColorBtn.setTextColor(laser.getOuterColor() & 0xFFFFFF); + addButton(outerColorBtn); + } + + @Override + protected void handleTypeButton(int id, GuiNpcButton button) { + int value = button.getValue(); + switch (id) { + case 106: + laser.setExplosive(value == 1); + break; + case 120: + editingTypeColorId = 120; + setSubGui(new SubGuiColorSelector(laser.getInnerColor())); + break; + case 121: + editingTypeColorId = 121; + setSubGui(new SubGuiColorSelector(laser.getOuterColor())); + break; + } + } + + @Override + public void subGuiClosed(SubGuiInterface subgui) { + if (subgui instanceof SubGuiColorSelector && editingTypeColorId != 0) { + SubGuiColorSelector colorSelector = (SubGuiColorSelector) subgui; + int rgb = colorSelector.color & 0x00FFFFFF; + if (editingTypeColorId == 120) { + laser.setInnerColor(rgb); + } else if (editingTypeColorId == 121) { + laser.setOuterColor(rgb); + } + editingTypeColorId = 0; + initGui(); + } else { + super.subGuiClosed(subgui); + } + } + + @Override + protected void handleTypeTextField(int id, GuiNpcTextField field) { + switch (id) { + case 100: + laser.setDamage(parseFloat(field, laser.getDamage())); + break; + case 101: + laser.setLaserWidth(parseFloat(field, laser.getLaserWidth())); + break; + case 102: + laser.setExpansionSpeed(parseFloat(field, laser.getExpansionSpeed())); + break; + case 103: + laser.setLingerTicks(field.getInteger()); + break; + case 104: + laser.setMaxDistance(parseFloat(field, laser.getMaxDistance())); + break; + case 105: + laser.setMaxLifetime(field.getInteger()); + break; + case 107: + laser.setExplosionRadius(parseFloat(field, laser.getExplosionRadius())); + break; + case 108: + laser.setKnockback(parseFloat(field, laser.getKnockback())); + break; + case 109: + laser.setKnockbackUp(parseFloat(field, laser.getKnockbackUp())); + 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 index f94e8d0cf..9f5ac4caf 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityOrb.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityOrb.java @@ -1,11 +1,13 @@ package noppes.npcs.client.gui.advanced.ability; import kamkeel.npcs.controllers.data.ability.type.AbilityOrb; +import noppes.npcs.client.gui.SubGuiColorSelector; 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; +import noppes.npcs.client.gui.util.SubGuiInterface; /** * GUI for configuring Orb ability type-specific settings. @@ -13,12 +15,18 @@ import noppes.npcs.client.gui.util.IAbilityConfigCallback; public class SubGuiAbilityOrb extends SubGuiAbilityConfig { private final AbilityOrb orb; + private int editingVisualColorId = 0; public SubGuiAbilityOrb(AbilityOrb ability, IAbilityConfigCallback callback) { super(ability, callback); this.orb = ability; } + @Override + protected boolean hasVisualSettings() { + return true; + } + @Override protected void initTypeTab(int startY) { int y = startY; @@ -71,6 +79,46 @@ public class SubGuiAbilityOrb extends SubGuiAbilityConfig { addTextField(createIntField(109, col2FieldX, y, 50, orb.getMaxLifetime())); } + @Override + protected void initVisualTab(int startY) { + int y = startY; + int labelX = guiLeft + 8; + int fieldX = guiLeft + 85; + int col2LabelX = guiLeft + 180; + int col2FieldX = guiLeft + 260; + + // Row 1: Inner Color + Outer Color + addLabel(new GuiNpcLabel(200, "ability.innerColor", labelX, y + 5)); + String innerHex = String.format("%06X", orb.getInnerColor() & 0xFFFFFF); + GuiNpcButton innerColorBtn = new GuiNpcButton(200, fieldX, y, 55, 20, innerHex); + innerColorBtn.setTextColor(orb.getInnerColor() & 0xFFFFFF); + addButton(innerColorBtn); + + addLabel(new GuiNpcLabel(201, "ability.outerColor", col2LabelX, y + 5)); + String outerHex = String.format("%06X", orb.getOuterColor() & 0xFFFFFF); + GuiNpcButton outerColorBtn = new GuiNpcButton(201, col2FieldX, y, 55, 20, outerHex); + outerColorBtn.setTextColor(orb.getOuterColor() & 0xFFFFFF); + outerColorBtn.setEnabled(orb.isOuterColorEnabled()); + addButton(outerColorBtn); + + y += 24; + + // Row 2: Outer Color Enabled + Outer Color Width + addLabel(new GuiNpcLabel(202, "ability.outerEnabled", labelX, y + 5)); + addButton(new GuiNpcButton(202, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, orb.isOuterColorEnabled() ? 1 : 0)); + + addLabel(new GuiNpcLabel(203, "ability.outerWidth", col2LabelX, y + 5)); + GuiNpcTextField widthField = createFloatField(203, col2FieldX, y, 55, orb.getOuterColorWidth()); + widthField.setEnabled(orb.isOuterColorEnabled()); + addTextField(widthField); + + y += 24; + + // Row 3: Rotation Speed + addLabel(new GuiNpcLabel(204, "ability.rotationSpeed", labelX, y + 5)); + addTextField(createFloatField(204, fieldX, y, 50, orb.getRotationSpeed())); + } + @Override protected void handleTypeButton(int id, GuiNpcButton button) { int value = button.getValue(); @@ -84,6 +132,42 @@ public class SubGuiAbilityOrb extends SubGuiAbilityConfig { } } + @Override + protected void handleVisualButton(int id, GuiNpcButton button) { + int value = button.getValue(); + switch (id) { + case 200: + editingVisualColorId = 200; + setSubGui(new SubGuiColorSelector(orb.getInnerColor())); + break; + case 201: + editingVisualColorId = 201; + setSubGui(new SubGuiColorSelector(orb.getOuterColor())); + break; + case 202: + orb.setOuterColorEnabled(value == 1); + initGui(); + break; + } + } + + @Override + public void subGuiClosed(SubGuiInterface subgui) { + if (subgui instanceof SubGuiColorSelector && editingVisualColorId != 0) { + SubGuiColorSelector colorSelector = (SubGuiColorSelector) subgui; + int rgb = colorSelector.color & 0x00FFFFFF; + if (editingVisualColorId == 200) { + orb.setInnerColor(rgb); + } else if (editingVisualColorId == 201) { + orb.setOuterColor(rgb); + } + editingVisualColorId = 0; + initGui(); + } else { + super.subGuiClosed(subgui); + } + } + @Override protected void handleTypeTextField(int id, GuiNpcTextField field) { switch (id) { @@ -113,4 +197,16 @@ public class SubGuiAbilityOrb extends SubGuiAbilityConfig { break; } } + + @Override + protected void handleVisualTextField(int id, GuiNpcTextField field) { + switch (id) { + case 203: + orb.setOuterColorWidth(parseFloat(field, orb.getOuterColorWidth())); + break; + case 204: + orb.setRotationSpeed(parseFloat(field, orb.getRotationSpeed())); + break; + } + } } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilitySweeper.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilitySweeper.java new file mode 100644 index 000000000..95f54619b --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilitySweeper.java @@ -0,0 +1,198 @@ +package noppes.npcs.client.gui.advanced.ability; + +import kamkeel.npcs.controllers.data.ability.type.AbilitySweeper; +import noppes.npcs.client.gui.SubGuiColorSelector; +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; +import noppes.npcs.client.gui.util.SubGuiInterface; + +/** + * GUI for configuring Sweeper ability type-specific settings. + * Sweeper is a low rotating beam that players can jump over. + */ +public class SubGuiAbilitySweeper extends SubGuiAbilityConfig { + + private final AbilitySweeper sweeper; + private int editingVisualColorId = 0; + + public SubGuiAbilitySweeper(AbilitySweeper ability, IAbilityConfigCallback callback) { + super(ability, callback); + this.sweeper = ability; + } + + @Override + protected boolean hasVisualSettings() { + return true; + } + + @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, sweeper.getDamage())); + + addLabel(new GuiNpcLabel(101, "ability.dmgInterval", col2LabelX, y + 5)); + addTextField(createIntField(101, col2FieldX, y, 50, sweeper.getDamageInterval())); + + y += 24; + + // Row 2: Beam Length + Beam Width + addLabel(new GuiNpcLabel(102, "ability.beamLength", labelX, y + 5)); + addTextField(createFloatField(102, fieldX, y, 50, sweeper.getBeamLength())); + + addLabel(new GuiNpcLabel(103, "ability.beamWidth", col2LabelX, y + 5)); + addTextField(createFloatField(103, col2FieldX, y, 50, sweeper.getBeamWidth())); + + y += 24; + + // Row 3: Beam Height + Sweep Speed + addLabel(new GuiNpcLabel(104, "ability.beamHeight", labelX, y + 5)); + addTextField(createFloatField(104, fieldX, y, 50, sweeper.getBeamHeight())); + + addLabel(new GuiNpcLabel(105, "ability.sweepSpeed", col2LabelX, y + 5)); + addTextField(createFloatField(105, col2FieldX, y, 50, sweeper.getSweepSpeed())); + + y += 24; + + // Row 4: Number of Rotations + Piercing + addLabel(new GuiNpcLabel(106, "ability.rotations", labelX, y + 5)); + addTextField(createIntField(106, fieldX, y, 50, sweeper.getNumberOfRotations())); + + addLabel(new GuiNpcLabel(107, "ability.piercing", col2LabelX, y + 5)); + addButton(new GuiNpcButton(107, col2FieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, sweeper.isPiercing() ? 1 : 0)); + + y += 24; + + // Row 5: Lock On Target + addLabel(new GuiNpcLabel(108, "ability.lockTarget", labelX, y + 5)); + addButton(new GuiNpcButton(108, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, sweeper.isLockOnTarget() ? 1 : 0)); + } + + @Override + protected void initVisualTab(int startY) { + int y = startY; + int labelX = guiLeft + 8; + int fieldX = guiLeft + 85; + int col2LabelX = guiLeft + 180; + int col2FieldX = guiLeft + 260; + + // Row 1: Inner Color + Outer Color + addLabel(new GuiNpcLabel(200, "ability.innerColor", labelX, y + 5)); + String innerHex = String.format("%06X", sweeper.getInnerColor() & 0xFFFFFF); + GuiNpcButton innerColorBtn = new GuiNpcButton(200, fieldX, y, 55, 20, innerHex); + innerColorBtn.setTextColor(sweeper.getInnerColor() & 0xFFFFFF); + addButton(innerColorBtn); + + addLabel(new GuiNpcLabel(201, "ability.outerColor", col2LabelX, y + 5)); + String outerHex = String.format("%06X", sweeper.getOuterColor() & 0xFFFFFF); + GuiNpcButton outerColorBtn = new GuiNpcButton(201, col2FieldX, y, 55, 20, outerHex); + outerColorBtn.setTextColor(sweeper.getOuterColor() & 0xFFFFFF); + outerColorBtn.setEnabled(sweeper.isOuterColorEnabled()); + addButton(outerColorBtn); + + y += 24; + + // Row 2: Outer Color Enabled + Outer Color Width + addLabel(new GuiNpcLabel(202, "ability.outerEnabled", labelX, y + 5)); + addButton(new GuiNpcButton(202, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, sweeper.isOuterColorEnabled() ? 1 : 0)); + + addLabel(new GuiNpcLabel(203, "ability.outerWidth", col2LabelX, y + 5)); + GuiNpcTextField widthField = createFloatField(203, col2FieldX, y, 55, sweeper.getOuterColorWidth()); + widthField.setEnabled(sweeper.isOuterColorEnabled()); + addTextField(widthField); + } + + @Override + protected void handleTypeButton(int id, GuiNpcButton button) { + int value = button.getValue(); + switch (id) { + case 107: + sweeper.setPiercing(value == 1); + break; + case 108: + sweeper.setLockOnTarget(value == 1); + break; + } + } + + @Override + protected void handleVisualButton(int id, GuiNpcButton button) { + int value = button.getValue(); + switch (id) { + case 200: + editingVisualColorId = 200; + setSubGui(new SubGuiColorSelector(sweeper.getInnerColor())); + break; + case 201: + editingVisualColorId = 201; + setSubGui(new SubGuiColorSelector(sweeper.getOuterColor())); + break; + case 202: + sweeper.setOuterColorEnabled(value == 1); + initGui(); + break; + } + } + + @Override + public void subGuiClosed(SubGuiInterface subgui) { + if (subgui instanceof SubGuiColorSelector && editingVisualColorId != 0) { + SubGuiColorSelector colorSelector = (SubGuiColorSelector) subgui; + int rgb = colorSelector.color & 0x00FFFFFF; + if (editingVisualColorId == 200) { + sweeper.setInnerColor(rgb); + } else if (editingVisualColorId == 201) { + sweeper.setOuterColor(rgb); + } + editingVisualColorId = 0; + initGui(); + } else { + super.subGuiClosed(subgui); + } + } + + @Override + protected void handleTypeTextField(int id, GuiNpcTextField field) { + switch (id) { + case 100: + sweeper.setDamage(parseFloat(field, sweeper.getDamage())); + break; + case 101: + sweeper.setDamageInterval(field.getInteger()); + break; + case 102: + sweeper.setBeamLength(parseFloat(field, sweeper.getBeamLength())); + break; + case 103: + sweeper.setBeamWidth(parseFloat(field, sweeper.getBeamWidth())); + break; + case 104: + sweeper.setBeamHeight(parseFloat(field, sweeper.getBeamHeight())); + break; + case 105: + sweeper.setSweepSpeed(parseFloat(field, sweeper.getSweepSpeed())); + break; + case 106: + sweeper.setNumberOfRotations(field.getInteger()); + break; + } + } + + @Override + protected void handleVisualTextField(int id, GuiNpcTextField field) { + switch (id) { + case 203: + sweeper.setOuterColorWidth(parseFloat(field, sweeper.getOuterColorWidth())); + break; + } + } +} diff --git a/src/main/resources/assets/customnpcs/lang/en_US.lang b/src/main/resources/assets/customnpcs/lang/en_US.lang index 7f0618786..f3bba0c1d 100644 --- a/src/main/resources/assets/customnpcs/lang/en_US.lang +++ b/src/main/resources/assets/customnpcs/lang/en_US.lang @@ -1534,6 +1534,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 @@ -1559,6 +1560,9 @@ ability.cnpc.guard=Guard ability.cnpc.heal=Heal ability.cnpc.hazard=Hazard ability.cnpc.trap=Trap +ability.cnpc.disc=Disc +ability.cnpc.laser_shot=Laser Shot +ability.cnpc.sweeper=Sweeper # Ability GUI - Type Tab @@ -1627,10 +1631,12 @@ ability.explRadius=Expl Rad # Beam Settings ability.beamLength=Length ability.beamWidth=Width +ability.beamHeight=Height ability.dmgInterval=Interval ability.sweepAngle=Sweep Ang ability.sweepSpeed=Sweep Spd ability.lockTarget=Lock Tgt +ability.rotations=Rotations # Guard Settings ability.dmgReduce=Dmg Reduce @@ -1667,6 +1673,30 @@ ability.triggerRadius=Trig Rad ability.armTime=Arm Time ability.maxTriggers=Max Trig +# Disc Settings +ability.discRadius=Disc Rad +ability.discThickness=Thickness +ability.boomerang=Boomerang +ability.boomerangDelay=Return Delay +ability.homingStr=Homing Str +ability.homingRange=Homing Rng +ability.explosionRad=Expl Rad +ability.rotationSpeed=Rot Speed +ability.innerColor=Inner Color +ability.outerColor=Outer Color +ability.outerEnabled=Outer Glow +ability.outerWidth=Outer Width +ability.noVisualSettings=No visual settings for this ability type + +# Laser Shot Settings +ability.laserWidth=Laser Width +ability.expansionSpeed=Expansion Spd +ability.lingerTicks=Linger Ticks + +# Energy Beam Settings +ability.headSize=Head Size +ability.attachedToOwner=Attached + # Ability Save/Load ability.save.confirm=Save ability preset ability.save.overwrite=Overwrite existing preset 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