/* * 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; import java.io.Serializable; import java.util.ArrayList; import java.util.List; import net.puppygames.applet.Game; import net.puppygames.applet.Screen; import net.puppygames.applet.Tickable; import org.lwjgl.util.ReadableRectangle; import org.lwjgl.util.Rectangle; import org.lwjgl.util.vector.Vector2f; import worm.animation.ThingWithLayers; import worm.entities.Bomb; import worm.entities.Building; import worm.entities.Bullet; import worm.entities.Gidrah; import worm.entities.Saucer; import worm.entities.Smartbomb; import worm.entities.Unit; import worm.features.LayersFeature; import worm.screens.GameScreen; import com.shavenpuppy.jglib.algorithms.Bresenham; import com.shavenpuppy.jglib.sprites.Animation; import com.shavenpuppy.jglib.sprites.ReadablePosition; import com.shavenpuppy.jglib.sprites.Sprite; import com.shavenpuppy.jglib.sprites.SpriteAllocator; import com.shavenpuppy.jglib.util.FPMath; import com.shavenpuppy.jglib.util.Util; /** * $Id: Entity.java,v 1.90 2010/10/31 12:38:03 foo Exp $ * @version $Revision: 1.90 $ * @author $Author: foo $ * <p> */ public abstract class Entity implements Tickable, Serializable, ReadablePosition, ThingWithLayers { private static final long serialVersionUID = 1L; private static final Rectangle BOUNDS = new Rectangle(); private static final Rectangle TEMP = new Rectangle(); private static final ArrayList<Entity> ENTITYCACHE = new ArrayList<Entity>(); private static final Bresenham BRESENHAM = new Bresenham(); /** Collision quadtree */ private static final CollisionManager COLLISIONMANAGER = new GridCollisionManager(MapRenderer.TILE_SIZE);// QuadTree(WormGameState.MAP_WIDTH, WormGameState.MAP_HEIGHT, 6); /** Node of the quadtree we're in */ private transient CollisionManager node; /** Location */ private float mapX, mapY, oldX, oldY, oldR; /** Tile X & Y */ private int tileX, tileY; /** Screen location */ private float screenX, screenY; /** Last known appearance */ private LayersFeature oldAppearance; /** Active? */ private boolean active; /** Flash */ private boolean flash; /** Visible? */ private boolean visible = true; /** Sprites */ private Sprite[] sprite; private int hackyTick = 4; /** offset totals */ private float yOffsetTotal = 0; private float xOffsetTotal = 0; /** * C'tor */ public Entity() { } /** * Returns the offset for any lasers this entity might be using (ok, the Saturn boss) * @return */ public float getBeamXOffset() { return getOffsetX(); } /** * Returns the offset for any lasers this entity might be using (ok, the Saturn boss) * @return */ public float getBeamYOffset() { return getOffsetY(); } /** * @param visible the visible to set */ public void setVisible(boolean visible) { this.visible = visible; if (sprite != null) { for (Sprite element : sprite) { if (element != null) { element.setVisible(visible); } } } } /** * @return the visible */ public boolean isVisible() { return visible; } /** * Drop a building on top of this entity. Damages the building by the specified number of hitpoints returned. * @return int */ public int crush() { return 0; } /** * Get the distance to a coordinate * @param xx * @param yy * @return distance, in pixels */ public float getDistanceTo(float xx, float yy) { return Vector2f.sub(new Vector2f(xx, yy), new Vector2f(getX(), getY()), null).length(); } /** * Get the distance to another entity * @param xx * @param yy * @return distance, in pixels */ public float getDistanceTo(Entity e) { return Vector2f.sub(new Vector2f(e.getX(), e.getY()), new Vector2f(getX(), getY()), null).length(); } /** * Can we see this location? * @param mapX * @param mapY * @param positive If non null, if this entity is detected, algorithm returns true * @param targetFlying If true, and positive is a flying gidrah, we ignore walls and simply return true * @return true if there is a LOS to the specified map location */ public boolean canSee(float mapX, float mapY, Entity positive, boolean targetFlying) { if (targetFlying && positive != null && positive.isFlying()) { return true; } WormGameState gameState = Worm.getGameState(); GameMap map = gameState.getMap(); ArrayList<Entity> entities = gameState.getEntities(); int n = entities.size(); // Create a list of solid entities we think are somewhere in the LOS ENTITYCACHE.clear(); for (int i = 0; i < n; i ++) { Entity e = entities.get(i); if (e != this && e.isActive() && e.isSolid()) { double dist = Util.distanceFromLineToPoint(getX(), getY(), mapX, mapY, e.getX(), e.getY()); if (dist >= 0.0 && dist <= e.getRadius()) { ENTITYCACHE.add(e); } } } int numCachedEntities = ENTITYCACHE.size(); BRESENHAM.plot((int) getX(), (int) getY(), (int) mapX, (int) mapY); int oldMapX = -1, oldMapY = -1; while (BRESENHAM.next()) { int x = BRESENHAM.getX() / MapRenderer.TILE_SIZE; int y = BRESENHAM.getY() / MapRenderer.TILE_SIZE; if (x != oldMapX || y != oldMapY) { for (int z = 0; z < GameMap.LAYERS; z ++) { Tile tile = map.getTile(x, y, z); if (tile != null && !tile.isBulletThrough()) { // Tile blocks LOS return false; } } oldMapX = x; oldMapY = y; } // Check entity list if (positive != null) { for (int i = 0; i < numCachedEntities; i ++) { Entity e = ENTITYCACHE.get(i); if (e == positive && e.getDistanceTo(BRESENHAM.getX(), BRESENHAM.getY()) < e.getRadius()) { return true; } } } // Skip 4 pixels at a time if (!BRESENHAM.next()) { break; } if (!BRESENHAM.next()) { break; } if (!BRESENHAM.next()) { break; } } return true; } /** * Respawn from saved state * @param screen */ public final void respawn(SpriteAllocator screen) { doRespawn(); } /** * Called after deserializing a game. */ protected void doRespawn() { } @Override public final void spawn(Screen screen) { createSprites(screen); if (sprite == null) { remove(); return; } active = true; Worm.getGameState().addEntity(this); doSpawn(); update(); } protected void createSprites(Screen screen) { setSprites(new Sprite[] { screen.allocateSprite(this) }); } /** * Called by the game state to add an entity to the appropriate list * @param gsi */ public abstract void addToGameState(GameStateInterface gsi); /** * Called by the game state to remove an entity from the appropriate list * @param gsi */ public abstract void removeFromGameState(GameStateInterface gsi); /** * Called by spawn() after the entity has been added to the screen */ protected void doSpawn() { } /** * Can the entity collide? * @return boolean */ public abstract boolean canCollide(); /** * Can the entity be crushed by a building? * @return true if the entity can be crushed */ public boolean canBeCrushed() { return isActive() && isSolid() && canCollide(); } /** * Is the entity alive? If not it is removed * @return boolean */ @Override public final boolean isActive() { return active; } /** * Remove the entity */ @Override public final void remove() { removeSprites(); if (!active) { // Already removed return; } active = false; Worm.getGameState().removeEntity(this); if (node != null) { node.remove(this); node = null; } doRemove(); } /** * Remove the sprite(s) */ protected final void removeSprites() { if (sprite != null) { for (Sprite element : sprite) { if (element != null) { element.deallocate(); } } sprite = null; } } /** * Sets a new bunch of sprites to use * @param newSprite */ @Override public final void setSprites(Sprite[] newSprite) { removeSprites(); sprite = newSprite; update(); } @Override public LayersFeature getAppearance() { return null; } @Override public void requestSetAppearance(LayersFeature newAppearance) { } protected void doRemove() { } /** * Collision with another entity * @param inCollisionWith The entity with which we have collided */ public abstract void onCollision(Entity entity); public void onCollisionWithSmartbomb(Smartbomb smartbomb) { } public void onCollisionWithGidrah(Gidrah gidrah) { } public void onCollisionWithUnit(Unit unit) { } public void onCollisionWithSaucer(Saucer saucer) { } public void onCollisionWithBuilding(Building building) { } public void onCollisionWithBullet(Bullet bullet) { } public void onCollisionWithBomb(Bomb bomb) { } /** * Get radius. Only applicable if isRound() returns true. * @return radius */ public abstract float getRadius(); /** * Get bounds. Only applicable if isRound() returns false. * @param bounds Destination rectangle to stash bounds in, or null, to create a new Rectangle * @returns bounds, or a new Rectangle, if bounds was null. */ public abstract Rectangle getBounds(Rectangle bounds); /** * @return true if this entity is round, or false if rectangular */ public abstract boolean isRound(); /** * Get all the entities who are touching this entity * @param dest A list to store the entities in, or null, to construct a new one * @return dest, or a new List if dest was null */ public final List<Entity> checkCollisions(List<Entity> dest) { if (node != null) { return node.checkCollisions(this, dest); } else { // Haven't got a node in the quadtree. Use the root assert false : this + " is not in the quadtree!"; return COLLISIONMANAGER.checkCollisions(this, dest); } } /** * Find out which entities are touching the specified rectangle * @param rect * @param dest * @return */ public static List<Entity> getCollisions(ReadableRectangle rect, List<Entity> dest) { return COLLISIONMANAGER.checkCollisions(rect, dest); } /** * Are we touching a specific point? * @param x * @param y * @return boolean */ public final boolean isTouching(float x, float y) { if (isRound() && getRadius() == 0) { return false; } if (!isRound() && getBounds(BOUNDS).isEmpty()) { return false; } if (isRound()) { return getDistanceTo(x, y) < getRadius(); } else { return BOUNDS.contains((int) x, (int) y); } } /** * Are we touching a circle? * @param x * @param y * @return boolean */ public final boolean isTouching(float x, float y, float radius) { if (isRound() && getRadius() == 0) { return false; } if (!isRound() && getBounds(BOUNDS).isEmpty()) { return false; } if (isRound()) { return getDistanceTo(x, y) < getRadius() + radius; } else { return rectRoundCollisionCheck(x, y, radius, BOUNDS); } } public final boolean isTouching(ReadableRectangle rect) { if (isRound() && getRadius() == 0) { return false; } if (!isRound() && getBounds(BOUNDS).isEmpty()) { return false; } if (isRound()) { return rectRoundCollisionCheck(mapX, mapY, getRadius(), rect); } else { return BOUNDS.intersects(rect); } } /** * Collision detection. * @param dest The entity to check for collision with. * @return true if this entity is touching the destination entity; note that an entity can never be touching itself */ public final boolean isTouching(Entity dest) { if (dest == this) { return false; } if (isRound() && getRadius() == 0) { return false; } if (dest.isRound() && dest.getRadius() == 0) { return false; } if (!isRound() && getBounds(BOUNDS).isEmpty()) { return false; } if (!dest.isRound() && dest.getBounds(TEMP).isEmpty()) { return false; } // At this point, BOUNDS and TEMP hold bounding rectangles, which we might // use if it's a rect-rect collision if (isRound() && dest.isRound()) { // Round-Round collision check float dx = dest.mapX + dest.getCollisionX() - (this.mapX + this.getCollisionX()); float dy = dest.mapY + dest.getCollisionY() - (this.mapY + this.getCollisionY()); dx *= dx; dy *= dy; return Math.sqrt(dx + dy) < getRadius() + dest.getRadius(); } else if (isRound() && !dest.isRound()) { // Round-Rect collison check return rectRoundCollisionCheck(mapX + getCollisionX(), mapY + getCollisionY(), getRadius(), TEMP); } else if (!isRound() && dest.isRound()) { // Rect-round collision check return rectRoundCollisionCheck(dest.mapX + dest.getCollisionX(), dest.mapY + dest.getCollisionY(), dest.getRadius(), BOUNDS); } else { // Rect-rect collision check return BOUNDS.intersects(TEMP); } } private boolean rectRoundCollisionCheck(float x, float y, float radius, ReadableRectangle bounds) { return GeomUtil.circleRectCollision(x, y, radius, bounds.getX(), bounds.getY(), bounds.getWidth(), bounds.getHeight()); } /** * Tick. Called every frame if this entity is alive. */ @Override public final void tick() { try { doTick(); float newR = getRadius(); if (node != null) { if (canCollide() && isActive()) { // If we've changed radius, remove and re-add to the quadtree if (oldR != newR) { oldR = newR; addToCollisionManager(); } } else { // Can't collide any more - remove from quadtree node.remove(this); node = null; } } else { if (canCollide() && isActive()) { // Add us in to the quadtree addToCollisionManager(); } } } catch (Exception e) { System.err.println("Error ticking " + this); e.printStackTrace(System.err); remove(); } } /** * Do ticking */ protected void doTick() { } /** * Recalculate screen positions relative to map scroll */ protected void calculateScreenPosition() { screenX = (int) mapX + GameScreen.getSpriteOffset().getX(); screenY = (int) mapY + GameScreen.getSpriteOffset().getY(); } protected final void setScreenX(float newScreenX) { screenX = newScreenX; } protected final void setScreenY(float newScreenY) { screenY = newScreenY; } /** * @return screen coordinates */ public final float getScreenX() { return screenX; } /** * @return screen coordinates */ public final float getScreenY() { return screenY; } /* * (non-Javadoc) * @see net.puppygames.applet.Tickable#update() */ @Override public final void update() { calculateScreenPosition(); if (sprite != null) { // Firstly, if we're significantly offscreen, hide all the sprites. if (getScreenX() < -48.0f || getScreenX() >= Game.getWidth() + 48 || getScreenY() < -48.0f - getZ() || getScreenY() >= Game.getHeight() + 48 + getZ()) { for (Sprite element : sprite) { element.setVisible(false); } doUpdate(); return; } else { for (Sprite element : sprite) { element.setVisible(visible); } } boolean searchForChildOffsets = false; for (Sprite element : sprite) { element.setLocation(screenX, screenY); if (element.getLayer() > Layers.SHADOW) { element.setFlash(flash); } // shall we bother checking anims for childOffset stuff? if (element.isDoChildOffset()) { searchForChildOffsets=true; } } if (searchForChildOffsets) { float xOffset = 0; float yOffset = 0; yOffsetTotal = 0; xOffsetTotal = 0; for (int i = 0; i < sprite.length; i ++) { boolean doOffset = false; // check for offset if (sprite[i].getChildXOffset() != 0) { xOffset = sprite[i].getChildXOffset(); xOffset *= FPMath.floatValue(sprite[0].getXScale()); if (isMirrored() && xOffset!=0) { // chaz hack! - if we've got any <offset anim commands they'd need to be mirrored too // so for now will disable mirroring of childOffset if <offset x=""/> present if (sprite[i].getOffset(null).x==0) { xOffset = -xOffset; } } xOffsetTotal += xOffset; doOffset = true; } if (sprite[i].getChildYOffset() != 0) { yOffset = sprite[i].getChildYOffset(); yOffset *= FPMath.floatValue(sprite[0].getYScale()); yOffsetTotal += yOffset; doOffset = true; } // if we've found an offset apply this to any sprites after where we found the offset if (doOffset) { for (int j = i+1; j < sprite.length; j ++) { if (sprite[j].isDoChildOffset()) { sprite[j].setLocation(screenX + xOffsetTotal, screenY + yOffsetTotal); sprite[j].setYSortOffset(-yOffsetTotal-j); // the '-j' is chaz hack! budge layers YSortOffset a tad to force render ok } } } } } LayersFeature currentAppearance = getCurrentAppearance(); float cx = getMapX() + getCollisionX(); float cy = getMapY() + getCollisionY(); if (currentAppearance != null && (GameScreen.isDiddlerOpen() || hackyTick > 0 || cx != oldX || cy != oldY || currentAppearance != oldAppearance)) { currentAppearance.updateColors(sprite, cx, cy); if (hackyTick > 0) { hackyTick --; } } oldX = cx; oldY = cy; oldAppearance = currentAppearance; } doUpdate(); } protected void doUpdate() { } /** * @param flash The flash to set. */ public final void setFlash(boolean flash) { this.flash = flash; } /** * @return Returns the flash. */ public final boolean isFlashing() { return flash; } /** * Set the location * @param x * @param y */ public final void setLocation(float x, float y) { float oldX = mapX; float oldY = mapY; this.mapX = x; this.mapY = y; // Update quadtree if (oldX != x || oldY != y) { tileX = fastFloor(getX() / MapRenderer.TILE_SIZE); tileY = fastFloor(getY() / MapRenderer.TILE_SIZE); if (canCollide() && isActive()) { addToCollisionManager(); } } onSetLocation(); } private static int fastFloor(float x) { int i = (int) x; return x >= 0.0f ? i : i == x ? i : i - 1; } protected void onSetLocation() { } /** * Add this entity to the collision manager */ protected final void addToCollisionManager() { if (node != null) { if (!node.remove(this)) { assert false : "Entity "+this+" was not found!"; } } node = COLLISIONMANAGER.add(this); if (node == null) { node = COLLISIONMANAGER; COLLISIONMANAGER.store(this); } } /** * Add to collision manager if we're not already added */ protected final void maybeAddToCollisionManager() { if (node != null) { return; } addToCollisionManager(); } /** * @return the X collision offset */ public final float getCollisionX() { if (isRound()) { return getRoundCollisionX(); } else { getBounds(TEMP); return TEMP.getX() - getMapX() + TEMP.getWidth() * 0.5f; } } protected float getRoundCollisionX() { return 0.0f; } /** * @return the Y collision offset */ public final float getCollisionY() { if (isRound()) { return getRoundCollisionY(); } else { getBounds(TEMP); return TEMP.getY() - getMapY() + TEMP.getHeight() * 0.5f; } } protected float getRoundCollisionY() { return 0.0f; } @Override public final float getX() { return getMapX() + getCollisionX(); } @Override public final float getY() { return getMapY() + getCollisionY(); } @Override public float getZ() { return 0.0f; } /** * @return Returns the x. */ public final float getMapX() { return mapX; } /** * @return Returns the y. */ public final float getMapY() { return mapY; } /** * Returns the "shooting" position - the offset at which bullets etc. should spawn from */ public float getOffsetX() { return getCollisionX(); } /** * Returns the "shooting" position - the offset at which bullets etc. should spawn from */ public float getOffsetY() { return getCollisionY(); } /** * @return the current sprite "event", or 0 if there is no sprite */ public final int getEvent() { if (sprite == null || sprite.length == 0 || sprite[0] == null) { return 0; } else { return sprite[0].getEvent(); } } /** * Set the current sprite event * @param event */ public final void setEvent(int event) { if (sprite != null && sprite[0] != null) { sprite[0].setEvent(event); } } /** * Set the sprite's appearance * @param appearance */ public final void setAnimation(int idx, Animation animation) { sprite[idx].setAnimation(animation); } /* * (non-Javadoc) * @see com.shavenpuppy.jglib.sprites.FlippedAndMirrored#isMirrored() */ public boolean isMirrored() { return sprite[0].isMirrored(); } /** * @param mirrored */ public void setMirrored(boolean mirrored) { if (sprite != null) { for (Sprite element : sprite) { element.setMirrored(mirrored); } } } /** * @param alpha */ public void setAlpha(int alpha) { if (sprite != null) { for (Sprite element : sprite) { element.setAlpha(alpha); } } } /** * @return true if the entity is shootable by the player's turrets */ public abstract boolean isShootable(); /** * @return true if the entity is a powerup */ public boolean isPowerup() { return false; } @Override public final Sprite getSprite(int n) { if (sprite == null) { // Not yet initialised return null; } return sprite[n]; } protected final int getNumSprites() { if (sprite == null) { return 0; } else { return sprite.length; } } /** * @return true if we should reflect laser beams */ public boolean isLaserProof() { return false; } /** * @return true if the entity is disruptor proof */ public boolean isDisruptorProof() { return false; } /** * Damage with a laser * @param amount * @return true if the beam is absorbed */ public boolean laserDamage(int amount) { return true; } /** * @return true if lasers should pass through this entity */ public boolean isLaserThrough() { return !isSolid() && !isLaserProof(); } /** * @return true if the laser fires over this entity when firing at aerial targets */ public boolean isLaserOver() { return isLaserThrough(); } public void capacitorDamage(int amount) { } public void stunDamage(int amount) { } public void disruptorDamage(int amount, boolean friendly) { } public void explosionDamage(int damageAmount, boolean friendly) { } /** * Augment the power of any weapon being used * @return */ public int getExtraDamage() { return 0; } /** * No two solid objects can occupy the same space at the same time. * @return true if this is a solid object */ public boolean isSolid() { return false; } /** * Is this entity attackable by gidrahs? * @return */ public boolean isAttackableByGidrahs() { return false; } /** * Is this entity attackable by units? * @return */ public boolean isAttackableByUnits() { return false; } /** * Is this entity clickable by the player? (Powerups, saucers) * @return */ public boolean isClickable() { return false; } /** * Can we hover the mouse over this entity? * @return */ public boolean isHoverable() { return true; } /** * Gets the mouse pointer we should use when hovering over this entity * @param clicked Whether the mouse is being clicked * @return a Mouse pointer appearance */ public LayersFeature getMousePointer(boolean clicked) { if (Worm.getGameState().inRangeOfCapacitor()) { return Worm.getGameState().isBezerk() ? Res.getMousePointerBezerkOnTarget() : Res.getMousePointerOnTarget(); } else { return Res.getMousePointer(); } } /** * Called when the player clicks on this entity * @param mode The current mode * @return a ClickAction constant */ public int onClicked(int mode) { return ClickAction.IGNORE; } /** * Called when the player hovers the mouse over this entity * @param mode The current mode */ public void onHovered(int mode) { } /** * Called when the player stops hovering the mouse over this entity * @param mode The current mode */ public void onLeave(int mode) { } /** * @return the entity's X tile coordinate */ public final int getTileX() { return tileX; } /** * @return the entity's Y tile coordinate */ public final int getTileY() { return tileY; } /** * For those entities that have an appearance that uses a LayersFeature, override * this method to return the current appearance. * <p>The default is to return null. * @return null, or a LayersFeature */ protected LayersFeature getCurrentAppearance() { return null; } public boolean usesAmmo() { return false; } public void onReloaded() { } /** * Process collisions */ public static void checkCollisions() { COLLISIONMANAGER.checkCollisions(); } public static void reset() { COLLISIONMANAGER.clear(); } public float getFinalXOffset() { return this.xOffsetTotal; } public float getFinalYOffset() { return this.yOffsetTotal; } public boolean isFlying() { return false; } /** * Called when the target has just completely deflected a bullet * @param target */ public void onBulletDeflected(Entity target) { } /** * @return a List of Entities this Entity should "ignore", or null */ public ArrayList<Entity> getIgnore() { return null; } }