/* * Copyright (c) 2003-onwards Shaven Puppy Ltd * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * * Neither the name of 'Shaven Puppy' nor the names of its contributors * may be used to endorse or promote products derived from this software * without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ package worm.effects; import java.util.ArrayList; import net.puppygames.applet.Game; import net.puppygames.applet.effects.Effect; import net.puppygames.applet.effects.Emitter; import net.puppygames.applet.effects.EmitterFeature; import org.lwjgl.util.ReadableColor; import org.lwjgl.util.ReadablePoint; import org.lwjgl.util.Rectangle; import worm.Entity; import worm.GameMap; import worm.Layers; import worm.MapRenderer; import worm.Tile; import worm.Worm; import worm.WormGameState; import worm.screens.GameScreen; import com.shavenpuppy.jglib.interpolators.Interpolator; import com.shavenpuppy.jglib.interpolators.LinearInterpolator; import com.shavenpuppy.jglib.interpolators.SineInterpolator; import com.shavenpuppy.jglib.openal.ALBuffer; import com.shavenpuppy.jglib.opengl.ColorUtil; import com.shavenpuppy.jglib.opengl.GLRenderable; import com.shavenpuppy.jglib.sound.SoundEffect; import com.shavenpuppy.jglib.util.ShortList; import com.shavenpuppy.jglib.util.Util; import static org.lwjgl.opengl.GL11.*; public class ElectronZapEffect extends Effect { private static final long serialVersionUID = 1L; /** Length of a segment */ private static final int SEGMENT_LENGTH = 8; /** Zap fade out duration in ticks */ private static final int FADE_DURATION = 15; /** Damage */ private static final int DAMAGE_INTERVAL = 2; static final Interpolator INTERPOLATOR; static { INTERPOLATOR = new Interpolator() { private static final long serialVersionUID = 1L; @Override public float interpolate(float a, float b, float ratio) { if (ratio < 0.5f) { return SineInterpolator.instance.interpolate(a, b, ratio * 2.0f); } else { return SineInterpolator.instance.interpolate(a, b, 1.0f - (ratio - 0.5f) * 2.0f); } } }; } private static final GLRenderable SETUP = new GLRenderable() { @Override public void render() { glDisable(GL_TEXTURE_2D); glEnable(GL_BLEND); glBlendFunc(GL_ONE, GL_ONE); glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE); } }; private static final ArrayList<Entity> TEMP_ENTITIES = new ArrayList<Entity>(); private static final Rectangle TEMP_RECT = new Rectangle(); /** Rendering indices */ private final ShortList indices = new ShortList(true, 32); private final float maxWidth; private final float maxWobble; private final float wobbleFactor; private final float widthFactor; private final boolean collides; private final EmitterFeature beamStartEmitter, beamEndEmitter; private final ALBuffer soundBuffer; private final ReadableColor color0, color1; private final int alpha; /** Source & target location */ private final float sx, sy; private float tx, ty; /** Actual beam endpoint */ private int x, y; /** Fading out */ private boolean fading, done; /** Ticker */ private int tick, soundTick, fadeSoundInTick; /** Segments */ private float[] x0, y0, x1, y1, x2, y2, x3, y3; /** Current length (distance) */ private int length; /** Number of segments */ private int numSegments; /** Indices start and end */ private int start, end, startPos, endPos; /** Sound effect */ private SoundEffect soundEffect; public ElectronZapEffect ( boolean collides, ALBuffer soundBuffer, ReadableColor color0, ReadableColor color1, int alpha, EmitterFeature beamStartEmitter, EmitterFeature beamEndEmitter, float sx, float sy, float maxWidth, float maxWobble, float wobbleFactor, float widthFactor ) { this.collides = collides; this.soundBuffer = soundBuffer; this.color0 = color0; this.color1 = color1; this.alpha = alpha; this.beamStartEmitter = beamStartEmitter; this.beamEndEmitter = beamEndEmitter; this.sx = sx; this.sy = sy; this.maxWidth = maxWidth; this.maxWobble = maxWobble; this.wobbleFactor = wobbleFactor; this.widthFactor = widthFactor; } public void setTarget(float tx, float ty) { this.tx = tx; this.ty = ty; double dx = sx - tx; double dy = sy - ty; length = (int) Math.sqrt(dx * dx + dy * dy); } @Override protected void doUpdate() { ReadablePoint offset = getOffset(); int ox, oy; if (offset == null) { ox = 0; oy = 0; } else { ox = offset.getX(); oy = offset.getY(); } // Calculate length & angle double dx = x - sx; double dy = y - sy; double tangent = Math.atan2(dy, dx) + Math.PI / 2.0; double dist = Math.sqrt(dx * dx + dy * dy); numSegments = (int) dist / SEGMENT_LENGTH + 1; if (x0 == null || x0.length < numSegments) { x0 = null; y0 = null; x1 = null; y1 = null; x2 = null; y2 = null; x3 = null; y3 = null; x0 = new float[numSegments]; y0 = new float[numSegments]; x1 = new float[numSegments]; y1 = new float[numSegments]; x2 = new float[numSegments]; y2 = new float[numSegments]; x3 = new float[numSegments]; y3 = new float[numSegments]; } for (int i = 0; i < numSegments; i ++) { float r2 = (float) i / (float) (numSegments - 1); double width = INTERPOLATOR.interpolate(0.0f, Math.min(maxWidth, i * widthFactor * SEGMENT_LENGTH), r2); double wobble = INTERPOLATOR.interpolate(0, Math.min(maxWobble, i * wobbleFactor * SEGMENT_LENGTH), r2); wobble *= Util.random() - 0.5; float xx = LinearInterpolator.instance.interpolate(sx, x, r2) + (float) (Math.cos(tangent) * wobble) + ox; float yy = LinearInterpolator.instance.interpolate(sy, y, r2) + (float) (Math.sin(tangent) * wobble) + oy; x0[i] = xx - (float) (Math.cos(tangent) * width); y0[i] = yy - (float) (Math.sin(tangent) * width); x1[i] = xx + (float) (Math.cos(tangent) * width); y1[i] = yy + (float) (Math.sin(tangent) * width); x2[i] = xx - (float) (Math.cos(tangent) * (width + 1.0)); y2[i] = yy - (float) (Math.sin(tangent) * (width + 1.0)); x3[i] = xx + (float) (Math.cos(tangent) * (width + 1.0)); y3[i] = yy + (float) (Math.sin(tangent) * (width + 1.0)); } if (soundEffect != null) { soundEffect.setGain(Worm.calcGain(tx, ty) * Game.getSFXVolume() * soundBuffer.getGain(), this); } } @Override public void finish() { if (!fading) { fading = true; tick = 0; } } @Override protected void doTick() { tick ++; if (soundEffect != null && soundEffect.getOwner() != this) { // We lost the looping sound effect, so bring it back in a mo soundTick ++; if (soundTick >= FADE_DURATION) { soundEffect = Game.allocateSound(soundBuffer, 0.0f, 1.0f, this); fadeSoundInTick = FADE_DURATION; } } if (fading && soundEffect != null) { soundEffect.setGain(Math.max(0.0f, Math.min(1.0f, Worm.calcGain(tx, ty) * Game.getSFXVolume() * (1.0f - (float) tick / (float) FADE_DURATION) * soundBuffer.getGain())), this); return; } if (fadeSoundInTick > 0) { fadeSoundInTick --; } if (soundEffect != null) { soundEffect.setGain(Math.max(0.0f, Math.min(1.0f, Worm.calcGain(tx, ty) * Game.getSFXVolume() * (1.0f - (float) fadeSoundInTick / (float) FADE_DURATION) * soundBuffer.getGain())), this); } int totalLength = 0; if (collides) { WormGameState gameState = Worm.getGameState(); x = (int) sx; y = (int) sy; GameMap map = gameState.getMap(); int tileX = x / MapRenderer.TILE_SIZE; int tileY = y / MapRenderer.TILE_SIZE; int ox = x, oy = y; outer: for (int i = 0; i < length; i ++) { x = (int) LinearInterpolator.instance.interpolate(sx, tx, (float) i / (float) length); y = (int) LinearInterpolator.instance.interpolate(sy, ty, (float) i / (float) length); // Check map collision int newTileX = x / MapRenderer.TILE_SIZE; int newTileY = y / MapRenderer.TILE_SIZE; if (newTileX != tileX || newTileY != tileY) { tileX = newTileX; tileY = newTileY; for (int tileZ = 0; tileZ < GameMap.LAYERS; tileZ ++) { Tile t = map.getTile(tileX, tileY, tileZ); if (t != null && !t.isBulletThrough()) { // Hit a wall break outer; } } } // Check collision with entities if (ox != x || oy != y) { TEMP_ENTITIES.clear(); TEMP_RECT.setBounds(x, y, 1, 1); Entity.getCollisions(TEMP_RECT, TEMP_ENTITIES); int numEntities = TEMP_ENTITIES.size(); for (int j = 0; j < numEntities; j ++) { Entity entity = TEMP_ENTITIES.get(j); if (entity.isActive() && entity.isShootable() && entity.canCollide()) { if (tick % DAMAGE_INTERVAL == 0) { entity.capacitorDamage(1); } else { entity.stunDamage(3); } // And here the beam stops. break outer; } } ox = x; oy = y; } totalLength ++; } } else { x = (int) tx; y = (int) ty; } if (!collides || totalLength > 0) { // Spawn sparks and ting at end of beam Emitter e = beamEndEmitter.spawn(GameScreen.getInstance()); e.setLocation(x, y); e.setOffset(GameScreen.getSpriteOffset()); Emitter e2 = beamStartEmitter.spawn(GameScreen.getInstance()); e2.setLocation(sx, sy); e2.setOffset(GameScreen.getSpriteOffset()); } } @Override public boolean isEffectActive() { return (!fading || tick < FADE_DURATION) && !done; } @Override protected void doRemove() { done = true; if (soundEffect != null) { soundEffect.stop(this); soundEffect = null; } } @Override protected void render() { if (!isStarted() || !isVisible()) { return; } glRender(SETUP); int endAlpha = fading ? (int) LinearInterpolator.instance.interpolate(255.0f, 0.0f, (float) tick / FADE_DURATION) : 255; indices.ensureCapacity(numSegments * 4); indices.clear(); for (int i = 0; i < numSegments; i ++) { if (i == 0 || i == numSegments - 1) { ColorUtil.setGLColorPre(color0, 0, this); } else { ColorUtil.setGLColorPre(color0, endAlpha * alpha / 255, this); } indices.add(glVertex2f(x2[i], y2[i])); indices.add(glVertex2f(x3[i], y3[i])); } glRender(GL_TRIANGLE_STRIP, indices.toArray(null)); indices.clear(); ColorUtil.setGLColorPre(color1, endAlpha * alpha / 255, this); for (int i = 0; i < numSegments; i ++) { indices.add(glVertex2f(x0[i], y0[i])); indices.add(glVertex2f(x1[i], y1[i])); } glRender(GL_TRIANGLE_STRIP, indices.toArray(null)); } @Override protected void doSpawnEffect() { soundEffect = Game.allocateSound(soundBuffer, 1.0f, 1.0f, this); } @Override public int getDefaultLayer() { return Layers.CAPACITOR_EFFECT; } }