/*
* Copyright (c) 2003-onwards Shaven Puppy Ltd
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* * Neither the name of 'Shaven Puppy' nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package worm;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
import org.lwjgl.util.Rectangle;
/**
* A game map. Note that this is not a Resource.
* @author Cas
*/
public class GameMap implements Serializable {
private static final long serialVersionUID = 1L;
/** Max layers */
public static final int LAYERS = 3;
/** Fill tile index */
private final short fill;
/** The map, marked transient because we serialize it specially */
private transient MapClip map;
/** Visibility of bottom left corner of each tile */
private transient IntGrid visibility;
/** Gidrah occupation (origin at -1, -1, and bigger than main map by 1) */
private transient IntGrid occupied;
/** Gidrah attacking (origin at -1, -1, and bigger than main map by 1) */
private transient IntGrid attacking;
/** Danger level (total turret coverage) */
private transient IntGrid danger;
/** Cost (used to calc actual movement speed of gids) */
private transient IntGrid cost;
/** Difficulty (used by gids to guess actual movement speed; affected by barricades) */
private transient IntGrid difficulty;
/** Fade level */
private transient IntGrid fade;
/** Listener */
private transient MapListener listener;
/**
* C'tor
*/
public GameMap(int width, int height, short fill) {
this.fill = fill;
map = new MapClip(width, height, LAYERS, fill);
visibility = new IntGrid(width + 1, height + 1, 0);
occupied = new IntGrid(width + 2, height + 2, 0);
danger = new IntGrid(width, height, 0);
attacking = new IntGrid(width + 2, height + 2, 0);
fade = new IntGrid(width, height, 0);
cost = new IntGrid(width, height, 0);
difficulty = new IntGrid(width, height, 0);
}
/**
* @return Returns the width.
*/
public int getWidth() {
return map.getWidth();
}
/**
* @return Returns the height.
*/
public int getHeight() {
return map.getHeight();
}
/**
* Gets the danger level of a tile - this is the number of nearby turrets.
* @param x
* @param y
* @return a visibility value
*/
public int getDanger(int x, int y) {
return danger.getValue(x, y);
}
/**
* Gets the cost of traversing a tile
* @param x
* @param y
* @return the cost of tile traversal
*/
public int getCost(int x, int y) {
return cost.getValue(x, y);
}
/**
* Gets the extra guessed difficulty of traversing a tile
* @param x
* @param y
* @return the cost of tile traversal
*/
public int getDifficulty(int x, int y) {
return difficulty.getValue(x, y);
}
/**
* Gets the visibility of the bottom left corner of a tile.
* @param x
* @param y
* @return a visibility value
*/
public int getVisibility(int x, int y) {
return visibility.getValue(x, y);
}
public int getFade(int x, int y) {
return fade.getValue(x, y);
}
public int getTFade(int x, int y) {
return Math.max(0, fade.getValue(x, y) - 4);
}
public int getFFade(int x, int y) {
return Math.max(0, fade.getValue(x, y) - 2);
}
public void setFade(int x, int y, int value) {
fade.setValue(x, y, value);
}
/**
* Determines if a square is occupied by a gidrah
* @param x
* @param y
* @return true if it is
*/
public boolean isOccupied(int x, int y) {
return occupied.getValue(x + 1, y + 1) != 0;
}
public boolean isAttacking(int x, int y) {
return attacking.getValue(x + 1, y + 1) != 0;
}
/**
* Sets the occupied state of a square. If the square must not already be occupied.
* @param x
* @param y
* @param occ
*/
public void setOccupied(int x, int y) {
occupied.setValue(x + 1, y + 1, 1);
}
public void clearOccupied(int x, int y) {
occupied.setValue(x + 1, y + 1, 0);
}
public void setAttacking(int x, int y) {
attacking.setValue(x + 1, y + 1, 1);
}
public void clearAttacking(int x, int y) {
attacking.setValue(x + 1, y + 1, 0);
}
/**
* @param x
* @param y
* @param newValue
*/
public void setVisibility(int x, int y, int newValue) {
visibility.setValue(x, y, newValue);
}
/**
* @param x
* @param y
* @param newValue
*/
public void setCost(int x, int y, int newValue) {
cost.setValue(x, y, newValue);
}
/**
* @param x
* @param y
* @param newValue
*/
public void setDifficulty(int x, int y, int newValue) {
difficulty.setValue(x, y, newValue);
}
/**
* @param x
* @param y
* @param newValue
*/
public void setDanger(int x, int y, int newValue) {
danger.setValue(x, y, newValue);
}
/**
* Stash in a TileInfo
* @param x
* @param y
* @param tileInfo Destination, or null
* @return tileInfo, or a new TileInfo if TileInfo was null
*/
public TileInfo toTileInfo(int x, int y, TileInfo tileInfo) {
if (tileInfo == null) {
tileInfo = new TileInfo();
}
tileInfo.set(getTile(x, y, tileInfo.get()));
return tileInfo;
}
/**
* Get the tiles at a particular location. If the location is out of bounds, the tiles are nulled
* @param x
* @param y
* @param dest Destination array of Tiles, or null
* @return dest[], or a new array of Tiles, or null
*/
public Tile[] getTile(int x, int y, Tile[] dest) {
if (dest == null) {
dest = new Tile[LAYERS];
}
for (int i = 0; i < LAYERS; i ++) {
dest[i] = map.getTile(x, y, i);
}
return dest;
}
/**
* Get the tile at a particular location and layer
* @param x
* @param y
* @param z
* @return
* @see droid.MapClip#getTile(int, int, int)
*/
public Tile getTile(int x, int y, int z) {
return map.getTile(x, y, z);
}
/**
* Sets a tile at a particular location. If out of bounds, this is a no-op.
* @param x
* @param y
* @param newTile the new tile to set (may not be null)
*/
public void setTile(final int x, final int y, final int z, final Tile newTile) {
// Will anything actually happen?
if (!isValidDraw(x, y, z, newTile)) {
// No, so just bomb out
return;
}
// Remember the old tile for a sec..
Tile oldTile = map.getTile(x, y, z);
oldTile.onCleared(this, x, y);
// Draw onto the map
map.setTile(x, y, z, newTile);
newTile.onDrawn(this, x, y);
// Calculate tile rules if the map changed on layer 0
if (z == 0 && groupChanged(newTile.getGroup(), oldTile.getGroup())) {
// Recursively start twiddling neighbours
recalculateNeighbours(x, y);
}
// Calculate visibility
calcVis(x, y);
calcVis(x + 1, y);
calcVis(x + 1, y + 1);
calcVis(x, y + 1);
calcVis(x - 1, y + 1);
calcVis(x - 1, y);
calcVis(x - 1, y - 1);
calcVis(x, y - 1);
calcVis(x + 1, y - 1);
if (listener != null) {
listener.onChanged(x, y);
}
}
private void calcVis(int x, int y) {
outer: for (int z = 0; z < LAYERS; z ++) {
for (int yy = y - 1; yy <= y; yy ++) {
for (int xx = x - 1; xx <= x; xx ++) {
Tile t = getTile(xx, yy, z);
if (t == null || !t.isSolid()) {
continue outer;
}
}
}
// This whole layer is solid at this point, so we're invisible to the outside.
setVisibility(x, y, 0);
return;
}
setVisibility(x, y, 1);
}
/**
* Checks to see if a coordinate is in bounds
* @param x
* @param y
* @return true if (x, y) is within the boundary of the map
*/
private boolean isInBounds(int x, int y) {
return x >= 0 && y >= 0 && x < getWidth() && y < getHeight();
}
/**
* Determines whether a setTile operation will actually have any effect
* @param x
* @param y
* @param z
* @param newTile
* @return true if a draw will have an effect
*/
private boolean isValidDraw(final int x, final int y, final int z, final Tile newTile) {
// Bomb out straight away if it's out-of-bounds
if (!isInBounds(x, y)) {
return false;
}
// Ensure no-one draws items on solid floortiles
if (map.getTile(x, y, 0).isSolid() && z > 0 && newTile != Tile.getEmptyTile()) {
return false;
}
// Looks like a draw will occur
return true;
}
/**
* Clear the items at the specified position (thats things in layer 2 and above)
* @param x
* @param y
*/
public void clearItem(final int x, final int y) {
for (int i = 2; i < LAYERS; i ++) {
setTile(x, y, i, Tile.getEmptyTile());
}
}
/**
* @param g1
* @param g2
* @return
*/
private boolean groupChanged(String g1, String g2) {
if (g1 == g2) {
return false;
}
if (g1 == null || g2 == null) {
return true;
}
return !g1.equals(g2);
}
/**
* Recalculate tile's neighbours. We do this by reading and drawing the
* NSEW neighbours of a tile. Tile rules then cause the appropriate tile
* to be drawn instead if necessary.
* @param x
* @param y
*/
private void recalculateNeighbours(int x, int y) {
Tile t = map.getTile(x, y + 1, 0);
if (t != null) {
t.toMap(this, x, y + 1, false);
}
t = map.getTile(x + 1, y, 0);
if (t != null) {
t.toMap(this, x + 1, y, false);
}
t = map.getTile(x, y - 1, 0);
if (t != null) {
t.toMap(this, x, y - 1, false);
}
t = map.getTile(x - 1, y, 0);
if (t != null) {
t.toMap(this, x - 1, y, false);
}
}
/**
* Override standard object writing so we can gzip the stream for MapStorage
* @param stream
* @throws IOException
*/
private void writeObject(ObjectOutputStream stream) throws IOException {
stream.defaultWriteObject();
GZIPOutputStream gzos = new GZIPOutputStream(stream);
ObjectOutputStream oos = new ObjectOutputStream(gzos);
oos.writeObject(map);
oos.writeObject(visibility);
oos.writeObject(occupied);
oos.writeObject(danger);
oos.writeObject(attacking);
oos.writeObject(fade);
oos.writeObject(cost);
oos.writeObject(difficulty);
oos.flush();
gzos.finish();
}
/**
* Override standard object reading to read the zipped MapStorage
* @param stream
* @throws IOException
* @throws ClassNotFoundException
*/
private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException {
stream.defaultReadObject();
GZIPInputStream gzis = new GZIPInputStream(stream, 1024 * 1024);
ObjectInputStream ois = new ObjectInputStream(gzis);
map = (MapClip) ois.readObject();
visibility = (IntGrid) ois.readObject();
occupied = (IntGrid) ois.readObject();
danger = (IntGrid) ois.readObject();
attacking = (IntGrid) ois.readObject();
fade = (IntGrid) ois.readObject();
cost = (IntGrid) ois.readObject();
difficulty = (IntGrid) ois.readObject();
}
/**
* Given a set of pixel coordinates, ensure that the rectangle is in a clear bit of map, that is,
* with no solid or impassable terrain under it.
* @param bounds A {@link Rectangle} in pixel coordinates
* @return true if the rectangle lies in a clear space
*/
public boolean isClearPX(Rectangle bounds) {
for (int y = bounds.getY() / MapRenderer.TILE_SIZE; y <= (bounds.getHeight() - 1) / MapRenderer.TILE_SIZE; y ++) {
for (int x = bounds.getX() / MapRenderer.TILE_SIZE; x <= (bounds.getWidth() - 1) / MapRenderer.TILE_SIZE; x ++) {
for (int z = 0; z < GameMap.LAYERS; z ++) {
Tile tile = map.getTile(x, y, z);
if (tile != null && (tile.isSolid() || tile.isImpassable())) {
return false;
}
}
}
}
return true;
}
public void setListener(MapListener listener) {
this.listener = listener;
}
}