/*
* 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.Game;
import net.puppygames.applet.TickableObject;
import net.puppygames.applet.effects.*;
import org.lwjgl.opengl.Display;
import org.lwjgl.util.Color;
import org.lwjgl.util.Rectangle;
import worm.*;
import worm.entities.Gidrah;
import worm.entities.Turret;
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.Util;
import static org.lwjgl.opengl.GL11.*;
/**
* The laser weapon
* @author Cas
*/
public class LaserFeature extends WeaponFeature {
private static final Rectangle TEMP = new Rectangle();
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;
/** 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;
/** 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 LaserInstance weapon;
final int mapWidth;
final int mapHeight;
Sprite beamEmitterOverlaySprite;
TickableObject tickableObject;
boolean fading;
int tick;
double sx, sy, tx, ty;
boolean enemyFire;
boolean aerialTargets;
boolean harmless;
transient Emitter beamStartEmitter;
class Segment {
double x0, y0, x1, y1;
}
ArrayList<Segment> segments = new ArrayList<Segment>();
private LaserBeam(LaserInstance weapon) {
this.weapon = weapon;
enemyFire = weapon.entity instanceof Gidrah;
if (!enemyFire) {
aerialTargets = ((Turret) 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 doSpawn() {
beamEmitterOverlaySprite = getScreen().allocateSprite(getScreen());
beamEmitterOverlaySprite.setImage(beamEmitterOverlayResource);
beamEmitterOverlaySprite.setLayer(layer + 1);
beamEmitterOverlaySprite.setColors(color);
tickableObject = new TickableObject() {
@Override
protected void render() {
glRender(new GLRenderable() {
@Override
public void render() {
glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
glEnable(GL_BLEND);
glHint(GL_LINE_SMOOTH_HINT, GL_NICEST);
glBlendFunc(GL_SRC_ALPHA, GL_ONE);
glLineWidth(width * Display.getDisplayMode().getHeight() / Game.getHeight());
glDisable(GL_TEXTURE_2D);
}
});
float alpha;
if (fading) {
alpha = LinearInterpolator.instance.interpolate(1.0f, 0.0f, (float) tick / (float) fadeDuration);
} else {
alpha = 1.0f;
}
alpha *= 1.0f - Util.random() / 5.0f;
ColorUtil.setGLColor(color, (int) (alpha * 255), this);
glBegin(GL_LINES);
int ox = getOffset().getX();
int oy = getOffset().getY();
for (int i = 0; i < segments.size(); i ++) {
Segment segment = segments.get(i);
glVertex2f((float) segment.x0 + ox, (float) segment.y0 + oy);
glVertex2f((float) segment.x1 + ox, (float) segment.y1 + oy);
}
glEnd();
glRender(new GLRenderable() {
@Override
public void render() {
glLineWidth(1.0f);
}
});
}
};
tickableObject.setLayer(layer);
tickableObject.spawn(GameScreen.getInstance());
}
@Override
protected void doRender() {
}
@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;
int totalLength = 0;
Entity lastBounce = null;
if (targetOnly) {
// This is the Saturn boss laser
currentSegment.x1 = tx;
currentSegment.y1 = ty;
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 = new Segment();
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);
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);
beamEmitterOverlaySprite.setLocation((int) sx + ox, (int) sy + oy, 0);
beamEmitterOverlaySprite.setScale(Util.random(FPMath.HALF + FPMath.EIGHTH, FPMath.HALF - FPMath.EIGHTH));
}
@Override
public boolean isActive() {
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;
}
if (tickableObject != null) {
tickableObject.remove();
tickableObject = 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();
}
}