/* * Copyright (c) 2014 tabletoptool.com team. * All rights reserved. This program and the accompanying materials * are made available under the terms of the GNU Public License v3.0 * which accompanies this distribution, and is available at * http://www.gnu.org/licenses/gpl.html * * Contributors: * rptools.com team - initial implementation * tabletoptool.com team - further development */ package com.t3.model.grid; import java.awt.Dimension; import java.awt.Graphics2D; import java.awt.Rectangle; import java.awt.geom.Area; import java.awt.image.BufferedImage; import java.util.List; import java.util.Map; import javax.swing.Action; import javax.swing.KeyStroke; import org.apache.log4j.Logger; import com.t3.client.AppPreferences; import com.t3.client.tool.PointerTool; import com.t3.client.ui.zone.ZoneRenderer; import com.t3.client.walker.ZoneWalker; import com.t3.guid.GUID; import com.t3.model.Asset; import com.t3.model.CellPoint; import com.t3.model.ModelChangeEvent; import com.t3.model.Token; import com.t3.model.TokenFootprint; import com.t3.model.TokenFootprint.OffsetTranslator; import com.t3.model.Zone; import com.t3.model.Zone.Event; import com.t3.model.ZonePoint; import com.t3.persistence.FileUtil; import com.t3.xstreamversioned.version.SerializationVersion; /** * Base class for grids. * * @author trevor */ @SerializationVersion(0) public abstract class Grid implements Cloneable { private static final Logger log = Logger.getLogger(Grid.class); /** * The minimum grid size (minimum on any dimension). The default value is 9 because the algorithm for determining * whether a given square cell can be entered due to fog blocking the cell is based on the cell being split into * 3x3, then the center further being split into 3x3; thus at least 9 pixels horizontally and vertically are * required. */ public static final int MIN_GRID_SIZE = 9; public static final int MAX_GRID_SIZE = 350; private static final Dimension NO_DIM = new Dimension(); private int offsetX = 0; private int offsetY = 0; private int size; private Zone zone; private Area cellShape; protected Map<KeyStroke, Action> movementKeys = null; public Grid() { setSize(AppPreferences.getDefaultGridSize()); } public Grid(Grid grid) { setSize(grid.getSize()); setOffset(grid.offsetX, grid.offsetY); } public void drawCoordinatesOverlay(Graphics2D g, ZoneRenderer renderer) { // Do nothing -- my default } /** * Set the facing options for tokens/objects on a grid. Each grid type can providing facings to the edges, the * vertices, both, or neither. * * If both are false, tokens on that grid will not be able to rotate with the mouse and keyboard controls for * setting facing. * * @param faceEdges * - Tokens can face edges. * @param faceVertices * - Tokens can face vertices. */ public void setFacings(boolean faceEdges, boolean faceVertices) { // Handle it in the individual grid types } public int[] getFacingAngles() { return null; } protected List<TokenFootprint> loadFootprints(String name, OffsetTranslator... translators) { Object obj = FileUtil.objFromResource(Asset.class,name); @SuppressWarnings("unchecked") List<TokenFootprint> footprintList = (List<TokenFootprint>) obj; for (TokenFootprint footprint : footprintList) { for (OffsetTranslator ot : translators) { footprint.addOffsetTranslator(ot); } } return footprintList; } public TokenFootprint getDefaultFootprint() { for (TokenFootprint footprint : getFootprints()) { if (footprint.isDefault()) { return footprint; } } // None specified, use the first return getFootprints().get(0); } public TokenFootprint getFootprint(GUID guid) { if (guid == null) { return getDefaultFootprint(); } for (TokenFootprint footprint : getFootprints()) { if (footprint.getId().equals(guid)) { return footprint; } } return getDefaultFootprint(); } public abstract List<TokenFootprint> getFootprints(); @Override public Object clone() throws CloneNotSupportedException { return super.clone(); // Grid newGrid = (Grid) super.clone(); // return newGrid; } /** * @return Coordinates in Cell-space of the ZonePoint */ public abstract CellPoint convert(ZonePoint zp); /** * @return A ZonePoint whose position within the cell depends on the grid type:<br> * <i>SquareGrid</i> - top right of cell (x_min, y_min)<br> * <i>HexGrid</i> - center of cell<br> * For HexGrids Use getCellOffset() to move ZonePoint from center to top right */ public abstract ZonePoint convert(CellPoint cp); public abstract GridCapabilities getCapabilities(); public int getTokenSpace() { return getSize(); } public double getCellWidth() { return 0; } public double getCellHeight() { return 0; } /** * @return The offset required to translate from the center of a cell to the top right (x_min, y_min) of the cell's * bounding rectangle. Used for non-square grids only.<br> * <br> * Why? Because mySquareGrid.convert(CellPoint cp) returns a ZonePoint in the top right corner(x_min, y_min) * of the square-cell, whereas myHexGrid.convert(CellPoint cp) returns a ZonePoint in the center of the * hex-cell. Thus adding the CellOffset allows us to position the ZonePoint returned by * myHexGrid.convert(CellPoint cp) in an equivalent position to that returned by * mySquareGrid.convert(CellPoint cp)....I think ;) */ public Dimension getCellOffset() { return NO_DIM; } public Zone getZone() { return zone; } public void setZone(Zone zone) { this.zone = zone; } public Area getCellShape() { return cellShape; } public BufferedImage getCellHighlight() { return null; } protected abstract Area createCellShape(int size); protected void setCellShape(Area cellShape) { this.cellShape = cellShape; } /** * @param Both * The grid's x and y offset components */ public void setOffset(int offsetX, int offsetY) { this.offsetX = offsetX; this.offsetY = offsetY; fireGridChanged(); } /** * @return The x component of the grid's offset. */ public int getOffsetX() { return offsetX; } /** * @return The y component of the grid's offset */ public int getOffsetY() { return offsetY; } public ZoneWalker createZoneWalker() { return null; } /** * Sets the grid size and creates the grid cell shape * * @param size * The size of the grid<br> * <i>SquareGrid</i> - edge length<br> * <i>HexGrid</i> - edge to edge diameter */ public void setSize(int size) { this.size = constrainSize(size); cellShape = createCellShape(size); fireGridChanged(); } /** * Constrains size to MIN_GRID_SIZE <= size <= MAX_GRID_SIZE * * @return The size after it has been constrained */ protected final int constrainSize(int size) { if (size < MIN_GRID_SIZE) { size = MIN_GRID_SIZE; } else if (size > MAX_GRID_SIZE) { size = MAX_GRID_SIZE; } return size; } /** * @return The size of the grid<br> * <br> * *<i>SquareGrid</i> - edge length<br> * *<i>HexGrid</i> - edge to edge diameter */ public int getSize() { return size; } private void fireGridChanged() { if (zone != null) { zone.fireModelChangeEvent(new ModelChangeEvent(this, Event.GRID_CHANGED)); } } /** * Draws the grid scaled to the renderer's scale and within the renderer's boundaries */ public void draw(ZoneRenderer renderer, Graphics2D g, Rectangle bounds) { // Do nothing } public abstract Rectangle getBounds(CellPoint cp); /** * Override if getCapabilities.isSecondDimensionAdjustmentSupported() returns true * * @param length * the second settable dimension * @return */ public void setSecondDimension(double length) { } /** * Override if getCapabilities.isSecondDimensionAdjustmentSupported() returns true * * @return length the curent value of the second settable dimension */ public double getSecondDimension() { return 0; } /** * Installs a list of which which actions go with which keystrokes for the purpose of moving the token. * * @param callback * The object whose methods are invoked when the event occurs * @param actionMap * the map of existing keystrokes we want to add ourselves to */ abstract public void installMovementKeys(PointerTool callback, Map<KeyStroke, Action> actionMap); abstract public void uninstallMovementKeys(Map<KeyStroke, Action> actionMap); static class DirectionCalculator { private static final int NW = 1; private static final int N = 2; private static final int NE = 4; private static final int W = 8; private static final int CENTER = 16; private static final int E = 32; private static final int SW = 64; private static final int S = 128; private static final int SE = 256; public int getDirection(int dirx, int diry) { int TopRow = (NW | N | NE); int MidRow = (W | CENTER | E); int BotRow = (SW | S | SE); int LeftCol = (NW | W | SW); int MidCol = (N | CENTER | S); int RightCol = (NE | E | SE); int direction = TopRow | MidRow | BotRow; if (dirx > 0) direction &= (LeftCol | MidCol); // two left columns if (dirx < 0) direction &= (MidCol | RightCol); // two right columns if (diry > 0) direction &= (TopRow | MidRow); // two top rows if (diry < 0) direction &= (MidRow | BotRow); // two bottom rows if (dirx == 0) direction &= ~MidRow; if (diry == 0) direction &= ~MidCol; direction &= ~CENTER; // Always turn off the center since we don't check it using the outside iterations... return direction; } } private static final DirectionCalculator calculator = new DirectionCalculator(); /** * Tests the grid cell location to determine whether a token is allowed to move into it when such movement is * player-initiated. (The GM may always move a token into a given grid cell.) This implementation only handles * square grids. When a hex and/or gridless implementation is created, this method should be refactored to the * {@link SquareGrid} class and this method changed to always return <code>true</code>. * <p> * Theory of operation: * <ol> * <li>Break the area to check into a 3x3 set of pieces. * <li>Determine which direction the token is coming from. * <li>For the three pieces of the 3x3 set which are closest to that incoming direction, if all of the three contain * fog, the cell cannot be entered. Return <code>false</code>. Otherwise, at least one does not contain fog. Proceed * to the next step. * <li>Select the region encompassed by the center of the 3x3 set of pieces. * <li>Break this region into another 3x3 set of pieces. * <li>If at least 6 of these pieces are fog-free, then the space is open. Return <code>true</code>. * <li>If at least 4 of these pieces contain fog, then the space is closed. Return <code>false</code>. * </ol> * * @param token * token whose movement is being validated; passed in case token state is needed * @param areaToCheck * destination area to check, measured in ZonePoint units * @param dirx * direction token is traveling along the X axis * @param diry * direction token is traveling along the Y axis * @param exposedFog * area in which fog has been cleared away * @return true or false whether the token may move into the area */ public boolean validateMove(Token token, Rectangle areaToCheck, int dirx, int diry, Area exposedFog) { int direction = calculator.getDirection(dirx, diry); Rectangle bounds = new Rectangle(); int bit = 1; if (areaToCheck.width < 9 || (dirx == 0 && diry == 0)) direction = (512 - 1) & ~DirectionCalculator.CENTER; for (int dy = 0; dy < 3; dy++) { for (int dx = 0; dx < 3; dx++, bit *= 2) { if ((direction & bit) == 0) continue; oneThird(areaToCheck, dx, dy, bounds); // The 'fog' variable defines areas where fog has been cleared away if (!exposedFog.contains(bounds)) continue; return checkCenterRegion(areaToCheck, exposedFog); } } // Everything is covered with fog. Or at least, the three regions that we wanted to use to enter the destination area. return false; } /** * Check the middle region by subdividing into 3x3 and checking to see if at least 6 are open. * * @param regionToCheck * rectangular region to check for hard fog * @param fog * defines areas where fog is currently covering the background * @return */ private boolean checkCenterRegion(Rectangle regionToCheck, Area fog) { Rectangle center = new Rectangle(); Rectangle bounds = new Rectangle(); oneThird(regionToCheck, 1, 1, center); // selects the CENTER piece int closedSpace = 0; int openSpace = 0; for (int dy = 0; dy < 3; dy++) { for (int dx = 0; dx < 3; dx++) { oneThird(center, dx, dy, bounds); if (bounds.width < 1 || bounds.height < 1) continue; if (!fog.intersects(bounds)) { if (++closedSpace > 3) return false; } else { if (++openSpace > 5) return true; } } } if (log.isInfoEnabled()) log.info("Center region of size " + regionToCheck.getSize() + " contains neither 4+ closed spaces nor 6+ open spaces?!"); return openSpace >= closedSpace; } /** * Divides the specified region into one of nine parts, where the column and row range from 0..2. The destination * Rectangle must already exist (no check for this is made) and it must not be a reference to the same object as the * region to divide (also not checked). * * @param regionToDivide * region to subdivide * @param column * column in the 3x3 grid * @param row * row in the 3x3 grid * @param destination * one of nine possible pieces represented as a Rectangle */ private void oneThird(Rectangle regionToDivide, int column, int row, Rectangle destination) { int width = regionToDivide.width * column / 3; int height = regionToDivide.height * row / 3; destination.x = regionToDivide.x + width; destination.y = regionToDivide.y + height; destination.width = regionToDivide.width * (column + 1) / 3 - regionToDivide.width * column / 3; // don't simplify or roundoff will be introduced destination.height = regionToDivide.height * (row + 1) / 3 - regionToDivide.height * row / 3; } }