package squidpony.gdx.examples; import com.badlogic.gdx.ApplicationAdapter; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.graphics.Color; import com.badlogic.gdx.graphics.GL20; import com.badlogic.gdx.graphics.g2d.SpriteBatch; import com.badlogic.gdx.scenes.scene2d.Stage; import com.badlogic.gdx.utils.Array; import com.badlogic.gdx.utils.IntArray; import com.badlogic.gdx.utils.viewport.ScreenViewport; import squidpony.squidai.*; import squidpony.squidgrid.FOVCache; import squidpony.squidgrid.LOS; import squidpony.squidgrid.Radius; import squidpony.squidgrid.gui.gdx.*; import squidpony.squidgrid.mapping.DungeonGenerator; import squidpony.squidgrid.mapping.DungeonUtility; import squidpony.squidgrid.mapping.styled.TilesetType; import squidpony.squidmath.*; import java.util.ArrayList; import java.util.Map; import java.util.Set; public class SquidAIDemo extends ApplicationAdapter { private enum Phase {MOVE_ANIM, ATTACK_ANIM} SpriteBatch batch; private Phase phase = Phase.ATTACK_ANIM; private RNG rng; private LightRNG lrng; private SquidLayers display; private DungeonGenerator dungeonGen; private char[][] bareDungeon, lineDungeon; private double[][] res; private int[][] colors, bgColors, lights; private LOS los; private int width, height; private int cellWidth, cellHeight; private int numMonsters = 16; private SquidInput input; private static final Color bgColor = SColor.DARK_SLATE_GRAY; private Array<AnimatedEntity> teamRed, teamBlue; private IntArray redHealth, blueHealth; //private OrderedMap<AnimatedEntity, Integer> teamRed, teamBlue; private OrderedSet<Coord> redPlaces, bluePlaces; private Technique redCone, redCloud, blueBlast, blueBeam; private DijkstraMap getToRed, getToBlue; private Stage stage; private int framesWithoutAnimation = 0, moveLength = 5; private ArrayList<Coord> awaitedMoves; private int redIdx = 0, blueIdx = 0; private boolean blueTurn = false; private FOVCache cache; @Override public void create () { batch = new SpriteBatch(); width = 40; height = 40; cellWidth = 6; cellHeight = 12; display = new SquidLayers(width * 2, height, cellWidth, cellHeight, DefaultResources.narrowName); display.setAnimationDuration(0.35f); stage = new Stage(new ScreenViewport(), batch); lrng = new LightRNG(0x1337BEEF); rng = new RNG(lrng); dungeonGen = new DungeonGenerator(width, height, rng); // dungeonGen.addWater(10); //dungeonGen.addDoors(15, true); // change the TilesetType to lots of different choices to see what dungeon works best. bareDungeon = dungeonGen.generate(TilesetType.ROUND_ROOMS_DIAGONAL_CORRIDORS); cache = new FOVCache(bareDungeon, 9, Radius.CIRCLE); bareDungeon = DungeonUtility.closeDoors(bareDungeon); lineDungeon = DungeonUtility.doubleWidth(DungeonUtility.hashesToLines(bareDungeon)); // it's more efficient to get random floors from a packed set containing only (compressed) floor positions. short[] placement = CoordPacker.pack(bareDungeon, '.'); teamRed = new Array<>(numMonsters); teamBlue = new Array<>(numMonsters); redHealth = new IntArray(numMonsters); blueHealth = new IntArray(numMonsters); redPlaces = new OrderedSet<>(numMonsters); bluePlaces = new OrderedSet<>(numMonsters); for(int i = 0; i < numMonsters; i++) { Coord monPos = dungeonGen.utility.randomCell(placement); placement = CoordPacker.removePacked(placement, monPos.x, monPos.y); teamRed.add(display.animateActor(monPos.x, monPos.y, "50", 11, true)); redHealth.add(50); redPlaces.add(monPos); Coord monPosBlue = dungeonGen.utility.randomCell(placement); placement = CoordPacker.removePacked(placement, monPosBlue.x, monPosBlue.y); teamBlue.add(display.animateActor(monPosBlue.x, monPosBlue.y, "50", 25, true)); blueHealth.add(50); bluePlaces.add(monPosBlue); } // your choice of FOV matters here. los = new LOS(LOS.BRESENHAM); res = DungeonUtility.generateResistances(bareDungeon); ConeAOE cone = new ConeAOE(Coord.get(0, 0), 9, 0, 60, Radius.CIRCLE); cone.setMinRange(1); cone.setMaxRange(2); cone.setMetric(Radius.SQUARE); redCone = new Technique("Burning Breath", cone); redCone.setMap(bareDungeon); BlastAOE blast = new BlastAOE(Coord.get(0, 0), 3, Radius.CIRCLE); blast.setMinRange(3); blast.setMaxRange(5); blast.setMetric(Radius.CIRCLE); blueBlast = new Technique("Winter Orb", blast); blueBlast.setMap(bareDungeon); CloudAOE cloud = new CloudAOE(Coord.get(0, 0), 20, Radius.DIAMOND); cloud.setMinRange(4); cloud.setMaxRange(7); cloud.setMetric(Radius.CIRCLE); redCloud = new Technique("Acid Mist", cloud); redCloud.setMap(bareDungeon); BeamAOE beam = new BeamAOE(Coord.get(0, 0), 0.0, 8, 1, Radius.DIAMOND); beam.setMinRange(2); beam.setMaxRange(8); beam.setMetric(Radius.CIRCLE); blueBeam = new Technique("Atomic Death Ray", beam); blueBeam.setMap(bareDungeon); getToRed = new DijkstraMap(bareDungeon, DijkstraMap.Measurement.EUCLIDEAN); getToRed.rng = rng; getToBlue = new DijkstraMap(bareDungeon, DijkstraMap.Measurement.EUCLIDEAN); getToBlue.rng = rng; dijkstraAlert(); awaitedMoves = new ArrayList<>(10); colors = DungeonUtility.generatePaletteIndices(bareDungeon); bgColors = DungeonUtility.generateBGPaletteIndices(bareDungeon); lights = DungeonUtility.generateLightnessModifiers(bareDungeon); // just quit if we get a Q. input = new SquidInput(new SquidInput.KeyHandler() { @Override public void handle(char key, boolean alt, boolean ctrl, boolean shift) { switch (key) { case 'Q': case 'q': case SquidInput.ESCAPE: { Gdx.app.exit(); } } } }); // ABSOLUTELY NEEDED TO HANDLE INPUT // and then add display, our one visual component, to the list of things that act in Stage. display.setPosition(0, 0); stage.addActor(display); cache.awaitCache(); blast.setCache(cache); cone.setCache(cache); Gdx.input.setInputProcessor(input); } private void dijkstraAlert() { /* getToBlue.clearGoals(); getToBlue.resetMap(); getToRed.clearGoals(); getToRed.resetMap(); ArrayList<AnimatedEntity> redCopy = new ArrayList<AnimatedEntity>(teamRed.size()); redCopy.addAll(teamRed.keySet()); BLUE_LOOP: for(AnimatedEntity blue : teamBlue.keySet()) { for(AnimatedEntity red : redCopy) { if(los.isReachable(res, blue.gridX, blue.gridY, red.gridX, red.gridY, Radius.CIRCLE)) { getToBlue.setGoal(blue.gridX, blue.gridY); getToRed.setGoal(red.gridX, red.gridY); redCopy.remove(red); continue BLUE_LOOP; } } } getToBlue.scan(redPlaces); getToRed.scan(bluePlaces); */ } /** * Move a unit toward a good position to attack, but don't attack in this method. * @param idx the index of the unit in the appropriate ordered Map. */ private void startMove(int idx) { // if(health <= 0) return; int i = 0; DijkstraMap whichDijkstra; Technique whichTech; Set<Coord> whichFoes, whichAllies = new OrderedSet<>(8); AnimatedEntity ae = null; int health = 0; Coord user = null; if(blueTurn) { whichDijkstra = getToRed; whichTech = (idx % 2 == 0) ? blueBeam : blueBlast; whichFoes = redPlaces; whichAllies = bluePlaces; ae = teamBlue.get(idx); health = blueHealth.get(idx); if(ae == null || health <= 0) { phase = Phase.MOVE_ANIM; return; } user = Coord.get(ae.gridX, ae.gridY); } else { whichDijkstra = getToBlue; whichTech = (idx % 2 == 0) ? redCloud : redCone; whichFoes = bluePlaces; whichAllies = redPlaces; ae = teamRed.get(idx); health = redHealth.get(idx); if(ae == null || health <= 0) { phase = Phase.MOVE_ANIM; return; } user = Coord.get(ae.gridX, ae.gridY); } whichAllies.remove(user); /*for(Coord p : whichFoes) { AnimatedEntity foe = display.getAnimatedEntityByCell(p.x, p.y); if(los.isReachable(res, user.x, user.y, p.x, p.y) && foe != null && whichEnemyTeam.get(foe) != null && whichEnemyTeam.get(foe) > 0) { visibleTargets.add(p); } }*/ ArrayList<Coord> path = whichDijkstra.findTechniquePath(moveLength, whichTech, bareDungeon, (LOS)null, whichFoes, whichAllies, user, whichFoes); if(path.isEmpty()) path = whichDijkstra.findPath(moveLength, whichFoes, whichAllies, user, whichFoes.toArray(new Coord[whichFoes.size()])); /* System.out.println("User at (" + user.x + "," + user.y + ") using " + whichTech.name); */ /* boolean anyFound = false; for (int yy = 0; yy < height; yy++) { for (int xx = 0; xx < width; xx++) { System.out.print((whichDijkstra.targetMap[xx][yy] == null) ? "." : "@"); anyFound = (whichDijkstra.targetMap[xx][yy] != null) ? true : anyFound; } System.out.println(); }*/ awaitedMoves = new ArrayList<>(path); } public void move(AnimatedEntity ae, int newX, int newY) { display.slide(ae, newX, newY, 2, 0.075f); phase = Phase.MOVE_ANIM; } // check if a monster's movement would overlap with another monster. @SuppressWarnings("unused") private boolean checkOverlap(AnimatedEntity ae, int x, int y) { for(AnimatedEntity mon : teamRed) { if(mon.gridX == x && mon.gridY == y && !mon.equals(ae)) return true; } for(AnimatedEntity mon : teamBlue) { if(mon.gridX == x && mon.gridY == y && !mon.equals(ae)) return true; } return false; } private void postMove(int idx) { int i = 0; Technique whichTech; Set<Coord> whichFoes, whichAllies, visibleTargets = new OrderedSet<>(8); AnimatedEntity ae = null; int health = 0; Coord user = null; Color whichTint = Color.WHITE; Array<AnimatedEntity> whichEnemyTeam; IntArray whichEnemyHealth; OrderedMap<Coord, Double> effects; if (blueTurn) { whichTech = (idx % 2 == 0) ? blueBeam : blueBlast; whichFoes = redPlaces; whichAllies = bluePlaces; whichTint = Color.CYAN; whichEnemyTeam = teamRed; whichEnemyHealth = redHealth; ae = teamBlue.get(idx); health = blueHealth.get(idx); if (ae == null || health <= 0) { phase = Phase.ATTACK_ANIM; return; } user = Coord.get(ae.gridX, ae.gridY); } else { whichTech = (idx % 2 == 0) ? redCloud : redCone; whichFoes = bluePlaces; whichAllies = redPlaces; whichTint = Color.RED; whichEnemyTeam = teamBlue; whichEnemyHealth = blueHealth; ae = teamRed.get(idx); health = redHealth.get(idx); if (ae == null || health <= 0) { phase = Phase.ATTACK_ANIM; return; } user = Coord.get(ae.gridX, ae.gridY); } for(Coord p : whichFoes) { AnimatedEntity foe = display.getAnimatedEntityByCell(p.x, p.y); int foeIdx; if(los.isReachable(res, user.x, user.y, p.x, p.y) && foe != null && (foeIdx = whichEnemyTeam.indexOf(foe, true)) >= 0 && whichEnemyHealth.get(foeIdx) > 0) { visibleTargets.add(p); } } OrderedMap<Coord, ArrayList<Coord>> ideal = whichTech.idealLocations(user, visibleTargets, whichAllies); Coord targetCell = null; if(!ideal.isEmpty()) targetCell = ideal.firstKey(); if(targetCell != null) { effects = whichTech.apply(user, targetCell); for(Map.Entry<Coord, Double> power : effects.entrySet()) { Double strength = (idx % 2 == 0) ? rng.nextDouble() : power.getValue(); whichTint.a = strength.floatValue(); display.tint(power.getKey().x * 2 , power.getKey().y, whichTint, 0, display.getAnimationDuration()); display.tint(power.getKey().x * 2 + 1, power.getKey().y, whichTint, 0, display.getAnimationDuration()); AnimatedEntity tgt; for(int tgtIdx = 0; tgtIdx < whichEnemyTeam.size; tgtIdx++) { tgt = whichEnemyTeam.get(tgtIdx); if(tgt.gridX == power.getKey().x && tgt.gridY == power.getKey().y) { int currentHealth = Math.max(whichEnemyHealth.get(tgtIdx) - (int) (15 * strength), 0); whichEnemyTeam.set(tgtIdx, tgt); whichEnemyHealth.set(tgtIdx, currentHealth); tgt.setText(Integer.toString(currentHealth)); } } } } /* else { System.out.println("NO ATTACK POSITION: User at (" + user.x + "," + user.y + ") using " + whichTech.name); display.tint(user.x * 2 , user.y, highlightColor, 0, display.getAnimationDuration() * 3); display.tint(user.x * 2 + 1, user.y, highlightColor, 0, display.getAnimationDuration() * 3); } */ whichAllies.add(user); phase = Phase.ATTACK_ANIM; } public void putMap() { for (int i = 0; i < width; i++) { for (int j = 0; j < height; j++) { display.put(i * 2, j, lineDungeon[i * 2][j], colors[i][j], bgColors[i][j], lights[i][j]); display.put(i * 2 + 1, j, lineDungeon[i * 2 + 1][j], colors[i][j], bgColors[i][j], lights[i][j]); } } } @Override public void render () { // standard clear the background routine for libGDX Gdx.gl.glClearColor(bgColor.r / 255.0f, bgColor.g / 255.0f, bgColor.b / 255.0f, 1.0f); Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT); // not sure if this is always needed... Gdx.gl.glEnable(GL20.GL_BLEND); stage.act(); boolean blueWins = false, redWins = false; for(int bh = 0; bh < blueHealth.size; bh++) { if(blueHealth.get(bh) > 0) { redWins = false; break; }redWins = true; } for(int rh = 0; rh < redHealth.size; rh++) { if(redHealth.get(rh) > 0) { blueWins = false; break; }blueWins = true; } if (blueWins) { // still need to display the map, then write over it with a message. putMap(); display.putBoxedString(width / 2 - 11, height / 2 - 1, " BLUE TEAM WINS! "); display.putBoxedString(width / 2 - 11, height / 2 + 5, " q to quit. "); // because we return early, we still need to draw. stage.draw(); // q still needs to quit. if(input.hasNext()) input.next(); return; } else if(redWins) { putMap(); display.putBoxedString(width / 2 - 11, height / 2 - 1, " RED TEAM WINS! "); display.putBoxedString(width / 2 - 11, height / 2 + 5, " q to quit. "); // because we return early, we still need to draw. stage.draw(); // q still needs to quit. if(input.hasNext()) input.next(); return; } int i = 0; AnimatedEntity ae = null; int whichIdx = 0; if(blueTurn) { whichIdx = blueIdx; ae = teamBlue.get(blueIdx); } else { whichIdx = redIdx; ae = teamRed.get(redIdx); } // need to display the map every frame, since we clear the screen to avoid artifacts. putMap(); // if we are waiting for the player's input and get input, process it. if(input.hasNext()) { input.next(); } // if the user clicked, we have a list of moves to perform. if(!awaitedMoves.isEmpty()) { if(ae == null) { awaitedMoves.clear(); } // extremely similar to the block below that also checks if animations are done // this doesn't check for input, but instead processes and removes Points from awaitedMoves. else if(!display.hasActiveAnimations()) { ++framesWithoutAnimation; if (framesWithoutAnimation >= 2) { framesWithoutAnimation = 0; Coord m = awaitedMoves.remove(0); move(ae, m.x, m.y); } } } // if the previous blocks didn't happen, and there are no active animations, then either change the phase // (because with no animations running the last phase must have ended), or start a new animation soon. else if(!display.hasActiveAnimations()) { ++framesWithoutAnimation; if (framesWithoutAnimation >= 2) { framesWithoutAnimation = 0; switch (phase) { case ATTACK_ANIM: { phase = Phase.MOVE_ANIM; blueTurn = !blueTurn; if(!blueTurn) { whichIdx = (whichIdx + 1) % numMonsters; redIdx = (redIdx + 1) % numMonsters; blueIdx = (blueIdx + 1) % numMonsters; } dijkstraAlert(); startMove(whichIdx); } break; case MOVE_ANIM: { postMove(whichIdx); } } } } // if we do have an animation running, then how many frames have passed with no animation needs resetting else { framesWithoutAnimation = 0; } // stage has its own batch and must be explicitly told to draw(). this also causes it to act(). stage.draw(); OrderedSet<AnimatedEntity> entities = display.getAnimatedEntities(0); // display does not draw all AnimatedEntities by default. batch.begin(); for (int j = 0; j < entities.size(); j++) { display.drawActor(batch, 1.0f, entities.getAt(j), 0); } entities = display.getAnimatedEntities(2); for (int j = 0; j < entities.size(); j++) { display.drawActor(batch, 1.0f, entities.getAt(j), 2); } /* for(AnimatedEntity mon : teamBlue.keySet()) { display.drawActor(batch, 1.0f, mon); }*/ // batch must end if it began. batch.end(); } }