/* * 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.io.Serializable; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import org.lwjgl.util.Point; import org.lwjgl.util.Rectangle; import worm.Entity; import worm.GameMap; import worm.MapRenderer; import worm.Worm; import worm.WormGameState; import worm.features.GidrahFeature; import worm.path.AStar; import com.shavenpuppy.jglib.interpolators.CosineInterpolator; import com.shavenpuppy.jglib.interpolators.LinearInterpolator; import com.shavenpuppy.jglib.interpolators.OpenLinearInterpolator; import com.shavenpuppy.jglib.util.IntList; import com.shavenpuppy.jglib.util.Util; /** * Handles movement for a gidrah */ class GidrahMovement implements Movement { private static final long serialVersionUID = 1L; /** A queue of gidrahs who should rethink their routes */ private static final List<GidrahMovement> QUEUE = new ArrayList<GidrahMovement>(WormGameState.MAX_GIDRAHS); /** A Set which shadows QUEUE */ private static final Set<GidrahMovement> QUEUESET = new HashSet<GidrahMovement>(); private static final ArrayList<Entity> COLLISIONS = new ArrayList<Entity>(); private static final Rectangle BOUNDS = new Rectangle(); private static final float GIDRAH_MAX_SPEED = 4.0f; // in ticks per 16 pixels private static final int MAX_TOTAL_THINK_TIME = 256; private static final int MAX_THINK_TIME = 256; private static final int MAX_RAMP_UP_DURATION = 4800; private static final int RAMP_UP_DURATION = 2400; private static final int RAMP_UP_PER_LEVEL = 60; private static final float SUICIDE_DISTANCE = 256.0f; private static final float BASE_DISTANCE = 512.0f; private static final float DANGER_SPEEDUP_FACTOR = 0.5f; private static final int RETHINK_MAX = 300; private static final int MAX_FAILS = 100; public static int totalThinkTime; private static class PointPair implements Serializable { private static final long serialVersionUID = 1L; final Point a = new Point(), b = new Point(); @Override public int hashCode() { return a.hashCode() ^ b.hashCode(); } @Override public boolean equals(Object obj) { PointPair ep = (PointPair) obj; return ep.a.equals(a) && ep.b.equals(b) || ep.a.equals(b) && ep.b.equals(a); } @Override public String toString() { return "PointPair["+a+","+b+"]"; } public Rectangle getBounds() { Rectangle ret = new Rectangle(); ret.add(a.getX() + 1, a.getY() + 1); ret.add(b.getX() + 1, b.getY() + 1); return ret; } } private static final PointPair SCRATCH_POINT_PAIR = new PointPair(); /** The gidrah */ private final Gidrah gidrah; /** Pathfinding */ private final AStar astar; /** Topology */ private final GidrahGameMapTopology topology; /** Path */ private final IntList path = new IntList(true, WormGameState.ABS_MAX_SIZE); /** gameState */ private final WormGameState gameState; /** Game map */ private final GameMap map; /** Gidrah feature */ private final GidrahFeature feature; /** Movement start and end */ private float sourceMapX, sourceMapY, targetMapX, targetMapY; /** Movement paused */ private boolean paused; /** Thinking */ private boolean thinking; /** Knocked back */ private boolean knockedBack; /** Movement tick */ private int tick, currentSpeed, rampTick, rethinkTick; /** Number of squares to move */ private int moves; /** Occupied position */ private int occupiedX, occupiedY; /** Current path start and end */ private PointPair startAndEnd; /** Diagonal preference */ private boolean diagonal; /** Fail count */ private int failCount; /** * C'tor */ GidrahMovement(Gidrah gidrah) { this.gidrah = gidrah; this.feature = gidrah.getFeature(); this.gameState = Worm.getGameState(); this.map = gameState.getMap(); diagonal = feature.getDiagonal(); topology = new GidrahGameMapTopology(this); astar = new AStar(topology); if (!feature.isGidlet()) { map.setOccupied(occupiedX = gidrah.getTileX(), occupiedY = gidrah.getTileY()); } } boolean isDiagonal() { return diagonal; } @Override public void attack() { map.setAttacking(occupiedX, occupiedY); } @Override public void dontAttack() { map.clearAttacking(occupiedX, occupiedY); } @Override public void adjust(float newX, float newY) { sourceMapX = newX; sourceMapY = newY; targetMapX = occupiedX * MapRenderer.TILE_SIZE; targetMapY = occupiedY * MapRenderer.TILE_SIZE; thinking = false; paused = false; float dx = targetMapX - sourceMapX; float dy = targetMapY - sourceMapY; if (dx == 0.0f && dy == 0.0f) { return; } // getspeed is the time the gidrah takes to move 16 pixels. // therefore getspeed/16 is teh time the gidrah takes to move 1 pixel // therefore the movement should be distance * getspeed / 16 tick = currentSpeed = (int) Math.max(1, (Math.sqrt(dx * dx + dy * dy) * getSpeed() / 16.0f)); knockedBack = true; } /** * @return true if the move succeeded */ boolean updateLocation() { float ratio = (float) tick / currentSpeed; float oldX = gidrah.getMapX(); float oldY = gidrah.getMapY(); float newX; gidrah.setLocation ( newX = LinearInterpolator.instance.interpolate(targetMapX, sourceMapX, ratio), LinearInterpolator.instance.interpolate(targetMapY, sourceMapY, ratio) ); // Gidlets aren't allowed to bump into other gidrahs if (feature.isGidlet()) { // If we bump into another gidrah, rollback Entity.getCollisions(gidrah.getBounds(BOUNDS), COLLISIONS); for (int i = 0; i < COLLISIONS.size(); i ++) { Entity e = COLLISIONS.get(i); if (e == gidrah) { continue; } if (e instanceof Gidrah && System.identityHashCode(e) > System.identityHashCode(gidrah)) { // Rollback gidrah.setLocation(oldX, oldY); return false; } } } if (oldX < newX) { gidrah.setMirrored(false); } else if (oldX > newX) { gidrah.setMirrored(true); } return true; } @Override public void remove() { astar.cancel(); if (!feature.isGidlet()) { map.clearOccupied(occupiedX, occupiedY); } // Remove from the queue if we're in there if (QUEUESET.contains(this)) { QUEUESET.remove(this); QUEUE.remove(this); } } @Override public boolean isMoving() { return !paused && !thinking; } @Override public void tick() { if (paused) { tick --; if (tick <= 0) { paused = false; // Choose next square to move to and start moving chooseDestination(); } } else { if (thinking) { think(); if (thinking) { // Still thinking return; } } if (tick > 0) { tick --; if (!updateLocation()) { tick ++; } else { if (!gameState.isLevelActive()) { rampTick ++; } } } if (tick <= 0) { // Stop moving now, maybe moves --; if (moves <= 0) { if (knockedBack) { knockedBack = false; } else { tick = getPause(); } if (tick > 0) { paused = true; } else { chooseDestination(); } } else { chooseDestination(); } } } } int getSpeed() { float minSpeed = feature.getSpeed(); // in pixels/sec float maxSpeed = feature.getSpeed() * MAX_SPEED_MULTIPLIER; // in pixels/sec float ret; float difficulty = gameState.getDifficulty(); if (feature.isExploding()) { // Speed up as we get in range of the target Entity target = gidrah.getTarget(); if (target != null && target.isActive()) { float distance = gidrah.getDistanceTo(target); if (distance > 0.0f) { float ratio = distance / SUICIDE_DISTANCE; minSpeed = CosineInterpolator.instance.interpolate(minSpeed * 4.0f, minSpeed, ratio); maxSpeed = CosineInterpolator.instance.interpolate(maxSpeed * 4.0f, maxSpeed, ratio); } } } else { // Speed up as we get in range of the base Building base = gameState.getBase(); if (base != null && base.isActive()) { float distance = gidrah.getDistanceTo(base); if (distance > 0.0f && distance <= BASE_DISTANCE) { float ratio = distance / BASE_DISTANCE; maxSpeed = LinearInterpolator.instance.interpolate(maxSpeed * (1.0f + difficulty), maxSpeed, ratio); } } } ret = OpenLinearInterpolator.instance.interpolate(minSpeed, maxSpeed, difficulty); if (gameState.isRushActive()) { ret = (int) LinearInterpolator.instance.interpolate ( ret, ret * LinearInterpolator.instance.interpolate(1.0f, 3.0f, gameState.getGidrahDeathRatio()), getSpeedupRamp() ); } if (gidrah.isTangled()) { ret = Math.min(maxSpeed, ret * 0.25f); } else if (!gidrah.getFeature().isBoss()) { // Increase gidrah speed with danger, unless we're a boss ret += Math.max(0, map.getDanger(gidrah.getTileX(), gidrah.getTileY()) - gidrah.getFeature().getArmour()) * DANGER_SPEEDUP_FACTOR * difficulty; } // ret now contains the speed of the gidrah in pixels / 60ticks. We'll convert this into the number of ticks it takes to move // 16 pixels, which is what getSpeed() needs to return return (int) Math.max(GIDRAH_MAX_SPEED, 960.0f / ret); } float getSpeedupRamp() { if (gameState.isRushActive()) { return Math.min(1.0f, (float) rampTick / (float) Math.min(MAX_RAMP_UP_DURATION, RAMP_UP_DURATION + gameState.getLevel() * RAMP_UP_PER_LEVEL)); // ramp up duration takes longer and longer } else { return 0.0f; } } int getPause() { return (int) LinearInterpolator.instance.interpolate(feature.getPause(), feature.getPause() / Movement.MAX_SPEED_MULTIPLIER, gameState.getDifficulty()); } /** * @return the gidrah */ public Gidrah getGidrah() { return gidrah; } @Override public void reset() { path.clear(); } /** * Choose the next square to move to */ void chooseDestination() { if (moves <= 0) { moves = Util.random(feature.getMoves(), (int) (feature.getMoves() * Movement.MAX_SPEED_MULTIPLIER)); } sourceMapX = gidrah.getMapX(); sourceMapY = gidrah.getMapY(); // Now, where are we going? // Firstly if we've got a path that leads to the destination, let's check to see if the next step in that path is clear. If it is clear, // let's carry on using it instead of invoking A*. if (next()) { return; } path.clear(); startAndEnd = new PointPair(); startAndEnd.a.setLocation(gidrah.getTileX(), gidrah.getTileY()); Entity target = gidrah.getTarget(); startAndEnd.b.setLocation(target.getTileX(), target.getTileY()); if (startAndEnd.a.equals(startAndEnd.b)) { // Already at target! if (feature.isGidlet()) { targetMapX = target.getTileX() * MapRenderer.TILE_SIZE + Util.random(0, MapRenderer.TILE_SIZE - 1); targetMapY = target.getTileY() * MapRenderer.TILE_SIZE + Util.random(0, MapRenderer.TILE_SIZE - 1); thinking = false; paused = false; tick = getSpeed(); currentSpeed = tick; } else { // Rethink target. Kill if necessary. rethinkTick ++; if (rethinkTick > RETHINK_MAX) { gidrah.remove(); } else { gidrah.findTarget(); } } return; } astar.findPath(GidrahGameMapTopology.pack(gidrah.getTileX(), gidrah.getTileY()), GidrahGameMapTopology.pack(target.getTileX(), target.getTileY()), path); thinking = true; //think(); } /** * Take the next step along our chosen path. * @return false if we need to calculate a new path */ boolean next() { if (path.size() == 0) { return false; } int nextTarget = path.remove(0); int targetTileX = GidrahGameMapTopology.getX(nextTarget); int targetTileY = GidrahGameMapTopology.getY(nextTarget); //assert targetTileX != 0 || targetTileY != 0; if (topology.canMove(gidrah.getTileX(), gidrah.getTileY(), targetTileX, targetTileY)) { // If this is a diagonal move, we take a bit longer over it, and ensure at least 1 route using HV movement only if (Math.abs(gidrah.getTileX() - targetTileX) + Math.abs(gidrah.getTileY() - targetTileY) > 1) { tick = (int) (getSpeed() * 1.42f); } else { tick = getSpeed(); } tick *= topology.getSpeed(targetTileX, targetTileY); currentSpeed = tick; targetMapX = targetTileX * MapRenderer.TILE_SIZE + (feature.isGidlet() ? Util.random(0, MapRenderer.TILE_SIZE - 1) : 0); targetMapY = targetTileY * MapRenderer.TILE_SIZE + (feature.isGidlet() ? Util.random(0, MapRenderer.TILE_SIZE - 1) : 0); thinking = false; paused = false; if (!feature.isGidlet()) { // Erase occupation of current square map.clearOccupied(occupiedX, occupiedY); // and occupy the destination square map.setOccupied(occupiedX = targetTileX, occupiedY = targetTileY); } return true; } else { return false; } } /** * Search A* pathfinder for a few steps. If we find the goal, start moving. If we fail, choose destination again. If * we still haven't found anything, just return. */ void think() { // long timeThen = Sys.getTime(); for (int i = 0; i < MAX_THINK_TIME && ++totalThinkTime < MAX_TOTAL_THINK_TIME; i ++) { // System.out.println("Step "+i); switch (astar.nextStep()) { case AStar.SEARCH_STATE_SUCCEEDED: failCount = 0; // long timeNow = Sys.getTime(); //System.out.println("Route found: "+((double)(timeNow - timeThen)) / Sys.getTimerResolution()+"s, path "+path.size()+", steps "+astar.getNumSteps()); // Found the goal! Move one step closer. // System.out.println("Gidrah "+gidrah+" found goal in "+astar.getNumSteps()+" steps"); startAndEnd = new PointPair(); startAndEnd.a.setLocation(gidrah.getTileX(), gidrah.getTileY()); startAndEnd.b.setLocation(gidrah.getTarget().getTileX(), gidrah.getTarget().getTileY()); if (!next()) { chooseDestination(); } // Remove from the queue if we're in there if (QUEUESET.contains(this)) { QUEUESET.remove(this); QUEUE.remove(this); } return; case AStar.SEARCH_STATE_FAILED: // Total failure. Wait a bit then think again. // System.out.println("Gidrah "+gidrah+" totally failed to find goal after "+astar.getNumSteps()+" steps"); thinking = false; paused = true; tick = Util.random(10, 30); path.clear(); // Remove from the queue if we're in there if (QUEUESET.contains(this)) { QUEUESET.remove(this); QUEUE.remove(this); } failCount ++; if (failCount > MAX_FAILS) { gidrah.onMovementFail(); } return; case AStar.SEARCH_STATE_SEARCHING: // Carry on searching; break; default: assert false; break; } } // Just return return; } @Override public void maybeRethink(Rectangle bounds) { // Does our path intersect the bounds? if (startAndEnd != null && startAndEnd.getBounds().intersects(bounds)) { int n = path.size(); for (int i = 0; i < n; i ++) { int coord = path.get(i); int x = GidrahGameMapTopology.getX(coord); int y = GidrahGameMapTopology.getY(coord); if (bounds.contains(x, y)) { // Queue for a rethink queue(this); return; } } } } private static void queue(GidrahMovement gm) { if (QUEUESET.contains(gm)) { // Already queued return; } QUEUE.add(gm); QUEUESET.add(gm); } private static void processQueue() { if (QUEUE.size() == 0) { return; } // If the gidrah at the head of the queue is not thinking, start it thinking. When it finds a route // it'll remove itself from the queue. GidrahMovement gm = QUEUE.get(0); if (!gm.thinking) { gm.reset(); } } public static void resetTotalThinkTime() { totalThinkTime = 0; // Also process queue processQueue(); } public static void init() { QUEUE.clear(); QUEUESET.clear(); } }