/*
* 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.entities;
import java.util.ArrayList;
import net.puppygames.applet.Screen;
import net.puppygames.applet.effects.BlastEffect;
import net.puppygames.applet.effects.Emitter;
import net.puppygames.applet.effects.EmitterFeature;
import org.lwjgl.util.Rectangle;
import worm.Entity;
import worm.GameMap;
import worm.GameStateInterface;
import worm.MapRenderer;
import worm.SFX;
import worm.Tile;
import worm.Worm;
import worm.features.BulletFeature;
import worm.features.LayersFeature;
import worm.features.ResearchFeature;
import worm.screens.GameScreen;
import com.shavenpuppy.jglib.interpolators.SineInterpolator;
import com.shavenpuppy.jglib.util.FPMath;
import com.shavenpuppy.jglib.util.Util;
/**
* Bullets
* @author Cas
*/
public class Bullet extends Entity {
private static final long serialVersionUID = 1L;
private static final int MIN_REMAINING_RANGE = 5;
private static final int MAX_REMAINING_RANGE = 30;
private static final double JITTER = Math.PI / 32.0;
private static final ArrayList<Entity> SHOOTABLE_ENTITIES = new ArrayList<Entity>();
/** Range, in ticks */
private int tick = 1;
/** Direction */
private float dx, dy;
/** Start */
private float sx, sy;
/** Target */
private float tx, ty;
/** Angle in Yakly degrees */
private int angle;
/** Speed */
private float speed;
/** Leftover movement */
private float leftoverMovement;
/** Emitters */
private transient Emitter[] emitter;
/** Bullet feature */
private final BulletFeature feature;
/** Source of the bullet */
private final Entity source;
/** Exploding? */
private boolean exploding;
/** Blast effect */
private transient BlastEffect blastEffect;
/** Range to target */
private int rangeToTarget;
/** Damage */
private int damage;
/** Armour piercing */
private int ap;
/** Dangerous to buildings? */
private boolean dangerousToBuildings;
/** Dangerous to gidrahs? */
private boolean dangerousToGidrahs;
/** Dangerous to gidlets? */
private boolean dangerousToGidlets;
/** Total distance moved */
private int moved;
/** Last entity we bounced off */
private Entity lastBounce;
/** Bounce tick */
private int lastBounceTick;
/** angle of last hit */
private double hitAngle;
/** Remaining range after bounce */
private int remainingRange;
/** Spawned in a wall? */
private int wallThruState;
private static final int WALLTHRU_UNKNOWN = 0;
private static final int WALLTHRU_IN_WALL = 1;
private static final int WALLTHRU_NORMAL = 2;
/**
* C'tor
*/
public Bullet(Entity source, float sx, float sy, float tx, float ty, BulletFeature feature, int extraDamage) {
this.source = source;
this.dangerousToBuildings = feature.isExploding() || source instanceof Gidrah;
this.dangerousToGidrahs = !(source instanceof Gidrah);
this.dangerousToGidlets = dangerousToGidrahs && (feature.isMini() || feature.isExploding());
this.feature = feature;
this.speed = feature.getSpeed();
this.damage = feature.getDamage() + extraDamage;
if (dangerousToGidrahs && feature.isExploding() && Worm.getGameState().isResearched(ResearchFeature.ADVANCEDEXPLOSIVES)) {
damage += feature.getDamage();
}
this.ap = feature.getArmourPiercing() + (feature.isBlaster() ? (Worm.getGameState().isResearched(ResearchFeature.ANATOMY) ? 1 : 0) : 0);
setLocation(sx, sy);
this.sx = sx;
this.sy = sy;
this.tx = tx;
this.ty = ty;
// If the bullet has a max range, we'll use that and just move it for as far as it's allowed;
// otherwise it will fly to its target location and beyond, unless it's "targeted"
if (speed > 0) {
float ddx = tx - sx;
float ddy = ty - sy;
rangeToTarget = (int) Math.sqrt(ddx * ddx + ddy * ddy);
dx = ddx / rangeToTarget;
dy = ddy / rangeToTarget;
calcAngle();
}
}
public void setRemainingRange(int remainingRange) {
this.remainingRange = remainingRange;
}
/**
* @return the armour piercing factor
*/
public int getArmourPiercing() {
return ap;
}
/**
* @return the stun for this bullet
*/
public int getStun() {
return feature.getStun();
}
private void calcAngle() {
angle = FPMath.fpYaklyDegrees(Math.atan2(dy, dx));
for (int i = 0; i < getNumSprites(); i ++) {
getSprite(i).setAngle(angle);
}
if (emitter != null) {
for (Emitter element : emitter) {
if (element != null) {
element.setAngle((float) Math.toDegrees(Math.atan2(dy, dx)));
}
}
}
}
@Override
public final boolean canCollide() {
return exploding || !feature.isExploding();
}
@Override
public void onCollision(Entity entity) {
entity.onCollisionWithBullet(this);
}
@Override
public void onCollisionWithBullet(Bullet bullet) {
if (bullet.dangerousToBuildings != dangerousToBuildings) {
onHit(true, bullet);
}
}
/**
* Bounce!
* @param target The target we're bouncing off of
*/
public void bounce(Entity target) {
// remember angle so can set deflect emitter
hitAngle=getAngle();
// Stop daft bouncing
if (lastBounce == target) {
lastBounceTick ++;
if (lastBounceTick <= 8) {
remove();
return;
}
} else {
lastBounceTick = 0;
lastBounce = target;
}
spawnRicochetEmitter();
SFX.ricochet(getMapX(), getMapY(), 1.0f);
remainingRange = Util.random(MIN_REMAINING_RANGE, MAX_REMAINING_RANGE);
// At what angle have we hit the entity?
double ddx = target.getMapX() - getMapX();
double ddy = target.getMapY() - getMapY();
double angleOfCollision = Math.atan2(ddy, ddx);
// Now reflect about this angle
double diff = Math.atan2(dy, dx) - angleOfCollision;
double reflection = angleOfCollision + Math.PI - diff + Math.random() * JITTER - JITTER * 0.5;
dx = (float) Math.cos(reflection);
dy = (float) Math.sin(reflection);
calcAngle();
// // Bullet is now dangerous to the player's buildings
// dangerous = true;
move();
}
/**
* @return true if this bullet is dangerous to buildings
*/
public boolean isDangerousToBuildings() {
return dangerousToBuildings;
}
public boolean isDangerousToGidrahs() {
return dangerousToGidrahs;
}
public boolean isDangerousToGidlets() {
return dangerousToGidlets;
}
/* (non-Javadoc)
* @see worm.Entity#isHoverable()
*/
@Override
public boolean isHoverable() {
return false;
}
/**
* @return true if this bullet passes through aliens
*/
public boolean isPassThrough() {
return feature.isPassThrough();
}
/**
* Called when the bullet has struck a target
* @param wasDamaged Whether the target took damage
* @param target The target that was hit
*/
public void onHit(boolean wasDamaged, Entity target) {
if (exploding) {
return;
}
if (target != null && !wasDamaged) {
bounce(target);
source.onBulletDeflected(target);
} else {
ricochet();
}
}
@Override
protected final void doSpawn() {
init();
for (int i = 0; i < getNumSprites(); i ++) {
getSprite(i).setAngle(angle);
}
}
@Override
protected void createSprites(Screen screen) {
feature.getAppearance().createSprites(screen, this);
}
private void init() {
emitter = feature.getAppearance().createEmitters(GameScreen.getInstance(), getMapX(), getMapY());
if (emitter != null) {
for (Emitter element : emitter) {
if (element != null) {
element.setLocation(getMapX(), getMapY());
GameScreen.getInstance().detachTickable(element);
}
}
}
calcAngle();
spawnFlashEmitter();
}
@Override
protected void doRemove() {
if (emitter != null) {
for (Emitter element : emitter) {
if (element != null) {
element.remove();
}
}
emitter = null;
}
}
@Override
protected void doRespawn() {
init();
}
@Override
protected void doTick() {
if (exploding) {
addToCollisionManager();
tick ++;
if (tick > feature.getExplosionDuration()) {
remove();
return;
}
} else if (speed > 0.0f) {
if (remainingRange > 0) {
remainingRange --;
if (remainingRange == 0) {
ricochet();
SFX.ricochet(getX(), getY(), 1.0f);
return;
}
}
if (feature.isTargeted()) {
if (getDistanceTo(sx, sy) >= rangeToTarget) {
ricochet();
return;
}
}
int ms = (int) speed;
float rem = speed - ms;
leftoverMovement += rem;
if (leftoverMovement >= 1.0f) {
leftoverMovement -= 1.0f;
ms ++;
}
moved += ms;
int tileX = (int) (getX() / MapRenderer.TILE_SIZE);
int tileY = (int) (getY() / MapRenderer.TILE_SIZE);
for (int i = 0; i < ms && isActive(); i ++) {
tick ++;
move();
if (
getX() < 2.0f
|| getY() < 2.0f
|| getX() >= Worm.getGameState().getMap().getWidth() * MapRenderer.TILE_SIZE - 2.0f
|| getY() >= Worm.getGameState().getMap().getHeight() * MapRenderer.TILE_SIZE - 2.0f
)
{
// Missed
if (exploding) {
ricochet();
} else {
remove();
}
return;
}
// Are we at target?
if (feature.isTargeted()) {
if (getDistanceTo(sx, sy) >= rangeToTarget) {
ricochet();
return;
}
}
if (canCollide()) {
checkCollisions(SHOOTABLE_ENTITIES);
for (int j = 0; j < SHOOTABLE_ENTITIES.size(); j ++) {
Entity target = SHOOTABLE_ENTITIES.get(j);
this.onCollision(target);
target.onCollision(this);
if (!isActive()) {
return;
}
}
}
// Check for collision with solid things
int newTileX = (int) (getMapX() / MapRenderer.TILE_SIZE);
int newTileY = (int) (getMapY() / MapRenderer.TILE_SIZE);
if (newTileX != tileX || newTileY != tileY) {
tileX = newTileX;
tileY = newTileY;
boolean inWall = false;
outer: for (int z = 0; z < GameMap.LAYERS; z ++) {
Tile t = Worm.getGameState().getMap().getTile(tileX, tileY, z);
if (t != null && !t.isBulletThrough()) {
switch (wallThruState) {
case WALLTHRU_UNKNOWN:
// We're in a wall
wallThruState = WALLTHRU_IN_WALL;
inWall = true;
break outer;
case WALLTHRU_IN_WALL:
// Still in a wall
inWall = true;
break outer;
case WALLTHRU_NORMAL:
// Hit something
SFX.ricochet(getMapX(), getMapY(), 1.0f);
ricochet();
return;
default:
assert false : wallThruState;
}
}
}
if (!inWall) {
wallThruState = WALLTHRU_NORMAL;
}
}
}
speed -= feature.getDeceleration();
if (speed < 1.0f) {
speed = 0.0f;
ricochet();
SFX.ricochet(getMapX(), getMapY(), 1.0f);
return;
}
}
if (emitter != null) {
for (Emitter element : emitter) {
if (element != null && element.isActive()) {
element.tick();
}
}
}
}
private void move() {
setLocation(getMapX() + dx, getMapY() + dy);
}
@Override
protected void onSetLocation() {
if (emitter != null) {
for (Emitter element : emitter) {
if (element != null) {
element.setLocation(getMapX(), getMapY());
}
}
}
}
/**
* Do a ricochet and remove the bullet
*/
protected void ricochet() {
if (!isActive()) {
return;
}
spawnRicochetEmitter();
if (feature.isExploding()) {
if (!exploding) {
exploding = true;
dangerousToBuildings = true;
dangerousToGidrahs = true;
tick = 0;
blastEffect = feature.createBlastEffect(dangerousToGidrahs, (int) getMapX(), (int) getMapY());
setVisible(false);
}
} else {
remove();
}
}
private void spawnRicochetEmitter() {
EmitterFeature ef = feature.getRicochetEmitter();
if (ef != null) {
Emitter e = ef.spawn(GameScreen.getInstance());
// add dx and dy * speed - so ricochet emitter appears more into gidrah
e.setLocation(getMapX() + dx*speed*0.5f, getMapY() + dy*speed*0.5f);
e.setAngle(getAngle());
e.setOffset(GameScreen.getSpriteOffset());
}
}
private void spawnFlashEmitter() {
EmitterFeature ef = feature.getFlashEmitter();
if (ef != null) {
Emitter e = ef.spawn(GameScreen.getInstance());
e.setLocation(getMapX(), getMapY());
e.setAngle(getAngle());
e.setOffset(GameScreen.getSpriteOffset());
}
}
/* (non-Javadoc)
* @see storm.Entity#addToGameState(storm.GameStateInterface)
*/
@Override
public void addToGameState(GameStateInterface gsi) {
// No need to do anything
}
@Override
public void removeFromGameState(GameStateInterface gsi) {
// No need to do anything
}
@Override
public boolean isShootable() {
return dangerousToBuildings;
}
@Override
public Rectangle getBounds(Rectangle bounds) {
return null;
}
@Override
public float getRadius() {
if (blastEffect != null) {
return blastEffect.getRadius();
} else {
return 3.0f;
}
}
@Override
public boolean isRound() {
return true;
}
/**
* Called when this bullet passes through an alien
*/
public void onPassThrough() {
damage --;
if (damage == 0) {
ricochet();
}
}
/**
* @return bullet damage
*/
public int getDamage() {
if (feature.getBaseSpeed() > 0.0f) {
return (int) SineInterpolator.instance.interpolate(0.0f, damage, speed / feature.getBaseSpeed());
} else {
return damage;
}
}
/**
* @return Returns the source.
*/
public Entity getSource() {
return source;
}
/**
* @return Returns the exploding.
*/
public boolean isExploding() {
return exploding;
}
/* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return "Bullet["+feature+" from "+source+"]";
}
public float getDX() {
return dx;
}
public float getDY() {
return dy;
}
/**
* @return Returns the anlge (so can set ricochet emitter)
*/
public float getAngle() {
return (float) Math.toDegrees(Math.atan2(dy, dx));
}
/**
* @return Returns the hitAngle - angle before last bounce (so can set deflect emitter)
*/
public float getHitAngle() {
return (float) hitAngle;
}
public void setAppearance(LayersFeature newAppearance) {
}
}