com.github.squidpony.SquidBasicDemo.java Source code

Java tutorial

Introduction

Here is the source code for com.github.squidpony.SquidBasicDemo.java

Source

package com.github.squidpony;

import com.badlogic.gdx.ApplicationAdapter;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.InputAdapter;
import com.badlogic.gdx.InputMultiplexer;
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.viewport.StretchViewport;
import squidpony.FakeLanguageGen;
import squidpony.squidai.DijkstraMap;
import squidpony.squidgrid.gui.gdx.*;
import squidpony.squidgrid.mapping.DungeonGenerator;
import squidpony.squidgrid.mapping.DungeonUtility;
import squidpony.squidmath.Coord;
import squidpony.squidmath.GreasedRegion;
import squidpony.squidmath.RNG;

import java.util.ArrayList;
import java.util.Arrays;

/**
 * The main class of the game, constructed once in each of the platform-specific Launcher classes. Doesn't use any
 * platform-specific code.
 */
// If this is an example project in gdx-setup, then squidlib-util and the squidlib (the display module) are always
// dependencies. If you didn't change those dependencies, this class should run out of the box.
//
// This class is useful as a starting point, since it has dungeon generation and some of the trickier parts of input
// handling (using the mouse to get a path for the player) already filled in. You can remove any imports or usages of
// classes that you don't need.

// A main game class that uses LibGDX to display, which is the default for SquidLib, needs to extend ApplicationAdapter
// or something related, like Game. Game adds features that SquidLib doesn't currently use, so ApplicationAdapter is
// perfectly fine for these uses.
public class SquidBasicDemo extends ApplicationAdapter {
    SpriteBatch batch;

    private RNG rng;
    private SquidLayers display;
    private DungeonGenerator dungeonGen;
    private char[][] decoDungeon, bareDungeon, lineDungeon;
    private int[][] colorIndices, bgColorIndices;
    /** In number of cells */
    private int gridWidth;
    /** In number of cells */
    private int gridHeight;
    /** The pixel width of a cell */
    private int cellWidth;
    /** The pixel height of a cell */
    private int cellHeight;
    private SquidInput input;
    private Color bgColor;
    private Stage stage;
    private DijkstraMap playerToCursor;
    private Coord cursor, player;
    private ArrayList<Coord> toCursor;
    private ArrayList<Coord> awaitedMoves;
    private float secondsWithoutMoves;
    private String[] lang;
    private int langIndex = 0;

