package it.marteEngine.entity; import it.marteEngine.ME; import it.marteEngine.StateManager; import it.marteEngine.World; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import org.newdawn.slick.Animation; import org.newdawn.slick.Color; import org.newdawn.slick.GameContainer; import org.newdawn.slick.Graphics; import org.newdawn.slick.Image; import org.newdawn.slick.Input; import org.newdawn.slick.SlickException; import org.newdawn.slick.SpriteSheet; import org.newdawn.slick.geom.Rectangle; import org.newdawn.slick.geom.Shape; import org.newdawn.slick.geom.Vector2f; //TODO modify hitbox coordinates to a real shape without changing method interface. //TODO a shape can be rotated and scaled when the entity is rotated and scaled. public abstract class Entity implements Comparable<Entity> { /** default collidable type SOLID */ public static final String SOLID = "Solid"; /** predefined type for player */ public static final String PLAYER = "Player"; /** the world this entity lives in */ public World world = null; /** unique identifier */ public String name; /** x position */ public float x; /** y position */ public float y; /** * If this entity is centered the x,y position is in the center. otherwise * the x,y position is the top left corner. */ public boolean centered = false; /** * width of the entity. not necessarily the width of the hitbox. Used for * world wrapping */ public int width; /** * height of the entity. not necessarily the height of the hitbox. Used for * world wrapping */ public int height; public float previousx, previousy; /** start x and y position stored for reseting for example. very helpful */ public float startx, starty; public boolean wrapHorizontal = false; public boolean wrapVertical = false; /** speed vector (x,y): specifies x and y movement per update call in pixels */ public Vector2f speed = new Vector2f(0, 0); /** * angle in degrees from 0 to 360, used for drawing the entity rotated. NOT * used for direction! */ protected int angle = 0; /** scale used for both horizontal and vertical scaling. */ public float scale = 1.0f; /** * color of the entity, mainly used for alpha transparency, but could also * be used for tinting */ private Color color = new Color(Color.white); private AlarmContainer alarms; protected SpriteSheet sheet; private Map<String, Animation> animations = new HashMap<String, Animation>(); private String currentAnim; public int duration = 200; public int depth = -1; /** static image for non-animated entity */ public Image currentImage; /** input commands */ public Map<String, int[]> commands = new HashMap<String, int[]>(); /** The types this entity can collide with */ private HashSet<String> collisionTypes = new HashSet<String>(); /** true if this entity can receive updates */ public boolean active = true; public boolean collidable = true; public boolean visible = true; public float hitboxOffsetX; public float hitboxOffsetY; public int hitboxWidth; public int hitboxHeight; public StateManager stateManager; /** * Create a new entity positioned at the (x,y) coordinates. */ public Entity(float x, float y) { this.x = x; this.y = y; this.startx = x; this.starty = y; stateManager = new StateManager(); alarms = new AlarmContainer(this); } /** * Create a new entity positioned at the (x,y) coordinates. Displayed as an * image. */ public Entity(float x, float y, Image image) { this(x, y); setGraphic(image); } /** * Set if the image or animation must be centered */ public void setCentered(boolean center) { int whalf = 0, hhalf = 0; if (currentImage != null) { whalf = currentImage.getWidth() / 2; hhalf = currentImage.getHeight() / 2; } if (currentAnim != null) { whalf = animations.get(currentAnim).getWidth() / 2; hhalf = animations.get(currentAnim).getHeight() / 2; } if (center) { // modify hitbox position accordingly - move it a bit up and left this.hitboxOffsetX -= whalf; this.hitboxOffsetY -= hhalf; this.centered = true; } else { if (centered) { // reset hitbox position to top left origin this.hitboxOffsetX += whalf; this.hitboxOffsetY += hhalf; } this.centered = false; } } public void update(GameContainer container, int delta) throws SlickException { previousx = x; previousy = y; if (stateManager != null && stateManager.currentState() != null) { stateManager.update(container, delta); return; } updateAnimation(delta); if (speed != null) { x += speed.x; y += speed.y; } checkWorldBoundaries(); previousx = x; previousy = y; } protected void updateAnimation(int delta) { if (animations != null) { if (currentAnim != null) { Animation anim = animations.get(currentAnim); if (anim != null) { anim.update(delta); } } } } public void render(GameContainer container, Graphics g) throws SlickException { if (stateManager != null && stateManager.currentState() != null) { stateManager.render(g); return; } float xpos = x, ypos = y; if (currentAnim != null) { Animation anim = animations.get(currentAnim); int w = anim.getWidth(); int h = anim.getHeight(); int whalf = w / 2; int hhalf = h / 2; if (centered) { xpos = x - (whalf * scale); ypos = y - (hhalf * scale); } if (angle != 0) { g.rotate(x, y, angle); } anim.draw(xpos, ypos, w * scale, h * scale, color); if (angle != 0) g.resetTransform(); } else if (currentImage != null) { currentImage.setAlpha(color.a); int w = currentImage.getWidth() / 2; int h = currentImage.getHeight() / 2; if (centered) { xpos -= w; ypos -= h; currentImage.setCenterOfRotation(w, h); } else currentImage.setCenterOfRotation(0, 0); if (angle != 0) { currentImage.setRotation(angle); } if (scale != 1.0f) { if (centered) g.translate(xpos - (w * scale - w), ypos - (h * scale - h)); else g.translate(xpos, ypos); g.scale(scale, scale); g.drawImage(currentImage, 0, 0); } else g.drawImage(currentImage, xpos, ypos); if (scale != 1.0f) g.resetTransform(); } if (ME.debugEnabled && collidable) { g.setColor(ME.borderColor); Rectangle hitBox = new Rectangle(x + hitboxOffsetX, y + hitboxOffsetY, hitboxWidth, hitboxHeight); g.draw(hitBox); g.setColor(Color.white); g.drawRect(x, y, 1, 1); // draw entity center if (width != 0 && height != 0) { float centerX = x + width / 2; float centerY = y + height / 2; g.setColor(Color.green); g.drawRect(centerX, centerY, 1, 1); g.setColor(Color.white); } } } /** * Set an image as graphic */ public void setGraphic(Image image) { this.currentImage = image; this.width = image.getWidth(); this.height = image.getHeight(); } /** * Set a sprite sheet as graphic */ public void setGraphic(SpriteSheet sheet) { this.sheet = sheet; this.width = sheet.getSprite(0, 0).getWidth(); this.height = sheet.getSprite(0, 0).getHeight(); } public void addAnimation(String animName, boolean loop, int row, int... frames) { Animation anim = new Animation(false); anim.setLooping(loop); for (int frame : frames) { anim.addFrame(sheet.getSprite(frame, row), duration); } addAnimation(animName, anim); } public Animation addAnimation(SpriteSheet sheet, String animName, boolean loop, int row, int... frames) { Animation anim = new Animation(false); anim.setLooping(loop); for (int frame : frames) { anim.addFrame(sheet.getSprite(frame, row), duration); } addAnimation(animName, anim); return anim; } /** * Add animation to entity. The frames can be flipped horizontally and/or * vertically. */ public void addFlippedAnimation(String animName, boolean loop, boolean fliphorizontal, boolean flipvertical, int row, int... frames) { Animation anim = new Animation(false); anim.setLooping(loop); for (int frame : frames) { anim.addFrame( sheet.getSprite(frame, row).getFlippedCopy(fliphorizontal, flipvertical), duration); } addAnimation(animName, anim); } /** * Add an animation.The first animation added is set as the current * animation. */ public void addAnimation(String animName, Animation animation) { boolean firstAnim = animations.isEmpty(); animations.put(animName, animation); if (firstAnim) { setAnim(animName); } } /** * Start playing the animation stored as animName. * * @param animName * The name of the animation to play * @throws IllegalArgumentException * If there is no animation stored as animName * @see #addAnimation(String, org.newdawn.slick.Animation) */ public void setAnim(String animName) { if (!animations.containsKey(animName)) { throw new IllegalArgumentException("No animation for " + animName); } currentAnim = animName; Animation currentAnimation = animations.get(currentAnim); width = currentAnimation.getWidth(); height = currentAnimation.getHeight(); } /** * define commands to handle inputs * * @param command * name of the command * @param keys * keys or mouse input from {@link Input} class */ public void define(String command, int... keys) { commands.put(command, keys); } /** * Check if a command is down */ public boolean check(String command) { if (!commands.containsKey(command)) return false; int[] checked = commands.get(command); Input input = world.container.getInput(); for (int i = 0; i < checked.length; i++) { if (input.isKeyDown(checked[i])) { return true; } else if (checked[i] < 10) { // 10 is max number of button on a mouse, @see Input if (input.isMousePressed(checked[i])) { return true; } } } return false; } /** * Check if a command is pressed */ public boolean pressed(String command) { if (!commands.containsKey(command)) return false; int[] checked = commands.get(command); Input input = world.container.getInput(); for (int i = 0; i < checked.length; i++) { if (input.isKeyPressed(checked[i])) { return true; } else if (checked[i] == Input.MOUSE_LEFT_BUTTON || checked[i] == Input.MOUSE_RIGHT_BUTTON) { if (input.isMousePressed(checked[i])) { return true; } } } return false; } /** * Compare to another entity on zLevel */ public int compareTo(Entity o) { if (depth == o.depth) return 0; if (depth > o.depth) return 1; return -1; } /** * Set the hitbox used for collision detection. If an entity has an hitbox, * it is collidable against other entities. * * @param xOffset * The offset of the hitbox on the x axis. Relative to the top * left point of the entity. * @param yOffset * The offset of the hitbox on the y axis. Relative to the top * left point of the entity. * @param width * The width of the rectangle in pixels * @param height * The height of the rectangle in pixels */ public void setHitBox(float xOffset, float yOffset, int width, int height) { this.hitboxOffsetX = xOffset; this.hitboxOffsetY = yOffset; this.hitboxWidth = width; this.hitboxHeight = height; this.width = width; this.height = height; this.collidable = true; } /** * Add a type that this entity can collide with. To allow collision with * other entities add at least 1 type. For example in a space invaders game. * To allow a ship to collide with a bullet and a monster: * ship.addType("bullet", "monster") * * @param types * The types that this entity can collide with. */ public boolean addType(String... types) { return collisionTypes.addAll(Arrays.asList(types)); } /** * Reset the types that this entity can collide with */ public void clearTypes() { collisionTypes.clear(); } /** * Check for a collision with another entity of the given entity type. Two * entities collide when the hitbox of this entity intersects with the * hitbox of another entity. * <p/> * The hitbox starts at the provided x,y coordinates. The size and offset of * the hitbox is set in the {@link #setHitBox(float, float, int, int)} * method. * <p/> * If a collision occurred then both the entities are notified of the * collision by the {@link #collisionResponse(Entity)} method. * * @param type * The type of another entity to check for collision. * @param x * The x coordinate where the the collision check needs to be * done. * @param y * The y coordinate where the the collision check needs to be * done. * @return The first entity that is colliding with this entity at the x,y * coordinates, or NULL if there is no collision. */ public Entity collide(String type, float x, float y) { if (type == null || type.isEmpty()) return null; // offset for (Entity entity : world.getEntities()) { if (entity.collidable && entity.isType(type)) { if (!entity.equals(this) && x + hitboxOffsetX + hitboxWidth > entity.x + entity.hitboxOffsetX && y + hitboxOffsetY + hitboxHeight > entity.y + entity.hitboxOffsetY && x + hitboxOffsetX < entity.x + entity.hitboxOffsetX + entity.hitboxWidth && y + hitboxOffsetY < entity.y + entity.hitboxOffsetY + entity.hitboxHeight) { this.collisionResponse(entity); entity.collisionResponse(this); return entity; } } } return null; } /** * Checks for collision against multiple types. * * @see #collide(String, float, float) */ public Entity collide(String[] types, float x, float y) { for (String type : types) { Entity e = collide(type, x, y); if (e != null) return e; } return null; } /** * Checks if this Entity collides with a specific Entity. * * @param other * The Entity to check for collision * @param x * The x coordinate where the the collision check needs to be * done. * @param y * The y coordinate where the the collision check needs to be * done. * @return The entity that is colliding with the other entity at the x,y * coordinates, or NULL if there is no collision. */ public Entity collideWith(Entity other, float x, float y) { if (other.collidable) { if (!other.equals(this) && x + hitboxOffsetX + hitboxWidth > other.x + other.hitboxOffsetX && y + hitboxOffsetY + hitboxHeight > other.y + other.hitboxOffsetY && x + hitboxOffsetX < other.x + other.hitboxOffsetX + other.hitboxWidth && y + hitboxOffsetY < other.y + other.hitboxOffsetY + other.hitboxHeight) { this.collisionResponse(other); other.collisionResponse(this); return other; } return null; } return null; } public List<Entity> collideInto(String type, float x, float y) { if (type == null || type.isEmpty()) return null; ArrayList<Entity> collidingEntities = null; for (Entity entity : world.getEntities()) { if (entity.collidable && entity.isType(type)) { if (!entity.equals(this) && x + hitboxOffsetX + hitboxWidth > entity.x + entity.hitboxOffsetX && y + hitboxOffsetY + hitboxHeight > entity.y + entity.hitboxOffsetY && x + hitboxOffsetX < entity.x + entity.hitboxOffsetX + entity.hitboxWidth && y + hitboxOffsetY < entity.y + entity.hitboxOffsetY + entity.hitboxHeight) { this.collisionResponse(entity); entity.collisionResponse(this); if (collidingEntities == null) collidingEntities = new ArrayList<Entity>(); collidingEntities.add(entity); } } } return collidingEntities; } /** * Checks if this Entity contains the specified point. The * {@link #collisionResponse(Entity)} is called to notify this entity of the * collision. * * @param x * The x coordinate of the point to check * @param y * The y coordinate of the point to check * @return If this entity contains the specified point */ public boolean collidePoint(float x, float y) { if (x >= this.x - hitboxOffsetX && y >= this.y - hitboxOffsetY && x < this.x - hitboxOffsetX + width && y < this.y - hitboxOffsetY + height) { this.collisionResponse(null); return true; } return false; } /** * overload if you want to act on addition to the world */ public void addedToWorld() { } /** * overload if you want to act on removal from the world */ public void removedFromWorld() { } /** * Response to a collision with another entity * * @param other * The other entity that collided with us. */ public void collisionResponse(Entity other) { } /** * overload if you want to act on leaving world boundaries */ public void leftWorldBoundaries() { } public Image getCurrentImage() { return currentImage; } public void setWorld(World world) { this.world = world; } public void checkWorldBoundaries() { if ((x + width) < 0) { leftWorldBoundaries(); if (wrapHorizontal) { x = this.world.width + 1; } } if (x > this.world.width) { leftWorldBoundaries(); if (wrapHorizontal) { x = (-width + 1); } } if ((y + height) < 0) { leftWorldBoundaries(); if (wrapVertical) { y = this.world.height + 1; } } if (y > this.world.height) { leftWorldBoundaries(); if (wrapVertical) { y = (-height + 1); } } } public String toString() { StringBuffer sb = new StringBuffer(); sb.append("name: ").append(name); sb.append(", types: ").append(collisionTypesToString()); sb.append(", depth: ").append(depth); sb.append(", x: ").append(x); sb.append(", y: ").append(y); return sb.toString(); } public String[] getCollisionTypes() { return collisionTypes.toArray(new String[collisionTypes.size()]); } public boolean isType(String type) { return collisionTypes.contains(type); } /** * remove ourselves from world */ public void destroy() { this.world.remove(this); this.visible = false; } /***************** some methods to deal with angles and vectors ************************************/ public int getAngleToPosition(Vector2f otherPos) { Vector2f diff = otherPos.sub(new Vector2f(x, y)); return (((int) diff.getTheta()) + 90) % 360; } public int getAngleDiff(int angle1, int angle2) { return ((((angle2 - angle1) % 360) + 540) % 360) - 180; } public Vector2f getPointWithAngleAndDistance(int angle, float distance) { Vector2f point; float tx, ty; double theta = StrictMath.toRadians(angle + 90); tx = (float) (this.x + distance * StrictMath.cos(theta)); ty = (float) (this.y + distance * StrictMath.sin(theta)); point = new Vector2f(tx, ty); return point; } public float getDistance(Entity other) { return getDistance(new Vector2f(other.x, other.y)); } public float getDistance(Vector2f otherPos) { Vector2f myPos = new Vector2f(x, y); return myPos.distance(otherPos); } public static Vector2f calculateVector(float angle, float magnitude) { Vector2f v = new Vector2f(); v.x = (float) Math.sin(Math.toRadians(angle)); v.x *= magnitude; v.y = (float) -Math.cos(Math.toRadians(angle)); v.y *= magnitude; return v; } public static float calculateAngle(float x, float y, float x1, float y1) { double angle = Math.atan2(y - y1, x - x1); return (float) (Math.toDegrees(angle) - 90); } /***************** some methods to deal with alarms ************************************/ /** * Add an alarm with the given parameters and add it to this Entity */ public void addAlarm(String alarmName, int triggerTime, boolean oneShot) { addAlarm(alarmName, triggerTime, oneShot, true); } /** * Add an alarm with given parameters and add it to this Entity */ public void addAlarm(String alarmName, int triggerTime, boolean oneShot, boolean startNow) { Alarm alarm = new Alarm(alarmName, triggerTime, oneShot); alarms.addAlarm(alarm, startNow); } public boolean restartAlarm(String alarmName) { return alarms.restartAlarm(alarmName); } public boolean pauseAlarm(String alarmName) { return alarms.pauseAlarm(alarmName); } public boolean resumeAlarm(String alarmName) { return alarms.resumeAlarm(alarmName); } public boolean destroyAlarm(String alarmName) { return alarms.destroyAlarm(alarmName); } public boolean hasAlarm(String alarmName) { return alarms.hasAlarm(alarmName); } /** * Overwrite this method if your entity shall react on alarms that reached * their triggerTime. * * @param alarmName * the name of the alarm that triggered right now */ public void alarmTriggered(String alarmName) { // this method needs to be overwritten to deal with alarms } /** * this method is called automatically by the World and must not be called * by your game code. Don't touch this method ;-) Consider it private! */ public void updateAlarms(int delta) { alarms.update(delta); } public int getAngle() { return angle; } // TODO: add proper rotation for the hitbox/shape here!!! public void setAngle(int angle) { this.angle = angle; } public Color getColor() { return color; } public void setColor(Color color) { this.color = color; } public float getAlpha() { return color.a; } public void setAlpha(float alpha) { if (alpha >= 0.0f && alpha <= 1.0f) color.a = alpha; } public void setPosition(Vector2f pos) { if (pos != null) { this.x = pos.x; this.y = pos.y; } } public boolean isCurrentAnim(String animName) { return currentAnim.equals(animName); } public String toCsv() { return "" + (int) x + "," + (int) y + "," + name + "," + collisionTypesToString(); } private String collisionTypesToString() { StringBuffer sb = new StringBuffer(); for (String type : collisionTypes) { if (sb.length() > 0) { sb.append(", "); } sb.append(type); } return sb.toString(); } /** * @param shape * the shape to check for intersection * @return The entities that intersect with their hitboxes into the given * shape */ public List<Entity> intersect(Shape shape) { if (shape == null) return null; List<Entity> result = new ArrayList<Entity>(); for (Entity entity : world.getEntities()) { if (entity.collidable && !entity.equals(this)) { Rectangle rec = new Rectangle(entity.x, entity.y, entity.width, entity.height); if (shape.intersects(rec)) { result.add(entity); } } } return result; } }