com.squidpony.the.tsar.TsarGame.java Source code

Java tutorial

Introduction

Here is the source code for com.squidpony.the.tsar.TsarGame.java

Source

package com.squidpony.the.tsar;

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.Actor;
import com.badlogic.gdx.scenes.scene2d.Stage;
import com.badlogic.gdx.utils.viewport.ScreenViewport;
import squidpony.FakeLanguageGen;
import squidpony.panel.IColoredString;
import squidpony.panel.ICombinedPanel;
import squidpony.squidai.DijkstraMap;
import squidpony.squidgrid.Direction;
import squidpony.squidgrid.FOV;
import squidpony.squidgrid.Radius;
import squidpony.squidgrid.gui.gdx.*;
import squidpony.squidgrid.mapping.DungeonGenerator;
import squidpony.squidgrid.mapping.DungeonUtility;
import squidpony.squidgrid.mapping.SerpentMapGenerator;
import squidpony.squidmath.Coord;
import squidpony.squidmath.CoordPacker;
import squidpony.squidmath.RNG;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.Map;

public class TsarGame extends ApplicationAdapter {
    private enum Phase {
        WAIT, PLAYER_ANIM, MONSTER_ANIM
    }

    SpriteBatch batch;

    private Phase phase = Phase.WAIT;
    private RNG rng;
    private SquidLayers display;
    private SquidMessageBox messages;
    /** Non-{@code null} iff '?' was pressed before */
    private /*Nullable*/ Actor help;
    private DungeonGenerator dungeonGen;
    private char[][] decoDungeon, bareDungeon, lineDungeon;
    private double[][] res;
    private int[][] lights;
    private Color[][] colors, bgColors;
    private double[][] fovmap, pathMap;
    private AnimatedEntity player;
    private FOV fov;
    /** In number of cells */
    private int width;
    /** In number of cells */
    private int height;
    /** The pixel width of a cell */
    private int cellWidth;
    /** The pixel height of a cell */
    private int cellHeight;
    private SquidInput input;
    private double counter;
    private boolean[][] seen;
    private int health = 7;
    private SquidColorCenter fgCenter, bgCenter;
    private Color bgColor;
    private HashMap<AnimatedEntity, Integer> monsters;
    private DijkstraMap getToPlayer, playerToCursor;
    private Stage stage;
    private int framesWithoutAnimation = 0;
    private Coord cursor;
    private ArrayList<Coord> toCursor;
    private ArrayList<Coord> awaitedMoves;
    private String lang;
    private SquidColorCenter[] colorCenters;
    private int currentCenter;
    private boolean changingColors = false;

@Override
public void create () {
    // gotta have a random number generator. We can seed an RNG with any long we want, or a String, or nothing (which
    // will use a random seed).
    rng = new RNG(0xBADBEEFB0BBL);

    // for demo purposes, we allow changing the SquidColorCenter and the filter effect associated with it.
    // next, we populate the colorCenters array with the SquidColorCenters that will modify any colors we request
    // of them using the filter we specify. Only one SquidColorCenter will be used at any time for foreground, and
    // sometimes another will be used for background.
    colorCenters = new SquidColorCenter[14];
    // MultiLerpFilter here is given two colors to tint everything toward one of; this is meant to reproduce the
    // "Hollywood action movie poster" style of using primarily light orange (explosions) and gray-blue (metal).

    colorCenters[0] = new SquidColorCenter(new Filters.MultiLerpFilter(
            new Color[]{SColor.GAMBOGE_DYE, SColor.COLUMBIA_BLUE},
            new float[]{0.25f, 0.2f}
    ));
    colorCenters[1] = colorCenters[0];

    // MultiLerpFilter here is given three colors to tint everything toward one of; this is meant to look bolder.

    colorCenters[2] = new SquidColorCenter(new Filters.MultiLerpFilter(
            new Color[]{SColor.RED_PIGMENT, SColor.MEDIUM_BLUE, SColor.LIME_GREEN},
            new float[]{0.2f, 0.25f, 0.25f}
    ));
    colorCenters[3] = colorCenters[2];

    // ColorizeFilter here is given a slightly-grayish dark brown to imitate a sepia tone.

    colorCenters[4] = new SquidColorCenter(new Filters.ColorizeFilter(SColor.CLOVE_BROWN, 0.7f, -0.05f));
    colorCenters[5] = new SquidColorCenter(new Filters.ColorizeFilter(SColor.CLOVE_BROWN, 0.65f, 0.07f));

    // HallucinateFilter makes all the colors very saturated and move even when you aren't doing anything.

    colorCenters[6] = new SquidColorCenter(new Filters.HallucinateFilter());
    colorCenters[7] = colorCenters[6];

    // SaturationFilter here is used to over-saturate the colors slightly. Background is less saturated.

    colorCenters[8] = new SquidColorCenter(new Filters.SaturationFilter(1.35f));
    colorCenters[9] = new SquidColorCenter(new Filters.SaturationFilter(1.15f));

    // SaturationFilter here is used to de-saturate the colors slightly. Background is less saturated.

    colorCenters[10] = new SquidColorCenter(new Filters.SaturationFilter(0.7f));
    colorCenters[11] = new SquidColorCenter(new Filters.SaturationFilter(0.5f));

    colorCenters[12] = DefaultResources.getSCC();
    colorCenters[13] = colorCenters[12];

    fgCenter = colorCenters[12];
    bgCenter = colorCenters[13];
    currentCenter = 6;
    batch = new SpriteBatch();
    width = 120;
    height = 35;
    cellWidth = 8;
    cellHeight = 17;
    // the font will try to load Inconsolata-LGC as a bitmap font from resources.
    // 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(width, height, cellWidth, cellHeight, DefaultResources.getStretchableFont(), bgCenter, fgCenter);
    display.setTextSize(cellWidth, cellHeight + 1);
    display.setAnimationDuration(0.03f);
    messages = new SquidMessageBox(width, 5, DefaultResources.getStretchableFont()
            .width(cellWidth).height(cellHeight).initBySize());
    messages.setTextSize(cellWidth, cellHeight + 1);

    stage = new Stage(new ScreenViewport(), batch);

    //These need to have their positions set before adding any entities if there is an offset involved.
    messages.setPosition(0, 0);
    display.setPosition(0, messages.getHeight());
    messages.appendMessage("Your affair with The Tsar's daughter has you in deep trouble deep in prison!");
    messages.appendWrappingMessage("Use numpad or vi-keys (hjklyubn) to 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 DungeonGenerator(width, height, rng);
    dungeonGen.addWater(12, 6);
    dungeonGen.addGrass(6);
    dungeonGen.addBoulders(15);
    dungeonGen.addDoors(25, false);
    SerpentMapGenerator serpent = new SerpentMapGenerator(width, height, rng, 0.15);
    serpent.putCaveCarvers(1);
    serpent.putBoxRoomCarvers(5);
    serpent.putRoundRoomCarvers(2);
    char[][] sg = serpent.generate();
    decoDungeon = dungeonGen.generate(sg);

    // change the TilesetType to lots of different choices to see what dungeon works best.
    //bareDungeon = dungeonGen.generate(TilesetType.DEFAULT_DUNGEON);
    bareDungeon = dungeonGen.getBareDungeon();
    lineDungeon = DungeonUtility.hashesToLines(dungeonGen.getDungeon(), true);
    // it's more efficient to get random floors from a packed set containing only (compressed) floor positions.
    short[] placement = CoordPacker.pack(bareDungeon, '.');
    Coord pl = dungeonGen.utility.randomCell(placement);
    placement = CoordPacker.removePacked(placement, pl.x, pl.y);
    int numMonsters = 25;
    monsters = new HashMap<AnimatedEntity, Integer>(numMonsters);
    for(int i = 0; i < numMonsters; i++)
    {
        Coord monPos = dungeonGen.utility.randomCell(placement);
        placement = CoordPacker.removePacked(placement, monPos.x, monPos.y);
        monsters.put(display.animateActor(monPos.x, monPos.y, '',
                fgCenter.filter(display.getPalette().get(11))), 0);
    }
    // your choice of FOV matters here.
    fov = new FOV(FOV.RIPPLE_TIGHT);
    getToPlayer = new DijkstraMap(decoDungeon, DijkstraMap.Measurement.CHEBYSHEV);
    getToPlayer.rng = rng;
    getToPlayer.setGoal(pl);
    pathMap = getToPlayer.scan(null);
    res = DungeonUtility.generateResistances(decoDungeon);
    fovmap = fov.calculateFOV(res, pl.x, pl.y, 8, Radius.SQUARE);

    player = display.animateActor(pl.x, pl.y, Character.forDigit(health, 10),
            fgCenter.filter(display.getPalette().get(30)));
    cursor = Coord.get(-1, -1);
    toCursor = new ArrayList<Coord>(10);
    awaitedMoves = new ArrayList<Coord>(10);
    playerToCursor = new DijkstraMap(decoDungeon, DijkstraMap.Measurement.EUCLIDEAN);
    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);
    // 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 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':
                {
                    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;
                }
                case 'f':
                case 'F':
                {
                    currentCenter = (currentCenter + 1) % 7;
                    // idx is 3 when we use the HallucinateFilter, which needs special work
                    changingColors = currentCenter == 3;
                    fgCenter = colorCenters[currentCenter * 2];
                    bgCenter = colorCenters[currentCenter * 2 + 1];
                    display.setFGColorCenter(fgCenter);
                    display.setBGColorCenter(bgCenter);
                    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.isEmpty()) {
                if (toCursor.isEmpty()) {
                    cursor = Coord.get(screenX, screenY);
                    //Uses DijkstraMap to get a path. from the player's position to the cursor
                    toCursor = playerToCursor.findPath(30, null, null, Coord.get(player.gridX, player.gridY), cursor);
                }
                awaitedMoves = new ArrayList<Coord>(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.isEmpty())
                return false;
            if(cursor.x == screenX && cursor.y == screenY)
            {
                return false;
            }
            if(fovmap[screenX][screenY] > 0.0) {
                cursor = Coord.get(screenX, screenY);
                //Uses DijkstraMap to get a path. from the player's position to the cursor
                toCursor = playerToCursor.findPath(30, null, null, Coord.get(player.gridX, player.gridY), cursor);
            }
            return false;
        }
    }));
    // 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(messages);

}

    /**
     * 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 xmod
     * @param ymod
     */
    private void move(int xmod, int ymod) {
        clearHelp();

        if (health <= 0)
            return;

        int newX = player.gridX + xmod, newY = player.gridY + ymod;
        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.gridX, player.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);
                display.slide(player, newX, newY);

                for (AnimatedEntity ae : monsters.keySet()) {
                    if (newX == ae.gridX && newY == ae.gridY) {
                        monsters.remove(ae);
                        break;
                    }
                }
            }

            phase = Phase.PLAYER_ANIM;
        }
    }

    // check if a monster's movement would overlap with another monster.
    private boolean checkOverlap(AnimatedEntity ae, int x, int y, ArrayList<Coord> futureOccupied) {
        for (AnimatedEntity mon : monsters.keySet()) {
            if (mon.gridX == x && mon.gridY == y && !mon.equals(ae))
                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.
        getToPlayer.setGoal(player.gridX, player.gridY);
        // 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).
        LinkedHashSet<Coord> monplaces = new LinkedHashSet<Coord>(monsters.size());
        for (AnimatedEntity ae : monsters.keySet()) {
            monplaces.add(Coord.get(ae.gridX, ae.gridY));
        }
        pathMap = getToPlayer.scan(monplaces);

        // recalculate FOV, store it in fovmap for the render to use.
        fovmap = fov.calculateFOV(res, player.gridX, player.gridY, 8, Radius.SQUARE);
        // handle monster turns
        ArrayList<Coord> nextMovePositions = new ArrayList<Coord>(25);
        for (Map.Entry<AnimatedEntity, Integer> mon : monsters.entrySet()) {
            // monster values are used to store their aggression, 1 for actively stalking the player, 0 for not.
            if (mon.getValue() > 0 || fovmap[mon.getKey().gridX][mon.getKey().gridY] > 0.1) {
                if (mon.getValue() == 0) {
                    messages.appendMessage(
                            "The AMED GUAD shouts at you, \""
                                    + FakeLanguageGen.RUSSIAN_AUTHENTIC.sentence(rng, 1, 3,
                                            new String[] { ",", ",", ",", " -" }, new String[] { "!" }, 0.25)
                                    + "\"");
                }
                // 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;
                for (Direction d : rng.shuffle(Direction.OUTWARDS, new Direction[8])) {
                    Coord tmp = Coord.get(mon.getKey().gridX + d.deltaX, mon.getKey().gridY + d.deltaY);
                    if (pathMap[tmp.x][tmp.y] < best
                            && !checkOverlap(mon.getKey(), 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;
                    }
                }
                if (choice != null) {
                    Coord tmp = Coord.get(mon.getKey().gridX + choice.deltaX, mon.getKey().gridY + choice.deltaY);
                    // if we would move into the player, instead damage the player and give newMons the current
                    // position of this monster.
                    if (player.gridX == tmp.x && player.gridY == tmp.y) {
                        display.wiggle(player);
                        health--;
                        player.setText("" + health);
                        monsters.put(mon.getKey(), 1);
                    }
                    // otherwise store the new position in newMons.
                    else {
                        /*if (fovmap[mon.getKey().x][mon.getKey().y] > 0.0) {
                        display.put(mon.getKey().x, mon.getKey().y, 'M', 11);
                        }*/
                        nextMovePositions.add(Coord.get(tmp.x, tmp.y));
                        monsters.put(mon.getKey(), 1);
                        display.slide(mon.getKey(), tmp.x, tmp.y);

                    }
                } else {
                    monsters.put(mon.getKey(), 1);
                }
            } else {
                monsters.put(mon.getKey(), mon.getValue());
            }
        }

    }

    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<Color>();
        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(" guard" + (nbMonsters == 1 ? "" : "s") + " left", null);

        IColoredString<Color> helping1 = new IColoredString.Impl<Color>("Use numpad or vi-keys (hjklyubn) to move.",
                Color.WHITE);
        IColoredString<Color> helping2 = new IColoredString.Impl<Color>(
                "Use ? for help, f to change colors, q to quit.", Color.WHITE);
        IColoredString<Color> helping3 = new IColoredString.Impl<Color>(
                "Click the top or bottom border of the lower message box to scroll.", Color.WHITE);

        /* The panel's width */
        final int w = Math.max(helping3.length(), cs.length());
        /* The panel's height. */
        final int h = 5;
        final SquidPanel bg = new SquidPanel(w, h, display.getTextFactory());
        final SquidPanel fg = new SquidPanel(w, h, display.getTextFactory());
        final GroupCombinedPanel<Color> gcp = new GroupCombinedPanel<Color>();
        /*
         * We're setting them late just for the demo, as it avoids giving 'w'
         * and 'h' at construction time.
         */
        gcp.setPanels(bg, fg);

        /*
         * Set the position (the center), using libgdx's 'setPosition'
         * method, that takes the bottom left corner as input.
         */
        gcp.setPosition(((width / 2) - (w / 2)) * cellWidth, (height / 2) * cellHeight);

        /* Fill the background with some grey */
        gcp.fill(ICombinedPanel.What.BG, new Color(0.3f, 0.3f, 0.3f, 0.9f));

        /* Now, to set the text we have to follow SquidPanel's convention */
        /* First 0: align left, second 0: first line */
        gcp.putFG(0, 0, cs);

        /* 0: align left, 2: third line */
        gcp.putFG(0, 2, helping1);
        gcp.putFG(0, 3, helping2);
        gcp.putFG(0, 4, helping3);

        help = gcp;

        stage.addActor(gcp);
    }

    private void clearHelp() {
        if (help == null)
            /* Nothing to do */
            return;
        help.clear();
        stage.getActors().removeValue(help, true);
        help = null;
    }

    public void putMap() {
        for (int i = 0; i < width; i++) {
            for (int j = 0; j < height; 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, lineDungeon[i][j], fgCenter.filter(colors[i][j]),
                            bgCenter.filter(bgColors[i][j]), lights[i][j] + (int) (-105 + 320 * 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);
                }
            }
        }
        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, lights[pt.x][pt.y] + (int) (170 * fovmap[pt.x][pt.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);

        // 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.isEmpty()) {
            // 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:
                        Coord m = awaitedMoves.remove(0);
                        toCursor.remove(0);
                        move(m.x - player.gridX, m.y - player.gridY);
                        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;
        }

        // stage has its own batch and must be explicitly told to draw(). this also causes it to act().
        stage.draw();
        if (help == null) {
            // disolay 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);
            for (AnimatedEntity mon : monsters.keySet()) {
                // monsters are only drawn if within FOV.
                if (fovmap[mon.gridX][mon.gridY] > 0.0) {
                    display.drawActor(batch, 1.0f, mon);
                }
            }
            // batch must end if it began.
            batch.end();
        }
        // if using a filter that changes each frame, clear the known relationship between requested and actual colors
        if (changingColors) {
            fgCenter.clearCache();
            bgCenter.clearCache();
        }
    }

    @Override
    public void resize(int width, int height) {
        super.resize(width, height);
        input.getMouse().reinitialize((float) width / this.width, (height - messages.getHeight()) / this.height,
                this.width, this.height, 0, 0);
    }
}