package squidpony.gdx.tests; import com.badlogic.gdx.*; import com.badlogic.gdx.backends.lwjgl.LwjglApplication; import com.badlogic.gdx.backends.lwjgl.LwjglApplicationConfiguration; 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.Actor; import com.badlogic.gdx.scenes.scene2d.Stage; import com.badlogic.gdx.utils.viewport.StretchViewport; import com.badlogic.gdx.utils.viewport.Viewport; import squidpony.FakeLanguageGen; import squidpony.panel.IColoredString; import squidpony.squidai.CustomDijkstraMap; import squidpony.squidai.DijkstraMap; import squidpony.squidgrid.Adjacency; import squidpony.squidgrid.FOV; import squidpony.squidgrid.Radius; import squidpony.squidgrid.gui.gdx.*; import squidpony.squidgrid.mapping.DungeonUtility; import squidpony.squidgrid.mapping.MixedGenerator; import squidpony.squidgrid.mapping.SectionDungeonGenerator; import squidpony.squidgrid.mapping.SerpentMapGenerator; import squidpony.squidmath.*; import java.util.ArrayList; import java.util.List; public class RotationDemo extends ApplicationAdapter { private enum Phase {WAIT, PLAYER_ANIM, MONSTER_ANIM} private static class Creature { public AnimatedEntity entity, marker; public int state, pos; public Creature(AnimatedEntity ae, AnimatedEntity marker, int pos, int state) { entity = ae; this.marker = marker; this.state = state; move(pos); } public Creature change(int state) { this.state = state; return this; } public Creature change(AnimatedEntity ae) { entity = ae; return this; } public Creature move(int pos) { marker.gridX = entity.gridX = adjacency.extractX(pos); marker.gridY = entity.gridY = adjacency.extractY(pos); this.pos = pos; marker.setDirection(adjacency.directions[adjacency.extractR(pos)]); return this; } } SpriteBatch batch; private Phase phase = Phase.WAIT; private StatefulRNG rng; private SquidLayers display; private SquidMessageBox messages; /** * Non-{@code null} iff '?' was pressed before */ private /*Nullable*/ Actor help; private SectionDungeonGenerator dungeonGen; private char[][] decoDungeon, bareDungeon, lineDungeon; private double[][] res; private int[][] lights; private Color[][] colors, bgColors; private double[][] fovmap; private Creature player; private FOV fov; public static final int INTERNAL_ZOOM = 1; /** * In number of cells */ private static final int width = 80; /** * In number of cells */ private static final int height = 25; /** * The pixel width of a cell */ private static final int cellWidth = 18 * INTERNAL_ZOOM; /** * The pixel height of a cell */ private static final int cellHeight = 28 * INTERNAL_ZOOM; private VisualInput input; private double counter; private boolean[][] seen; private int health = 9; private SquidColorCenter fgCenter, bgCenter; private Color bgColor; private ArrayList<Color> playerMarkColors, monsterMarkColors; private OrderedMap<Integer, Creature> monsters; private CustomDijkstraMap getToPlayer, playerToCursor; private Stage stage; private int framesWithoutAnimation = 0; private IntVLA toCursor, awaitedMoves; private String lang; private TextCellFactory textFactory; private Viewport viewport; private float currentZoomX = INTERNAL_ZOOM, currentZoomY = INTERNAL_ZOOM; public static final Adjacency adjacency = new Adjacency.RotationAdjacency(width, height, DijkstraMap.Measurement.EUCLIDEAN); @Override public void create() { // gotta have a random number generator. We seed a LightRNG with any long we want, then pass that to an RNG. rng = new StatefulRNG(0xBADBEEFB0BBL); fgCenter = DefaultResources.getSCC(); bgCenter = DefaultResources.getSCC(); playerMarkColors = fgCenter.rainbow(64); monsterMarkColors = fgCenter.rainbow(0.75f, 0.65f, 64); //playerMarkColors = fgCenter.loopingGradient(SColor.HAN_PURPLE, SColor.PSYCHEDELIC_PURPLE, 64); //monsterMarkColors = fgCenter.loopingGradient(SColor.CRIMSON, SColor.ORANGE_RED, 64); batch = new SpriteBatch(); // getStretchableFont loads an embedded font, Inconsolata-LGC-Custom, that is a distance field font as mentioned // earlier. We set the smoothing multiplier on it only because we are using internal zoom to increase sharpness // on small details, but if the smoothing is incorrect some sizes look blurry or over-sharpened. This can be set // manually if you use a constant internal zoom; here we use 1f for internal zoom 1, about 2/3f for zoom 2, and // about 1/2f for zoom 3. If you have more zooms as options for some reason, this formula should hold for many // cases but probably not all. //textFactory = DefaultResources.getStretchableDejaVuFont().setSmoothingMultiplier(2f / (INTERNAL_ZOOM + 1f)) // .width(cellWidth).height(cellHeight).initBySize(); textFactory = DefaultResources.getStretchableCodeFont().setSmoothingMultiplier(2f / (INTERNAL_ZOOM + 1f)) .width(cellWidth).height(cellHeight).initBySize(); //.setDirectionGlyph('ˆ') //textFactory = DefaultResources.getStretchableSciFiFont().setSmoothingMultiplier(2f / (INTERNAL_ZOOM + 1f)) // .width(cellWidth).height(cellHeight).initBySize(); // Creates a layered series of text grids in a SquidLayers object, using the previously set-up textFactory and // SquidColorCenters. display = new SquidLayers(width, height, cellWidth, cellHeight, textFactory.copy(), bgCenter, fgCenter).addExtraLayer(); //display.getBackgroundLayer().setOnlyRenderEven(true); display.setAnimationDuration(0.1f); messages = new SquidMessageBox(width, 4, textFactory.copy()); // a bit of a hack to increase the text height slightly without changing the size of the cells they're in. // this causes a tiny bit of overlap between cells, which gets rid of an annoying gap between vertical lines. // if you use '#' for walls instead of box drawing chars, you don't need this. messages.setTextSize(cellWidth + INTERNAL_ZOOM * 3, cellHeight + INTERNAL_ZOOM * 4); display.setTextSize(cellWidth + INTERNAL_ZOOM * 3, cellHeight + INTERNAL_ZOOM * 4); //The subCell SquidPanel uses a smaller size here; the numbers 8 and 16 should change if cellWidth or cellHeight //change, and the INTERNAL_ZOOM multiplier keeps things sharp, the same as it does all over here. viewport = new StretchViewport(width * cellWidth, (height + 4) * cellHeight); stage = new Stage(viewport, batch); //These need to have their positions set before adding any entities if there is an offset involved. messages.setBounds(0, 0, cellWidth * width, cellHeight * 4); display.setPosition(0, messages.getHeight()); messages.appendWrappingMessage("You are the purple '^', and enemies are red. Click a cell to turn and move. " + "Use ? for help, f to change colors, q to quit. " + "Click the top or bottom border of this box to scroll."); counter = 0; dungeonGen = new SectionDungeonGenerator(width, height, rng); dungeonGen.addWater(0, 25, 6); dungeonGen.addGrass(MixedGenerator.CAVE_FLOOR, 20); dungeonGen.addBoulders(0, 7); dungeonGen.addDoors(18, false); SerpentMapGenerator serpent = new SerpentMapGenerator(width, height, rng); serpent.putCaveCarvers(1); serpent.putWalledBoxRoomCarvers(2); serpent.putWalledRoundRoomCarvers(2); char[][] mg = serpent.generate(); decoDungeon = dungeonGen.generate(mg, serpent.getEnvironment()); //decoDungeon = dungeonGen.generate(); bareDungeon = dungeonGen.getBareDungeon(); lineDungeon = DungeonUtility.hashesToLines(dungeonGen.getDungeon(), true); /* decoDungeon = new char[][]{ {'#','#','#','#',}, {'#','.','.','#',}, {'#','.','.','#',}, {'#','#','#','#',}, }; // change the TilesetType to lots of different choices to see what dungeon works best. bareDungeon = new char[][]{ {'#','#','#','#',}, {'#','.','.','#',}, {'#','.','.','#',}, {'#','#','#','#',}, }; lineDungeon = new char[][]{ {'#','#','#','#',}, {'#','.','.','#',}, {'#','.','.','#',}, {'#','#','#','#',}, }; lineDungeon = DungeonUtility.hashesToLines(lineDungeon); */ adjacency.addCostRule('"', 1.0); adjacency.addCostRule('"', 0.001, true); adjacency.addCostRule('~', 1.0); adjacency.addCostRule('/', 1.0); adjacency.addCostRule('~', 0.001, true); adjacency.addCostRule(',', 0.001, true); adjacency.addCostRule('.', 0.001, true); adjacency.addCostRule('/', 0.001, true); // it's more efficient to get random floors from a packed set containing only (compressed) floor positions. final GreasedRegion placement = new GreasedRegion(bareDungeon, '.'); final Coord pl = placement.singleRandom(rng); placement.remove(pl); int numMonsters = 25; monsters = new OrderedMap<>(numMonsters); int p; for (int i = 0; i < numMonsters; i++) { Coord monPos = placement.singleRandom(rng); placement.remove(monPos); p = adjacency.composite(monPos.x, monPos.y, rng.nextIntHasty(4), 0); monsters.put(p >> 3, new Creature(display.animateActor(monPos.x, monPos.y, ((i & 1) == 0) ? 'g' : 'K', //'Я', SColor.SCARLET), display.directionMarker(monPos.x, monPos.y, monsterMarkColors, 1.5f, 3, false), p, 0)); } // your choice of FOV matters here. fov = new FOV(FOV.RIPPLE_TIGHT); res = DungeonUtility.generateResistances(decoDungeon); fovmap = fov.calculateFOV(res, pl.x, pl.y, 8, Radius.SQUARE); getToPlayer = new CustomDijkstraMap(decoDungeon, adjacency, rng); getToPlayer.setGoal(adjacency.composite(pl.x, pl.y, rng.nextIntHasty(4), 0)); player = new Creature(display.animateActor(pl.x, pl.y, '@', SColor.CAPE_JASMINE, false), display.directionMarker(pl.x, pl.y, playerMarkColors, 2f, 3, false), adjacency.composite(pl.x, pl.y, rng.nextIntHasty(4), 0), health); /* player = new Creature(display.animateActor(1, 1, ' ', SColor.HAN_PURPLE, false), display.directionMarker(pl.x, pl.y, SColor.HAN_PURPLE, 3, false), adjacency.composite(1, 1, 4, 0), health); */ // fgCenter.filter(display.getPalette().get(30))); toCursor = new IntVLA(10); awaitedMoves = new IntVLA(10); playerToCursor = new CustomDijkstraMap(decoDungeon, adjacency, rng); final int[][] initialColors = DungeonUtility.generatePaletteIndices(decoDungeon), initialBGColors = DungeonUtility.generateBGPaletteIndices(decoDungeon); colors = new Color[width][height]; bgColors = new Color[width][height]; ArrayList<Color> palette = display.getPalette(); bgColor = SColor.DARK_SLATE_GRAY; for (int i = 0; i < width; i++) { for (int j = 0; j < height; j++) { colors[i][j] = palette.get(initialColors[i][j]); bgColors[i][j] = palette.get(initialBGColors[i][j]); } } lights = DungeonUtility.generateLightnessModifiers(decoDungeon, counter); seen = new boolean[width][height]; /* lang = FakeLanguageGen.RUSSIAN_AUTHENTIC.sentence(rng, 4, 6, new String[]{",", ",", ",", " -"}, new String[]{"..."}, 0.25); */ lang = FakeLanguageGen.RUSSIAN_ROMANIZED.sentence(rng, 4, 6, new String[]{",", ",", ",", " -"}, new String[]{"..."}, 0.25); // this is a big one. // SquidInput can be constructed with a KeyHandler (which just processes specific keypresses), a SquidMouse // (which is given an InputProcessor implementation and can handle multiple kinds of mouse move), or both. // keyHandler is meant to be able to handle complex, modified key input, typically for games that distinguish // between, say, 'q' and 'Q' for 'quaff' and 'Quip' or whatever obtuse combination you choose. The // implementation here handles hjklyubn keys for 8-way movement, numpad for 8-way movement, arrow keys for // 4-way movement, and wasd for 4-way movement. Shifted letter keys produce capitalized chars when passed to // KeyHandler.handle(), but we don't care about that so we just use two case statements with the same body, // one for the lower case letter and one for the upper case letter. // You can also set up a series of future moves by clicking within FOV range, using mouseMoved to determine the // path to the mouse position with a DijkstraMap (called playerToCursor), and using touchUp to actually trigger // the event when someone clicks. input = new VisualInput(new SquidInput.KeyHandler() { @Override public void handle(char key, boolean alt, boolean ctrl, boolean shift) { switch (key) { /* case SquidInput.UP_ARROW: case 'k': case 'w': case 'K': case 'W': { move(0, -1); break; } case SquidInput.DOWN_ARROW: case 'j': case 's': case 'J': case 'S': { move(0, 1); break; } case SquidInput.LEFT_ARROW: case 'h': case 'a': case 'H': case 'A': { move(-1, 0); break; } case SquidInput.RIGHT_ARROW: case 'l': case 'd': case 'L': case 'D': { move(1, 0); break; } case SquidInput.UP_LEFT_ARROW: case 'y': case 'Y': { move(-1, -1); break; } case SquidInput.UP_RIGHT_ARROW: case 'u': case 'U': { move(1, -1); break; } case SquidInput.DOWN_RIGHT_ARROW: case 'n': case 'N': { move(1, 1); break; } case SquidInput.DOWN_LEFT_ARROW: case 'b': case 'B': { move(-1, 1); break; } */ case '?': { toggleHelp(); break; } case 'Q': case 'q': case SquidInput.ESCAPE: { Gdx.app.exit(); break; } } } }, new SquidMouse(cellWidth, cellHeight, width, height, 0, 0, new InputAdapter() { // if the user clicks within FOV range and there are no awaitedMoves queued up, generate toCursor if it // hasn't been generated already by mouseMoved, then copy it over to awaitedMoves. @Override public boolean touchUp(int screenX, int screenY, int pointer, int button) { if (fovmap[screenX][screenY] > 0.0 && awaitedMoves.size == 0) { if (toCursor.size == 0) { //Uses DijkstraMap to get a path from the player's position to the cursor toCursor = playerToCursor.findPath(30, null, null, player.pos, adjacency.composite(screenX, screenY, 0, 0)); } awaitedMoves.clear(); awaitedMoves.addAll(toCursor); } return false; } @Override public boolean touchDragged(int screenX, int screenY, int pointer) { return mouseMoved(screenX, screenY); } // causes the path to the mouse position to become highlighted (toCursor contains a list of points that // receive highlighting). Uses DijkstraMap.findPath() to find the path, which is surprisingly fast. @Override public boolean mouseMoved(int screenX, int screenY) { if (awaitedMoves.size != 0) return false; if (fovmap[screenX][screenY] > 0.0) { //Uses DijkstraMap to get a path. from the player's position to the cursor toCursor = playerToCursor.findPath(30, null, null, player.pos, adjacency.composite(screenX, screenY, 0, 0)); } return false; } })); //set this to true to test visual input on desktop input.forceButtons = false; //actions to give names to in the visual input menu input.init("filter", "??? help?", "quit"); // ABSOLUTELY NEEDED TO HANDLE INPUT Gdx.input.setInputProcessor(new InputMultiplexer(stage, input)); // and then add display and messages, our two visual components, to the list of things that act in Stage. stage.addActor(display); // stage.addActor(subCell); // this is not added since it is manually drawn after other steps stage.addActor(messages); viewport = input.resizeInnerStage(stage); } /** * Move the player or open closed doors, remove any monsters the player bumped, then update the DijkstraMap and * have the monsters that can see the player try to approach. * In a fully-fledged game, this would not be organized like this, but this is a one-file demo. * * @param pos */ private void move(int pos) { clearHelp(); if (health <= 0) return; int oldX = player.entity.gridX, oldY = player.entity.gridY, newX = adjacency.extractX(pos), newY = adjacency.extractY(pos); if (newX >= 0 && newY >= 0 && newX < width && newY < height && bareDungeon[newX][newY] != '#') { // '+' is a door. if (lineDungeon[newX][newY] == '+') { decoDungeon[newX][newY] = '/'; lineDungeon[newX][newY] = '/'; // changes to the map mean the resistances for FOV need to be regenerated. res = DungeonUtility.generateResistances(decoDungeon); // recalculate FOV, store it in fovmap for the render to use. fovmap = fov.calculateFOV(res, player.entity.gridX, player.entity.gridY, 8, Radius.SQUARE); } else { // recalculate FOV, store it in fovmap for the render to use. fovmap = fov.calculateFOV(res, newX, newY, 8, Radius.SQUARE); //player.marker.setDirection(Direction.CLOCKWISE[adjacency.extractR(pos)]); display.slide(player.entity, newX, newY); display.slide(player.marker, newX, newY); player.move(pos); monsters.remove(pos >>> 3); } phase = Phase.PLAYER_ANIM; } } // check if a monster's movement would overlap with another monster. private boolean checkOverlap(Creature mon, int x, int y, ArrayList<Coord> futureOccupied) { int comp = adjacency.composite(x, y, 0, 0) >> 3; if (monsters.containsKey(comp) && !mon.equals(monsters.get(comp))) return true; for (Coord p : futureOccupied) { if (x == p.x && y == p.y) return true; } return false; } private void postMove() { phase = Phase.MONSTER_ANIM; // The next two lines are important to avoid monsters treating cells the player WAS in as goals. getToPlayer.clearGoals(); getToPlayer.resetMap(); // now that goals are cleared, we can mark the current player position as a goal. // this is an important piece of DijkstraMap usage; the argument is a Set of Points for squares that // temporarily cannot be moved through (not walls, which are automatically known because the map char[][] // was passed to the DijkstraMap constructor, but things like moving creatures and objects). int[] monplaces = new int[monsters.size()]; for (int i = 0; i < monplaces.length; i++) { monplaces[i] = monsters.getAt(i).pos; } //pathMap = getToPlayer.scan(monplaces); // recalculate FOV, store it in fovmap for the render to use. fovmap = fov.calculateFOV(res, player.entity.gridX, player.entity.gridY, 8, Radius.SQUARE); // handle monster turns int ms = monsters.size(), tmp; IntVLA impassable = new IntVLA(ms), path; for (int i = 0; i < ms; i++) { impassable.add(monsters.getAt(i).pos); } int[] playerGoal = new int[]{player.pos}; for (int i = 0; i < ms; i++) { Integer pos = monsters.keyAt(i); if(pos == null) continue; Creature mon = monsters.getAt(i); // monster values are used to store their aggression, 1 for actively stalking the player, 0 for not. if (mon.state > 0 || fovmap[adjacency.extractX(pos << 3)][adjacency.extractY(pos << 3)] > 0.1) { if (mon.state == 0) { /* messages.appendMessage("The AЯMED GUAЯD shouts at you, \"" + FakeLanguageGen.RUSSIAN_AUTHENTIC.sentence(rng, 1, 3, new String[]{",", ",", ",", " -"}, new String[]{"!"}, 0.25) + "\""); */ messages.appendMessage("The ARMED GUARD shouts at you, \"" + FakeLanguageGen.RUSSIAN_ROMANIZED.sentence(rng, 1, 3, new String[]{",", ",", ",", " -"}, new String[]{"!"}, 0.25) + "\""); } path = getToPlayer.findPath(30, impassable, null, mon.pos, playerGoal); if(path.size == 0) { mon.change(1); continue; } tmp = path.first(); if (tmp >>> 3 == player.pos >>> 3) { display.tint(player.entity.gridX, player.entity.gridY, SColor.PURE_CRIMSON, 0, 0.415f); health--; //player.setText("" + health); mon.change(1); } // otherwise store the new position in newMons. else { mon.change(1); monsters.alter(pos, tmp >>> 3); display.slide(mon.entity, adjacency.extractX(tmp), adjacency.extractY(tmp)); display.slide(mon.marker, adjacency.extractX(tmp), adjacency.extractY(tmp)); mon.move(tmp); } // this block is used to ensure that the monster picks the best path, or a random choice if there // is more than one equally good best option. /* Direction choice = null; double best = 9999.0; Direction[] ds = new Direction[8]; rng.shuffle(Direction.OUTWARDS, ds); for (Direction d : ds) { Coord tmp = pos.translate(d); if (pathMap[tmp.x][tmp.y] < best && !checkOverlap(mon, tmp.x, tmp.y, nextMovePositions)) { // pathMap is a 2D array of doubles where 0 is the goal (the player). // we use best to store which option is closest to the goal. best = pathMap[tmp.x][tmp.y]; choice = d; } } */ } } } private void toggleHelp() { if (help != null) { clearHelp(); return; } final int nbMonsters = monsters.size(); /* Prepare the String to display */ final IColoredString<Color> cs = new IColoredString.Impl<>(); cs.append("Still ", null); final Color nbColor; if (nbMonsters <= 1) /* Green */ nbColor = Color.GREEN; else if (nbMonsters <= 5) /* Orange */ nbColor = Color.ORANGE; else /* Red */ nbColor = Color.RED; cs.appendInt(nbMonsters, nbColor); cs.append(" monster" + (nbMonsters == 1 ? "" : "s") + " to kill", null); IColoredString<Color> helping1 = new IColoredString.Impl<>("Use numpad or vi-keys (hjklyubn) to move.", Color.WHITE); IColoredString<Color> helping2 = new IColoredString.Impl<>("Use ? for help, f to change colors, q to quit.", Color.WHITE); IColoredString<Color> helping3 = new IColoredString.Impl<>("Click the top or bottom border of the lower message box to scroll.", Color.WHITE); /* Some grey color */ final Color bgColor = new Color(0.3f, 0.3f, 0.3f, 0.9f); final Actor a; /* * Use TextPanel. There's less work to do than with * GroupCombinedPanel, and we can use a more legible variable-width font. * It doesn't seem like it when reading this code, but this actually does * much more than GroupCombinedPanel, because we do line wrapping and * justifying, without having to worry about sizes since TextPanel lays * itself out. */ final TextPanel<Color> tp = new TextPanel<Color>(new GDXMarkup(), DefaultResources.getStretchablePrintFont()); tp.backgroundColor = SColor.DARK_SLATE_GRAY; final List<IColoredString<Color>> text = new ArrayList<>(); text.add(cs); /* No need to call IColoredString::wrap, TextPanel does it on its own */ text.add(helping1); text.add(helping2); text.add(helping3); final float w = width * cellWidth, aw = helping3.length() * cellWidth * 0.8f * INTERNAL_ZOOM; final float h = height * cellHeight, ah = cellHeight * 9f * INTERNAL_ZOOM; tp.init(aw, ah, text); a = tp.getScrollPane(); final float x = (w - aw) / 2f; final float y = (h - ah) / 2f; a.setPosition(x, y); stage.setScrollFocus(a); help = a; stage.addActor(a); } private void clearHelp() { if (help == null) /* Nothing to do */ return; help.clear(); stage.getActors().removeValue(help, true); help = null; } public void putMap() { boolean overlapping; for (int i = 0; i < width; i++) { for (int j = 0; j < height; j++) { overlapping = monsters.containsKey(adjacency.composite(i, j, 0, 0) >>> 3) || (player.entity.gridX == i && player.entity.gridY == j); // if we see it now, we remember the cell and show a lit cell based on the fovmap value (between 0.0 // and 1.0), with 1.0 being almost pure white at +215 lightness and 0.0 being rather dark at -105. if (fovmap[i][j] > 0.0) { seen[i][j] = true; display.put(i, j, (overlapping) ? ' ' : lineDungeon[i][j], fgCenter.filter(colors[i][j]), bgCenter.filter(bgColors[i][j]), lights[i][j] + (int) (-105 + 250 * fovmap[i][j])); // if we don't see it now, but did earlier, use a very dark background, but lighter than black. } else {// if (seen[i][j]) { display.put(i, j, lineDungeon[i][j], fgCenter.filter(colors[i][j]), bgCenter.filter(bgColors[i][j]), -140); } } } int pt, x, y; for (int p = 0; p < toCursor.size; p++) { pt = toCursor.get(p); // use a brighter light to trace the path to the cursor, from 170 max lightness to 0 min. display.highlight(x = adjacency.extractX(pt), y = adjacency.extractY(pt), lights[x][y] + (int) (170 * fovmap[x][y])); } } @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); // used as the z-axis when generating Simplex noise to make water seem to "move" counter += Gdx.graphics.getDeltaTime() * 15; // this does the standard lighting for walls, floors, etc. but also uses counter to do the Simplex noise thing. lights = DungeonUtility.generateLightnessModifiers(decoDungeon, counter); //textFactory.configureShader(batch); // you done bad. you done real bad. if (health <= 0) { // still need to display the map, then write over it with a message. putMap(); display.putBoxedString(width / 2 - 18, height / 2 - 10, " THE TSAR WILL HAVE YOUR HEAD! "); display.putBoxedString(width / 2 - 18, height / 2 - 5, " AS THE OLD SAYING GOES, "); display.putBoxedString(width / 2 - lang.length() / 2, height / 2, lang); display.putBoxedString(width / 2 - 18, 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; } // need to display the map every frame, since we clear the screen to avoid artifacts. putMap(); // if the user clicked, we have a list of moves to perform. if (awaitedMoves.size != 0) { // 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. if (!display.hasActiveAnimations()) { ++framesWithoutAnimation; if (framesWithoutAnimation >= 3) { framesWithoutAnimation = 0; switch (phase) { case WAIT: case MONSTER_ANIM: int m = awaitedMoves.removeIndex(0); toCursor.removeIndex(0); move(m); break; case PLAYER_ANIM: postMove(); break; } } } } // if we are waiting for the player's input and get input, process it. else if (input.hasNext() && !display.hasActiveAnimations() && phase == Phase.WAIT) { input.next(); } // 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 >= 3) { framesWithoutAnimation = 0; switch (phase) { case WAIT: break; case MONSTER_ANIM: { phase = Phase.WAIT; } break; case PLAYER_ANIM: { postMove(); } } } } // if we do have an animation running, then how many frames have passed with no animation needs resetting else { framesWithoutAnimation = 0; } input.show(); // stage has its own batch and must be explicitly told to draw(). this also causes it to act(). stage.getViewport().apply(true); stage.draw(); stage.act(); if (help == null) { // display does not draw all AnimatedEntities by default, since FOV often changes how they need to be drawn. batch.begin(); // the player needs to get drawn every frame, of course. display.drawActor(batch, 1.0f, player.entity); display.drawActor(batch, 1.0f, player.marker); int mSize = monsters.size(); Creature mon; for (int m = 0; m < mSize; m++) { mon = monsters.getAt(m); // monsters are only drawn if within FOV. if (fovmap[mon.entity.gridX][mon.entity.gridY] > 0.0) { display.drawActor(batch, 1.0f, mon.entity); display.drawActor(batch, 1.0f, mon.marker); } } messages.put(width >> 1, 0, Character.forDigit(health, 10), SColor.DARK_PINK); // batch must end if it began. batch.end(); } } @Override public void resize(int width, int height) { super.resize(width, height); // message box won't respond to clicks on the far right if the stage hasn't been updated with a larger size currentZoomX = width * 1f / RotationDemo.width; // total new screen height in pixels divided by total number of rows on the screen currentZoomY = height * 1f / (RotationDemo.height + messages.getGridHeight()); // message box should be given updated bounds since I don't think it will do this automatically messages.setBounds(0, 0, width, currentZoomY * messages.getGridHeight()); // SquidMouse turns screen positions to cell positions, and needs to be told that cell sizes have changed input.reinitialize(currentZoomX, currentZoomY, RotationDemo.width, RotationDemo.height, 0, 0, width, height); currentZoomX = cellWidth / currentZoomX; currentZoomY = cellHeight / currentZoomY; input.update(width, height, true); stage.getViewport().update(width, height, true); } public static void main (String[] arg) { LwjglApplicationConfiguration config = new LwjglApplicationConfiguration(); config.title = "SquidLib Demo: Rotation in Pathfinding"; config.width = width * cellWidth; config.height = height * cellHeight; config.addIcon("Tentacle-16.png", Files.FileType.Internal); config.addIcon("Tentacle-32.png", Files.FileType.Internal); config.addIcon("Tentacle-128.png", Files.FileType.Internal); new LwjglApplication(new RotationDemo(), config); } }