package net.alcuria.umbracraft.engine.map; import net.alcuria.umbracraft.Config; import net.alcuria.umbracraft.Db; import net.alcuria.umbracraft.Game; import net.alcuria.umbracraft.definitions.map.MapDefinition; import net.alcuria.umbracraft.definitions.tileset.TilesetDefinition; import com.badlogic.gdx.graphics.g2d.TextureRegion; import com.badlogic.gdx.math.MathUtils; import com.badlogic.gdx.math.Rectangle; import com.badlogic.gdx.utils.Array; import com.badlogic.gdx.utils.Disposable; /** An internal representation of a playable, explorable map. It consists largely * of an array of {@link Layer} objects and an array of {@link TextureRegion} * objects to render the map. * @author Andrew Keturi */ public class Map implements Disposable { private static final boolean debugRenderMode = false; private static int staticRow; private static int timer; private int[][] altMap; private int height; private Array<Layer> layers; private MapDefinition mapDef; private int maxAlt; private String name; private AutoTileAttributes[][] overlayTypeMap, typeMap; private TilesetDefinition tilesetDefinition; private TileView tileView; private int width; /** Creates the map, building all layers and calculating terrain tiles so the * map may be rendered. * @param id the map's id from the {@link Db}. */ public void create(String id) { if (id == null) { throw new NullPointerException("id cannot be null. Perhaps an area node's mapDefinition field is null?"); } name = id; // create alt map from definition mapDef = Game.db().map(id); if (mapDef == null) { throw new NullPointerException("Map not found: " + id); } // load the tileset if (mapDef.tileset == null) { throw new NullPointerException("Map '" + id + "' must define a tileset"); } tilesetDefinition = Game.db().tileset(mapDef.tileset); if (tilesetDefinition == null) { throw new NullPointerException("Cannot find tileset: " + mapDef.tileset); } String filename = tilesetDefinition.filename; width = mapDef.getWidth(); height = mapDef.getHeight(); altMap = new int[width][height]; overlayTypeMap = new AutoTileAttributes[width][height]; typeMap = new AutoTileAttributes[width][height]; for (int i = 0; i < width; i++) { for (int j = 0; j < height; j++) { altMap[i][j] = mapDef.tiles.get(i).get(height - j - 1).altitude; // only initialize type indices where we know there is a terrain type if (mapDef.tiles.get(i).get(height - j - 1).type > 0) { typeMap[i][j] = new AutoTileAttributes(tilesetDefinition); typeMap[i][j].setType(mapDef.tiles.get(i).get(height - j - 1).type, false); } if (mapDef.tiles.get(i).get(height - j - 1).overlayType > 0) { overlayTypeMap[i][j] = new AutoTileAttributes(tilesetDefinition); overlayTypeMap[i][j].setType(mapDef.tiles.get(i).get(height - j - 1).overlayType, true); } } } // build map's terrains int[] terrains = { tilesetDefinition.terrain1, tilesetDefinition.terrain2, tilesetDefinition.overlay }; // this second terrain FUCKS UP my autotiles boolean[] isOverlay = { false, false, true }; for (int k = 0; k < terrains.length; k++) { final int terrain = terrains[k]; if (terrain > 0) { // go thru and set everything for (int i = 0; i < width; i++) { for (int j = 0; j < height; j++) { //AK /** first, initialize the AutoTileAttributes as needed, * for areas with nonzero types. Next, At this point we * need to iterate thru the map and look at the type in * the new AutoTileAttributes class. If it's non null, * we need to look at the adjacent tiles in order to set * some additional smart attributes for that tile * (namely the four corners) */ if ((!isOverlay[k] && typeMap[i][j] != null && typeMap[i][j].isInitialized()) || (isOverlay[k] && overlayTypeMap[i][j] != null && overlayTypeMap[i][j].isInitialized())) { // get surrounding mask // top topright right rightdown _ down downleft left lefttop int[] dX = { 0, 1, 1, 1, 0, -1, -1, -1 }; int[] dY = { 1, 1, 0, -1, -1, -1, 0, 1 }; int value = 0b0000_0000; int mask = 0b1000_0000; int alt = altMap[i][j]; for (int l = 0; l < dX.length; l++) { final int typeAt = isOverlay[k] ? getOverlayTypeAt(i + dX[l], j + dY[l]) : getTypeAt(i + dX[l], j + dY[l]); if (typeAt == terrain) {// && alt == getAltitudeAt(i + dX[l], j + dY[l])) { value = value ^ mask; } mask = mask >>> 1; } setTypeAt(isOverlay[k] ? overlayTypeMap : typeMap, i, j, terrain, value); } } } } } // create tiles from definition tileView = new TileView(filename, tilesetDefinition); // create list of all altitudes Array<Integer> altitudes = new Array<Integer>(); for (int i = 0; i < altMap.length; i++) { for (int j = 0; j < altMap[0].length; j++) { if (!altitudes.contains(Integer.valueOf(altMap[i][j]), false)) { altitudes.add(new Integer(altMap[i][j])); } } } altitudes.sort(); maxAlt = altitudes.get(altitudes.size - 1); // build the layers layers = new Array<Layer>(); for (Integer altitude : altitudes) { Layer layer = new Layer(); layer.alt = altitude; layer.data = new Tile[width][height]; for (int i = 0; i < altMap.length; i++) { for (int j = -getMaxAltitude(); j < altMap[0].length; j++) { if (getAltitudeAt(i, j) >= altitude) { if (isInBounds(i, j + altitude)) { layer.data[i][j + altitude] = new Tile(createEdge(i, j, altitude), layer.alt); // create edge final int overlay = getOverlayTypeAt(i, j); if (overlay == tilesetDefinition.overlayPiece1 || overlay == tilesetDefinition.overlayPiece2 || overlay == tilesetDefinition.overlayPiece3 || overlay == tilesetDefinition.overlayPiece4) { // 1 should be the forest overlay layer.data[i][j + altitude].overId = overlay; } } // check if we need to create a wall if (j - 1 >= 0 && j - 1 < altMap[0].length) { int drop = (altitude - getAltitudeAt(i, j - 1)); while (drop > 0) { if (isInBounds(i, (j + altitude) - drop)) { if (getTypeAt(i, j) == tilesetDefinition.treeWall) { layer.data[i][(j + altitude) - drop] = new Tile(createTreeWall(i, j, drop, altitude), layer.alt); } else { layer.data[i][(j + altitude) - drop] = new Tile(createWall(i, j, drop, altitude, getAltitudeAt(i, j - 1)), layer.alt); } } drop--; } } } } } layers.add(layer); } } private int createEdge(int i, int j, int altitude) { if (tilesetDefinition == null) { return 0; } if (getTypeAt(i, j - 1) == tilesetDefinition.stairs) { return getTypeAt(i, j) == tilesetDefinition.stairs ? tilesetDefinition.stairs + 2 : tilesetDefinition.floor; } // top right down left int mask = 0b0000; // we check if the altitude drops down OR we're adjacent to a tree and it's a drop down to the tree if (getAltitudeAt(i, j + 1) < altitude || (getTypeAt(i, j + 1) == tilesetDefinition.treeWall && getAltitudeAt(i, j + 1) - getAltitudeAt(i, j) < 4)) { mask = mask ^ 0b1000; } if (getAltitudeAt(i + 1, j) < altitude || (getTypeAt(i + 1, j) == tilesetDefinition.treeWall && getAltitudeAt(i + 1, j) - getAltitudeAt(i, j) < 4)) { mask = mask ^ 0b0100; } if (getAltitudeAt(i, j - 1) < altitude || (getTypeAt(i, j - 1) == tilesetDefinition.treeWall && getAltitudeAt(i, j - 1) - getAltitudeAt(i, j) < 4)) { mask = mask ^ 0b0010; } if (getAltitudeAt(i - 1, j) < altitude || (getTypeAt(i - 1, j) == tilesetDefinition.treeWall && getAltitudeAt(i - 1, j) - getAltitudeAt(i, j) < 4)) { mask = mask ^ 0b0001; } // now to switch on every possibility switch (mask) { case 0b0001: return tilesetDefinition.edge - 1; case 0b0010: return tilesetDefinition.edge + 16; case 0b0100: return tilesetDefinition.edge + 1; case 0b1000: return tilesetDefinition.edge - 16; case 0b1100: return tilesetDefinition.edge - 15; case 0b1001: return tilesetDefinition.edge - 17; case 0b0110: return tilesetDefinition.edge + 17; case 0b0011: return tilesetDefinition.edge + 15; } //TODO: More cases (0101, 1010, 1111, etc) return getTypeAt(i, j); } private int createTreeWall(int i, int j, int drop, int altitude) { Game.log(String.format("Create tree wall: i=%d j=%d drop=%d alt=%d", i, j, drop, altitude)); int calculatedId; if (getAltitudeAt(i - 1, j) < altitude) { calculatedId = tilesetDefinition.treeWall + 1; } else if (getAltitudeAt(i + 1, j) < altitude) { calculatedId = tilesetDefinition.treeWall + 6; } else { int left = i; while (getAltitudeAt(left, j) == altitude && left > 0) { left--; } if ((i - left) % 2 == 0) { calculatedId = tilesetDefinition.treeWall + 2; } else { calculatedId = tilesetDefinition.treeWall + 3; } } calculatedId = calculatedId - ((4 - drop) * (Config.tilesetWidth / Config.tileWidth)); return calculatedId; } private int createWall(int i, int j, int drop, int altitude, int baseAlt) { if (getTypeAt(i, j - 1) == tilesetDefinition.stairs) { return tilesetDefinition.stairs + 1; } if (tilesetDefinition.legacyWalls) { if (drop == altitude - baseAlt) { // lower walls if (getAltitudeAt(i - 1, j) < altitude || getTypeAt(i - 1, j) == tilesetDefinition.treeWall) { return tilesetDefinition.wall - 1; } else if (getAltitudeAt(i + 1, j) < altitude || getTypeAt(i + 1, j) == tilesetDefinition.treeWall) { return tilesetDefinition.wall + 1; } else { return tilesetDefinition.wall; } } else { // upper walls if (getAltitudeAt(i - 1, j) < altitude || getTypeAt(i - 1, j) == tilesetDefinition.treeWall) { return tilesetDefinition.wall - 17; } else if (getAltitudeAt(i + 1, j) < altitude || getTypeAt(i + 1, j) == tilesetDefinition.treeWall) { return tilesetDefinition.wall - 15; } else { return tilesetDefinition.wall - 16; } } } else { final int cols = Config.tilesetWidth / Config.tileWidth; int wall = cols * tilesetDefinition.wallHeight - (Math.min(drop, tilesetDefinition.wallHeight)) * cols; if (altitude - baseAlt == 1) { wall = 0; // this is a hack to fix cliffs of height 1, we should in general start with the bottom and work up but... } if (getAltitudeAt(i - 1, j) < altitude || getTypeAt(i - 1, j) == tilesetDefinition.treeWall) { return tilesetDefinition.wall - wall - 1; } else if (getAltitudeAt(i + 1, j) < altitude || getTypeAt(i + 1, j) == tilesetDefinition.treeWall) { return tilesetDefinition.wall - wall + 1; } else { return tilesetDefinition.wall - wall; } } } @Override public void dispose() { } /** @param f * @param g * @return the altitude at tile f, g */ public float getAltitudeAt(float f, float g) { return getAltitudeAt((int) f, (int) g); } /** Gets the altitude, in tiles, at some tile coordinates. * @param x x tile * @param y y tile * @return */ public int getAltitudeAt(int x, int y) { // clamp to the size of the map so it's assumed tiles outside the map are the same as edge tiles try { x = MathUtils.clamp(x, 0, altMap.length - 1); y = MathUtils.clamp(y, 0, altMap[0].length - 1); return altMap[x][y]; } catch (NullPointerException npe) { return 0; } } /** @return A rectangle to use as the bounds of the map. */ public Rectangle getBounds() { return new Rectangle(0, mapDef.bottomClamp * Config.tileWidth, Game.map().getWidth() * Config.tileWidth, Game.map().getHeight() * Config.tileWidth); } /** @return the current {@link TilesetDefinition} */ public TilesetDefinition getDefinition() { return tilesetDefinition; } /** @return the height (not altitude) of the map */ public int getHeight() { return height; } /** @return the tallest altitude on this map */ public int getMaxAltitude() { return maxAlt; } /** @return the name of the map */ public String getName() { return name; } /** Gets the overlay type at some tile coordinates and does bounds checking * too! * @param x x tile * @param y y tile * @return */ public int getOverlayTypeAt(int x, int y) { if (x >= 0 && x < altMap.length && y >= 0 && y < altMap[0].length && overlayTypeMap[x][y] != null) { return overlayTypeMap[x][y].getType(); } return 0; } /** Gets the terrain type at some tile coordinates and does bounds checking * too! * @param x x tile * @param y y tile * @return */ public int getTypeAt(int x, int y) { // clamp to the size of the map so it's assumed tiles outside the map are the same as edge tiles try { x = MathUtils.clamp(x, 0, altMap.length - 1); y = MathUtils.clamp(y, 0, altMap[0].length - 1); return typeMap[x][y].getType(); } catch (NullPointerException npe) { return 0; } } private int getWaterDefinition(int x, int y, int alt) { if (getAltitudeAt(x, y + 1) > alt) { if (getAltitudeAt(x - 1, y + 1) <= alt) { return tilesetDefinition.water + 1; } else if (getAltitudeAt(x + 1, y + 1) <= alt) { return tilesetDefinition.water + 3; } return tilesetDefinition.water + 2; } return tilesetDefinition.water; } public int getWidth() { return width; } /** @param x * @param y * @return <code>true</code> if the tile x,y is in bounds */ public boolean isInBounds(int x, int y) { return x >= 0 && x < altMap.length && y >= 0 && y < altMap[0].length; } /** @param x the x coordinate, in tiles * @param y the y coordinate, in tiles * @return <code>true</code> if the coordinates contain stairs */ public boolean isStairs(int x, int y) { return getTypeAt(x, y) == tilesetDefinition.stairs; } /** Renders every visible layer. * @param row the map row to render, in tiles * @param xOffset the camera offset in tiles, to ensure we only render tiles * visible in the x axis */ public void render(int row, int xOffset) { if (debugRenderMode) { timer = (timer + 1) % 600; if (timer == 0) { Game.log(staticRow + ""); staticRow = (staticRow + 1) % 10; } row = staticRow; } final int tileSize = Config.tileWidth; if (layers == null) { return; } for (int i = xOffset, n = xOffset + Config.viewWidth / Config.tileWidth + 1; i < n; i++) { int alt = getAltitudeAt(i, row); Tile[][] data = null; // we need to get the data for the tiles at this altitude // FIXME: no looping please for (int k = 0; k < layers.size; k++) { if (layers.get(k).alt == alt) { data = layers.get(k).data; break; } } if (data == null || row < 0 || row >= altMap[0].length) { return; } // prevents bottom rows from creeping up during rendering int drop = alt - getAltitudeAt(i, row + 1); // this works, but why? for (int j = alt; j >= drop; j--) { try { if (i >= 0 && i < data.length && row >= 0 && row < data[i].length && data[i][row + j] != null) { // if we have a special overlay here draw it if (typeMap[i][row] != null && typeMap[i][row].getType() != tilesetDefinition.stairs) { tileView.draw(typeMap[i][row], i * tileSize, row * tileSize + alt * tileSize); } else { // dont draw pink Game.batch().draw(tileView.get(data[i][row + j].id), (i * tileSize), (row * tileSize) + j * tileSize, tileSize, tileSize); } if (data[i][row + j].overId > 0) { Game.batch().draw(tileView.get(data[i][row + j].overId), (i * tileSize), (row * tileSize) + j * tileSize, tileSize, tileSize); } } } catch (ArrayIndexOutOfBoundsException | NullPointerException e2) { //FIXME: Halp. someting up with rendering very top and very bottom rows. //Game.log("render oob " + i + " " + j + " " + row); } } if (mapDef.waterLevel > alt || mapDef.waterLevel > getAltitudeAt(i, row + 1)) { //FIXME: kinda hacky, will need to rewrite rendering at some point to remove the concept of a "layer" -- its making things more complicated than they need to final int waterDefinition = getWaterDefinition(i, row, alt); if (waterDefinition > tilesetDefinition.water) { Game.batch().draw(tileView.get(waterDefinition), (i * tileSize), ((row + 1) * tileSize + mapDef.waterLevel * tileSize), tileSize, tileSize); } Game.batch().draw(tileView.get(tilesetDefinition.water), (i * tileSize), (row * tileSize + mapDef.waterLevel * tileSize), tileSize, tileSize); } } } public void renderOverlays(int xOffset, int yOffset) { final int tileSize = Config.tileWidth; for (int i = xOffset, n = xOffset + Config.viewWidth / Config.tileWidth + 1; i < n; i++) { for (int j = yOffset, m = yOffset + Config.viewWidth / Config.tileWidth + 1; j < m; j++) { final int overlayId = getOverlayTypeAt(i, j); if (overlayId > 0) { tileView.draw(overlayTypeMap[i][j], i * tileSize, j * tileSize + mapDef.overlayHeight * tileSize); if (j == 0) { // draw down for (int k = 1; k < 5; k++) { Game.batch().draw(tileView.get(overlayId), (i * tileSize), ((j - k) * tileSize) + mapDef.overlayHeight * tileSize, tileSize, tileSize); } } } } } } /** Given some tile coordinates, checks if they're valid and the tile type * there has not been defined., and if so, applies newType to those * coordinates. * @param x * @param y * @param newType * @param neighborMask */ private void setTypeAt(AutoTileAttributes[][] attribs, int x, int y, int newType, int neighborMask) { if (x >= 0 && x < altMap.length && y >= 0 && y < altMap[0].length && attribs[x][y] != null) { attribs[x][y].setMask(neighborMask); } } /** Updates the map * @param delta the time since the last frame */ public void update(float delta) { } }