/* * 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.weapons; import java.util.ArrayList; import net.puppygames.applet.effects.Effect; import net.puppygames.applet.effects.Emitter; import net.puppygames.applet.effects.EmitterFeature; import net.puppygames.applet.widgets.Beam; import org.lwjgl.util.Color; import org.lwjgl.util.ReadablePoint; import org.lwjgl.util.Rectangle; import worm.Entity; import worm.GameMap; import worm.MapRenderer; import worm.Res; import worm.Tile; import worm.Worm; import worm.WormGameState; import worm.entities.Gidrah; import worm.entities.PlayerWeaponInstallation; import worm.screens.GameScreen; import com.shavenpuppy.jglib.interpolators.LinearInterpolator; import com.shavenpuppy.jglib.opengl.ColorUtil; import com.shavenpuppy.jglib.opengl.GLRenderable; import com.shavenpuppy.jglib.resources.Range; import com.shavenpuppy.jglib.sprites.Sprite; import com.shavenpuppy.jglib.sprites.SpriteImage; import com.shavenpuppy.jglib.util.FPMath; import com.shavenpuppy.jglib.util.ShortList; import com.shavenpuppy.jglib.util.Util; import static org.lwjgl.opengl.GL11.*; /** * The laser weapon * @author Cas */ public class LaserFeature extends WeaponFeature { private static final long serialVersionUID = 1L; private static final Rectangle TEMP = new Rectangle(); private static final GLRenderable SETUP = new GLRenderable() { @Override public void render() { glEnable(GL_TEXTURE_2D); glEnable(GL_BLEND); glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE); } }; private static final ArrayList<Entity> ENTITIES = new ArrayList<Entity>(); /** Damage */ private Range damage; /** Beam duration */ private int duration; /** Fade duration */ private int fadeDuration; /** Beam width */ private float width, innerWidth; /** Layer */ private int layer; /** Beam length */ private float length; /** Sweep angle, in degrees */ private int sweep; /** Target only: beam doesn't strike intermediate targets or walls */ private boolean targetOnly; /** Reflection emitter */ private EmitterFeature reflectionEmitter; /** Beam emitter */ private EmitterFeature beamEmitter; /** Color */ private Color color, innerColor; /** Beam emitter sprite overlay */ private String beamEmitterOverlay; private transient SpriteImage beamEmitterOverlayResource; /** * Instance of the laser */ private class LaserInstance extends WeaponInstance { private static final long serialVersionUID = 1L; private transient LaserBeam beam; private double angle; private int tick; private boolean reverse; /** * C'tor * @param entity */ public LaserInstance(Entity entity) { super(entity); } @Override protected void doFire(float targetX, float targetY) { // Spawn a beam if (beam != null) { return; } reverse = !reverse; beam = new LaserBeam(this); beam.sx = entity.getMapX() + entity.getOffsetX(); beam.sy = entity.getMapY() + entity.getOffsetY(); beam.tx = targetX; beam.ty = targetY; beam.setOffset(GameScreen.getSpriteOffset()); beam.spawn(GameScreen.getInstance()); double dx = beam.tx - beam.sx; double dy = beam.ty - beam.sy; angle = Math.atan2(dy, dx); tick = 0; } @Override public void tick() { super.tick(); if (beam != null) { beam.sx = entity.getMapX() + entity.getOffsetX() + entity.getBeamXOffset(); beam.sy = entity.getMapY() + entity.getOffsetY() + entity.getBeamYOffset(); if (getEntity() == null || targetOnly) { beam.tx = getTargetX(); beam.ty = getTargetY(); } else { // Sweep angle if (tick < duration) { tick ++; double radians = Math.toRadians(sweep); float ratio = (float) tick / (float) duration; double newAngle = reverse ? LinearInterpolator.instance.interpolate((float) (angle + radians), (float) (angle - radians), ratio) : LinearInterpolator.instance.interpolate((float) (angle - radians), (float) (angle + radians), ratio); beam.tx = beam.sx + Math.cos(newAngle) * 100.0; beam.ty = beam.sy + Math.sin(newAngle) * 100.0; } } if (!beam.isActive()) { beam = null; } } } @Override public boolean isReady() { return super.isReady() && beam == null; // Prevent cooling towers causing beams to overlap } @Override public void remove() { if (beam != null) { beam.finish(); beam = null; } } } private class LaserBeam extends Effect { final ShortList indices = new ShortList(); final LaserInstance weapon; final int mapWidth; final int mapHeight; Sprite beamEmitterOverlaySprite; boolean fading; int tick; double sx, sy, tx, ty; boolean enemyFire; boolean aerialTargets; boolean harmless; transient Beam innerBeam, outerBeam; transient Emitter beamStartEmitter; class Segment { double x0, y0, x1, y1; float startRatio, endRatio; } ArrayList<Segment> segments = new ArrayList<Segment>(); private LaserBeam(LaserInstance weapon) { this.weapon = weapon; enemyFire = weapon.entity instanceof Gidrah; if (!enemyFire) { aerialTargets = ((PlayerWeaponInstallation) weapon.entity).isFiringAtAerialTargets(); } beamStartEmitter = beamEmitter.spawn(GameScreen.getInstance()); beamStartEmitter.setOffset(GameScreen.getSpriteOffset()); mapWidth = Worm.getGameState().getMap().getWidth(); mapHeight = Worm.getGameState().getMap().getHeight(); } @Override protected void render() { ReadablePoint offset = getOffset(); int ox, oy; if (offset == null) { ox = 0; oy = 0; } else { ox = offset.getX(); oy = offset.getY(); } float alpha; if (fading) { alpha = LinearInterpolator.instance.interpolate(1.0f, 0.0f, (float) tick / (float) fadeDuration); } else { alpha = 1.0f; } float innerAlpha = Math.max(0.0f, alpha - (Util.random() * 0.25f)); float outerAlpha = Math.max(0.0f, alpha - (Util.random() * 0.25f)); glRender(SETUP); glRender(Res.getLaserTexture()); for (int i = 0; i < segments.size(); i ++) { Segment segment = segments.get(i); outerBeam.setLocation((float) segment.x0 + ox, (float) segment.y0 + oy, (float) segment.x1 + ox, (float) segment.y1 + oy); innerBeam.setLocation((float) segment.x0 + ox, (float) segment.y0 + oy, (float) segment.x1 + ox, (float) segment.y1 + oy); innerBeam.setStartColor(ColorUtil.setAlpha(ColorUtil.premultiply(innerColor, (int) (innerAlpha * segment.startRatio), null), 0, null)); innerBeam.setEndColor(ColorUtil.setAlpha(ColorUtil.premultiply(innerColor, (int) (innerAlpha * segment.endRatio), null), 0, null)); outerBeam.setStartColor(ColorUtil.setAlpha(ColorUtil.premultiply(color, (int) (outerAlpha * segment.startRatio), null), 0, null)); outerBeam.setEndColor(ColorUtil.setAlpha(ColorUtil.premultiply(color, (int) (outerAlpha * segment.endRatio), null), 0, null)); outerBeam.render(this); innerBeam.render(this); } } @Override protected void doSpawnEffect() { outerBeam = new Beam(); outerBeam.setWidth(width); innerBeam = new Beam(); innerBeam.setWidth(innerWidth); } @Override public int getDefaultLayer() { return layer; } @Override protected void doTick() { WormGameState gameState = Worm.getGameState(); tick ++; double diffx = tx - sx; double diffy = ty - sy; double angle = Math.atan2(diffy, diffx); double dx = Math.cos(angle); double dy = Math.sin(angle); double x = sx, y = sy; segments.clear(); Segment currentSegment = new Segment(); currentSegment.x0 = sx; currentSegment.y0 = sy; currentSegment.startRatio = 255.0f; int totalLength = 0; Entity lastBounce = null; if (targetOnly) { // This is the Saturn boss laser currentSegment.x1 = tx; currentSegment.y1 = ty; currentSegment.endRatio = 255.0f; totalLength = 1; TEMP.setBounds((int) tx, (int) ty, 1, 1); ENTITIES.clear(); Entity.getCollisions(TEMP, ENTITIES); int numEntities = ENTITIES.size(); // Check collision with entities inner: for (int j = 0; j < numEntities; j ++) { Entity entity = ENTITIES.get(j); if (entity.isActive() && entity.isShootable() || !entity.isShootable() && enemyFire && entity.canCollide() && entity.isSolid()) { double ddx = entity.getMapX() + entity.getCollisionX() - tx; double ddy = entity.getMapY() + entity.getCollisionY() - ty; if (entity.isRound()) { double dist = Math.sqrt(ddx * ddx + ddy * ddy); if (dist > entity.getRadius()) { // Missed continue inner; } } else { if (!entity.getBounds(TEMP).contains((int) tx, (int) ty)) { // Missed continue inner; } } if (entity == weapon.getEntity()) { continue inner; } if (!harmless) { entity.laserDamage((int) damage.getValue()); if (enemyFire) { harmless = true; } } } } } else { boolean dangerous = false; GameMap map = gameState.getMap(); int tileX = (int) (x / MapRenderer.TILE_SIZE); int tileY = (int) (y / MapRenderer.TILE_SIZE); outer: for (int i = 0; i < length; i ++) { x += dx; y += dy; Segment lastSegment = currentSegment; // Check map collision unless we're firing at aerial targets if (!aerialTargets) { int newTileX = (int) (x / MapRenderer.TILE_SIZE); int newTileY = (int) (y / MapRenderer.TILE_SIZE); if (newTileX < 0 || newTileY < 0 || newTileX >= mapWidth || newTileY >= mapHeight) { break; } 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; } } } } TEMP.setBounds((int) x, (int) y, 1, 1); ENTITIES.clear(); Entity.getCollisions(TEMP, ENTITIES); int numEntities = ENTITIES.size(); // Check collision with entities inner: for (int j = 0; j < numEntities; j ++) { Entity entity = ENTITIES.get(j); if (entity.isActive() && entity.isShootable() || !entity.isShootable() && enemyFire && entity.canCollide() && entity.isSolid()) { if (aerialTargets && (!entity.isFlying() || entity.isLaserOver())) { // Ignore ground targets if targeting aerial targets continue; } if (entity == lastBounce) { // Ignore last reflection continue; } double ddx = entity.getX() - x; double ddy = entity.getY() - y; if (entity.isRound()) { double dist = Math.sqrt(ddx * ddx + ddy * ddy); if (dist > entity.getRadius()) { // Missed continue inner; } } else { if (!entity.getBounds(TEMP).contains((int) x, (int) y)) { // Missed continue inner; } } if (!dangerous && entity == weapon.getEntity() || entity.isLaserThrough()) { sx = currentSegment.x0 = x; sy = currentSegment.y0 = y; continue inner; } else if (entity.isLaserProof()) { // Reflect! dangerous = true; lastBounce = entity; // First record current segment segments.add(currentSegment); currentSegment.endRatio = LinearInterpolator.instance.interpolate(255.0f, 0.0f, totalLength / length); currentSegment = new Segment(); currentSegment.startRatio = LinearInterpolator.instance.interpolate(255.0f, 0.0f, totalLength / length); currentSegment.x0 = lastSegment.x1; currentSegment.y0 = lastSegment.y1; // Spawn sparks and ting Emitter e = reflectionEmitter.spawn(GameScreen.getInstance()); e.setLocation((int) lastSegment.x1, (int) lastSegment.y1); e.setOffset(GameScreen.getSpriteOffset()); // Reflect double angleOfCollision = Math.atan2(ddy, ddx); // Now reflect about this angle double diff = Math.atan2(dy, dx) - angleOfCollision; double reflection = angleOfCollision + Math.PI - diff; dx = Math.cos(reflection); dy = Math.sin(reflection); // Go back to where we were & move one more x = lastSegment.x1 + dx; y = lastSegment.y1 + dy; weapon.entity.onBulletDeflected(entity); } else { boolean hit = true; if (!harmless) { hit = entity.laserDamage((int) damage.getValue() + entity.getExtraDamage()); if (enemyFire) { harmless = true; } } if (hit) { break outer; } } } } currentSegment.x1 = x; currentSegment.y1 = y; totalLength ++; } } if (totalLength > 0) { segments.add(currentSegment); currentSegment.endRatio = LinearInterpolator.instance.interpolate(255.0f, 0.0f, totalLength / length); if (tick > duration && !fading) { fading = true; tick = 0; } Emitter e = reflectionEmitter.spawn(GameScreen.getInstance()); e.setLocation((int) currentSegment.x1, (int) currentSegment.y1); e.setOffset(GameScreen.getSpriteOffset()); } else { remove(); } } @Override protected void doUpdate() { if (beamStartEmitter == null) { return; } int ox = getOffset().getX(); int oy = getOffset().getY(); beamStartEmitter.setLocation((float) sx + ox, (float) sy + oy); if (beamEmitterOverlaySprite == null) { beamEmitterOverlaySprite = getScreen().allocateSprite(getScreen()); beamEmitterOverlaySprite.setImage(beamEmitterOverlayResource); beamEmitterOverlaySprite.setLayer(layer + 1); beamEmitterOverlaySprite.setColors(color); } beamEmitterOverlaySprite.setLocation((int) sx + ox, (int) sy + oy); beamEmitterOverlaySprite.setScale(Util.random(FPMath.HALF + FPMath.EIGHTH, FPMath.HALF - FPMath.EIGHTH)); } @Override public boolean isEffectActive() { return !fading || tick < fadeDuration; } @Override public void finish() { if (!fading) { fading = true; tick = 0; if (beamStartEmitter != null) { beamStartEmitter.remove(); beamStartEmitter = null; } } } @Override protected void doRemove() { if (beamStartEmitter != null) { beamStartEmitter.remove(); beamStartEmitter = null; } if (beamEmitterOverlaySprite != null) { beamEmitterOverlaySprite.deallocate(); beamEmitterOverlaySprite = null; } } } /** * C'tor * @param name */ public LaserFeature(String name) { super(name); } @Override public WeaponInstance spawn(Entity entity) { return new LaserInstance(entity); } public int getDamage() { return (int) damage.getMax(); } @Override public boolean isLaser() { return true; } @Override protected String getDamageStats() { StringBuilder sb = new StringBuilder(); sb.append((int) damage.getMin() * duration); sb.append("-"); sb.append((int) damage.getMax() * duration); return sb.toString(); } }