diff options
author | Jérémy Zurcher <jeremy@asynk.ch> | 2015-06-30 06:21:50 +0200 |
---|---|---|
committer | Jérémy Zurcher <jeremy@asynk.ch> | 2015-06-30 06:21:50 +0200 |
commit | d74bcf9bf390418df35d94b54d59df0170033f65 (patch) | |
tree | 2ceae1bd287c42b4f109c47d2360199d6076a156 /core/src/ch/asynk/creepingarmor/engine | |
parent | 06868b70f82ed30e46ea3fd012a0befc8380bbad (diff) | |
download | RustAndDust-d74bcf9bf390418df35d94b54d59df0170033f65.zip RustAndDust-d74bcf9bf390418df35d94b54d59df0170033f65.tar.gz |
TankOnTank -> CreepingArmor
Diffstat (limited to 'core/src/ch/asynk/creepingarmor/engine')
35 files changed, 4218 insertions, 0 deletions
diff --git a/core/src/ch/asynk/creepingarmor/engine/Attack.java b/core/src/ch/asynk/creepingarmor/engine/Attack.java new file mode 100644 index 0000000..e4fa678 --- /dev/null +++ b/core/src/ch/asynk/creepingarmor/engine/Attack.java @@ -0,0 +1,28 @@ +package ch.asynk.creepingarmor.engine; + +public class Attack +{ + public Pawn attacker; + public Pawn target; + public int distance; + public boolean isClear; + public boolean isFlank; + + public Attack(Pawn attacker) + { + this.attacker = attacker; + } + + public String toString() + { + return String.format("attack : %s -> %s dist:%d clear:%b flank:%b", attacker, target, distance, isClear, isFlank); + } + + public void reset() + { + target = null; + distance = 0;; + isClear = false; + isFlank = false; + } +} diff --git a/core/src/ch/asynk/creepingarmor/engine/Board.java b/core/src/ch/asynk/creepingarmor/engine/Board.java new file mode 100644 index 0000000..4c42003 --- /dev/null +++ b/core/src/ch/asynk/creepingarmor/engine/Board.java @@ -0,0 +1,554 @@ +package ch.asynk.creepingarmor.engine; + +import java.util.Iterator; +import java.util.Collection; +import java.util.ArrayList; +import java.util.LinkedHashSet; + +import com.badlogic.gdx.Gdx; + +import com.badlogic.gdx.utils.Disposable; + +import com.badlogic.gdx.graphics.Texture; +import com.badlogic.gdx.graphics.g2d.Batch; +import com.badlogic.gdx.graphics.g2d.Sprite; +import com.badlogic.gdx.graphics.glutils.ShapeRenderer; + +import com.badlogic.gdx.math.Matrix4; + +import ch.asynk.creepingarmor.engine.gfx.Animation; +import ch.asynk.creepingarmor.engine.gfx.animations.AnimationSequence; +import ch.asynk.creepingarmor.engine.gfx.animations.RunnableAnimation; +import ch.asynk.creepingarmor.engine.gfx.animations.MoveToAnimation.MoveToAnimationCb; + +public abstract class Board implements Disposable, Animation +{ + private int cols; + private int rows; + private final Tile neighbours[] = new Tile[6]; + + public interface TileBuilder + { + public Tile getNewTile(float x, float y, int col, int row, boolean offmap); + } + + public static class Config + { + public int cols; + public int rows; + public int x0; // bottom left x offset + public int y0; // bottom left y offset + public int w; // hex width + public int dw; // half hex : w/2 + public int s; // hex side + public float dh; // hex top : s/2 + public float h; // square height : s + dh + public float slope; // north-west side slope : (dh / (float) dw) + } + + private Config cfg; + private Tile[] tiles; + private SearchBoard searchBoard; + private Sprite board; + private Orientation sides[]; + + private boolean transform; + private Matrix4 prevTransform; + private Matrix4 nextTransform; + + private int tileCount = 0; + private int pawnCount = 0; + private int animationCount = 0; + private final ArrayList<Animation> animations = new ArrayList<Animation>(2); + private final ArrayList<Animation> nextAnimations = new ArrayList<Animation>(2); + private final LinkedHashSet<Tile> tilesToDraw = new LinkedHashSet<Tile>(); + + protected SelectedTile selectedTile; + + protected Board(int cols, int rows) + { + // add a frame of OFFMAP Tiles + this.cols = (cols + 2); + this.rows = (rows + 2); + searchBoard = new SearchBoard(this, cols, rows); + initSides(); + } + + public Board(TileBuilder tileBuilder, Config cfg, Texture boardTexture, SelectedTile selectedTile) + { + board = new Sprite(boardTexture); + this.cfg = cfg; + // add a frame of OFFMAP Tiles + this.cols = (cfg.cols + 2); + this.rows = (cfg.rows + 2); + this.tiles = new Tile[this.cols * this.rows]; + searchBoard = new SearchBoard(this, cfg.cols, cfg.rows); + + int idx = 0; + boolean evenRow = false; + float y = cfg.y0 - cfg.dh + cfg.s - cfg.h; + for (int i = -1; i < (cfg.rows + 1); i++) { + float x = cfg.x0 + cfg.dw - cfg.w; + if (!evenRow) x += cfg.dw; + for ( int j = -1; j < (cfg.cols + 1); j ++) { + boolean offmap = ((j < 0) || (i < 0) || (j >= cfg.cols) || (i >= cfg.rows)); + this.tiles[idx] = tileBuilder.getNewTile(x, y, (j + ((i + 1) / 2)), i, offmap); + idx += 1; + x += cfg.w; + } + y += cfg.h; + evenRow = !evenRow; + } + + initSides(); + + this.selectedTile = selectedTile; + } + + private void initSides() + { + this.sides = new Orientation[6]; + sides[0] = Orientation.NORTH; + sides[1] = Orientation.NORTH_EAST; + sides[2] = Orientation.SOUTH_EAST; + sides[3] = Orientation.SOUTH; + sides[4] = Orientation.SOUTH_WEST; + sides[5] = Orientation.NORTH_WEST; + } + + @Override + public void dispose() + { + for (int i = 0; i < (this.cols * this.rows); i++) + tiles[i].dispose(); + tilesToDraw.clear(); + for (int i = 0, n = nextAnimations.size(); i < n; i++) + nextAnimations.get(i).dispose(); + animations.clear(); + for (int i = 0, n = animations.size(); i < n; i++) + animations.get(i).dispose(); + animations.clear(); + if (selectedTile != null) + selectedTile.dispose(); + Move.clearPool(); + Path.clearPool(); + } + + public float getWidth() + { + return board.getWidth(); + } + + public float getHeight() + { + return board.getHeight(); + } + + public void setPosition(float x, float y) + { + board.setPosition(x, y); + if ((x != 0.0f) || (y != 0.0f)) { + transform = true; + prevTransform = new Matrix4(); + nextTransform = new Matrix4(); + nextTransform.translate(x, y, 0); + } else + transform = false; + } + + public Orientation getSide(int i) + { + return sides[i]; + } + + protected int getTileOffset(int col, int row) + { + col = (col + 1 - ((row + 1) / 2)); + row = (row + 1); + if ((col < 0) || (row < 0) || (row >= this.rows) || (col >= this.cols)) + return -1; + + return (col + (row * this.cols)); + } + + protected Tile getTile(int col, int row) + { + int offset = getTileOffset(col, row); + if (offset < 0) + return null; + return tiles[offset]; + } + + public void setAdjacentTiles(Tile tile, Tile tiles[]) + { + tiles[0] = getAdjTileAt(tile, sides[0].opposite()); + tiles[1] = getAdjTileAt(tile, sides[1].opposite()); + tiles[2] = getAdjTileAt(tile, sides[2].opposite()); + tiles[3] = getAdjTileAt(tile, sides[3].opposite()); + tiles[4] = getAdjTileAt(tile, sides[4].opposite()); + tiles[5] = getAdjTileAt(tile, sides[5].opposite()); + } + + public Tile getAdjTileAt(Tile tile, Orientation o) + { + Tile t = null; + switch(o) { + case NORTH: + t = getTile((tile.col + 1), tile.row); + break; + case NORTH_EAST: + t = getTile(tile.col, (tile.row - 1)); + break; + case SOUTH_EAST: + t = getTile((tile.col - 1), (tile.row - 1)); + break; + case SOUTH: + t = getTile((tile.col - 1), tile.row); + break; + case SOUTH_WEST: + t = getTile(tile.col, (tile.row + 1)); + break; + case NORTH_WEST: + t = getTile((tile.col + 1), (tile.row + 1)); + break; + } + return t; + } + + protected abstract void animationsOver(); + + protected void addAnimation(Animation a) + { + nextAnimations.add(a); + } + + public int animationCount() + { + return animations.size(); + } + + private void stats() + { + boolean print = false; + + if (tileCount != tilesToDraw.size()) { + tileCount = tilesToDraw.size(); + print = true; + } + + if (animationCount != animations.size()) { + animationCount = animations.size(); + print = true; + } + + if (print) + Gdx.app.debug("Board", " tiles:" + tileCount + " pawns:" + pawnCount + " animations:" + animationCount); + } + + public boolean animate(float delta) + { + boolean over = (animations.size() > 0); + Iterator<Animation> iter = animations.iterator(); + while (iter.hasNext()) { + Animation a = iter.next(); + if (a.animate(delta)) + iter.remove(); + } + if (over && (animations.size() == 0)) + animationsOver(); + + for (int i = 0, n = nextAnimations.size(); i < n; i++) + animations.add(nextAnimations.get(i)); + nextAnimations.clear(); + + selectedTile.animate(delta); + + return true; + } + + public void draw(Batch batch) + { + board.draw(batch); + + if (transform) { + prevTransform.set(batch.getTransformMatrix()); + batch.setTransformMatrix(nextTransform); + } + + Iterator<Tile> tileIter = tilesToDraw.iterator(); + while (tileIter.hasNext()) + tileIter.next().draw(batch); + + Iterator<Animation> animationIter = animations.iterator(); + while (animationIter.hasNext()) + animationIter.next().draw(batch); + + selectedTile.draw(batch); + + if (transform) + batch.setTransformMatrix(prevTransform); + } + + public void drawDebug(ShapeRenderer debugShapes) + { + stats(); + if (transform) { + prevTransform.set(debugShapes.getTransformMatrix()); + debugShapes.setTransformMatrix(nextTransform); + } + + Iterator<Tile> iter = tilesToDraw.iterator(); + while (iter.hasNext()) + iter.next().drawDebug(debugShapes); + + Iterator<Animation> animationIter = animations.iterator(); + while (animationIter.hasNext()) + animationIter.next().drawDebug(debugShapes); + + if (transform) + debugShapes.setTransformMatrix(prevTransform); + } + + protected int collectPossibleMoves(Pawn pawn, Collection<Tile> moves) + { + return searchBoard.possibleMovesFrom(pawn, moves); + } + + protected int collectPossibleTargets(Pawn pawn, Collection<Pawn> targets) + { + return searchBoard.possibleTargetsFrom(pawn, targets); + } + + protected int collectPossibleTargets(Pawn pawn, Collection<Pawn> units, Collection<Pawn> targets) + { + targets.clear(); + for (Pawn target : units) { + if (pawn.canEngage(target) && searchBoard.canAttack(pawn, target, true)) + targets.add(target); + } + + return targets.size(); + } + + protected int collectMoveAssists(Pawn pawn, Collection<Pawn> assists) + { + assists.clear(); + setAdjacentTiles(pawn.getTile(), neighbours); + for (int i = 0; i < 6; i++) { + Tile tile = neighbours[i]; + if (tile != null) { + Iterator<Pawn> pawns = tile.iterator(); + while(pawns.hasNext()) { + Pawn p = pawns.next(); + if (!pawn.isEnemy(p) && p.canMove()) + assists.add(p); + } + } + } + return assists.size(); + } + + protected int collectAttackAssists(Pawn pawn, Pawn target, Collection<Pawn> units, Collection<Pawn> assists) + { + assists.clear(); + for (Pawn p : units) { + if ((p != pawn) && p.canEngage(target) && searchBoard.canAttack(p, target, !p.canAssistEngagementWithoutLos())) + assists.add(p); + } + + return assists.size(); + } + + public Orientation findBestEntry(Pawn pawn, Tile to, int allowedMoves) + { + Orientation entry = Orientation.KEEP; + int cost = Integer.MAX_VALUE; + boolean road = false; + + setAdjacentTiles(to, neighbours); + for (int i = 0; i < 6; i++) { + Tile t = neighbours[i]; + if (t.isOffMap()) { + Orientation o = Orientation.fromAdj(t.col, t.row, to.col, to.row); + if (o.isInSides(allowedMoves)) { + o = o.opposite(); + boolean r = to.road(o); + int c = to.costFrom(pawn, o); + if ((c < cost) || (r && (c == cost))) { + entry = o; + cost = c; + road = r; + } + } + } + } + + return entry.opposite(); + } + + public void enableOverlayOn(Tile tile, int i, boolean enable) + { + if(tile.enableOverlay(i, enable)) + tilesToDraw.add(tile); + else + tilesToDraw.remove(tile); + } + + public void enableOverlayOn(Tile tile, int i, Orientation o, boolean enable) + { + if(tile.enableOverlay(i, enable, o.r())) + tilesToDraw.add(tile); + else + tilesToDraw.remove(tile); + } + + private int pushPawnOnto(Pawn pawn, Tile tile) + { + if (!tile.isOffMap()) + tilesToDraw.add(tile); + return tile.push(pawn); + } + + public int removePawn(Pawn pawn) + { + Tile tile = pawn.getTile(); + if (tile == null) + return 0; + int n = tile.remove(pawn); + if (!tile.mustBeDrawn()) + tilesToDraw.remove(tile); + return n; + } + + public Pawn setPawnOnto(Pawn pawn, Move move) + { + pawn.move(move); + return setPawnOnto(pawn, move.to, move.orientation); + } + + public Pawn setPawnOnto(Pawn pawn, Tile tile, Orientation o) + { + pawn.setOnTile(tile, o.r()); + pushPawnOnto(pawn, tile); + return pawn; + } + + private RunnableAnimation getSetPawnOntoAnimation(final Pawn pawn) + { + return RunnableAnimation.get(pawn, new Runnable() { + @Override + public void run() { + Tile to = pawn.move.to; + if (!to.isOffMap()) + setPawnOnto(pawn, to, pawn.move.orientation); + } + }); + } + + protected void movePawn(final Pawn pawn, Move move, MoveToAnimationCb cb) + { + pawn.move(move); + removePawn(pawn); + + AnimationSequence seq = pawn.getMoveAnimation(move.iterator(), (move.steps() + 1), cb); + seq.addAnimation(getSetPawnOntoAnimation(pawn)); + addAnimation(seq); + } + + protected void enterPawn(final Pawn pawn, Move move) + { + pawn.move(move); + setPawnOnto(pawn, move.to, move.orientation); + } + + protected void revertLastPawnMove(final Pawn pawn) + { + removePawn(pawn); + + addAnimation(RunnableAnimation.get(pawn, new Runnable() { + @Override + public void run() { + pushPawnOnto(pawn, pawn.getTile()); + } + })); + + pawn.revertLastMove(); + } + + public void attack(final Pawn pawn, final Pawn target, boolean clearVisibility) + { + if (!pawn.canEngage(target) || !searchBoard.canAttack(pawn, target, clearVisibility)) + throw new RuntimeException(String.format("%s cannot attack %s", pawn, target)); + } + + public Tile getTileAt(float mx, float my) + { + // compute row + float y = (my - cfg.y0); + int row = (int) (y / cfg.h); + boolean oddRow = ((row % 2) == 1); + if (y < 0.f) { + row = -1; + oddRow = true; + } + + // compute col + float x = (mx - cfg.x0); + if (oddRow) x -= cfg.dw; + int col = (int) (x / cfg.w); + if (x < 0.f) + col = -1; + + int colOffset = ((row + 1) / 2); + + // check upper boundaries + float dy = (y - (row * cfg.h)); + if (dy > cfg.s) { + dy -= cfg.s; + float dx = (x - (col * cfg.w)); + col += colOffset; + if (dx < cfg.dw) { + if ((dx * cfg.slope) < dy) { + // upper left corner + row += 1; + colOffset = ((row +1) / 2); + } + } else { + if (((cfg.w - dx) * cfg.slope) < dy) { + // upper right corner + row += 1; + col += 1; + colOffset = ((row +1) / 2); + } + } + } else + col += colOffset; + + return getTile(col, row); + } + + public int distance(Tile from, Tile to) + { + return distance(from.col, from.row, to.col, to.row); + } + + public int distance(int col0, int row0, int col1, int row1) + { + int dx = Math.abs(col1 - col0); + int dy = Math.abs(row1 - row0); + int dz = Math.abs((col0 - row0) - (col1 - row1)); + + if (dx > dy) { + if (dx > dz) + return dx; + else + return dz; + } else { + if (dy > dz) + return dy; + else + return dz; + } + } +} + diff --git a/core/src/ch/asynk/creepingarmor/engine/Faction.java b/core/src/ch/asynk/creepingarmor/engine/Faction.java new file mode 100644 index 0000000..b45f351 --- /dev/null +++ b/core/src/ch/asynk/creepingarmor/engine/Faction.java @@ -0,0 +1,6 @@ +package ch.asynk.creepingarmor.engine; + +public interface Faction +{ + public boolean isEnemy(Faction other); +} diff --git a/core/src/ch/asynk/creepingarmor/engine/HeadedPawn.java b/core/src/ch/asynk/creepingarmor/engine/HeadedPawn.java new file mode 100644 index 0000000..3b78cd5 --- /dev/null +++ b/core/src/ch/asynk/creepingarmor/engine/HeadedPawn.java @@ -0,0 +1,88 @@ +package ch.asynk.creepingarmor.engine; + +import com.badlogic.gdx.graphics.g2d.Batch; +import com.badlogic.gdx.graphics.g2d.Sprite; +import com.badlogic.gdx.graphics.g2d.TextureAtlas; +import com.badlogic.gdx.graphics.g2d.TextureRegion; +import com.badlogic.gdx.graphics.glutils.ShapeRenderer; + +import com.badlogic.gdx.math.Vector3; + +public abstract class HeadedPawn extends Pawn +{ + private Sprite head; + protected Orientation orientation; + + public HeadedPawn(Faction faction, String pawn, String head, TextureAtlas pawns, TextureAtlas overlays) + { + super(faction, pawn, pawns, overlays); + this.head = new Sprite(pawns.findRegion(head)); + this.orientation = Orientation.KEEP; + this.descr += " " + orientation; + } + + @Override + public void dispose() + { + super.dispose(); + } + + @Override + public void setAlpha(float alpha) + { + super.setAlpha(alpha); + head.setAlpha(alpha); + } + + @Override + public float getRotation() + { + return orientation.r(); + } + + @Override + public Orientation getOrientation() + { + return orientation; + } + + @Override + public void setPosition(float x, float y) + { + super.setPosition(x, y); + float cx = x + (getWidth() / 2f); + float cy = y + (getHeight() / 2f); + head.setPosition((cx - (head.getWidth() / 2f)), (cy - (head.getHeight() / 2f))); + } + + @Override + public void setRotation(float z) + { + getPosition().z = z; + head.setRotation(z); + this.orientation = Orientation.fromRotation(z); + } + + @Override + public void setPosition(float x, float y, float z) + { + setPosition(x, y); + setRotation(z); + } + + @Override + public void draw(Batch batch) + { + head.draw(batch); + super.draw(batch); + } + + @Override + public void drawDebug(ShapeRenderer debugShapes) + { + float w = head.getWidth(); + float h = head.getHeight(); + debugShapes.rect(head.getX(), head.getY(), (w / 2f), (h / 2f), w, h, head.getScaleX(), head.getScaleY(), head.getRotation()); + super.drawDebug(debugShapes); + } +} diff --git a/core/src/ch/asynk/creepingarmor/engine/Meteorology.java b/core/src/ch/asynk/creepingarmor/engine/Meteorology.java new file mode 100644 index 0000000..6d5319e --- /dev/null +++ b/core/src/ch/asynk/creepingarmor/engine/Meteorology.java @@ -0,0 +1,24 @@ +package ch.asynk.creepingarmor.engine; + +public class Meteorology +{ + public enum Day { DAY, NIGHT }; + public enum Season { SUMMER, SPRING, WINTER, FALL }; + public enum Weather { CLEAR, RAIN, SNOW, WIND }; + + public Day day; + public Season season; + public Weather weather; + + public Meteorology() + { + clear(); + } + + public void clear() + { + day = Day.DAY; + season = Season.SUMMER; + weather = Weather.CLEAR; + } +} diff --git a/core/src/ch/asynk/creepingarmor/engine/Move.java b/core/src/ch/asynk/creepingarmor/engine/Move.java new file mode 100644 index 0000000..2f33982 --- /dev/null +++ b/core/src/ch/asynk/creepingarmor/engine/Move.java @@ -0,0 +1,158 @@ +package ch.asynk.creepingarmor.engine; + +import java.util.Iterator; + +import com.badlogic.gdx.utils.Pool; +import com.badlogic.gdx.math.Vector3; + +public class Move extends Path implements Iterable<Vector3> +{ + public enum MoveType + { + REGULAR, + SET, + ENTER, + EXIT; + } + + private static final Pool<Move> movePool = new Pool<Move>() + { + @Override + protected Move newObject() { + return new Move(); + } + }; + + public static Move get(Pawn pawn, Tile from, Tile to, Orientation orientation, Path path) + { + Move m = movePool.obtain(); + m.pawn = pawn; + m.from = from; + m.to = to; + m.orientation = orientation; + if (path != null) { + m.init(path.tiles.size()); + m.cost = path.cost; + m.roadMarch = path.roadMarch; + for (Tile tile : path.tiles) + m.tiles.add(tile); + } else { + m.init(0); + } + + return m; + } + + public static void clearPool() + { + movePool.clear(); + } + + public static Move getEnter(Pawn pawn, Tile to, Orientation orientation) + { + Move m = get(pawn, null, to, orientation, null); + m.type = MoveType.ENTER; + m.cost = to.costFrom(pawn, orientation); + return m; + } + + public static Move getSet(Pawn pawn, Tile to, Orientation orientation) + { + Move m = get(pawn, null, to, orientation, null); + m.type = MoveType.SET; + m.cost = 0; + return m; + } + + public Pawn pawn; + public Tile from; + public Tile to; + public Orientation orientation; + public MoveType type; + + public Move() + { + super(); + this.pawn = null; + this.from = null; + this.to = null; + this.orientation = Orientation.KEEP; + this.type = MoveType.REGULAR; + } + + @Override + public void reset() + { + pawn = null; + from = null; + to = null; + orientation = Orientation.KEEP; + type = MoveType.REGULAR; + super.reset(); + } + + @Override + public void dispose() + { + tiles.clear(); + movePool.free(this); + } + + public boolean isSet() + { + return (type == MoveType.SET); + } + + public boolean isEnter() + { + return (type == MoveType.ENTER); + } + + public boolean isRegular() + { + return (type == MoveType.REGULAR); + } + + public boolean isFinal() + { + return (type != MoveType.ENTER); + } + + public int steps() + { + int steps = 0; + + Tile tile = from; + Orientation o = pawn.getOrientation(); + for (Tile next : tiles) { + Orientation nextO = Orientation.fromMove(tile.col, tile.row, next.col, next.row); + if (nextO != o) { + steps += 2; + o = nextO; + } else + steps += 1; + tile = next; + } + if (orientation != Orientation.fromMove(tile.col, tile.row, to.col, to.row)) + steps += 2; + else + steps +=1; + + return steps; + } + + @Override + public String toString() + { + if (from == null) + return String.format("%s %s c:%d", to.toShort(), orientation, cost); + else + return String.format("%s->%s %s c:%d", from.toShort(), to.toShort(), orientation, cost); + } + + @Override + public Iterator<Vector3> iterator() + { + return new PathIterator(pawn, from, to, orientation, tiles); + } +} diff --git a/core/src/ch/asynk/creepingarmor/engine/Objective.java b/core/src/ch/asynk/creepingarmor/engine/Objective.java new file mode 100644 index 0000000..c923cdd --- /dev/null +++ b/core/src/ch/asynk/creepingarmor/engine/Objective.java @@ -0,0 +1,49 @@ +package ch.asynk.creepingarmor.engine; + +public class Objective +{ + protected Faction curFaction; + protected Faction prevFaction; + private boolean persistent; + + public Objective(Faction faction, boolean persistent) + { + this.curFaction = faction; + this.prevFaction = faction; + this.persistent = persistent; + } + + public boolean is(Faction faction) + { + return (curFaction == faction); + } + + public Faction faction() + { + return curFaction; + } + + public boolean set(Faction faction) + { + if (faction == curFaction) + return false; + + prevFaction = curFaction; + curFaction = faction; + return true; + } + + public boolean unset() + { + if (persistent) + return false; + revert(); + return true; + } + + public Faction revert() + { + curFaction = prevFaction; + return curFaction; + } +} diff --git a/core/src/ch/asynk/creepingarmor/engine/ObjectiveSet.java b/core/src/ch/asynk/creepingarmor/engine/ObjectiveSet.java new file mode 100644 index 0000000..be1b0bb --- /dev/null +++ b/core/src/ch/asynk/creepingarmor/engine/ObjectiveSet.java @@ -0,0 +1,78 @@ +package ch.asynk.creepingarmor.engine; + +import java.util.Collection; +import java.util.HashMap; + +public class ObjectiveSet extends HashMap<Tile, Objective> +{ + public interface ObjectiveCb + { + public void showObjective(Tile tile, Faction faction); + } + + private final Board board; + private final HashMap<Objective, Tile> modified; + + public ObjectiveSet(Board board, int n) + { + super(n); + this.board = board; + this.modified = new HashMap<Objective, Tile>(10); + } + + public void add(Tile tile, Faction faction, boolean persistent) + { + put(tile, new Objective(faction, persistent)); + } + + public int count(Faction faction) + { + int n = 0; + for (Objective objective : values()) { + if (objective.is(faction)) + n += 1; + } + return n; + } + + public Faction claim(Tile tile, Faction faction) + { + Objective objective = get(tile); + if (objective == null) + return null; + + if (objective.set(faction)) + modified.put(objective, tile); + return objective.faction(); + } + + public Faction unclaim(Tile tile) + { + Objective objective = get(tile); + if (objective == null) + return null; + + if (objective.unset()) + modified.remove(objective); + return objective.faction(); + } + + public void forget() + { + modified.clear(); + } + + public int modifiedCount() + { + return modified.size(); + } + + public void revert(ObjectiveCb cb) + { + for (Objective objective : modified.keySet()) { + objective.revert(); + cb.showObjective(modified.get(objective), objective.faction()); + } + modified.clear(); + } +} diff --git a/core/src/ch/asynk/creepingarmor/engine/Order.java b/core/src/ch/asynk/creepingarmor/engine/Order.java new file mode 100644 index 0000000..51fa8b0 --- /dev/null +++ b/core/src/ch/asynk/creepingarmor/engine/Order.java @@ -0,0 +1,16 @@ +package ch.asynk.creepingarmor.engine; + +import java.lang.Comparable; + +import com.badlogic.gdx.utils.Disposable; +import com.badlogic.gdx.utils.Pool; +import com.badlogic.gdx.utils.Json; + +public abstract class Order implements Disposable, Pool.Poolable, Json.Serializable, Comparable<Pawn> +{ + public interface OrderType + { + } + + public abstract boolean isA(OrderType type); +} diff --git a/core/src/ch/asynk/creepingarmor/engine/OrderList.java b/core/src/ch/asynk/creepingarmor/engine/OrderList.java new file mode 100644 index 0000000..c5d7037 --- /dev/null +++ b/core/src/ch/asynk/creepingarmor/engine/OrderList.java @@ -0,0 +1,64 @@ +package ch.asynk.creepingarmor.engine; + +import java.util.LinkedList; +import java.util.Iterator; + +import com.badlogic.gdx.utils.Json; +import com.badlogic.gdx.utils.JsonValue; +import com.badlogic.gdx.utils.JsonWriter.OutputType; + +public class OrderList extends LinkedList<Order> implements Json.Serializable +{ + public void dispose(Pawn pawn) + { + Iterator<Order> it = iterator(); + while(it.hasNext()) { + Order order = it.next(); + if (order.compareTo(pawn) == 0) { + it.remove(); + order.dispose(); + } + } + } + + public void dispose(Pawn pawn, Order.OrderType type) + { + Iterator<Order> it = iterator(); + while(it.hasNext()) { + Order order = it.next(); + if ((order.compareTo(pawn) == 0) && (order.isA(type))) { + it.remove(); + order.dispose(); + } + } + } + + public void dispose() + { + for (Order o : this) + o.dispose(); + clear(); + } + + public String toJson() + { + Json json = new Json(); + json.setOutputType(OutputType.json); + return json.toJson(this); + } + + @Override + public void write(Json json) + { + json.writeArrayStart("commands"); + for (Order o : this) + json.writeValue(o); + json.writeArrayEnd(); + } + + @Override + public void read(Json json, JsonValue jsonMap) + { + // TODO read(Json json, JsonValue jsonMap) + } +} diff --git a/core/src/ch/asynk/creepingarmor/engine/Orientation.java b/core/src/ch/asynk/creepingarmor/engine/Orientation.java new file mode 100644 index 0000000..a061d05 --- /dev/null +++ b/core/src/ch/asynk/creepingarmor/engine/Orientation.java @@ -0,0 +1,134 @@ +package ch.asynk.creepingarmor.engine; + +public enum Orientation +{ + ALL(0, 63), + KEEP(0, 0), + NORTH(270, 1), + NORTH_EAST(210, 2), + SOUTH_EAST(150, 4), + SOUTH(90, 8), + SOUTH_WEST (30, 16), + NORTH_WEST(330, 32); + + public static int offset = 0; + public static float delta = 5f; + private final int r; + public final int s; + + Orientation(int r, int s) { this.r = r; this.s = s; } + + public float r() { return offset + r; } + + public boolean isInSides(int sides) + { + return ((sides & s) == s); + } + + public Orientation left() + { + if (this == NORTH) return NORTH_WEST; + else return fromSide(s >> 1); + } + + public Orientation right() + { + if (this == NORTH_WEST) return NORTH; + else return fromSide(s << 1); + } + + public Orientation opposite() + { + return left().left().left(); + } + + public int allBut() + { + return ALL.s & (s ^ 0xFFFF); + } + + public int getFrontSides() + { + return s | left().s | right().s; + } + + public int getBackSides() + { + return opposite().getFrontSides(); + } + + public static Orientation fromSide(int s) + { + if (s == NORTH.s) return NORTH; + else if (s == NORTH_EAST.s) return NORTH_EAST; + else if (s == SOUTH_EAST.s) return SOUTH_EAST; + else if (s == SOUTH.s) return SOUTH; + else if (s == SOUTH_WEST.s) return SOUTH_WEST; + else if (s == NORTH_WEST.s) return NORTH_WEST; + else return KEEP; + } + + public static Orientation fromRotation(float r) + { + if (r < 0) r += 360f; + if ((r > (NORTH.r - 5f)) && (r < (NORTH.r + 5f))) return NORTH; + else if ((r > (NORTH_EAST.r - delta)) && (r < (NORTH_EAST.r + delta))) return NORTH_EAST; + else if ((r > (SOUTH_EAST.r - delta)) && (r < (SOUTH_EAST.r + delta))) return SOUTH_EAST; + else if ((r > (SOUTH.r - delta)) && (r < (SOUTH.r + delta))) return SOUTH; + else if ((r > (SOUTH_WEST.r - delta)) && (r < (SOUTH_WEST.r + delta))) return SOUTH_WEST; + else if ((r > (NORTH_WEST.r - delta)) && (r < (NORTH_WEST.r + delta))) return NORTH_WEST; + else return KEEP; + } + + public static Orientation fromMove(int col0, int row0, int col1, int row1) + { + int dx = col1 - col0; + int dy = row1 - row0; + + if (dy == 0) { + if (dx == 0) return KEEP; + if (dx > 0) return NORTH; + return SOUTH; + } + if (dy > 0) { + if (dx > 0) return NORTH_WEST; + return SOUTH_WEST; + } else { + if (dx < 0) return SOUTH_EAST; + return NORTH_EAST; + } + } + + public static Orientation fromAdj(Tile from, Tile to) + { + return fromAdj(from.col, from.row, to.col, to.row); + } + + public static Orientation fromAdj(int col0, int row0, int col1, int row1) + { + Orientation o = KEEP; + + if (row1 == row0) { + if (col1 == (col0 - 1)) { + o = SOUTH; + } else if (col1 == (col0 + 1)) { + o = NORTH; + } + } else if (row1 == (row0 - 1)) { + if (col1 == (col0 - 1)) { + o = SOUTH_EAST; + } else if (col1 == col0) { + o = NORTH_EAST; + } + + } else if (row1 == (row0 + 1)) { + if (col1 == col0) { + o = SOUTH_WEST; + } else if (col1 == (col0 + 1)) { + o = NORTH_WEST; + } + } + + return o; + } +} diff --git a/core/src/ch/asynk/creepingarmor/engine/Path.java b/core/src/ch/asynk/creepingarmor/engine/Path.java new file mode 100644 index 0000000..20da213 --- /dev/null +++ b/core/src/ch/asynk/creepingarmor/engine/Path.java @@ -0,0 +1,62 @@ +package ch.asynk.creepingarmor.engine; + +import java.util.ArrayList; + +import com.badlogic.gdx.utils.Pool; +import com.badlogic.gdx.utils.Disposable; + +public class Path implements Disposable, Pool.Poolable +{ + private static final Pool<Path> pathPool = new Pool<Path>() { + @Override + protected Path newObject() { + return new Path(); + } + }; + + public static Path get(int size) + { + Path p = pathPool.obtain(); + p.init(size); + return p; + } + + public static void clearPool() + { + pathPool.clear(); + } + + public int cost; + public boolean roadMarch; + public ArrayList<Tile> tiles; + + public Path() + { + this.cost = -1; + this.roadMarch = true; + this.tiles = null; + } + + protected void init(int size) + { + if (tiles == null) + tiles = new ArrayList<Tile>(size); + else + tiles. ensureCapacity(size); + } + + @Override + public void reset() + { + cost = -1; + roadMarch = true; + tiles.clear(); + } + + @Override + public void dispose() + { + tiles.clear(); + pathPool.free(this); + } +} diff --git a/core/src/ch/asynk/creepingarmor/engine/PathBuilder.java b/core/src/ch/asynk/creepingarmor/engine/PathBuilder.java new file mode 100644 index 0000000..bad14e8 --- /dev/null +++ b/core/src/ch/asynk/creepingarmor/engine/PathBuilder.java @@ -0,0 +1,266 @@ +package ch.asynk.creepingarmor.engine; + +import java.util.ArrayList; +import java.util.List; +import java.util.LinkedList; +import java.util.HashSet; +import java.util.LinkedHashSet; + +import com.badlogic.gdx.utils.Disposable; +import com.badlogic.gdx.math.Vector2; +import com.badlogic.gdx.math.Vector3; + +public class PathBuilder implements Disposable +{ + private final Board board; + + public Pawn pawn; + public Tile from; + public Tile to; + public int distance; + public Orientation orientation; + private List<Tile> stack; + private List<Tile> ctrlTiles; + private List<Path> paths; + private List<Path> filteredPaths; + private HashSet<Tile> tiles; + + public PathBuilder(Board board, int tSize, int stSize, int ftSize, int vectSize) + { + this.board = board; + this.tiles = new LinkedHashSet<Tile>(tSize); + this.stack = new ArrayList<Tile>(stSize); + this.ctrlTiles = new ArrayList<Tile>(ftSize); + this.paths = new LinkedList<Path>(); + this.filteredPaths = new LinkedList<Path>(); + this.to = null; + this.pawn = null; + this.orientation = Orientation.KEEP; + } + + public void init(Pawn pawn, Tile from) + { + this.pawn = pawn; + this.from = from; + } + + public void init(Pawn pawn) + { + init(pawn, pawn.getTile()); + } + + public void initRotation(Pawn pawn, Orientation o) + { + init(pawn, pawn.getTile()); + build(pawn.getTile()); + orientation = o; + } + + public boolean isSet() + { + return (to != null); + } + + @Override + public void dispose() + { + clear(); + } + + public void clear() + { + this.to = null; + this.distance = -1; + this.orientation = Orientation.KEEP; + for (Path path : this.paths) path.dispose(); + this.tiles.clear(); + this.stack.clear(); + this.ctrlTiles.clear(); + this.paths.clear(); + this.filteredPaths.clear(); + } + + public int size() + { + if (ctrlTiles.size() == 0) + return paths.size(); + return filteredPaths.size(); + } + + public boolean contains(Tile tile) + { + return tiles.contains(tile); + } + + public void enable(int i, boolean enable) + { + for (Tile tile : tiles) + board.enableOverlayOn(tile, i, enable); + } + + public int build(Tile to) + { + clear(); + this.to = to; + // from and to are not part of the path + this.distance = board.distance(from, to); + if (distance < 2) { + Orientation o = Orientation.fromMove(to.col, to.row, from.col, from.row); + Path path = Path.get(0); + path.roadMarch = to.road(o); + path.cost = to.costFrom(pawn, o); + paths.add(path); + } else { + findAllPaths(from, pawn.getMovementPoints(), true); + } + + // printToErr("paths", paths); + stack.clear(); + return paths.size(); + } + + private void findAllPaths(Tile from, int mvtLeft, boolean roadMarch) + { + Tile moves[] = new Tile[6]; + board.setAdjacentTiles(from, moves); + + for(int i = 0; i < 6; i++) { + Tile next = moves[i]; + if ((next == null) || next.isOffMap()) continue; + + Orientation o = board.getSide(i); + int m = (mvtLeft - next.costFrom(pawn, o)); + boolean r = roadMarch & next.road(o); + + int l = (m + (r ? pawn.getRoadMarchBonus() : 0)); + + if ((board.distance(next, to) <= l)) { + if (next == to) { + Path path = Path.get(stack.size() + 1); + for (Tile t: stack) { + path.tiles.add(t); + tiles.add(t); + } + path.roadMarch = r; + path.cost = (pawn.getMovementPoints() - m); + paths.add(path); + } else { + stack.add(next); + findAllPaths(next, m, r); + stack.remove(stack.size() - 1); + } + } + } + } + + public int toggleCtrlTile(Tile tile) + { + if (ctrlTiles.contains(tile)) + ctrlTiles.remove(tile); + else + ctrlTiles.add(tile); + return filterPaths(); + } + + private int filterPaths() + { + int s = ctrlTiles.size(); + + tiles.clear(); + filteredPaths.clear(); + for (Path path : paths) { + int ok = 0; + for (Tile filter : ctrlTiles) { + if (path.tiles.contains(filter)) + ok += 1; + } + if (ok == s) { + if (path.tiles.size() == (s + 0)) { // from and to are not part of the path + filteredPaths.clear(); + filteredPaths.add(path); + tiles.clear(); + for (Tile tile : path.tiles) tiles.add(tile); + break; + } else { + filteredPaths.add(path); + for (Tile tile : path.tiles) tiles.add(tile); + } + } + } + + // printToErr("filteredPaths", filteredPaths); + return filteredPaths.size(); + } + + public int pathCost(int i) + { + return paths.get(i).cost; + } + + public Move getMove() + { + if (size() != 1) { + System.err.println("ask for only move but they are many"); + return null; + } + + return Move.get(pawn, from, to, orientation, getPath(0)); + } + + public Move getExitMove() + { + Move move = getMove(); + move.type = Move.MoveType.EXIT; + return move; + } + + public boolean canExit(Orientation o) + { + List<Path> ps; + if (ctrlTiles.size() == 0) + ps = paths; + else + ps = filteredPaths; + + int mvt = pawn.getMovementPoints(); + int rBonus = pawn.getRoadMarchBonus(); + boolean road = to.road(o); + int cost = to.exitCost(); + + for (Path p : ps) { + int c = (p.cost + cost); + if ((c <= mvt) || (p.roadMarch && road && (c <= (mvt + rBonus)))) + return true; + } + return false; + } + + public Path getPath(int i) + { + if (ctrlTiles.size() == 0) + return paths.get(i); + return filteredPaths.get(i); + } + + public void setExit(Orientation o) + { + orientation = o; + Path path = getPath(0); + if (from != to) { + path.cost += 1; + path.tiles.add(to); + } + to = board.getAdjTileAt(to, o); + } + + private void printToErr(String what, List<Path> paths) + { + System.err.println(what + pawn + " ("+paths.size()+") " + from + " -> " + to); + for (Path path : paths) { + System.err.println(String.format(" - path (l:%d c:%d r:%b)", path.tiles.size(), path.cost, path.roadMarch)); + for(Tile tile : path.tiles) + System.err.println(" " + tile.toString()); + } + System.err.println(); + } +} diff --git a/core/src/ch/asynk/creepingarmor/engine/PathIterator.java b/core/src/ch/asynk/creepingarmor/engine/PathIterator.java new file mode 100644 index 0000000..ca74ed4 --- /dev/null +++ b/core/src/ch/asynk/creepingarmor/engine/PathIterator.java @@ -0,0 +1,73 @@ +package ch.asynk.creepingarmor.engine; + +import java.util.List; +import java.util.Iterator; + +import com.badlogic.gdx.math.Vector2; +import com.badlogic.gdx.math.Vector3; + +public class PathIterator implements Iterator<Vector3> +{ + private Pawn pawn; + private Tile to; + private Orientation o; + private Orientation orientation; + private Tile tile; + private Vector2 pos = new Vector2(); + private Vector3 v = new Vector3(); + private int i; + private List<Tile> path; + + public PathIterator(Pawn pawn, Tile from, Tile to, Orientation orientation, List<Tile> path) + { + this.pawn = pawn; + this.to = to; + this.tile = from; + this.orientation = orientation; + this.path = path; + this.o = pawn.getOrientation(); + this.v.set(pawn.getPosition().x, pawn.getPosition().y, o.r()); + this.i = 0; + } + + @Override + public boolean hasNext() + { + if ((tile == to) && (o == orientation)) + return false; + return true; + } + + @Override + public Vector3 next() + { + if (tile == to) { + v.z = orientation.r(); + o = orientation; + return v; + } + Tile nextTile; + if (i < path.size()) + nextTile = path.get(i); + else + nextTile = to; + Orientation nextO = Orientation.fromMove(tile.col, tile.row, nextTile.col, nextTile.row); + if (nextO != o) { + v.z = nextO.r(); + o = nextO; + return v; + } + pawn.getPosAt(nextTile, pos); + v.x = pos.x; + v.y = pos.y; + tile = nextTile; + i += 1; + return v; + } + + @Override + public void remove() + { + throw new UnsupportedOperationException(); + } +} diff --git a/core/src/ch/asynk/creepingarmor/engine/Pawn.java b/core/src/ch/asynk/creepingarmor/engine/Pawn.java new file mode 100644 index 0000000..7f82f1c --- /dev/null +++ b/core/src/ch/asynk/creepingarmor/engine/Pawn.java @@ -0,0 +1,365 @@ +package ch.asynk.creepingarmor.engine; + +import java.util.Iterator; + +import com.badlogic.gdx.utils.Disposable; + +import com.badlogic.gdx.graphics.g2d.Batch; +import com.badlogic.gdx.graphics.g2d.Sprite; +import com.badlogic.gdx.graphics.g2d.TextureAtlas; +import com.badlogic.gdx.graphics.g2d.TextureRegion; +import com.badlogic.gdx.graphics.glutils.ShapeRenderer; + +import com.badlogic.gdx.math.Vector2; +import com.badlogic.gdx.math.Vector3; + +import ch.asynk.creepingarmor.engine.gfx.Moveable; +import ch.asynk.creepingarmor.engine.gfx.StackedImages; +import ch.asynk.creepingarmor.engine.gfx.animations.MoveToAnimation; +import ch.asynk.creepingarmor.engine.gfx.animations.RunnableAnimation; +import ch.asynk.creepingarmor.engine.gfx.animations.AnimationSequence; + +public abstract class Pawn implements Moveable, Disposable +{ + public interface PawnType + { + } + + public interface PawnId + { + } + + private static final float MOVE_TIME = 0.4f; + + private Vector3 position; + private Vector3 prevPosition; + private Tile tile; + private Tile prevTile; + protected Faction faction; + protected String descr; + private Sprite sprite; + private StackedImages overlays; + protected Attack attack; + protected Move move; + + public abstract int getMovementPoints(); + public abstract int getRoadMarchBonus(); + public abstract int getAngleOfAttack(); + public abstract int getFlankSides(); + public abstract int getEngagementRangeFrom(Tile tile); + public abstract int getDefense(Tile tile); + + public abstract boolean isUnit(); + public abstract boolean isA(PawnId id); + public abstract boolean isA(PawnType type); + public abstract boolean isHq(); + public abstract boolean isHqOf(Pawn other); + public abstract boolean isHardTarget(); + + public abstract boolean canMove(); + public abstract boolean canRotate(); + public abstract boolean canEngage(); + public abstract boolean canEngage(Pawn other); + public abstract boolean canAssistEngagementWithoutLos(); + + public abstract void move(); + public abstract void engage(); + + public abstract void revertLastMove(); + + protected Pawn() + { + this.tile = null; + this.prevTile = null; + this.position = new Vector3(0f, 0f, 0f); + this.prevPosition = new Vector3(0f, 0f, 0f); + this.attack = new Attack(this); + } + + public Pawn(Faction faction, String name, TextureAtlas pawns, TextureAtlas overlays) + { + this(); + this.faction = faction; + this.descr = descr; + this.sprite = new Sprite(pawns.findRegion(name)); + this.overlays = new StackedImages(overlays); + } + + @Override + public String toString() + { + return descr; + } + + @Override + public void dispose() + { + } + + @Override + public Faction getFaction() + { + return faction; + } + + public void reset() + { + move = null; + attack.reset(); + } + + public void move(Move move) + { + switch(move.type) + { + case REGULAR: + if ((this.move != null) && (!this.move.isEnter())) + throw new RuntimeException("try to override an existing move instance"); + break; + case ENTER: + if (this.move != null) + throw new RuntimeException("try to override an existing move instance"); + break; + case SET: + break; + default: + throw new RuntimeException("unsupported MoveType"); + } + + this.move = move; + move(); + } + + public void setAttack(Pawn target, int distance) + { + attack.reset(); + attack.target = target; + attack.distance = distance; + } + + public boolean justEntered() + { + return ((move != null) && move.isEnter()); + } + + public boolean is(Faction faction) + { + return (this.faction == faction); + } + + public boolean isEnemy(Faction other) + { + return faction.isEnemy(other); + } + + public boolean isEnemy(Pawn other) + { + return faction.isEnemy(other.faction); + } + + public boolean isFlankAttack() + { + return (attack.isClear && attack.isFlank); + } + + public int attackDistance() + { + return attack.distance; + } + + public Tile getTile() + { + return tile; + } + + public Tile getPreviousTile() + { + return prevTile; + } + + public Vector3 getPosition() + { + return position; + } + + public Vector3 getPreviousPosition() + { + return prevPosition; + } + + private void revertPosition() + { + this.tile = this.prevTile; + this.prevTile = null; + position.set(prevPosition); + prevPosition.set(0f, 0f, 0f); + setPosition(position.x, position.y, position.z); + } + + public float getCenterX() + { + return (getX() + (getWidth() / 2f)); + } + + public float getCenterY() + { + return (getY() + (getHeight() / 2f)); + } + + public Vector2 getPosAt(Tile tile, Vector2 pos) + { + float x = (tile.getX() - (getWidth() / 2f)); + float y = (tile.getY() - (getHeight() / 2f)); + if (pos == null) + return new Vector2(x, y); + else + pos.set(x, y); + return pos; + } + + public void setOnTile(Tile tile, float z) + { + this.prevTile = this.tile; + this.tile = tile; + float x = (tile.getX() - (getWidth() / 2f)); + float y = (tile.getY() - (getHeight() / 2f)); + setPosition(x, y, z); + } + + @Override + public void setAlpha(float alpha) + { + sprite.setAlpha(alpha); + overlays.setAlpha(alpha); + } + + @Override + public float getX() + { + return sprite.getX(); + } + + @Override + public float getY() + { + return sprite.getY(); + } + + @Override + public float getWidth() + { + return sprite.getWidth(); + } + + @Override + public float getHeight() + { + return sprite.getHeight(); + } + + @Override + public float getRotation() + { + return sprite.getRotation(); + } + + public Orientation getOrientation() + { + return Orientation.fromRotation(getRotation()); + } + + public void translate(float dx, float dy) + { + setPosition((getX() + dx), (getY() + dy)); + } + + public void centerOn(float x, float y) + { + setPosition((x - (getWidth() / 2f)), (y - (getHeight() / 2f))); + } + + @Override + public void setPosition(float x, float y) + { + position.set(x, y, 0f); + sprite.setPosition(x, y); + float cx = x + (getWidth() / 2f); + float cy = y + (getHeight() / 2f); + overlays.centerOn(cx, cy); + } + + public void setRotation(float z) + { + position.z = z; + sprite.setRotation(z); + overlays.setRotation(z); + } + + @Override + public void setPosition(float x, float y, float z) + { + setPosition(x, y); + setRotation(z); + } + + public boolean hasOverlayEnabled() + { + return overlays.isEnabled(); + } + + public boolean enableOverlay(int i, boolean enable) + { + overlays.enable(i, enable); + if (enable) return true; + return hasOverlayEnabled(); + } + + public AnimationSequence getRotateAnimation(float z, int size) + { + prevPosition.set(position); + AnimationSequence seq = AnimationSequence.get(1 + size); + seq.addAnimation(MoveToAnimation.get(this, position.x, position.y, z, MOVE_TIME)); + + return seq; + } + + public AnimationSequence getMoveAnimation(Iterator<Vector3> vectors, int size, MoveToAnimation.MoveToAnimationCb cb) + { + prevPosition.set(position); + AnimationSequence seq = AnimationSequence.get(size); + while (vectors.hasNext()) + seq.addAnimation(MoveToAnimation.get(this, vectors.next(), MOVE_TIME, cb)); + + return seq; + } + + public AnimationSequence getRevertLastMoveAnimation(int size) + { + AnimationSequence seq = AnimationSequence.get(2 + size); + seq.addAnimation(MoveToAnimation.get(this, prevPosition, MOVE_TIME)); + seq.addAnimation(RunnableAnimation.get(this, new Runnable() { + @Override + public void run() { + revertPosition(); + } + })); + + return seq; + } + + @Override + public void draw(Batch batch) + { + sprite.draw(batch); + overlays.draw(batch); + } + + @Override + public void drawDebug(ShapeRenderer debugShapes) + { + float w = sprite.getWidth(); + float h = sprite.getHeight(); + debugShapes.rect(sprite.getX(), sprite.getY(), (w / 2f), (h / 2f), w, h, sprite.getScaleX(), sprite.getScaleY(), sprite.getRotation()); + overlays.drawDebug(debugShapes); + } +} diff --git a/core/src/ch/asynk/creepingarmor/engine/SearchBoard.java b/core/src/ch/asynk/creepingarmor/engine/SearchBoard.java new file mode 100644 index 0000000..2a7db3e --- /dev/null +++ b/core/src/ch/asynk/creepingarmor/engine/SearchBoard.java @@ -0,0 +1,545 @@ +package ch.asynk.creepingarmor.engine; + +import java.util.List; +import java.util.ArrayList; +import java.util.Set; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.ArrayDeque; +import java.util.Iterator; +import java.util.Collection; + +public class SearchBoard +{ + public class Node + { + public int col; + public int row; + public int search; + public int remaining; + public Node parent; + public boolean roadMarch; + + public Node(int col, int row) + { + this.col = col; + this.row = row; + } + + @Override + public String toString() + { + return col + ";" + row; + } + } + + private int cols; + private int rows; + private Board board; + private int searchCount; + private Node nodes[]; + + private ArrayDeque<Node> stack; + private LinkedList<Node> queue; + private ArrayDeque<Node> roadMarch; + private List<Node> los; + + public SearchBoard(Board board, int cols, int rows) + { + this.cols = cols; + this.rows = rows; + this.board = board; + this.searchCount = 0; + + this.nodes = new Node[cols * rows]; + for (int j = 0; j < rows; j++) { + int dx = ((j + 1) / 2); + for (int i = 0; i < cols; i++) + nodes[i + (j * cols)] = new Node((i + dx), j); + } + + this.queue = new LinkedList<Node>(); + this.stack = new ArrayDeque<Node>(20); + this.roadMarch = new ArrayDeque<Node>(5); + this.los = new ArrayList<Node>(10); + } + + private boolean inMap(int col, int row) + { + if ((row < 0) || (row >= rows)) + return false; + + int colOffset = ((row + 1) / 2); + if ((col < colOffset) || ((col - colOffset) >= cols)) + return false; + + return true; + } + + private Tile getTile(Node node) + { + return board.getTile(node.col, node.row); + } + + private Node getNode(Tile tile) + { + return getNode(tile.col, tile.row); + } + + protected Node getNode(int col, int row) + { + int colOffset = ((row + 1) / 2); + if ((col < colOffset) || (row < 0) || (row >= rows) || ((col - colOffset) >= cols)) + return null; + + return nodes[((col - colOffset)) + (row * cols)]; + } + + public int distance(Node from, Node to) + { + return board.distance(from.col, from.row, to.col, to.row); + } + + public void adjacentMoves(Node src, Node a[]) + { + // move to enter dst by sides[i] + a[0] = getNode((src.col - 1), src.row); + a[1] = getNode(src.col, (src.row + 1)); + a[2] = getNode((src.col + 1), (src.row + 1)); + a[3] = getNode((src.col + 1), src.row); + a[4] = getNode(src.col, (src.row - 1)); + a[5] = getNode((src.col - 1), (src.row - 1)); + } + + public int possibleMovesFrom(Pawn pawn, Collection<Tile> moves) + { + moves.clear(); + searchCount += 1; + + Node adjacents[] = new Node[6]; + + Node from = getNode(pawn.getTile()); + from.parent = null; + from.search = searchCount; + from.remaining = pawn.getMovementPoints(); + from.roadMarch = true; + + if (from.remaining <= 0) + return moves.size(); + + int roadMarchBonus = pawn.getRoadMarchBonus(); + boolean first = true; + + stack.push(from); + + while (stack.size() != 0) { + Node src = stack.pop(); + + if (src.remaining < 0) + continue; + if (src.remaining == 0) { + if (src.roadMarch) + roadMarch.push(src); + continue; + } + + adjacentMoves(src, adjacents); + + for(int i = 0; i < 6; i++) { + Node dst = adjacents[i]; + if (dst != null) { + + Tile t = getTile(dst); + int cost = t.costFrom(pawn, board.getSide(i)); + boolean mayMoveOne = first && t.atLeastOneMove(pawn); + int r = src.remaining - cost; + boolean roadMarch = (src.roadMarch && t.road(board.getSide(i))); + + if (dst.search == searchCount) { + if ((r >= 0) && ((r > dst.remaining) || (roadMarch && ((r + roadMarchBonus) >= dst.remaining)))) { + dst.remaining = r; + dst.parent = src; + dst.roadMarch = roadMarch; + stack.push(dst); + moves.add(getTile(dst)); + } + } else { + dst.search = searchCount; + if ((r >= 0) || mayMoveOne) { + dst.parent = src; + dst.remaining = r; + dst.roadMarch = roadMarch; + stack.push(dst); + moves.add(getTile(dst)); + } else { + dst.parent = null; + dst.remaining = -1; + } + } + } + } + first = false; + } + + for (Node n : roadMarch) n.remaining = roadMarchBonus; + while(roadMarch.size() != 0) { + Node src = roadMarch.pop(); + + adjacentMoves(src, adjacents); + + for(int i = 0; i < 6; i++) { + Node dst = adjacents[i]; + if (dst != null) { + + Tile t = getTile(dst); + if (!t.road(board.getSide(i))) + continue; + int cost = t.costFrom(pawn, board.getSide(i)); + int r = src.remaining - cost; + + if (dst.search == searchCount) { + if ((r >= 0) && (r > dst.remaining)) { + dst.remaining = r; + dst.parent = src; + dst.roadMarch = true; + roadMarch.push(dst); + moves.add(getTile(dst)); + } + } else { + dst.search = searchCount; + if (r >= 0) { + dst.parent = src; + dst.remaining = r; + dst.roadMarch = true; + roadMarch.push(dst); + moves.add(getTile(dst)); + } else { + dst.parent = null; + dst.remaining = -1; + } + } + } + } + } + + return moves.size(); + } + + private void adjacentTargets(Node src, int angle, Node a[]) + { + // move in allowed directions + if (Orientation.NORTH.isInSides(angle)) + a[0] = getNode((src.col + 1), src.row); + else + a[0] = null; + + if (Orientation.NORTH_EAST.isInSides(angle)) + a[1] = getNode(src.col, (src.row - 1)); + else + a[1] = null; + + if (Orientation.SOUTH_EAST.isInSides(angle)) + a[2] = getNode((src.col - 1), (src.row - 1)); + else + a[2] = null; + + if (Orientation.SOUTH.isInSides(angle)) + a[3] = getNode((src.col - 1), src.row); + else + a[3] = null; + + if (Orientation.SOUTH_WEST.isInSides(angle)) + a[4] = getNode(src.col, (src.row + 1)); + else + a[4] = null; + + if (Orientation.NORTH_WEST.isInSides(angle)) + a[5] = getNode((src.col + 1), (src.row + 1)); + else + a[5] = null; + } + + public int possibleTargetsFrom(Pawn pawn, Collection<Pawn> targets) + { + targets.clear(); + searchCount += 1; + + Node adjacents[] = new Node[6]; + + int range = pawn.getEngagementRangeFrom(pawn.getTile()); + int angle = pawn.getAngleOfAttack(); + int extendedAngle = pawn.getOrientation().opposite().allBut(); + + Node from = getNode(pawn.getTile()); + from.search = searchCount; + from.remaining = range; + + if (range <= 0) + return targets.size(); + + queue.add(from); + + boolean first = true; + while (queue.size() != 0) { + Node src = queue.remove(); + + if (src.remaining <= 0) + continue; + + if (!first && (((range - src.remaining) % 2) == 0)) + adjacentTargets(src, extendedAngle, adjacents); + else + adjacentTargets(src, angle, adjacents); + + first = false; + int rangeLeft = src.remaining - 1; + + for(int i = 0; i < 6; i++) { + Node dst = adjacents[i]; + if (dst != null) { + if (dst.search == searchCount) { + if ((rangeLeft > dst.remaining)) + dst.remaining = rangeLeft; + } else { + dst.search = searchCount; + dst.remaining = rangeLeft; + queue.add(dst); + Tile t = getTile(dst); + if (hasClearLineOfSight(from, dst, angle)) { + Iterator<Pawn> it = t.iterator(); + while (it.hasNext()) { + Pawn target = it.next(); + if (pawn.canEngage(target)) + targets.add(target); + } + } + } + } + } + } + + return targets.size(); + } + + public boolean canAttack(Pawn pawn, Pawn target, boolean clearVisibility) + { + Node from = getNode(pawn.getTile()); + Node to = getNode(target.getTile()); + + pawn.setAttack(target, distance(from, to)); + + if (pawn.attack.distance > pawn.getEngagementRangeFrom(pawn.getTile())) + return false; + + List<Node> los = lineOfSight(from.col, from.row, to.col, to.row, clearVisibility); + Node last = los.get(los.size() -1); + if (last != to) + return false; + + if (!validatePathAngle(pawn.getAngleOfAttack(), los)) + return false; + + pawn.attack.isClear = isClearAttack(getTile(from), los); + pawn.attack.isFlank = isFlankAttack(target.getFlankSides(), los); + + return true; + } + + private boolean hasClearLineOfSight(Node from, Node to, int angleOfAttack) + { + List<Node> los = lineOfSight(from.col, from.row, to.col, to.row, true); + Node last = los.get(los.size() -1); + if ((last.col != to.col) || (last.row != to.row)) + return false; + return validatePathAngle(angleOfAttack, los); + } + + private boolean isFlankAttack(int angle, List<Node> los) + { + Node from = los.get(los.size() - 2); + Node to = los.get(los.size() - 1); + Orientation o = Orientation.fromMove(to.col, to.row, from.col, from.row); + return o.isInSides(angle); + } + + private boolean isClearAttack(Tile from, List<Node> los) + { + int n = los.size() - 1; + for (int i = 1; i < n; i++) { + if (getTile(los.get(i)).blockLineOfSightFrom(from)) + return false; + } + return true; + } + + private boolean validatePathAngle(int angle, List<Node> los) + { + int forth = 0; + Node prev = null; + for (Node next : los) { + if (prev != null) { + Orientation o = Orientation.fromMove(prev.col, prev.row, next.col, next.row); + if (!o.isInSides(angle)) { + forth -= 1; + if (forth < 0) + return false; + } + forth += 1; + } + prev = next; + } + + return true; + } + + public List<Node> lineOfSight(int x0, int y0, int x1, int y1, boolean clearVisibility) + { + los.clear(); + Tile from = board.getTile(x0, y0); + + // orthogonal axis + int ox0 = x0 - ((y0 +1) / 2); + int ox1 = x1 - ((y1 +1) / 2); + + int dy = y1 - y0; + int dx = ox1 - ox0; + + int xs = 1; + int ys = 1; + if (dx < 0) xs = -1; + if (dy < 0) ys = -1; + boolean sig = !(((dx < 0) && (dy >= 0)) || ((dx >= 0) && (dy < 0))); + + dy = Math.abs(dy); + dx = Math.abs(2 * dx); + if ((dy % 2) == 1) { + if ((y0 % 2) == 0) dx += xs; + else { + dx -= xs; + Math.abs(dx); + } + } + + if (dx == 0) + return verticalLineOfSight(x0, y0, x1, y1, clearVisibility); + if (dx == (3 * dy)) + return diagonalLineOfSight(x0, y0, x1, y1, clearVisibility); + + int dx3 = 3 * dx; + int dy3 = 3 * dy; + + int x = x0; + int y = y0; + int e = -2 * dx; + + boolean flat = (dx > (3 * dy)); + boolean diag = (dx == (3 * dy)); + + los.add(getNode(x, y)); + while((x != x1) || (y != y1)) { + if (e > 0) { + e -= (dy3 + dx3); + y += ys; + if (!sig) + x -= xs; + } else { + e += dy3; + if ((e > -dx) || (!flat && (e == -dx))) { + e -= dx3; + y += ys; + if (sig) + x += xs; + } else if ((e < -dx3) || (diag && (e == -dx3))) { + e += dx3; + y -= ys; + if (!sig) + x += xs; + } else { + e += dy3; + x += xs; + } + } + los.add(getNode(x, y)); + if(clearVisibility && board.getTile(x, y).blockLineOfSightFrom(from)) return los; + } + + return los; + } + + private List<Node> verticalLineOfSight(int x0, int y0, int x1, int y1, boolean clearVisibility) + { + Tile from = board.getTile(x0, y0); + + int d = ( (y1 > y0) ? 1 : -1); + int x = x0; + int y = y0; + + Tile t = null; + los.add(getNode(x, y)); + while((x != x1) || (y != y1)) { + boolean ok = false; + + y += d; + t = board.getTile(x, y); + if (!t.isOffMap()) los.add(getNode(x, y)); + if (!clearVisibility || !t.blockLineOfSightFrom(from)) + ok = true; + + x += d; + t = board.getTile(x, y); + if (!t.isOffMap()) los.add(getNode(x, y)); + if (!clearVisibility || !t.blockLineOfSightFrom(from)) + ok = true; + + if (!ok) + return los; + + y += d; + t = board.getTile(x, y); + if (!t.isOffMap()) los.add(getNode(x, y)); + } + + return los; + } + + private List<Node> diagonalLineOfSight(int x0, int y0, int x1, int y1, boolean clearVisibility) + { + Tile from = board.getTile(x0, y0); + + int dy = ( (y1 > y0) ? 1 : -1); + int dx = ( (x1 > x0) ? 1 : -1); + boolean sig = !(((dx < 0) && (dy >= 0)) || ((dx >= 0) && (dy < 0))); + + int x = x0; + int y = y0; + + Tile t = null; + los.add(getNode(x, y)); + while((x != x1) || (y != y1)) { + boolean ok = false; + + x += dx; + t = board.getTile(x, y); + if (!t.isOffMap()) los.add(getNode(x, y)); + if (!clearVisibility || !t.blockLineOfSightFrom(from)) + ok = true; + + y += dy; + if (!sig) + x -= dx; + t = board.getTile(x, y); + if (!t.isOffMap()) los.add(getNode(x, y)); + if (!clearVisibility || !t.blockLineOfSightFrom(from)) + ok = true; + + if (!ok) + return los; + + x += dx; + t = board.getTile(x, y); + if (!t.isOffMap()) los.add(getNode(x, y)); + } + + return los; + } +} diff --git a/core/src/ch/asynk/creepingarmor/engine/SelectedTile.java b/core/src/ch/asynk/creepingarmor/engine/SelectedTile.java new file mode 100644 index 0000000..4157f4c --- /dev/null +++ b/core/src/ch/asynk/creepingarmor/engine/SelectedTile.java @@ -0,0 +1,80 @@ +package ch.asynk.creepingarmor.engine; + +import com.badlogic.gdx.utils.Disposable; + +import com.badlogic.gdx.graphics.Texture; +import com.badlogic.gdx.graphics.g2d.Batch; +import com.badlogic.gdx.graphics.glutils.ShapeRenderer; + +import ch.asynk.creepingarmor.engine.gfx.Drawable; +import ch.asynk.creepingarmor.engine.gfx.Animation; +import ch.asynk.creepingarmor.engine.gfx.animations.Sprites; + +public class SelectedTile implements Disposable, Drawable, Animation +{ + private Sprites sprites; + public Tile tile; + public boolean visible; + public float x; + public float y; + private float elapsed; + private int frame; + private float[] seq; + + public SelectedTile(Texture texture, float[] seq) + { + this.sprites = new Sprites(texture, seq.length, 1); + this.visible = false; + this.tile = null; + this.elapsed = 0f; + this.seq = seq; + } + + public void hide() + { + tile = null; + visible = false; + } + + public void set(Tile tile) + { + this.visible = true; + this.tile = tile; + this.frame = 0; + this.elapsed = 0f; + this.x = (tile.getX() - (sprites.width / 2f)); + this.y = (tile.getY() - (sprites.height / 2f)); + } + + public void dispose() + { + sprites.dispose(); + } + + @Override + public boolean animate(float delta) + { + if (visible) { + elapsed += delta; + if (elapsed > seq[frame]) { + frame = ((frame + 1) % sprites.frames.length); + elapsed = 0f; + } + } + return false; + } + + @Override + public void draw(Batch batch) + { + if (visible) + batch.draw(sprites.frames[frame], x, y); + } + + @Override + public void drawDebug(ShapeRenderer debugShapes) + { + if (visible) + debugShapes.rect(x, y, sprites.width, sprites.height); + } +} diff --git a/core/src/ch/asynk/creepingarmor/engine/Tile.java b/core/src/ch/asynk/creepingarmor/engine/Tile.java new file mode 100644 index 0000000..bd4138a --- /dev/null +++ b/core/src/ch/asynk/creepingarmor/engine/Tile.java @@ -0,0 +1,175 @@ +package ch.asynk.creepingarmor.engine; + +import java.util.List; +import java.util.Iterator; +import java.util.ArrayDeque; + +import com.badlogic.gdx.utils.Disposable; + +import com.badlogic.gdx.graphics.g2d.Batch; +import com.badlogic.gdx.graphics.g2d.TextureAtlas; +import com.badlogic.gdx.graphics.glutils.ShapeRenderer; +import com.badlogic.gdx.math.Vector2; + +import ch.asynk.creepingarmor.engine.Board; +import ch.asynk.creepingarmor.engine.gfx.Drawable; +import ch.asynk.creepingarmor.engine.gfx.StackedImages; + +public abstract class Tile implements Drawable, Disposable, Iterable<Pawn> +{ + public interface TileTerrain + { + } + + protected int col; + protected int row; + protected float x; + protected float y; + private StackedImages overlays; + protected ArrayDeque<Pawn> stack; + + public abstract int defense(); + public abstract int exitCost(); + public abstract int costFrom(Pawn pawn, Orientation side); + + public abstract boolean isOffMap(); + public abstract boolean isA(TileTerrain terrain); + public abstract boolean road(Orientation side); + public abstract boolean atLeastOneMove(Pawn pawn); + public abstract boolean blockLineOfSightFrom(Tile tile); + + protected Tile(int col, int row) + { + this.col = col; + this.row = row; + } + + public Tile(float x, float y, int col, int row, TextureAtlas atlas) + { + this.stack = new ArrayDeque<Pawn>(); + this.x = x; + this.y = y; + this.col = col; + this.row = row; + this.overlays = new StackedImages(atlas); + this.overlays.centerOn(x, y); + } + + public float getX() { return x; } + public float getY() { return y; } + public int getCol() { return col; } + public int getRow() { return row; } + + @Override + public String toString() + { + return String.format("(%d;%d) %s", col, row, (isOffMap() ? "x" : "")); + } + + public String toShort() + { + return String.format("(%d;%d)", col, row); + } + + @Override + public void dispose() + { + stack.clear(); + overlays.dispose(); + } + + public boolean isEmpty() + { + return stack.isEmpty(); + } + + public Iterator<Pawn> iterator() + { + return stack.iterator(); + } + + public int push(Pawn pawn) + { + if (!stack.contains(pawn)) + stack.push(pawn); + return stack.size(); + } + + public int remove(Pawn pawn) + { + stack.remove(pawn); + return stack.size(); + } + + private Pawn getTopPawn() + { + if (isEmpty()) return null; + return stack.getFirst(); + } + + public boolean hasUnits() + { + if (isEmpty()) return false; + Iterator<Pawn> itr = iterator(); + while(itr.hasNext()) { + if (itr.next().isUnit()) + return true; + } + return false; + } + + public boolean mustBeDrawn() + { + if (!isEmpty()) return true; + return hasOverlayEnabled(); + } + + public boolean disableOverlays() + { + overlays.disableAll(); + return !isEmpty(); + } + + public boolean hasOverlayEnabled() + { + return overlays.isEnabled(); + } + + public boolean isOverlayEnabled(int i) + { + return overlays.isEnabled(i); + } + + public boolean enableOverlay(int i, boolean enable) + { + overlays.enable(i, enable); + if (enable) return true; + return mustBeDrawn(); + } + + public boolean enableOverlay(int i, boolean enable, float r) + { + overlays.enable(i, enable); + overlays.rotate(i, r); + if (enable) return true; + return mustBeDrawn(); + } + + @Override + public void draw(Batch batch) + { + overlays.draw(batch); + Pawn pawn = getTopPawn(); + if (pawn != null) + pawn.draw(batch); + } + + @Override + public void drawDebug(ShapeRenderer debugShapes) + { + overlays.drawDebug(debugShapes); + Pawn pawn = getTopPawn(); + if (pawn != null) + pawn.drawDebug(debugShapes); + } +} diff --git a/core/src/ch/asynk/creepingarmor/engine/gfx/Animation.java b/core/src/ch/asynk/creepingarmor/engine/gfx/Animation.java new file mode 100644 index 0000000..ab82ea5 --- /dev/null +++ b/core/src/ch/asynk/creepingarmor/engine/gfx/Animation.java @@ -0,0 +1,8 @@ +package ch.asynk.creepingarmor.engine.gfx; + +import com.badlogic.gdx.utils.Disposable; + +public interface Animation extends Disposable, Drawable +{ + public boolean animate(float delta); +} diff --git a/core/src/ch/asynk/creepingarmor/engine/gfx/Drawable.java b/core/src/ch/asynk/creepingarmor/engine/gfx/Drawable.java new file mode 100644 index 0000000..5f5117a --- /dev/null +++ b/core/src/ch/asynk/creepingarmor/engine/gfx/Drawable.java @@ -0,0 +1,10 @@ +package ch.asynk.creepingarmor.engine.gfx; + +import com.badlogic.gdx.graphics.g2d.Batch; +import com.badlogic.gdx.graphics.glutils.ShapeRenderer; + +public interface Drawable +{ + public void draw(Batch batch); + public void drawDebug(ShapeRenderer debugShapes); +} diff --git a/core/src/ch/asynk/creepingarmor/engine/gfx/Moveable.java b/core/src/ch/asynk/creepingarmor/engine/gfx/Moveable.java new file mode 100644 index 0000000..a20810d --- /dev/null +++ b/core/src/ch/asynk/creepingarmor/engine/gfx/Moveable.java @@ -0,0 +1,16 @@ +package ch.asynk.creepingarmor.engine.gfx; + +import ch.asynk.creepingarmor.engine.Faction; + +public interface Moveable extends Drawable +{ + public void setAlpha(float alpha); + public float getX(); + public float getY(); + public float getWidth(); + public float getHeight(); + public float getRotation(); + public void setPosition(float x, float y); + public void setPosition(float x, float y, float r); + public Faction getFaction(); +} diff --git a/core/src/ch/asynk/creepingarmor/engine/gfx/StackedImages.java b/core/src/ch/asynk/creepingarmor/engine/gfx/StackedImages.java new file mode 100644 index 0000000..60d4a42 --- /dev/null +++ b/core/src/ch/asynk/creepingarmor/engine/gfx/StackedImages.java @@ -0,0 +1,99 @@ +package ch.asynk.creepingarmor.engine.gfx; + +import com.badlogic.gdx.utils.Disposable; + +import com.badlogic.gdx.graphics.g2d.Sprite; +import com.badlogic.gdx.graphics.g2d.Batch; +import com.badlogic.gdx.graphics.g2d.TextureAtlas; +import com.badlogic.gdx.graphics.glutils.ShapeRenderer; +import com.badlogic.gdx.utils.Array; + +public class StackedImages implements Drawable, Disposable +{ + private boolean enabled[]; + private Array<Sprite> sprites; + + public StackedImages(TextureAtlas atlas) + { + this.sprites = atlas.createSprites(); + this.enabled = new boolean[sprites.size]; + } + + @Override + public void dispose() + { + } + + public void disableAll() + { + for (int i = 0; i < sprites.size; i++) + enabled[i] = false; + } + + public void enable(int i, boolean enable) + { + enabled[i] = enable; + } + + public boolean isEnabled(int i) + { + return enabled[i]; + } + + public boolean isEnabled() + { + for (int i = 0; i < sprites.size; i++) + if (enabled[i]) return true; + return false; + } + + public void setAlpha(float alpha) + { + for (int i = 0, n = sprites.size; i < n; i++) + sprites.get(i).setAlpha(alpha); + } + + public void rotate(int i, float r) + { + sprites.get(i).setRotation(r); + } + + public void setRotation(float r) + { + for (int i = 0, n = sprites.size; i < n; i++) + sprites.get(i).setRotation(r); + } + + public void translate(float dx, float dy) + { + for (int i = 0, n = sprites.size; i < n; i++) + sprites.get(i).translate(dx, dy); + } + + public void centerOn(float cx, float cy) + { + for (int i = 0, n = sprites.size; i < n; i++) { + float x = (cx - (sprites.get(i).getWidth() / 2f)); + float y = (cy - (sprites.get(i).getHeight() / 2f)); + sprites.get(i).setPosition(x, y); + } + } + + @Override + public void draw(Batch batch) + { + for (int i = 0, n = sprites.size; i < n; i++) { + if (enabled[i]) + sprites.get(i).draw(batch); + } + } + + @Override + public void drawDebug(ShapeRenderer shapes) + { + Sprite sprite = sprites.get(0); + float w = sprite.getWidth(); + float h = sprite.getHeight(); + shapes.rect(sprite.getX(), sprite.getY(), (w / 2f), (h / 2f), w, h, sprite.getScaleX(), sprite.getScaleY(), sprite.getRotation()); + } +} diff --git a/core/src/ch/asynk/creepingarmor/engine/gfx/animations/AnimationSequence.java b/core/src/ch/asynk/creepingarmor/engine/gfx/animations/AnimationSequence.java new file mode 100644 index 0000000..7ac9eb1 --- /dev/null +++ b/core/src/ch/asynk/creepingarmor/engine/gfx/animations/AnimationSequence.java @@ -0,0 +1,76 @@ +package ch.asynk.creepingarmor.engine.gfx.animations; + +import java.util.ArrayList; + +import com.badlogic.gdx.utils.Pool; +import com.badlogic.gdx.graphics.g2d.Batch; +import com.badlogic.gdx.graphics.glutils.ShapeRenderer; + +import ch.asynk.creepingarmor.engine.gfx.Animation; + +public class AnimationSequence implements Animation, Pool.Poolable +{ + private ArrayList<Animation> animations; + + private static final Pool<AnimationSequence> animationSequencePool = new Pool<AnimationSequence>() { + @Override + protected AnimationSequence newObject() { + return new AnimationSequence(); + } + }; + + public static AnimationSequence get(int capacity) + { + AnimationSequence seq = animationSequencePool.obtain(); + if (seq.animations == null) + seq.animations = new ArrayList<Animation>(capacity); + else + seq.animations.ensureCapacity(capacity); + + return seq; + } + + @Override + public void reset() + { + for (int i = 0, n = animations.size(); i < n; i++) + animations.get(i).dispose(); + animations.clear(); + } + + @Override + public void dispose() + { + animationSequencePool.free(this); + } + + public void addAnimation(Animation animation) + { + animations.add(animation); + } + + @Override + public boolean animate(float delta) + { + if(animations.isEmpty()) return true; + + Animation animation = animations.get(0); + if (animation.animate(delta)) { + animations.remove(0); + } + + return (animations.isEmpty()); + } + + @Override + public void draw(Batch batch) + { + animations.get(0).draw(batch); + } + + @Override + public void drawDebug(ShapeRenderer debugShapes) + { + animations.get(0).drawDebug(debugShapes); + } +} diff --git a/core/src/ch/asynk/creepingarmor/engine/gfx/animations/DestroyAnimation.java b/core/src/ch/asynk/creepingarmor/engine/gfx/animations/DestroyAnimation.java new file mode 100644 index 0000000..16d84b0 --- /dev/null +++ b/core/src/ch/asynk/creepingarmor/engine/gfx/animations/DestroyAnimation.java @@ -0,0 +1,62 @@ +package ch.asynk.creepingarmor.engine.gfx.animations; + +import com.badlogic.gdx.utils.Disposable; +import com.badlogic.gdx.graphics.g2d.Batch; +import com.badlogic.gdx.graphics.glutils.ShapeRenderer; + +import ch.asynk.creepingarmor.engine.gfx.Moveable; +import ch.asynk.creepingarmor.engine.gfx.Animation; + +public class DestroyAnimation implements Disposable, Animation +{ + private static final float DELAY = 1.5f; + private static final float DURATION = 1.5f; + + private Moveable moveable; + private float x; + private float y; + private int alphaP; + private float elapsed; + + @Override + public void dispose() + { + } + + public void set(float duration, Moveable moveable) + { + this.moveable = moveable; + this.alphaP = 0; + this.elapsed = 0f; + this.x = (moveable.getX() + (moveable.getWidth() / 2f)); + this.y = (moveable.getY() + (moveable.getHeight() / 2f)); + } + + @Override + public boolean animate(float delta) + { + elapsed += delta; + if (elapsed < DELAY) + return false; + + int a = (int) (((elapsed - DELAY) / DURATION) * 10); + if (a != alphaP) { + alphaP = a; + moveable.setAlpha(1f - (alphaP / 10f)); + } + + return (elapsed >= (DELAY + DURATION)); + } + + @Override + public void draw(Batch batch) + { + moveable.draw(batch); + } + + @Override + public void drawDebug(ShapeRenderer debugShapes) + { + moveable.drawDebug(debugShapes); + } +} diff --git a/core/src/ch/asynk/creepingarmor/engine/gfx/animations/DiceAnimation.java b/core/src/ch/asynk/creepingarmor/engine/gfx/animations/DiceAnimation.java new file mode 100644 index 0000000..4d2ca67 --- /dev/null +++ b/core/src/ch/asynk/creepingarmor/engine/gfx/animations/DiceAnimation.java @@ -0,0 +1,141 @@ +package ch.asynk.creepingarmor.engine.gfx.animations; + +import java.util.Random; + +import com.badlogic.gdx.audio.Sound; +import com.badlogic.gdx.graphics.Texture; +import com.badlogic.gdx.graphics.g2d.TextureRegion; +import com.badlogic.gdx.graphics.g2d.Batch; +import com.badlogic.gdx.graphics.glutils.ShapeRenderer; + +import ch.asynk.creepingarmor.engine.gfx.Drawable; +import ch.asynk.creepingarmor.engine.gfx.Animation; + +public class DiceAnimation implements Animation, Drawable +{ + private static final float DURATION = 0.7f; + private static final float DURATION_SCATTERING = 0.5f; + private static final int DICE_DIMENSION = 24; + + private static Random random = new Random(); + private static Sprites dice; + private static Sound sound; + private static double sndId; + private static float volume; + private static int[][] rolls = new int[][]{ + { 25, 40, 55, 70, 85, 100, 115, 99, 83, 67, 51, 36, 37, 52, 67, 66, 65, 64 }, + { 58, 74, 59, 60, 45, 62, 78, 94, 109, 108, 123, 106, 89, 71, 70, 69, 68 }, + { 106, 121, 120, 103, 86, 70, 54, 37, 20, 19, 18, 34, 50, 51, 52, 69, 86, 103, 119, 128 }, + { 95, 79, 93, 92, 91, 90, 104, 103, 102, 85, 84, 67, 66, 65, 49, 32, 16, 0 }, + { 22, 39, 56, 73, 90, 107, 124, 128, 113, 98, 83, 68, 53, 38, 23, 0, 25, 42, 59, 76 }, + { 79, 78, 61, 76, 91, 106, 121, 120, 119, 102, 101, 84, 68, 52, 37, 38, 39, 40, 41, 58, 75, 74, 73, 72 }, + }; + + private float x; + private float y; + private int frame; + private int[] roll; + private float elapsed; + private float duration; + // public boolean stop; + + public static void init(Texture texture, int cols, int rows, Sound s) + { + dice = new Sprites(texture, cols, rows); + sound = s; + sndId = -1; + } + + public static void initSound(float v) + { + sndId = -1; + volume = v; + } + + public static void free() + { + sound.dispose(); + dice.dispose(); + } + + public void translate(float dx, float dy) + { + x += dx; + y += dy; + } + + public float getX() + { + return x; + } + + public float getY() + { + return y; + } + + public int getWidth() + { + return DICE_DIMENSION; + } + + public int getHeight() + { + return DICE_DIMENSION; + } + + public void setPosition(float x, float y) + { + this.x = x; + this.y = y; + } + + public void set(int result) + { + this.frame = 0; + this.elapsed = 0f; + this.roll = rolls[result - 1]; + this.duration = DURATION + (DURATION_SCATTERING * random.nextFloat()); + // this.stop = false; + } + + public boolean isDone() + { + return (elapsed >= duration); + } + + @Override + public void dispose() + { + } + + @Override + public boolean animate(float delta) + { + // if (stop) + // return true; + elapsed += delta; + if (elapsed < duration) { + int idx = (int) (roll.length * elapsed / duration); + if (idx >= roll.length) + idx = (roll.length -1); + frame = roll[idx]; + } + if (sndId == -1) + sndId = sound.play(volume); + + return false; + } + + @Override + public void draw(Batch batch) + { + batch.draw(dice.frames[frame], x, y, DICE_DIMENSION, DICE_DIMENSION); + } + + @Override + public void drawDebug(ShapeRenderer debugShapes) + { + debugShapes.rect(x, y, dice.frames[frame].getRegionWidth(), dice.frames[frame].getRegionHeight()); + } +} diff --git a/core/src/ch/asynk/creepingarmor/engine/gfx/animations/FireAnimation.java b/core/src/ch/asynk/creepingarmor/engine/gfx/animations/FireAnimation.java new file mode 100644 index 0000000..c221460 --- /dev/null +++ b/core/src/ch/asynk/creepingarmor/engine/gfx/animations/FireAnimation.java @@ -0,0 +1,87 @@ +package ch.asynk.creepingarmor.engine.gfx.animations; + +import java.util.Random; + +import com.badlogic.gdx.audio.Sound; +import com.badlogic.gdx.graphics.Texture; + +public class FireAnimation +{ + public static Random random = new Random(); + + public static Sprites infantryFire; + public static Sprites tankFire; + public static Sprites explosion; + + public static Sound infantryFireSnd; + public static Sound tankFireSnd; + public static Sound tankFireSndLong; + public static Sound explosionSnd; + public static Sound explosionSndLong; + + public static double infantryFireSndLongId; + public static double tankFireSndLongId; + public static double explosionSndLongId; + + public static void init( + Texture infantryFireT, int iCols, int iRows, + Texture tankFireT, int sCols, int sRows, + Texture explosionT, int eCols, int eRows, + Sound infantryFireS, + Sound tankFireS, + Sound tankFireLongS, + Sound explosionS, + Sound explosionLongS) + { + infantryFire = new Sprites(infantryFireT, iCols, iRows); + tankFire = new Sprites(tankFireT, sCols, sRows); + explosion = new Sprites(explosionT, eCols, eRows); + infantryFireSnd = infantryFireS; + tankFireSnd = tankFireS; + tankFireSndLong = tankFireLongS; + explosionSnd = explosionS; + explosionSndLong = explosionLongS; + + reset(); + } + + public static void reset() + { + infantryFireSndLongId = -1; + tankFireSndLongId = -1; + explosionSndLongId = -1; + } + + public static void free() + { + tankFire.dispose(); + explosion.dispose(); + + tankFireSnd.dispose(); + tankFireSndLong.dispose(); + explosionSnd.dispose(); + explosionSndLong.dispose(); + } + + public static void infantryFireSndPlay(float volume) + { + if (infantryFireSndLongId == -1) + infantryFireSndLongId = infantryFireSnd.play(volume); + } + + public static void tankFireSndPlay(float volume) + { + if (tankFireSndLongId == -1) + tankFireSndLongId = tankFireSndLong.play(volume); + else + tankFireSnd.play(volume); + } + + public static void explosionSndPlay(float volume) + { + if (explosionSndLongId == -1) + explosionSndLongId = explosionSndLong.play(volume); + else + explosionSnd.play(volume); + } +} diff --git a/core/src/ch/asynk/creepingarmor/engine/gfx/animations/InfantryFireAnimation.java b/core/src/ch/asynk/creepingarmor/engine/gfx/animations/InfantryFireAnimation.java new file mode 100644 index 0000000..b478a2b --- /dev/null +++ b/core/src/ch/asynk/creepingarmor/engine/gfx/animations/InfantryFireAnimation.java @@ -0,0 +1,222 @@ +package ch.asynk.creepingarmor.engine.gfx.animations; + +import java.util.Random; + +import com.badlogic.gdx.utils.Disposable; +import com.badlogic.gdx.utils.Pool; + +import com.badlogic.gdx.audio.Sound; +import com.badlogic.gdx.graphics.Texture; +import com.badlogic.gdx.graphics.g2d.TextureRegion; +import com.badlogic.gdx.graphics.g2d.Batch; +import com.badlogic.gdx.graphics.glutils.ShapeRenderer; + +import ch.asynk.creepingarmor.engine.gfx.Drawable; +import ch.asynk.creepingarmor.engine.gfx.Animation; + +public class InfantryFireAnimation implements Disposable, Animation, Pool.Poolable +{ + class Shot + { + public TextureRegion fireRegion; + public float fire_a; + public float fire_x; + public float fire_y; + public float fire_w; + public float fire_dx; + public float fire_dy; + public float fire_dw; + + public boolean fired; + public boolean hit; + public boolean completed; + + public float fire_time; + public float hit_time; + public float end_time; + + public int hit_frame; + + public Shot(TextureRegion region) + { + this.fireRegion = region; + } + + public void set(float delay, float x0, float y0, float x1, float y1, float w, float a) + { + float dx = (x1 - x0); + float dy = (y1 - y0); + + // timing + float fire_duration = ((FireAnimation.random.nextFloat() * TIME_SCATTERING) + (w / SHOT_SPEED)); + float hit_duration = (FireAnimation.infantryFire.rows * HIT_FRAME_DURATION); + + this.fired = false; + this.fire_time = delay; + this.hit_time = (this.fire_time + fire_duration); + this.end_time = (this.hit_time + hit_duration); + + // fire vars + this.fire_a = a; + this.fire_x = x0; + this.fire_y = y0; + this.fire_w = 0; + this.fire_dx = (dx / fire_duration); + this.fire_dy = (dy / fire_duration); + this.fire_dw = (w / fire_duration); + this.hit_frame = 0; + } + + public boolean animate(float delta) + { + if (!fired && (elapsed < fire_time)) + return false; + + if (!fired) { + fired = true; + FireAnimation.infantryFireSndPlay(volume); + } + + if (!hit && (elapsed < hit_time)) { + fire_w += (fire_dw * delta); + fire_x += (fire_dx * delta); + fire_y += (fire_dy * delta); + fireRegion.setRegionWidth((int) fire_w); + return false; + } + + if (!hit) + hit = true; + + if (elapsed < end_time) { + int frame = (int) ((elapsed - hit_time) / HIT_FRAME_DURATION); + if (frame != hit_frame) { + hit_frame = frame; + fireRegion.setRegion(FireAnimation.infantryFire.frames[hit_frame]); + fireRegion.setRegionWidth((int) fire_w); + } + return false; + } + + completed = true; + return true; + } + + public void draw(Batch batch) + { + if (fired && !completed) + batch.draw(fireRegion, fire_x, fire_y, 0, 0, fireRegion.getRegionWidth(), fireRegion.getRegionHeight(), 1f, 1f, fire_a); + } + } + + private static final int SHOT_COUNT = 19; + private static final float SHOT_DELAY = (1.6f / SHOT_COUNT); + private static final float SHOT_SCATTERING = 40f; + private static final float TIME_SCATTERING = 0.6f; + private static final float START_DELAY = 0.8f; + private static final float SHOT_SPEED = 1000f; + private static final float HIT_FRAME_DURATION = 0.05f; + + private Shot[] shots; + + private float elapsed; + + private float volume; + + private static final Pool<InfantryFireAnimation> fireAnimationPool = new Pool<InfantryFireAnimation>() { + @Override + protected InfantryFireAnimation newObject() { + return new InfantryFireAnimation(); + } + }; + + public static InfantryFireAnimation get(float volume, float x0, float y0, float x1, float y1, float halfWidth) + { + InfantryFireAnimation a = fireAnimationPool.obtain(); + a.set(volume, x0, y0, x1, y1, halfWidth); + return a; + } + + public InfantryFireAnimation() + { + this.shots = new Shot[SHOT_COUNT]; + for (int i = 0; i < shots.length; i++) + shots[i] = new Shot(new TextureRegion(FireAnimation.infantryFire.frames[0])); + } + + private void set(float volume, float x0, float y0, float x1, float y1, float halfWidth) + { + this.volume = volume; + this.elapsed = 0f; + + float delay = START_DELAY + (FireAnimation.random.nextFloat() * TIME_SCATTERING); + + y0 -= (FireAnimation.infantryFire.height / 2.0f); + double r = Math.atan2((y0 - y1), (x0 - x1)); + x0 -= ((float) (Math.cos(r) * halfWidth)); + y0 -= ((float) (Math.sin(r) * halfWidth)); + + float dx = (x1 - x0); + float dy = (y1 - y0); + float w = (float) Math.sqrt((dx * dx) + (dy * dy)); + double dr = (Math.atan2(halfWidth, w) / 2f); + + double a = (r + (dr / 2f)); + double da = (dr / (float) SHOT_COUNT); + + for (Shot shot : shots) { + float x = (float) (x0 - (Math.cos(a) * w)); + float y = (float) (y0 - (Math.sin(a) * w)); + + shot.set(delay, x0, y0, x, y, w, (float) Math.toDegrees(a)); + + delay += SHOT_DELAY; + a -= 2 * (da * FireAnimation.random.nextFloat()); + } + } + + @Override + public void reset() + { + } + + @Override + public void dispose() + { + fireAnimationPool.free(this); + } + + @Override + public boolean animate(float delta) + { + elapsed += delta; + + boolean completed = true; + for (Shot shot : shots) + completed &= shot.animate(delta); + + return completed; + } + + @Override + public void draw(Batch batch) + { + for (Shot shot : shots) + shot.draw(batch); + } + + @Override + public void drawDebug(ShapeRenderer debugShapes) + { + // debugShapes.end(); + // debugShapes.begin(ShapeRenderer.ShapeType.Line); + // debugShapes.identity(); + // debugShapes.translate(fire_x, fire_y, 0); + // debugShapes.rotate(0, 0, 1, fire_a); + // debugShapes.translate(-fire_x, -fire_y, 0); + // debugShapes.rect(fire_x, fire_y, fire_w, FireAnimation.infantryFire.height); + // debugShapes.end(); + // debugShapes.begin(ShapeRenderer.ShapeType.Line); + // debugShapes.identity(); + } +} diff --git a/core/src/ch/asynk/creepingarmor/engine/gfx/animations/MoveToAnimation.java b/core/src/ch/asynk/creepingarmor/engine/gfx/animations/MoveToAnimation.java new file mode 100644 index 0000000..a1d548c --- /dev/null +++ b/core/src/ch/asynk/creepingarmor/engine/gfx/animations/MoveToAnimation.java @@ -0,0 +1,126 @@ +package ch.asynk.creepingarmor.engine.gfx.animations; + +import com.badlogic.gdx.math.Vector3; +import com.badlogic.gdx.utils.Pool; +import com.badlogic.gdx.graphics.g2d.Batch; +import com.badlogic.gdx.graphics.glutils.ShapeRenderer; + +import ch.asynk.creepingarmor.engine.gfx.Moveable; + +public class MoveToAnimation extends TimedAnimation +{ + public interface MoveToAnimationCb { + void moveToAnimationLeave(Moveable moveable, float x, float y, float r); + void moveToAnimationEnter(Moveable moveable, float x, float y, float r); + void moveToAnimationDone(Moveable moveable, float x, float y, float r); + } + + private Moveable moveable; + private float fromX; + private float fromY; + private float fromR; + private float toX; + private float toY; + private float toR; + private float rDelta; + private boolean notified; + private MoveToAnimationCb cb; + + private static final Pool<MoveToAnimation> moveToAnimationPool = new Pool<MoveToAnimation>() { + @Override + protected MoveToAnimation newObject() { + return new MoveToAnimation(); + } + }; + + public static MoveToAnimation get(Moveable moveable, Vector3 v, float duration) + { + return get(moveable, v.x, v.y, v.z, duration); + } + + public static MoveToAnimation get(Moveable moveable, Vector3 v, float duration, MoveToAnimationCb cb) + { + return get(moveable, v.x, v.y, v.z, duration, cb); + } + + public static MoveToAnimation get(Moveable moveable, float x, float y, float r, float duration) + { + return get(moveable, x, y, r, duration, null); + } + + public static MoveToAnimation get(Moveable moveable, float x, float y, float r, float duration, MoveToAnimationCb cb) + { + MoveToAnimation a = moveToAnimationPool.obtain(); + + a.moveable = moveable; + a.toX = x; + a.toY = y; + a.toR = r; + a.duration = duration; + a.cb = cb; + a.rDelta = 0; + a.notified = false; + + return a; + } + + @Override + public void dispose() + { + moveToAnimationPool.free(this); + } + + @Override + protected void begin() + { + fromX = moveable.getX(); + fromY = moveable.getY(); + fromR = moveable.getRotation(); + notified = ((fromX == toX) && (fromY == toY)); + + if (Math.abs(toR - fromR) <= 180.f) + rDelta = (toR - fromR); + else { + if (toR > fromR) + rDelta = (toR - 360 - fromR); + else + rDelta = (toR + 360 - fromR); + } + } + + @Override + protected void end() + { + if (cb != null) + cb.moveToAnimationDone(moveable, (toX + (moveable.getWidth() / 2)), (toY + (moveable.getHeight() / 2)), toR); + dispose(); + } + + @Override + protected void update(float percent) + { + if ((cb != null) && !notified && (percent >= 0.5)) { + float dw = (moveable.getWidth() / 2); + float dh = (moveable.getHeight() / 2); + cb.moveToAnimationLeave(moveable, (fromX + dw), (fromY + dh), fromR); + cb.moveToAnimationEnter(moveable, (toX + dw), (toY + dh), toR); + notified = true; + } + if (percent == 1f) + moveable.setPosition(toX, toY, (int) toR); + else + moveable.setPosition(fromX + ((toX - fromX) * percent), fromY + ((toY - fromY) * percent), (fromR + (rDelta * percent))); + } + + @Override + public void draw(Batch batch) + { + moveable.draw(batch); + } + + @Override + public void drawDebug(ShapeRenderer debugShapes) + { + moveable.drawDebug(debugShapes); + } +} diff --git a/core/src/ch/asynk/creepingarmor/engine/gfx/animations/PromoteAnimation.java b/core/src/ch/asynk/creepingarmor/engine/gfx/animations/PromoteAnimation.java new file mode 100644 index 0000000..1d8d98e --- /dev/null +++ b/core/src/ch/asynk/creepingarmor/engine/gfx/animations/PromoteAnimation.java @@ -0,0 +1,98 @@ +package ch.asynk.creepingarmor.engine.gfx.animations; + +import java.lang.Math; + +import com.badlogic.gdx.audio.Sound; +import com.badlogic.gdx.graphics.g2d.TextureAtlas; +import com.badlogic.gdx.graphics.g2d.TextureRegion; +import com.badlogic.gdx.graphics.g2d.Batch; +import com.badlogic.gdx.graphics.glutils.ShapeRenderer; + +import ch.asynk.creepingarmor.engine.gfx.Drawable; +import ch.asynk.creepingarmor.engine.gfx.Animation; + +public class PromoteAnimation implements Animation, Drawable +{ + private static PromoteAnimation instance = new PromoteAnimation(); + + private static final float DURATION = 0.3f; + private static final float MAX_SCALE = 2f; + + private static Sound usSound; + private static Sound geSound; + private static Sound snd; + private static TextureRegion region; + + private float x0; + private float y0; + private float x; + private float y; + private float scale; + private float step; + private float volume; + private float elapsed; + + public static void init(TextureAtlas atlas, Sound usSnd, Sound geSnd) + { + region = atlas.findRegion("stars"); + usSound = usSnd; + geSound = geSnd; + } + + public static void free() + { + } + + protected void PromoteAnimation() + { + } + + public static PromoteAnimation get(boolean us, float x, float y, float v) + { + x = (x - (region.getRegionWidth() / 2.0f)); + y = (y - (region.getRegionHeight() / 2.0f)); + + instance.volume = v; + instance.x0 = x; + instance.y0 = y; + instance.scale = 0f; + instance.elapsed = 0f; + snd = (us ? usSound : geSound); + + return instance; + } + + @Override + public void dispose() + { + } + + @Override + public boolean animate(float delta) + { + elapsed += delta; + if (elapsed >= DURATION) { + snd.play(volume); + return true; + } + + float s = MAX_SCALE * (float) Math.sin(Math.PI / DURATION * elapsed); + scale = 1f + s; + x = x0 - ((region.getRegionWidth() * scale) / 4f); + y = y0 - ((region.getRegionHeight() * scale) / 4f); + + return false; + } + + @Override + public void draw(Batch batch) + { + batch.draw(region, x, y, 0, 0, region.getRegionWidth(), region.getRegionHeight(), scale, scale, 0f); + } + + @Override + public void drawDebug(ShapeRenderer debugShapes) + { + debugShapes.rect(x, y, region.getRegionWidth(), region.getRegionHeight()); + } +} diff --git a/core/src/ch/asynk/creepingarmor/engine/gfx/animations/RunnableAnimation.java b/core/src/ch/asynk/creepingarmor/engine/gfx/animations/RunnableAnimation.java new file mode 100644 index 0000000..6c09904 --- /dev/null +++ b/core/src/ch/asynk/creepingarmor/engine/gfx/animations/RunnableAnimation.java @@ -0,0 +1,67 @@ +package ch.asynk.creepingarmor.engine.gfx.animations; + +import com.badlogic.gdx.utils.Pool; +import com.badlogic.gdx.graphics.g2d.Batch; +import com.badlogic.gdx.graphics.glutils.ShapeRenderer; + +import ch.asynk.creepingarmor.engine.gfx.Moveable; +import ch.asynk.creepingarmor.engine.gfx.Animation; + +public class RunnableAnimation implements Animation, Pool.Poolable +{ + private Runnable runnable; + private Moveable moveable; + private boolean ran; + + private static final Pool<RunnableAnimation> runnableAnimationPool = new Pool<RunnableAnimation>() { + @Override + protected RunnableAnimation newObject() { + return new RunnableAnimation(); + } + }; + + public static RunnableAnimation get(Moveable moveable, Runnable runnable) + { + RunnableAnimation a = runnableAnimationPool.obtain(); + a.runnable = runnable; + a.moveable = moveable; + return a; + } + + @Override + public void reset() + { + ran = false; + } + + @Override + public void dispose() + { + runnableAnimationPool.free(this); + } + + @Override + public boolean animate(float delta) + { + if (ran) return true; + + runnable.run(); + runnable = null; + ran = true; + dispose(); + + return true; + } + + @Override + public void draw(Batch batch) + { + moveable.draw(batch); + } + + @Override + public void drawDebug(ShapeRenderer debugShapes) + { + moveable.drawDebug(debugShapes); + } +} diff --git a/core/src/ch/asynk/creepingarmor/engine/gfx/animations/SoundAnimation.java b/core/src/ch/asynk/creepingarmor/engine/gfx/animations/SoundAnimation.java new file mode 100644 index 0000000..9c6a289 --- /dev/null +++ b/core/src/ch/asynk/creepingarmor/engine/gfx/animations/SoundAnimation.java @@ -0,0 +1,83 @@ +package ch.asynk.creepingarmor.engine.gfx.animations; + +import com.badlogic.gdx.utils.Pool; +import com.badlogic.gdx.audio.Sound; +import com.badlogic.gdx.graphics.g2d.Batch; +import com.badlogic.gdx.graphics.glutils.ShapeRenderer; + +public class SoundAnimation extends TimedAnimation +{ + public enum Action + { + FADE_IN, + FADE_OUT + }; + + private Sound sound; + private long soundId; + private Action action; + private float volume; + + private static final Pool<SoundAnimation> soundAnimationPool = new Pool<SoundAnimation>() { + @Override + protected SoundAnimation newObject() { + return new SoundAnimation(); + } + }; + + public static SoundAnimation get(Action action, Sound sound, long soundId, float volume, float duration) + { + SoundAnimation a = soundAnimationPool.obtain(); + + a.action = action; + a.sound = sound; + a.soundId = soundId; + a.volume = volume; + a.duration = duration; + + return a; + } + + @Override + public void dispose() + { + soundAnimationPool.free(this); + } + + @Override + protected void begin() + { + } + + @Override + protected void end() + { + dispose(); + } + + @Override + protected void update(float percent) + { + float v; + switch(action) { + case FADE_IN: + v = ( volume * percent); + sound.setVolume(soundId, v); + break; + case FADE_OUT: + v = (volume - ( volume * percent)); + sound.setVolume(soundId, v); + break; + } + } + + @Override + public void draw(Batch batch) + { + } + + @Override + public void drawDebug(ShapeRenderer debugShapes) + { + } +} diff --git a/core/src/ch/asynk/creepingarmor/engine/gfx/animations/SpriteAnimation.java b/core/src/ch/asynk/creepingarmor/engine/gfx/animations/SpriteAnimation.java new file mode 100644 index 0000000..377cf6b --- /dev/null +++ b/core/src/ch/asynk/creepingarmor/engine/gfx/animations/SpriteAnimation.java @@ -0,0 +1,76 @@ +package ch.asynk.creepingarmor.engine.gfx.animations; + +import java.util.Random; + +import com.badlogic.gdx.utils.Disposable; + +import com.badlogic.gdx.graphics.Texture; +import com.badlogic.gdx.graphics.g2d.Batch; +import com.badlogic.gdx.graphics.g2d.TextureRegion; +import com.badlogic.gdx.graphics.glutils.ShapeRenderer; + +import ch.asynk.creepingarmor.engine.gfx.Drawable; +import ch.asynk.creepingarmor.engine.gfx.Animation; + +public class SpriteAnimation implements Disposable, Animation +{ + private static Random random = new Random(); + private Sprites sprites; + private float duration; + private float frameDuration; + private float elapsed; + private float x0; + private float y0; + private float x1; + private float y1; + private int randFreq; + + public SpriteAnimation(Texture texture, int cols, int rows, int randFreq) + { + this.sprites = new Sprites(texture, cols, rows); + this.randFreq = randFreq; + } + + @Override + public void dispose() + { + sprites.dispose(); + } + + public void init(float duration, float x, float y) + { + this.duration = duration; + this.frameDuration = (duration / (float) sprites.frames.length); + this.x0 = x - (sprites.width / 2f); + this.y0 = y - (sprites.height / 2f); + this.elapsed = 0f; + randPos(); + } + + private void randPos() + { + this.x1 = this.x0 + (random.nextInt(sprites.width) - (sprites.width / 2)); + this.y1 = this.y0 + (random.nextInt(sprites.height) - (sprites.height / 2)); + } + + @Override + public boolean animate(float delta) + { + elapsed += delta; + return (elapsed >= duration); + } + + @Override + public void draw(Batch batch) + { + int n = (((int)(elapsed / frameDuration)) % sprites.frames.length); + if ((n > 0) && (n % randFreq) == 0) + randPos(); + batch.draw(sprites.frames[n], x1, y1); + } + + @Override + public void drawDebug(ShapeRenderer debugShapes) + { + } +} diff --git a/core/src/ch/asynk/creepingarmor/engine/gfx/animations/Sprites.java b/core/src/ch/asynk/creepingarmor/engine/gfx/animations/Sprites.java new file mode 100644 index 0000000..407b8a5 --- /dev/null +++ b/core/src/ch/asynk/creepingarmor/engine/gfx/animations/Sprites.java @@ -0,0 +1,38 @@ +package ch.asynk.creepingarmor.engine.gfx.animations; + +import com.badlogic.gdx.utils.Disposable; +import com.badlogic.gdx.graphics.Texture; +import com.badlogic.gdx.graphics.g2d.TextureRegion; + +public class Sprites implements Disposable +{ + public Texture texture; + public TextureRegion[] frames; + public final int width; + public final int height; + public final int cols; + public final int rows; + + public Sprites(Texture texture, int cols, int rows) + { + this.cols = cols; + this.rows = rows; + this.width = (texture.getWidth() / cols); + this.height = (texture.getHeight() / rows); + this.texture = texture; + TextureRegion[][] tmp = TextureRegion.split(texture, width, height); + frames = new TextureRegion[cols * rows]; + int idx = 0; + for (int i = 0; i < rows; i++) { + for (int j = 0; j < cols; j++) { + frames[idx++] = tmp[i][j]; + } + } + } + + @Override + public void dispose() + { + texture.dispose(); + } +} diff --git a/core/src/ch/asynk/creepingarmor/engine/gfx/animations/TankFireAnimation.java b/core/src/ch/asynk/creepingarmor/engine/gfx/animations/TankFireAnimation.java new file mode 100644 index 0000000..55ee435 --- /dev/null +++ b/core/src/ch/asynk/creepingarmor/engine/gfx/animations/TankFireAnimation.java @@ -0,0 +1,196 @@ +package ch.asynk.creepingarmor.engine.gfx.animations; + +import java.util.Random; + +import com.badlogic.gdx.utils.Disposable; +import com.badlogic.gdx.utils.Pool; + +import com.badlogic.gdx.graphics.g2d.TextureRegion; +import com.badlogic.gdx.graphics.g2d.Batch; +import com.badlogic.gdx.graphics.glutils.ShapeRenderer; + +import ch.asynk.creepingarmor.engine.gfx.Drawable; +import ch.asynk.creepingarmor.engine.gfx.Animation; + +public class TankFireAnimation implements Disposable, Animation, Pool.Poolable +{ + private static final float SHOT_SCATTERING = 60f; + private static final float TIME_SCATTERING = 0.6f; + private static final float START_DELAY = 0.8f; + private static final float SHOT_SPEED = 900f; + private static final float EXPLOSION_FRAME_DURATION = 0.07f; + + private TextureRegion fireRegion; + private float fire_a; + private float fire_x; + private float fire_y; + private float fire_w; + private float fire_dx; + private float fire_dy; + private float fire_dw; + + private float smoke_df; + private int smoke_frame; + + private float explosion_x; + private float explosion_y; + private float explosion_df; + private int explosion_frame; + + private boolean fired; + private boolean hit; + private float elapsed; + private float fire_time; + private float hit_time; + private float end_time; + + private float volume; + + private static final Pool<TankFireAnimation> fireAnimationPool = new Pool<TankFireAnimation>() { + @Override + protected TankFireAnimation newObject() { + return new TankFireAnimation(); + } + }; + + public static TankFireAnimation get(float volume, float x0, float y0, float x1, float y1, float halfWidth) + { + TankFireAnimation a = fireAnimationPool.obtain(); + a.set(volume, x0, y0, x1, y1, halfWidth); + return a; + } + + public TankFireAnimation() + { + this.fireRegion = new TextureRegion(FireAnimation.tankFire.frames[0]); + } + + private void set(float volume, float x0, float y0, float x1, float y1, float halfWidth) + { + this.fired = false; + this.hit = false; + this.volume = volume; + + // fire geometry + y0 -= (FireAnimation.tankFire.height / 2.0f); + x1 += ((SHOT_SCATTERING * FireAnimation.random.nextFloat()) - (SHOT_SCATTERING / 2f)); + y1 += ((SHOT_SCATTERING * FireAnimation.random.nextFloat()) - (SHOT_SCATTERING / 2f)); + + double r = Math.atan2((y0 - y1), (x0 - x1)); + float xadj = (float) (Math.cos(r) * halfWidth); + float yadj = (float) (Math.sin(r) * halfWidth); + x0 -= xadj; + y0 -= yadj; + + float a = (float) Math.toDegrees(r); + float dx = (x1 - x0); + float dy = (y1 - y0); + float w = (float) Math.sqrt((dx * dx) + (dy * dy)); + + // timing + float delay = START_DELAY + (FireAnimation.random.nextFloat() * TIME_SCATTERING); + float fire_duration = ((FireAnimation.random.nextFloat() * TIME_SCATTERING) + (w / SHOT_SPEED)); + float explosion_duration = (FireAnimation.explosion.cols * EXPLOSION_FRAME_DURATION); + + this.elapsed = 0f; + this.fire_time = delay; + this.hit_time = (fire_time + fire_duration); + this.end_time = (hit_time + explosion_duration); + + // fire vars + this.fire_a = a; + this.fire_x = x0; + this.fire_y = y0; + this.fire_w = 0; + this.fire_dx = (dx / fire_duration); + this.fire_dy = (dy / fire_duration); + this.fire_dw = (w / fire_duration); + + // smoke var + this.smoke_df = (FireAnimation.tankFire.rows / explosion_duration); + this.smoke_frame = 0; + + // explosion vars + this.explosion_x = (x1 - (FireAnimation.explosion.width / 2.0f)); + this.explosion_y = (y1 - (FireAnimation.explosion.height / 2.0f)); + this.explosion_df = (FireAnimation.explosion.cols / explosion_duration); + this.explosion_frame = (FireAnimation.random.nextInt(FireAnimation.explosion.rows) * FireAnimation.explosion.cols); + } + + @Override + public void reset() + { + } + + @Override + public void dispose() + { + fireAnimationPool.free(this); + } + + @Override + public boolean animate(float delta) + { + elapsed += delta; + + if (!fired && (elapsed < fire_time)) + return false; + + if (!fired) { + fired = true; + FireAnimation.tankFireSndPlay(volume); + } + + if (!hit && (elapsed < hit_time)) { + fire_w += (fire_dw * delta); + fire_x += (fire_dx * delta); + fire_y += (fire_dy * delta); + fireRegion.setRegionWidth((int) fire_w); + return false; + } + + if (!hit) { + hit = true; + FireAnimation.explosionSndPlay(volume); + } + + if (elapsed < end_time) { + int frame = (int) ((elapsed - hit_time) * smoke_df); + if (frame != smoke_frame) { + smoke_frame = frame; + fireRegion.setRegion(FireAnimation.tankFire.frames[smoke_frame]); + fireRegion.setRegionWidth((int) fire_w); + } + return false; + } + + return true; + } + + @Override + public void draw(Batch batch) + { + if (fired) + batch.draw(fireRegion, fire_x, fire_y, 0, 0, fireRegion.getRegionWidth(), fireRegion.getRegionHeight(), 1f, 1f, fire_a); + + if (hit) { + int frame = (explosion_frame + (int) ((elapsed - hit_time) * explosion_df)); + batch.draw(FireAnimation.explosion.frames[frame], explosion_x, explosion_y); + } + } + + @Override + public void drawDebug(ShapeRenderer debugShapes) + { + debugShapes.end(); + debugShapes.begin(ShapeRenderer.ShapeType.Line); + debugShapes.identity(); + debugShapes.translate(fire_x, fire_y, 0); + debugShapes.rotate(0, 0, 1, fire_a); + debugShapes.translate(-fire_x, -fire_y, 0); + debugShapes.rect(fire_x, fire_y, fire_w, FireAnimation.tankFire.height); + debugShapes.end(); + debugShapes.begin(ShapeRenderer.ShapeType.Line); + debugShapes.identity(); + } +} diff --git a/core/src/ch/asynk/creepingarmor/engine/gfx/animations/TimedAnimation.java b/core/src/ch/asynk/creepingarmor/engine/gfx/animations/TimedAnimation.java new file mode 100644 index 0000000..376b03e --- /dev/null +++ b/core/src/ch/asynk/creepingarmor/engine/gfx/animations/TimedAnimation.java @@ -0,0 +1,48 @@ +package ch.asynk.creepingarmor.engine.gfx.animations; + +import com.badlogic.gdx.utils.Pool; + +import ch.asynk.creepingarmor.engine.gfx.Animation; + +public abstract class TimedAnimation implements Animation, Pool.Poolable +{ + private float time; + private boolean began; + private boolean completed; + protected float duration; + + abstract protected void begin(); + abstract protected void end(); + abstract protected void update(float percent); + + @Override + public void reset() + { + time = 0f; + began = false; + completed = false; + } + + @Override + public boolean animate(float delta) + { + if (completed) return true; + + if (!began) { + begin(); + began = true; + } + + time += delta; + completed = (time >= duration); + + if (!completed) { + update(time / duration); + return false; + } + + update(1); + end(); + return true; + } +} |