package squidpony.gdx.examples;
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.Camera;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.math.Vector3;
import com.badlogic.gdx.scenes.scene2d.Actor;
import com.badlogic.gdx.scenes.scene2d.Stage;
import com.badlogic.gdx.scenes.scene2d.actions.TemporalAction;
import com.badlogic.gdx.utils.viewport.StretchViewport;
import com.badlogic.gdx.utils.viewport.Viewport;
import squidpony.FakeLanguageGen;
import squidpony.panel.IColoredString;
import squidpony.squidai.DijkstraMap;
import squidpony.squidgrid.FOV;
import squidpony.squidgrid.Radius;
import squidpony.squidgrid.SpatialMap;
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.List;
public class EverythingDemo extends ApplicationAdapter {
private enum Phase {WAIT, PLAYER_ANIM, MONSTER_ANIM}
private static class Monster {
public AnimatedEntity entity;
public int state;
public Monster(Actor actor, int x, int y, int state) {
entity = new AnimatedEntity(actor, x, y);
this.state = state;
}
public Monster(AnimatedEntity ae, int state) {
entity = ae;
this.state = state;
}
public Monster change(int state) {
this.state = state;
return this;
}
public Monster change(AnimatedEntity ae) {
entity = ae;
return this;
}
public Monster move(int x, int y) {
entity.gridX = x;
entity.gridY = y;
return this;
}
}
SpriteBatch batch;
private Phase phase = Phase.WAIT;
private StatefulRNG rng;
private SquidLayers display;
//private SquidPanel subCell;
// for more convenient access to some methods
private SquidPanel fg;
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;
private AnimatedEntity player;
private FOV fov;
/**
* In number of cells
*/
private int width;
/**
* In number of cells
*/
private int height;
/**
* In number of cells
*/
private int totalWidth;
/**
* In number of cells
*/
private int totalHeight;
/**
* The pixel width of a cell
*/
private int cellWidth;
/**
* The pixel height of a cell
*/
private int cellHeight;
private VisualInput input;
private double counter;
private boolean[][] seen;
private int health = 7;
private SquidColorCenter fgCenter, bgCenter;
private Color bgColor;
private SpatialMap<Integer, Monster> monsters;
private DijkstraMap getToPlayer, playerToCursor;
private Stage stage, messageStage;
private int framesWithoutAnimation = 0;
private Coord cursor;
private List<Coord> toCursor;
private List<Coord> awaitedMoves;
private String lang;
private SquidColorCenter[] colorCenters;
private int currentCenter;
private boolean changingColors = false;
private TextCellFactory textFactory;
public static final int INTERNAL_ZOOM = 1;
private Viewport viewport, messageViewport;
private Camera camera;
private float currentZoomX = INTERNAL_ZOOM, currentZoomY = INTERNAL_ZOOM;
@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);
// 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[20];
// 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));
// WiggleFilter here is used to randomize the colors slightly.
colorCenters[12] = new SquidColorCenter(new Filters.WiggleFilter());
colorCenters[13] = colorCenters[12];
// PaletteFilter here is used to limit colors to specific sets.
colorCenters[14] = new SquidColorCenter(new Filters.PaletteFilter(SColor.DAWNBRINGER_16));
colorCenters[15] = new SquidColorCenter(new Filters.PaletteFilter(SColor.DAWNBRINGER_16));
colorCenters[16] = DefaultResources.getSCC();
colorCenters[17] = colorCenters[16];
colorCenters[18] = new SquidColorCenter(new Filters.DistinctRedGreenFilter());
colorCenters[19] = colorCenters[18];
batch = new SpriteBatch();
width = 90;
height = 26;
totalWidth = width * 3;
totalHeight = height * 3;
//Only needed if totalWidth and/or totalHeight is 257 or larger
Coord.expandPoolTo(totalWidth, totalHeight);
dungeonGen = new DungeonGenerator(totalWidth, totalHeight, rng);
dungeonGen.addWater(16, 6);
dungeonGen.addGrass(15);
dungeonGen.addBoulders(5);
dungeonGen.addDoors(12, true);
//SerpentMapGenerator mix = new SerpentMapGenerator(totalWidth, totalHeight, rng, 0.35);
//mix.putCaveCarvers(2);
//mix.putWalledBoxRoomCarvers(3);
//mix.putWalledRoundRoomCarvers(2);
//char[][] mg = mix.generate();
decoDungeon = dungeonGen.generate(TilesetType.DEFAULT_DUNGEON);
// 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);
//NOTE: cellWidth and cellHeight are assigned values that are significantly larger than the corresponding sizes
//in the EverythingDemoLauncher's main method. Because they are scaled up by an integer here, they can be scaled
//down when rendered, allowing certain small details to appear sharper. This _only_ works with distance field,
//a.k.a. stretchable, fonts! INTERNAL_ZOOM is a tradeoff between rendering more pixels to increase quality (when
// values are high) or rendering fewer pixels for speed (when values are low). Using 2 seems to work well.
cellWidth = 10 * INTERNAL_ZOOM;
cellHeight = 22 * INTERNAL_ZOOM;
// 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.getStretchableSlabFont().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, decoDungeon);
//NOT USED CURRENTLY
//subCell is a SquidPanel, the same class that SquidLayers has for each of its layers, but we want to render
//certain effects on top of all other panels, which can't be done in the all-in-one-pass rendering of the grids
//in SquidLayers, though it could be done with a slight hassle if the effects are made into AnimatedEntity
//objects or Actors, then rendered separately like the monsters are (see render() below). It is called subCell
//because its text will be made smaller than a full cell, and appears in the upper left corner for things like
//the current health of the player and an '!' for alerted monsters.
//subCell = new SquidPanel(width, height, textFactory.copy(), fgCenter);
display.setAnimationDuration(0.11f);
// we use a "torchlight" effect where the field of view wavers slightly, so a slightly-yellow color like
// SColor.COSMIC_LATTE works well for the color of lighting. This tints everything very slightly yellow, but
// this is mostly unnoticeable for things like deep water that already have a vivid color here.
// if you check the JavaDocs on SColor.COSMIC_LATTE, you will (depending on IDE) probably see a nice preview
// of the actual color, which should be practically white but just a little closer to yellow.
display.setLightingColor(SColor.COSMIC_LATTE);
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 * 1.15f, cellHeight * 1.1f);
display.setTextSize(cellWidth * 1.15f, cellHeight * 1.1f);
//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.
//subCell.setTextSize(8 * INTERNAL_ZOOM, 16 * INTERNAL_ZOOM);
viewport = new StretchViewport(width * cellWidth, (height) * cellHeight);
messageViewport = new StretchViewport(width * cellWidth, (4) * cellHeight);
camera = viewport.getCamera();
stage = new Stage(viewport, batch);
messageStage = new Stage(messageViewport, 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, 0);
viewport.setScreenY((int)messages.getHeight());
//subCell.setPosition(0, messages.getHeight());
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;
// The display is almost all set up, so now we can tell it to use the filtered color centers we want.
// 8 is unfiltered. You can change this to 0-7 to use different filters, or press 'f' in play.
currentCenter = 8;
fgCenter = colorCenters[currentCenter * 2];
bgCenter = colorCenters[currentCenter * 2 + 1];
display.setFGColorCenter(fgCenter);
display.setBGColorCenter(bgCenter);
// it's more efficient to get random floors from a set containing only tightly-stored floor positions.
GreasedRegion placement = new GreasedRegion(bareDungeon, '.');
Coord pl = placement.singleRandom(rng);
display.setGridOffsetX(pl.x - (width >> 1));
display.setGridOffsetY(pl.y - (height >> 1));
fg = display.getForegroundLayer();
placement.remove(pl);
int numMonsters = 50;
monsters = new SpatialMap<>(numMonsters);
for (int i = 0; i < numMonsters; i++) {
Coord monPos = placement.singleRandom(rng);
placement = placement.remove(monPos);
monsters.put(monPos, i, new Monster(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);
res = DungeonUtility.generateResistances(decoDungeon);
fovmap = fov.calculateFOV(res, pl.x, pl.y, 8, Radius.SQUARE);
getToPlayer = new DijkstraMap(decoDungeon, DijkstraMap.Measurement.CHEBYSHEV);
getToPlayer.rng = rng;
getToPlayer.setGoal(pl);
getToPlayer.scan(null);
player = display.animateActor(pl.x, pl.y, '@',
fgCenter.loopingGradient(SColor.CAPE_JASMINE, SColor.HAN_PURPLE, 45), 1.5f, false);
// fgCenter.filter(display.getPalette().get(30)));
cursor = Coord.get(-1, -1);
toCursor = new ArrayList<>(10);
awaitedMoves = new ArrayList<>(10);
//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
//EUCLIDEAN value is used, which allows 8 directions of movement but prefers orthogonal moves, unless a
//diagonal move is clearly closer "as the crow flies." Alternatives are CHEBYSHEV, which allows 8 directions of
//movement at the same cost for all directions, and MANHATTAN, which means 4-way movement only, no diagonals.
playerToCursor = new DijkstraMap(decoDungeon, DijkstraMap.Measurement.EUCLIDEAN);
//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(pl);
playerToCursor.scan(null);
final int[][] initialColors = DungeonUtility.generatePaletteIndices(decoDungeon),
initialBGColors = DungeonUtility.generateBGPaletteIndices(decoDungeon);
colors = new Color[totalWidth][totalHeight];
bgColors = new Color[totalWidth][totalHeight];
ArrayList<Color> palette = display.getPalette();
bgColor = SColor.DARK_SLATE_GRAY;
for (int i = 0; i < totalWidth; i++) {
for (int j = 0; j < totalHeight; 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[decoDungeon.length][decoDungeon[0].length];
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 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;
}
case 'f':
case 'F': {
currentCenter = (currentCenter + 1) % 10;
//currentCenter = (currentCenter + 1 & 1) + 8; // for testing red-green color blindness filter
// 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;
}
case 'r': // red green color blindness mode on
{
changingColors = false;
fgCenter = colorCenters[18];
bgCenter = colorCenters[19];
display.setFGColorCenter(fgCenter);
display.setBGColorCenter(bgCenter);
break;
}
case 'R': // red green color blindness mode off
{
changingColors = false;
fgCenter = colorCenters[16];
bgCenter = colorCenters[17];
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) {
int sx = screenX + display.getGridOffsetX(), sy = screenY + display.getGridOffsetY();
if (fovmap[sx][sy] > 0.0 && awaitedMoves.isEmpty()) {
if (toCursor.isEmpty()) {
cursor = Coord.get(sx, sy);
//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);
//findPathPreScanned includes the current cell (goal) by default, which is helpful when
// you're finding a path to a monster or loot, and want to bump into it, but here can be
// confusing because you would "move into yourself" as your first move without this.
// Getting a sublist avoids potential performance issues with removing from the start of an
// ArrayList, since it keeps the original list around and only gets a "view" of it.
if(!toCursor.isEmpty())
toCursor = toCursor.subList(1, toCursor.size());
}
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.isEmpty())
return false;
int sx = screenX + display.getGridOffsetX(), sy = screenY + display.getGridOffsetY();
if((sx < 0 || sx >= totalWidth || sy < 0 || sy >= totalHeight)
|| (cursor.x == sx && cursor.y == sy))
{
return false;
}
if (fovmap[sx][sy] > 0.0) {
cursor = Coord.get(sx, sy);
//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);
//findPathPreScanned includes the current cell (goal) by default, which is helpful when
// you're finding a path to a monster or loot, and want to bump into it, but here can be
// confusing because you would "move into yourself" as your first move without this.
// Getting a sublist avoids potential performance issues with removing from the start of an
// ArrayList, since it keeps the original list around and only gets a "view" of it.
if(!toCursor.isEmpty())
toCursor = toCursor.subList(1, toCursor.size());
}
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, messageStage, input));
//subCell.setOffsetY(messages.getGridHeight() * cellHeight);
// 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
messageStage.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 xmod
* @param ymod
*/
private void move(int xmod, int ymod) {
clearHelp();
if (health <= 0) return;
int newX = player.gridX + xmod, newY = player.gridY + ymod;
float midX = player.gridX + xmod * 0.5f, midY = player.gridY + ymod * 0.5f;
if (newX >= 0 && newY >= 0 && newX < totalWidth && newY < totalHeight
&& 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);
//player.gridX = newX;
//player.gridY = newY;
monsters.remove(Coord.get(newX, newY));
//display.setGridOffsetX(newX - (width >> 1));
//display.setGridOffsetY(newY - (height >> 1));
//display.slideWorld(xmod, ymod, -1);
final Vector3 pos = camera.position.cpy(), original = camera.position.cpy(),
nextPos = camera.position.cpy().add(
midX > totalWidth - (width + 1) * 0.5f || midX < (width + 1) * 0.5f ? 0 : (xmod * cellWidth),
midY > totalHeight - (height + 1) * 0.5f || midY < (height + 1) * 0.5f ? 0 : (-ymod * cellHeight),
0);
display.slide(player, newX, newY);
display.addAction(
new TemporalAction(display.getAnimationDuration()) {
@Override
protected void update(float percent) {
pos.lerp(nextPos, percent);
camera.position.set(pos);
pos.set(original);
camera.update();
}
@Override
protected void end() {
super.end();
display.setGridOffsetX(player.gridX - (width >> 1));
display.setGridOffsetY(player.gridY - (height >> 1));
camera.position.set(original);
camera.update();
}
});
}
phase = Phase.PLAYER_ANIM;
}
}
// check if a monster's movement would overlap with another monster.
private boolean checkOverlap(Monster mon, int x, int y, ArrayList<Coord> futureOccupied) {
if (monsters.containsPosition(Coord.get(x, y)) && !mon.equals(monsters.get(Coord.get(x, y))))
return true;
for (Coord p : futureOccupied) {
if (x == p.x && y == p.y)
return true;
}
return false;
}
private void postMove() {
phase = Phase.MONSTER_ANIM;
Coord[] playerArray = {Coord.get(player.gridX, player.gridY)};
OrderedSet<Coord> monplaces = monsters.positions();
int monCount = monplaces.size();
// 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;
for(int ci = 0; ci < monCount; ci++)
{
Coord pos = monplaces.removeFirst();
Monster mon = monsters.get(pos);
//mon.entity.actor.setPosition(fg.adjustX(pos.x, false), fg.adjustY(pos.y));
// monster values are used to store their aggression, 1 for actively stalking the player, 0 for not.
if (mon.state > 0 || fovmap[pos.x][pos.y] > 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) + "\"");
}
getToPlayer.clearGoals();
nextMovePositions = getToPlayer.findPath(1, monplaces, null, pos, playerArray);
if (nextMovePositions != null && !nextMovePositions.isEmpty()) {
Coord tmp = nextMovePositions.get(0);
// if we would move into the player, instead damage the player and give newMons the current
// position of this monster.
if (tmp.x == player.gridX && tmp.y == player.gridY) {
display.tint(player.gridX, player.gridY, SColor.PURE_CRIMSON, 0, 0.415f);
health--;
//player.setText("" + health);
monsters.positionalModify(pos, mon.change(1));
monplaces.add(pos);
}
// 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);
}*/
monsters.positionalModify(pos, mon.change(1));
monsters.move(pos, tmp);
display.slide(mon.entity, tmp.x, tmp.y);
//mon.entity.gridX = tmp.x;
//mon.entity.gridY = tmp.y;
//mon.entity.actor.setPosition(fg.adjustX(tmp.x, false), fg.adjustY(tmp.y));
monplaces.add(tmp);
}
} else {
monsters.positionalModify(pos, mon.change(1));
monplaces.add(pos);
}
/*
// 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 = 9990.0;
Direction[] ds = new Direction[8];
rng.shuffle(Direction.OUTWARDS, ds);
for (Direction d : ds) {
Coord tmp = pos.translate(d);
if (monPathMap[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 = monPathMap[tmp.x][tmp.y];
choice = d;
}
}
if (choice != null) {
Coord tmp = pos.translate(choice);
// if we would move into the player, instead damage the player and give newMons the current
// position of this monster.
if (tmp.x == player.gridX && tmp.y == player.gridY) {
display.tint(player.gridX, player.gridY, SColor.PURE_CRIMSON, 0, 0.415f);
health--;
//player.setText("" + health);
monsters.positionalModify(pos, mon.change(1));
}
// otherwise store the new position in newMons.
else {
nextMovePositions.add(tmp);
monsters.positionalModify(pos, mon.change(1));
monsters.move(pos, tmp);
display.slide(mon.entity, tmp.x, tmp.y);
}
} else {
monsters.positionalModify(pos, mon.change(1));
}
*/
}
else {
monplaces.add(pos);
}
}
}
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);
IColoredString<Color> helping4 = new IColoredString.Impl<>("Each Я is an AЯMED GUAЯD; bump into them to kill them.", Color.WHITE);
IColoredString<Color> helping5 = new IColoredString.Impl<>("If an Я starts its turn next to where you just moved, you take damage.", 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);
text.add(helping4);
text.add(helping5);
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;
int offsetX = display.getGridOffsetX(), offsetY = display.getGridOffsetY();
// this will very occasionally go from very high to very low, but if a very large long for time is multiplied
// by a float, then you generally will get Float.POSITIVE_INFINITY, and similarly for some doubles. Infinite
// results are not good for the smooth noise we use the current time for! We want the time to go up slowly and
// steadily, so the animation of the "torchlight" effect looks right.
long tm = System.currentTimeMillis() & 0xfffffff;
for (int i = -1, ci = Math.max(0, offsetX-1); i <= width && ci < totalWidth; i++, ci++) {
for (int j = -1, cj = Math.max(0, offsetY-1); j <= height && cj < totalHeight; j++, cj++) {
overlapping = monsters.containsPosition(Coord.get(ci, cj)) || (player.gridX == ci && player.gridY == cj);
// 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 brighter at +75 lightness and 0.0 being rather dark at -105.
if (fovmap[ci][cj] > 0.0) {
seen[ci][cj] = true;
display.put(ci, cj, (overlapping) ? ' ' : lineDungeon[ci][cj], fgCenter.filter(colors[ci][cj]), bgCenter.filter(bgColors[ci][cj]),
lights[ci][cj] + (int) (-105 +
180 * (fovmap[ci][cj] * (1.0 + 0.2 * SeededNoise.noise(ci * 0.2, cj * 0.2, tm * 0.001, 10000)))));
// 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(ci, cj, lineDungeon[ci][cj], fgCenter.filter(colors[ci][cj]), bgCenter.filter(bgColors[ci][cj]), -140);
}
}
}
Coord pt;
for (int i = 0; i < toCursor.size(); i++) {
pt = toCursor.get(i);
// 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]));
}
messages.put(width - 10 >> 1, 0, "Health: " + health, SColor.RED_PIGMENT);
//if(pt != null)
// display.putString(0, 0, String.valueOf(monPathMap[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);
//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 + fg.getGridOffsetX(), height / 2 - 10 + fg.getGridOffsetY(), " THE TSAR WILL HAVE YOUR HEAD! ");
display.putBoxedString(width / 2 - 18 + fg.getGridOffsetX(), height / 2 - 5 + fg.getGridOffsetY(), " AS THE OLD SAYING GOES, ");
display.putBoxedString(width / 2 - lang.length() / 2 + fg.getGridOffsetX(), height / 2 + fg.getGridOffsetY(), lang);
display.putBoxedString(width / 2 - 18 + fg.getGridOffsetX(), height / 2 + 5 + fg.getGridOffsetY(), " q to quit. ");
// because we return early, we still need to draw.
messageViewport.apply(false);
messageStage.draw();
viewport.apply(false);
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);
// 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(m);
playerToCursor.scan(null);
}
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();
messageViewport.apply(false);
messageStage.act();
messageStage.draw();
// stage has its own batch and must be explicitly told to draw(). this also causes it to act().
stage.act();
viewport.apply(false);
stage.draw();
//subCell.erase();
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);
//subCell.put(player.gridX, player.gridY, Character.forDigit(health, 10), SColor.DARK_PINK);
for (Monster mon : monsters) {
// monsters are only drawn if within FOV.
if (fovmap[mon.entity.gridX][mon.entity.gridY] > 0.0) {
display.drawActor(batch, 1.0f, mon.entity);
//if (mon.state > 0)
// subCell.put(mon.entity.gridX, mon.entity.gridY, '!', SColor.DARK_RED);
}
}
//subCell.draw(batch, 1.0F);
// 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);
// 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 / this.width;
// total new screen height in pixels divided by total number of rows on the screen
currentZoomY = height * 1f / (this.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, this.width, this.height, 0, 0, width, height);
currentZoomX = cellWidth / currentZoomX;
currentZoomY = cellHeight / currentZoomY;
input.update(width, height, false);
messageViewport.update(width, height, false);
messageViewport.setScreenBounds(0, 0, width, (int)messages.getHeight());
viewport.update(width, height, false);
viewport.setScreenBounds(0, (int)messages.getHeight(), width, height - (int)messages.getHeight());
}
}