    @Override
    public void create() {
        //These variables, corresponding to the screen's width and height in cells and a cell's width and height in
        //pixels, must match the size you specified in the launcher for input to behave.
        //This is one of the more common places a mistake can happen.
        //In our desktop launcher, we gave these arguments to the configuration:
        //   config.width = 80 * 8;
        //  config.height = 40 * 18;
        //Here, config.height refers to the total number of rows to be displayed on the screen.
        //We're displaying 32 rows of dungeon, then 8 more rows of text generation to show some tricks with language.
        //gridHeight is 32 because that variable will be used for generating the dungeon and handling movement within
        //the upper 32 rows. Anything that refers to the full height, which happens rarely and usually for things like
        //screen resizes, just uses gridHeight + 8. Next to it is gridWidth, which is 80 because we want 80 grid spaces
        //across the whole screen. cellWidth and cellHeight are 8 and 18, and match the multipliers for config.width and
        //config.height, but in this case don't strictly need to because we soon use a "Stretchable" font. While
        //gridWidth and gridHeight are measured in spaces on the grid, cellWidth and cellHeight are the pixel dimensions
        //of an individual cell. The font will look more crisp if the cell dimensions match the config multipliers
        //exactly, and the stretchable fonts (technically, distance field fonts) can resize to non-square sizes and
        //still retain most of that crispness.
        gridWidth = 80;
        gridHeight = 24;
        cellWidth = 14;
        cellHeight = 21;
        // gotta have a random number generator. We can seed an RNG with any long we want, or even a String.
        rng = new RNG("SquidLib!");

        //Some classes in SquidLib need access to a batch to render certain things, so it's a good idea to have one.
        batch = new SpriteBatch();
        //Here we make sure our Stage, which holds any text-based grids we make, uses our Batch.
        stage = new Stage(new StretchViewport(gridWidth * cellWidth, (gridHeight + 8) * cellHeight), batch);
        // the font will try to load CM-Custom as an embedded bitmap font with a distance field effect.
        // this font is covered under the SIL Open Font License (fully free), so there's no reason it can't be used.
        display = new SquidLayers(gridWidth, gridHeight + 8, cellWidth, cellHeight,
                DefaultResources.getStretchableTypewriterFont());
        // 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.
        display.setTextSize(cellWidth, cellHeight + 1);

        // this makes animations very fast, which is good for multi-cell movement but bad for attack animations.
        display.setAnimationDuration(0.03f);

        //These need to have their positions set before adding any entities if there is an offset involved.
        //There is no offset used here, but it's still a good practice here to set positions early on.
        display.setPosition(0, 0);

        //This uses the seeded RNG we made earlier to build a procedural dungeon using a method that takes rectangular
        //sections of pre-drawn dungeon and drops them into place in a tiling pattern. It makes good "ruined" dungeons.
        dungeonGen = new DungeonGenerator(gridWidth, gridHeight, rng);
        //uncomment this next line to randomly add water to the dungeon in pools.
        //dungeonGen.addWater(15);
        //decoDungeon is given the dungeon with any decorations we specified. (Here, we didn't, unless you chose to add
        //water to the dungeon. In that case, decoDungeon will have different contents than bareDungeon, next.)
        decoDungeon = dungeonGen.generate();
        //getBareDungeon provides the simplest representation of the generated dungeon -- '#' for walls, '.' for floors.
        bareDungeon = dungeonGen.getBareDungeon();
        //When we draw, we may want to use a nicer representation of walls. DungeonUtility has lots of useful methods
        //for modifying char[][] dungeon grids, and this one takes each '#' and replaces it with a box-drawing character.
        lineDungeon = DungeonUtility.hashesToLines(decoDungeon);
        //Coord is the type we use as a general 2D point, usually in a dungeon.
        //Because we know dungeons won't be incredibly huge, Coord performs best for x and y values less than 256, but
        // by default it can also handle some negative x and y values (-3 is the lowest it can efficiently store). You
        // can call Coord.expandPool() or Coord.expandPoolTo() if you need larger maps to be just as fast.
        cursor = Coord.get(-1, -1);
        // here, we need to get a random floor cell to place the player upon, without the possibility of putting him
        // inside a wall. There are a few ways to do this in SquidLib. The most straightforward way is to randomly
        // choose x and y positions until a floor is found, but particularly on dungeons with few floor cells, this can
        // have serious problems -- if it takes too long to find a floor cell, either it needs to be able to figure out
        // that random choice isn't working and instead choose the first it finds in simple iteration, or potentially
        // keep trying forever on an all-wall map. There are better ways! These involve using a kind of specific storage
        // for points or regions, getting that to store only floors, and finding a random cell from that collection of
        // floors. The two kinds of such storage used commonly in SquidLib are the "packed data" as short[] produced by
        // CoordPacker (which use very little memory, but can be slow, and are treated as unchanging by CoordPacker so
        // any change makes a new array), and GreasedRegion objects (which use slightly more memory, tend to be faster
        // on almost all operations compared to the same operations with CoordPacker, and default to changing the
        // GreasedRegion object when you call a method on it instead of making a new one). Even though CoordPacker
        // sometimes has better documentation, GreasedRegion is generally a better choice; it was added to address
        // shortcomings in CoordPacker, particularly for speed, and the worst-case scenarios for data in CoordPacker are
        // no problem whatsoever for GreasedRegion. CoordPacker is called that because it compresses the information
        // for nearby Coords into a smaller amount of memory. GreasedRegion is called that because it encodes regions,
        // but is "greasy" both in the fatty-food sense of using more space, and in the "greased lightning" sense of
        // being especially fast. Both of them can be seen as storing regions of points in 2D space as "on" and "off."

        // Here we fill a GreasedRegion so it stores the cells that contain a floor, the '.' char, as "on."
        GreasedRegion placement = new GreasedRegion(bareDungeon, '.');
        //player is, here, just a Coord that stores his position. In a real game, you would probably have a class for
        //creatures, and possibly a subclass for the player. The singleRandom() method on GreasedRegion finds one Coord
        // in that region that is "on," or -1,-1 if there are no such cells. It takes an RNG object as a parameter, and
        // if you gave a seed to the RNG constructor, then the cell this chooses will be reliable for testing. If you
        // don't seed the RNG, any valid cell should be possible.
        player = placement.singleRandom(rng);
        //This is used to allow clicks or taps to take the player to the desired area.
        toCursor = new ArrayList<Coord>(200);
        //When a path is confirmed by clicking, we draw from this List to find which cell is next to move into.
        awaitedMoves = new ArrayList<Coord>(200);
        //DijkstraMap is the pathfinding swiss-army knife we use here to find a path to the latest cursor position.
        //DijkstraMap.Measurement is an enum that determines the possibility or preference to enter diagonals. Here, the
        // MANHATTAN value is used, which means 4-way movement only, no diagonals possible. Alternatives are CHEBYSHEV,
        // which allows 8 directions of movement at the same cost for all directions, and EUCLIDEAN, which allows 8
        // directions, but will prefer orthogonal moves unless diagonal ones are clearly closer "as the crow flies."
        playerToCursor = new DijkstraMap(decoDungeon, DijkstraMap.Measurement.MANHATTAN);
        //These next two lines mark the player as something we want paths to go to or from, and get the distances to the
        // player from all walkable cells in the dungeon.
        playerToCursor.setGoal(player);
        playerToCursor.scan(null);

        //The next three lines set the background color for anything we don't draw on, but also create 2D arrays of the
        //same size as decoDungeon that store simple indexes into a common list of colors, using the colors that looks
        // up as the colors for the cell with the same x and y.
        bgColor = SColor.DARK_SLATE_GRAY;
        colorIndices = DungeonUtility.generatePaletteIndices(decoDungeon);
        bgColorIndices = DungeonUtility.generateBGPaletteIndices(decoDungeon);
        // this creates an array of sentences, where each imitates a different sort of language or mix of languages.
        // this serves to demonstrate the large amount of glyphs SquidLib supports.
        // there's no need to put much effort into understanding this section yet, and many games won't use the language
        // generation code at all. If you want to know what this does, the parameters are:
        // minimum words in a sentence, maximum words in a sentence, "mid" punctuation that can be after a word (like a
        // comma), "end" punctuation that can be at the end of a sentence, frequency of "mid" punctuation (the chance in
        // 1.0 that a word will have something like a comma appended after it), and the limit on how many chars to use.
        lang = new String[] {
                FakeLanguageGen.ENGLISH.sentence(5, 10, new String[] { ",", ",", ",", ";" },
                        new String[] { ".", ".", ".", "!", "?", "..." }, 0.17, gridWidth - 4),
                FakeLanguageGen.GREEK_AUTHENTIC.sentence(5, 11, new String[] { ",", ",", ";" },
                        new String[] { ".", ".", ".", "!", "?", "..." }, 0.2, gridWidth - 4),
                FakeLanguageGen.GREEK_ROMANIZED.sentence(5, 11, new String[] { ",", ",", ";" },
                        new String[] { ".", ".", ".", "!", "?", "..." }, 0.2, gridWidth - 4),
                FakeLanguageGen.LOVECRAFT.sentence(3, 9, new String[] { ",", ",", ";" },
                        new String[] { ".", ".", "!", "!", "?", "...", "..." }, 0.15, gridWidth - 4),
                FakeLanguageGen.FRENCH.sentence(4, 12, new String[] { ",", ",", ",", ";", ";" },
                        new String[] { ".", ".", ".", "!", "?", "..." }, 0.17, gridWidth - 4),
                FakeLanguageGen.RUSSIAN_AUTHENTIC.sentence(6, 13, new String[] { ",", ",", ",", ",", ";", " -" },
                        new String[] { ".", ".", ".", "!", "?", "..." }, 0.25, gridWidth - 4),
                FakeLanguageGen.RUSSIAN_ROMANIZED.sentence(6, 13, new String[] { ",", ",", ",", ",", ";", " -" },
                        new String[] { ".", ".", ".", "!", "?", "..." }, 0.25, gridWidth - 4),
                FakeLanguageGen.JAPANESE_ROMANIZED.sentence(5, 13, new String[] { ",", ",", ",", ",", ";" },
                        new String[] { ".", ".", ".", "!", "?", "...", "..." }, 0.12, gridWidth - 4),
                FakeLanguageGen.SWAHILI.sentence(4, 9, new String[] { ",", ",", ",", ";", ";" },
                        new String[] { ".", ".", ".", "!", "?" }, 0.12, gridWidth - 4),
                FakeLanguageGen.SOMALI.sentence(4, 9, new String[] { ",", ",", ",", ";", ";" },
                        new String[] { ".", ".", ".", "!", "?" }, 0.12, gridWidth - 4),
                FakeLanguageGen.HINDI_ROMANIZED.sentence(4, 9, new String[] { ",", ",", ",", ";", ";" },
                        new String[] { ".", ".", ".", "!", "?" }, 0.12, gridWidth - 4),
                FakeLanguageGen.ARABIC_ROMANIZED.sentence(4, 9, new String[] { ",", ",", ",", ";", ";" },
                        new String[] { ".", ".", ".", "!", "?" }, 0.12, gridWidth - 4),
                FakeLanguageGen.NORSE.addModifiers(FakeLanguageGen.Modifier.SIMPLIFY_NORSE).sentence(4, 9,
                        new String[] { ",", ",", ",", ";", ";" }, new String[] { ".", ".", ".", "!", "?" }, 0.12,
                        gridWidth - 4),
                FakeLanguageGen.INUKTITUT.sentence(4, 9, new String[] { ",", ",", ",", ";", ";" },
                        new String[] { ".", ".", ".", "!", "?" }, 0.12, gridWidth - 4),
                FakeLanguageGen.NAHUATL.sentence(4, 9, new String[] { ",", ",", ",", ";", ";" },
                        new String[] { ".", ".", ".", "!", "?" }, 0.12, gridWidth - 4),
                FakeLanguageGen.FANTASY_NAME.sentence(4, 8, new String[] { ",", ",", ",", ";", ";" },
                        new String[] { ".", ".", ".", "!", "?", "..." }, 0.22, gridWidth - 4),
                FakeLanguageGen.FANCY_FANTASY_NAME.sentence(4, 8, new String[] { ",", ",", ",", ";", ";" },
                        new String[] { ".", ".", ".", "!", "?", "..." }, 0.22, gridWidth - 4),
                FakeLanguageGen.GOBLIN.sentence(4, 8, new String[] { ",", ",", ",", ";", ";" },
                        new String[] { ".", ".", ".", "?", "...", "..." }, 0.12, gridWidth - 4),
                FakeLanguageGen.ELF.sentence(4, 8, new String[] { ",", ",", ",", ";", ";" },
                        new String[] { ".", ".", ".", "?", "..." }, 0.22, gridWidth - 4),
                FakeLanguageGen.DEMONIC.sentence(4, 8, new String[] { ",", ",", ",", ";", ";" },
                        new String[] { ".", ".", ".", "!", "!", "..." }, 0.1, gridWidth - 4),
                FakeLanguageGen.INFERNAL.sentence(4, 8, new String[] { ",", ",", ",", ";", ";" },
                        new String[] { ".", ".", ".", "?", "?", "..." }, 0.22, gridWidth - 4),
                FakeLanguageGen.FRENCH.mix(FakeLanguageGen.JAPANESE_ROMANIZED, 0.65).sentence(5, 9,
                        new String[] { ",", ",", ",", ";" }, new String[] { ".", ".", ".", "!", "?", "?", "..." },
                        0.14, gridWidth - 4),
                FakeLanguageGen.ENGLISH.addAccents(0.5, 0.15).sentence(5, 10, new String[] { ",", ",", ",", ";" },
                        new String[] { ".", ".", ".", "!", "?", "..." }, 0.17, gridWidth - 4),
                // mixAll is useful when mixing many languages; it takes alternating languages and weights for
                // the preceding language, and doesn't care what order the pairs are given in (the mix method
                // does care, which can be confusing when more than two languages are mixed).
                FakeLanguageGen.mixAll(FakeLanguageGen.SWAHILI, 1.0, FakeLanguageGen.JAPANESE_ROMANIZED, 1.0,
                        FakeLanguageGen.FRENCH, 1.0, FakeLanguageGen.RUSSIAN_ROMANIZED, 1.0,
                        FakeLanguageGen.GREEK_ROMANIZED, 1.0, FakeLanguageGen.ENGLISH, 1.2, FakeLanguageGen.ELF,
                        1.0, FakeLanguageGen.LOVECRAFT, 0.75).sentence(5, 10, new String[] { ",", ",", ",", ";" },
                                new String[] { ".", ".", ".", "!", "?", "..." }, 0.2, gridWidth - 4), };

        // 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 hjkl keys (also called vi-keys), numpad, arrow keys, 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, i.e. one for 'A' and one for 'a'.
        // 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 SquidInput(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': {
                    //-1 is up on the screen
                    move(0, -1);
                    break;
                }
                case SquidInput.DOWN_ARROW:
                case 'j':
                case 's':
                case 'J':
                case 'S': {
                    //+1 is down on the screen
                    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 'Q':
                case 'q':
                case SquidInput.ESCAPE: {
                    Gdx.app.exit();
                    break;
                }
                }
            }
        },
                //The second parameter passed to a SquidInput can be a SquidMouse, which takes mouse or touchscreen
                //input and converts it to grid coordinates (here, a cell is 12 wide and 24 tall, so clicking at the
                // pixel position 15,51 will pass screenX as 1 (since if you divide 15 by 12 and round down you get 1),
                // and screenY as 2 (since 51 divided by 24 rounded down is 2)).
                new SquidMouse(cellWidth, cellHeight, gridWidth, gridHeight, 0, 0, new InputAdapter() {

                    // if the user clicks 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 (awaitedMoves.isEmpty()) {
                            if (toCursor.isEmpty()) {
                                cursor = Coord.get(screenX, screenY);
                                //This uses DijkstraMap.findPathPreScannned() to get a path as a List of Coord from the current
                                // player position to the position the user clicked on. The "PreScanned" part is an optimization
                                // that's special to DijkstraMap; because the whole map has already been fully analyzed by the
                                // DijkstraMap.scan() method at the start of the program, and re-calculated whenever the player
                                // moves, we only need to do a fraction of the work to find the best path with that info.
                                toCursor = playerToCursor.findPathPreScanned(cursor);
                            }
                            awaitedMoves.addAll(toCursor);
                        }
                        return true;
                    }

                    @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 Coords that
                    // receive highlighting). Uses DijkstraMap.findPathPreScanned() to find the path, which is rather fast.
                    @Override
                    public boolean mouseMoved(int screenX, int screenY) {
                        if (!awaitedMoves.isEmpty())
                            return false;
                        if (cursor.x == screenX && cursor.y == screenY) {
                            return false;
                        }
                        cursor = Coord.get(screenX, screenY);
                        //This uses DijkstraMap.findPathPreScannned() to get a path as a List of Coord from the current
                        // player position to the position the user clicked on. The "PreScanned" part is an optimization
                        // that's special to DijkstraMap; because the whole map has already been fully analyzed by the
                        // DijkstraMap.scan() method at the start of the program, and re-calculated whenever the player
                        // moves, we only need to do a fraction of the work to find the best path with that info.

                        toCursor = playerToCursor.findPathPreScanned(cursor);
                        return false;
                    }
                }));
        //Setting the InputProcessor is ABSOLUTELY NEEDED TO HANDLE INPUT
        Gdx.input.setInputProcessor(new InputMultiplexer(stage, input));
        //You might be able to get by with the next line instead of the above line, but the former is preferred.
        //Gdx.input.setInputProcessor(input);
        // and then add display, our one visual component, to the list of things that act in Stage.
        stage.addActor(display);

    }

    /**
     * Move the player if he isn't bumping into a wall or trying to go off the map somehow.
     * In a fully-fledged game, this would not be organized like this, but this is a one-file demo.
     * @param xmod
     * @param ymod
     */
    private void move(int xmod, int ymod) {
        int newX = player.x + xmod, newY = player.y + ymod;
        if (newX >= 0 && newY >= 0 && newX < gridWidth && newY < gridHeight && bareDungeon[newX][newY] != '#') {
            // changing the player Coord is all we need to do here, because we re-calculate the distances to the player
            // from all other cells only when we need to, that is, when the movement is finished (see render() ).
            player = player.translate(xmod, ymod);
        }
        // loops through the text snippets displayed whenever the player moves
        langIndex = (langIndex + 1) % lang.length;
    }

    /**
     * Draws the map, applies any highlighting for the path to the cursor, and then draws the player.
     */
    public void putMap() {
        for (int i = 0; i < gridWidth; i++) {
            for (int j = 0; j < gridHeight; j++) {
                display.put(i, j, lineDungeon[i][j], colorIndices[i][j], bgColorIndices[i][j], 40);
            }
        }
        for (Coord pt : toCursor) {
            // use a brighter light to trace the path to the cursor, from 170 max lightness to 0 min.
            display.highlight(pt.x, pt.y, 100);
        }
        //places the player as an '@' at his position in orange (6 is an index into SColor.LIMITED_PALETTE).
        display.put(player.x, player.y, '@', 6);
        //this helps compatibility with the HTML target, which doesn't support String.format()
        char[] spaceArray = new char[gridWidth];
        Arrays.fill(spaceArray, ' ');
        String spaces = String.valueOf(spaceArray);

        for (int i = 0; i < 6; i++) {
            display.putString(0, gridHeight + i + 1, spaces, 0, 1);
            display.putString(2, gridHeight + i + 1, lang[(langIndex + i) % lang.length], 0, 1);
        }
    }

    @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);

        // 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.isEmpty()) {
            // this doesn't check for input, but instead processes and removes Coords from awaitedMoves.
            secondsWithoutMoves += Gdx.graphics.getDeltaTime();
            if (secondsWithoutMoves >= 0.1) {
                secondsWithoutMoves = 0;
                Coord m = awaitedMoves.remove(0);
                toCursor.remove(0);
                move(m.x - player.x, m.y - player.y);
            }
            // this only happens if we just removed the last Coord from awaitedMoves, and it's only then that we need to
            // re-calculate the distances from all cells to the player. We don't need to calculate this information on
            // each part of a many-cell move (just the end), nor do we need to calculate it whenever the mouse moves.
            if (awaitedMoves.isEmpty()) {
                // the next two lines remove any lingering data needed for earlier paths
                playerToCursor.clearGoals();
                playerToCursor.resetMap();
                // the next line marks the player as a "goal" cell, which seems counter-intuitive, but it works because all
                // cells will try to find the distance between themselves and the nearest goal, and once this is found, the
                // distances don't change as long as the goals don't change. Since the mouse will move and new paths will be
                // found, but the player doesn't move until a cell is clicked, the "goal" is the non-changing cell, so the
                // player's position, and the "target" of a pathfinding method like DijkstraMap.findPathPreScanned() is the
                // currently-moused-over cell, which we only need to set where the mouse is being handled.
                playerToCursor.setGoal(player);
                playerToCursor.scan(null);
            }
        }
        // if we are waiting for the player's input and get input, process it.
        else if (input.hasNext()) {
            input.next();
        }

        // stage has its own batch and must be explicitly told to draw().
        stage.draw();
        // certain classes that use scene2d.ui widgets need to be told to act() to process input.
        stage.act();
    }

    @Override
    public void resize(int width, int height) {
        super.resize(width, height);
        //very important to have the mouse behave correctly if the user fullscreens or resizes the game!
        input.getMouse().reinitialize((float) width / this.gridWidth, (float) height / (this.gridHeight + 8),
                this.gridWidth, this.gridHeight, 0, 0);
    }
}