/*
* 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;
import java.awt.Color;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.geom.Area;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Set;
import org.apache.log4j.Logger;
import com.t3.MD5Key;
import com.t3.client.AppUtil;
import com.t3.client.TabletopTool;
import com.t3.client.tool.drawing.UndoPerZone;
import com.t3.client.ui.T3Frame;
import com.t3.client.ui.zone.PlayerView;
import com.t3.client.ui.zone.ZoneRenderer;
import com.t3.client.ui.zone.ZoneView;
import com.t3.guid.GUID;
import com.t3.guid.UniquelyIdentifiable;
import com.t3.language.I18N;
import com.t3.model.drawing.Drawable;
import com.t3.model.drawing.DrawableColorPaint;
import com.t3.model.drawing.DrawablePaint;
import com.t3.model.drawing.DrawableTexturePaint;
import com.t3.model.drawing.DrawnElement;
import com.t3.model.drawing.Pen;
import com.t3.model.grid.Grid;
import com.t3.model.initiative.InitiativeList;
import com.t3.model.initiative.InitiativeList.TokenInitiative;
import com.t3.util.StringUtil;
import com.t3.xstreamversioned.version.SerializationVersion;
/**
* This object represents the maps that will appear for placement of
* {@link Token}s.
* <p>
* Note: When adding new fields to this class, make sure to add functionality to
* the constructor, {@link #imported()}, {@link #optimize()}, and
* {@link #readResolve()} to ensure they are properly initialized for maximum
* compatibility.
*/
@SerializationVersion(0)
public class Zone extends BaseModel implements UniquelyIdentifiable {
private static final Logger log = Logger.getLogger(Zone.class);
@SerializationVersion(0)
public static enum VisionType {
OFF, DAY, NIGHT
}
@SerializationVersion(0)
public static enum Event {
TOKEN_ADDED, TOKEN_REMOVED, TOKEN_CHANGED, GRID_CHANGED, DRAWABLE_ADDED, DRAWABLE_REMOVED, FOG_CHANGED, LABEL_ADDED, LABEL_REMOVED, LABEL_CHANGED, TOPOLOGY_CHANGED, INITIATIVE_LIST_CHANGED, BOARD_CHANGED
}
@SerializationVersion(0)
public static enum Layer {
TOKEN("Token"), GM("Hidden"), OBJECT("Object"), BACKGROUND("Background");
private String displayName;
private Layer(String displayName) {
this.displayName = displayName;
}
@Override
public String toString() {
return displayName;
}
// A simple interface to allow layers to be turned on/off
private boolean drawEnabled = true;
public boolean isEnabled() {
return drawEnabled;
}
public void setEnabled(boolean enabled) {
drawEnabled = enabled;
}
}
public static final int DEFAULT_TOKEN_VISION_DISTANCE = 250; // In units
public static final int DEFAULT_PIXELS_CELL = 50;
public static final int DEFAULT_UNITS_PER_CELL = 5;
public static final DrawablePaint DEFAULT_FOG = new DrawableColorPaint(Color.black);
// The zones should be ordered. We could have the server assign each zone
// an incrementing number as new zones are created, but that would take a lot
// more elegance than we really need. Instead, let's just keep track of the
// time when it was created. This should give us sufficient granularity, because
// seriously -- what's the likelihood of two GMs separately creating a new zone at exactly
// the same millisecond since the epoch?
private long creationTime = System.currentTimeMillis();
private GUID id = new GUID(); // Ideally would be 'final', but that complicates imported()
private Grid grid;
private int gridColor = Color.black.getRGB();
private float imageScaleX = 1;
private float imageScaleY = 1;
private int tokenVisionDistance = DEFAULT_TOKEN_VISION_DISTANCE;
private int unitsPerCell = DEFAULT_UNITS_PER_CELL;
private List<DrawnElement> drawables = new LinkedList<DrawnElement>();
private List<DrawnElement> gmDrawables = new LinkedList<DrawnElement>();
private List<DrawnElement> objectDrawables = new LinkedList<DrawnElement>();
private List<DrawnElement> backgroundDrawables = new LinkedList<DrawnElement>();
private final Map<GUID, Label> labels = new LinkedHashMap<GUID, Label>();
private final Map<GUID, Token> tokenMap = new HashMap<GUID, Token>();
private Map<GUID, ExposedAreaMetaData> exposedAreaMeta = new HashMap<GUID, ExposedAreaMetaData>();
//FIXME replace this with by making the token hashmap a linkedhashmap
private final List<Token> tokenOrderedList = new LinkedList<Token>();
private InitiativeList initiativeList = new InitiativeList(this);
private Area exposedArea = new Area();
private boolean hasFog;
private DrawablePaint fogPaint;
private transient UndoPerZone undo;
private Area topology = new Area();
// The 'board' layer, at the very bottom of the layer stack.
// Itself has two sub-layers:
// The top one is an optional texture, typically a pre-drawn map.
// The bottom one is either an infinitely tiling texture or a color.
private DrawablePaint backgroundPaint;
private MD5Key mapAsset;
private Point boardPosition = new Point(0, 0);
private boolean drawBoard = true;
private boolean boardChanged = false;
private String name;
private boolean isVisible;
private VisionType visionType = VisionType.OFF;
// These are transitionary properties, very soon the width and height won't matter
private int height;
private int width;
private transient HashMap<String, Integer> tokenNumberCache;
/**
* Note: When adding new fields to this class, make sure to update all
* constructors, {@link #imported()}, {@link #readResolve()}, and
* potentially {@link #optimize()}.
*/
public Zone() {
// TODO: Was this needed?
// setGrid(new SquareGrid());
undo = new UndoPerZone(this); // registers as ModelChangeListener for drawables...
addModelChangeListener(undo);
}
public void setBackgroundPaint(DrawablePaint paint) {
backgroundPaint = paint;
}
public void setMapAsset(MD5Key id) {
mapAsset = id;
boardChanged = true;
}
public void setTokenVisionDistance(int units) {
tokenVisionDistance = units;
}
public int getTokenVisionDistance() {
return tokenVisionDistance;
}
public VisionType getVisionType() {
return visionType;
}
public void setVisionType(VisionType visionType) {
this.visionType = visionType;
}
/**
* Returns the distance in map pixels at a 1:1 zoom
*/
public int getTokenVisionInPixels() {
return (tokenVisionDistance * grid.getSize() / getUnitsPerCell());
}
public void setFogPaint(DrawablePaint paint) {
fogPaint = paint;
}
@Override
public String toString() {
return name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public MD5Key getMapAssetId() {
return mapAsset;
}
public DrawablePaint getBackgroundPaint() {
return backgroundPaint;
}
public DrawablePaint getFogPaint() {
return fogPaint != null ? fogPaint : DEFAULT_FOG;
}
/**
* Note: When adding new fields to this class, make sure to update all
* constructors, {@link #imported()}, {@link #readResolve()}, and
* potentially {@link #optimize()}.
* <p>
* JFJ 2010-10-27 Don't forget that since there are new zones AND new tokens
* created here from the old one being passed in, if you have any data that
* needs to transfer over, you will need to manually copy it as is done
* below for various items.
*/
public Zone(Zone zone) {
backgroundPaint = zone.backgroundPaint;
mapAsset = zone.mapAsset;
fogPaint = zone.fogPaint;
visionType = zone.visionType;
undo = new UndoPerZone(this); // Undo/redo manager isn't copied
addModelChangeListener(undo);
setName(zone.getName());
try {
grid = (Grid) zone.grid.clone();
grid.setZone(this);
} catch (CloneNotSupportedException cnse) {
TabletopTool.showError("Trying to copy the zone's grid; no grid assigned", cnse);
}
unitsPerCell = zone.unitsPerCell;
tokenVisionDistance = zone.tokenVisionDistance;
imageScaleX = zone.imageScaleX;
imageScaleY = zone.imageScaleY;
// In the following blocks we allocate a new linked list then fill it with null values
// because the Collections.copy() method requires the destination list to already be
// of a large enough size. I couldn't find any method that would copy individual
// elements as it populated the new linked lists except those from the Apache Commons
// library that use a Transformer and that seemed like a lot more work. :-/
if (zone.drawables != null && !zone.drawables.isEmpty()) {
drawables = new LinkedList<DrawnElement>();
drawables.addAll(Collections.nCopies(zone.drawables.size(), (DrawnElement) null));
Collections.copy(drawables, zone.drawables);
}
if (zone.objectDrawables != null && !zone.objectDrawables.isEmpty()) {
objectDrawables = new LinkedList<DrawnElement>();
objectDrawables.addAll(Collections.nCopies(zone.objectDrawables.size(), (DrawnElement) null));
Collections.copy(objectDrawables, zone.objectDrawables);
}
if (zone.backgroundDrawables != null && !zone.backgroundDrawables.isEmpty()) {
backgroundDrawables = new LinkedList<DrawnElement>();
backgroundDrawables.addAll(Collections.nCopies(zone.backgroundDrawables.size(), (DrawnElement) null));
Collections.copy(backgroundDrawables, zone.backgroundDrawables);
}
if (zone.gmDrawables != null && !zone.gmDrawables.isEmpty()) {
gmDrawables = new LinkedList<DrawnElement>();
gmDrawables.addAll(Collections.nCopies(zone.gmDrawables.size(), (DrawnElement) null));
Collections.copy(gmDrawables, zone.gmDrawables);
}
if (zone.labels != null && !zone.labels.isEmpty()) {
Iterator<GUID> i = zone.labels.keySet().iterator();
while (i.hasNext()) {
this.putLabel(new Label(zone.labels.get(i.next())));
}
}
exposedAreaMeta = new HashMap<GUID, ExposedAreaMetaData>(zone.exposedAreaMeta.size() * 4 / 3);
// Copy the tokens, save a map between old and new for the initiative list.
if (zone.initiativeList == null)
zone.initiativeList = new InitiativeList(zone);
Object[][] saveInitiative = new Object[zone.initiativeList.getSize()][2];
initiativeList.setZone(null);
if (zone.tokenMap != null && !zone.tokenMap.isEmpty()) {
Iterator<GUID> i = zone.tokenMap.keySet().iterator();
while (i.hasNext()) {
Token old = zone.tokenMap.get(i.next());
Token token = new Token(old);
if (old.getExposedAreaGUID() != null) {
GUID guid = new GUID();
token.setExposedAreaGUID(guid);
// Update the TEA on the new map, since we have the Token object available...
ExposedAreaMetaData eamd = zone.getExposedAreaMetaData(old.getExposedAreaGUID());
if (eamd != null)
exposeArea(eamd.getExposedAreaHistory(), token);
}
putToken(token);
List<Integer> list = zone.initiativeList.indexOf(old);
for (Integer integer : list) {
int index = integer.intValue();
saveInitiative[index][0] = token;
saveInitiative[index][1] = zone.initiativeList.getTokenInitiative(index);
}
}
}
// Set the initiative list using the newly create tokens.
if (saveInitiative.length > 0) {
for (int i = 0; i < saveInitiative.length; i++) {
Token token = (Token) saveInitiative[i][0];
initiativeList.insertToken(i, token);
TokenInitiative ti = initiativeList.getTokenInitiative(i);
TokenInitiative oldti = (TokenInitiative) saveInitiative[i][1];
ti.setHolding(oldti.isHolding());
ti.setState(oldti.getRawState());
}
}
initiativeList.setZone(this);
initiativeList.setCurrent(zone.initiativeList.getCurrent());
initiativeList.setRound(zone.initiativeList.getRound());
initiativeList.setHideNPC(zone.initiativeList.isHideNPC());
boardPosition = (Point) zone.boardPosition.clone();
exposedArea = (Area) zone.exposedArea.clone();
topology = (Area) zone.topology.clone();
isVisible = zone.isVisible;
hasFog = zone.hasFog;
}
@Override
public GUID getId() {
return id;
}
/**
* Should be invoked only when a Zone has been imported from an external
* source and needs to be cleaned up before being used. Currently this
* cleanup consists of allocating a new GUID, setting the creation time to
* `now', and resetting the initiative list (setting the related zone and
* clearing the model).
*/
public void imported() {
id = new GUID();
creationTime = System.currentTimeMillis();
initiativeList.setZone(this);
initiativeList.clearModel();
}
public int getHeight() {
return height;
}
public void setHeight(int height) {
this.height = height;
}
public int getWidth() {
return width;
}
public void setWidth(int width) {
this.width = width;
}
public boolean isVisible() {
return isVisible;
}
public void setVisible(boolean isVisible) {
this.isVisible = isVisible;
}
public void setGrid(Grid grid) {
this.grid = grid;
grid.setZone(this);
// tokenVisionDistance = DEFAULT_TOKEN_VISION_DISTANCE * grid.getSize() / unitsPerCell;
fireModelChangeEvent(new ModelChangeEvent(this, Event.GRID_CHANGED));
}
public Grid getGrid() {
return grid;
}
public int getGridColor() {
return gridColor;
}
public void setGridColor(int color) {
gridColor = color;
}
/**
* Board pseudo-object. Not making full object since this will change when
* new layer model is created
*/
public boolean isBoardChanged() {
return boardChanged;
}
public void setBoardChanged(boolean set) {
boardChanged = set;
}
public void setBoard(Point position) {
boardPosition.x = position.x;
boardPosition.y = position.y;
setBoardChanged(true);
fireModelChangeEvent(new ModelChangeEvent(mapAsset, Event.BOARD_CHANGED));
}
public void setBoard(int newX, int newY) {
boardPosition.x = newX;
boardPosition.y = newY;
setBoardChanged(true);
fireModelChangeEvent(new ModelChangeEvent(mapAsset, Event.BOARD_CHANGED));
}
public void setBoard(Point position, MD5Key asset) {
this.setMapAsset(asset);
this.setBoard(position);
}
public int getBoardX() {
return boardPosition.x;
}
public int getBoardY() {
return boardPosition.y;
}
public boolean drawBoard() {
return drawBoard;
}
public void setDrawBoard(boolean draw) {
drawBoard = draw;
}
//
// Misc Scale methods
//
public float getImageScaleX() {
return imageScaleX;
}
public void setImageScaleX(float imageScaleX) {
this.imageScaleX = imageScaleX;
}
public float getImageScaleY() {
return imageScaleY;
}
public void setImageScaleY(float imageScaleY) {
this.imageScaleY = imageScaleY;
}
//
// Fog
//
public boolean hasFog() {
return hasFog;
}
public void setHasFog(boolean flag) {
hasFog = flag;
fireModelChangeEvent(new ModelChangeEvent(this, Event.FOG_CHANGED));
}
/**
* Determines whether the given ZonePoint is visible when using the
* specified PlayerView. This currently includes checking the following
* criteria:
* <ol>
* <li>If fog is turned off, return true.
* <li>If the view is a GM view, return true.
* <li>If Vision is <b>Day</b> or <b>Night</b> and we're not using
* IndividualFOW, return intersection of point with exposedArea.
* <li>If Vision is off or we ARE using IndividualFOW, combine exposed areas
* of all owned tokens (with HasSight==true) and return intersection of
* point with the combined area.
* </ol>
*
* @param point
* @param view
* @return
*/
public boolean isPointVisible(ZonePoint point, PlayerView view) {
if (!hasFog() || view.isGMView()) {
return true;
}
if (TabletopTool.getServerPolicy().isUseIndividualFOW() && getVisionType() != VisionType.OFF) {
Area combined = new Area(exposedArea);
List<Token> toks = view.getTokens(); // only owned and HasSight tokens are returned
if (toks != null && !toks.isEmpty()) {
for (Token tok : toks) {
ExposedAreaMetaData meta = exposedAreaMeta.get(tok.getExposedAreaGUID());
if (meta != null)
combined.add(meta.getExposedAreaHistory());
}
}
return combined.contains(point.x, point.y);
} else {
return exposedArea.contains(point.x, point.y);
}
}
public boolean isEmpty() {
// @formatter:off
return (drawables == null || drawables.isEmpty()) &&
(gmDrawables == null || gmDrawables.isEmpty()) &&
(objectDrawables == null || objectDrawables.isEmpty()) &&
(backgroundDrawables == null || backgroundDrawables.isEmpty()) &&
(tokenOrderedList == null || tokenOrderedList.isEmpty()) &&
(labels == null || labels.isEmpty());
// @formatter:on
}
/**
* Determines if the passed non-<code>null</code> parameter represents a
* visible token. The current criteria works like this:
* <ol>
* <li>If the <i>Visible to Players</i> flag is off, return
* <code>false</code>.
* <li>If the fog-of-war for the map is off, return <code>true</code>.
* <li>If the player does not own the token and it's <i>Visible to Owner
* Only</i>, return <code>false</code>.
* <li>If the token's bounds intersect the exposed area for this map, return
* <code>true</code>.
* </ol>
*
* @param token
* @return
*/
public boolean isTokenVisible(Token token) {
if (token == null) {
return false;
}
// Base case, nothing is visible
if (!token.isVisible()) {
return false;
}
// Base case, everything is visible
if (!hasFog()) {
return true;
}
if (token.isVisibleOnlyToOwner() && !AppUtil.playerOwns(token)) {
return false;
}
// Token is visible, and there is fog
Rectangle tokenSize = token.getBounds(this);
Area combined = new Area(exposedArea);
PlayerView view = TabletopTool.getFrame().getZoneRenderer(this).getPlayerView();
if (TabletopTool.getServerPolicy().isUseIndividualFOW() && getVisionType() != VisionType.OFF) {
List<Token> toks = view.getTokens();
if (toks != null && !toks.isEmpty()) {
// Should this use FindTokenFunctions.OwnedFilter and zone.getTokenList()?
for (Token tok : toks) {
if (!AppUtil.playerOwns(tok)) {
continue;
}
if (exposedAreaMeta.containsKey(tok.getExposedAreaGUID())) {
combined.add(exposedAreaMeta.get(tok.getExposedAreaGUID()).getExposedAreaHistory());
}
}
}
}
return combined.intersects(tokenSize);
}
public void clearTopology() {
topology = new Area();
fireModelChangeEvent(new ModelChangeEvent(this, Event.TOPOLOGY_CHANGED));
}
public void addTopology(Area area) {
topology.add(area);
fireModelChangeEvent(new ModelChangeEvent(this, Event.TOPOLOGY_CHANGED));
}
public void removeTopology(Area area) {
topology.subtract(area);
fireModelChangeEvent(new ModelChangeEvent(this, Event.TOPOLOGY_CHANGED));
}
public Area getTopology() {
return topology;
}
public void clearExposedArea() {
exposedArea = new Area();
// There used to be a foreach loop here that iterated over getTokens() and called .clear() -- why?!
exposedAreaMeta.clear();
fireModelChangeEvent(new ModelChangeEvent(this, Event.FOG_CHANGED));
}
public void exposeArea(Area area, Token tok) {
if (area == null || area.isEmpty()) {
return;
}
if (tok != null) {
if (TabletopTool.isPersonalServer() || (TabletopTool.getServerPolicy().isUseIndividualFOW() && AppUtil.playerOwns(tok))) {
GUID tea = tok.getExposedAreaGUID();
ExposedAreaMetaData meta = exposedAreaMeta.get(tea);
if (meta == null) {
meta = new ExposedAreaMetaData();
exposedAreaMeta.put(tea, meta);
}
meta.addToExposedAreaHistory(area);
ZoneRenderer zr = TabletopTool.getFrame().getZoneRenderer(this.getId());
if (zr != null) // Could be null if the AutoSaveManager is saving the campaign by copying Zones, but not ZoneRenderers
zr.getZoneView().flush();
putToken(tok);
fireModelChangeEvent(new ModelChangeEvent(this, Event.FOG_CHANGED));
return; // FJE Added so that TEA isn't added to the GEA, below.
}
}
exposedArea.add(area);
fireModelChangeEvent(new ModelChangeEvent(this, Event.FOG_CHANGED));
}
/**
* Retrieves the selected tokens and adds the passed in area to their
* exposed area. (Why are we passing in a <code>Set<GUID></code> when
* <code>Set<Token></code> would be much more efficient?)
*
* @param area
* @param selectedToks
*/
public void exposeArea(Area area, Set<GUID> selectedToks) {
if (area == null || area.isEmpty()) {
return;
}
if (getVisionType() == VisionType.OFF) {
// Why is this done here and then again below???
// And just because Vision==Off doesn't mean we aren't doing IF...
exposedArea.add(area);
}
if (selectedToks != null && !selectedToks.isEmpty() && (TabletopTool.getServerPolicy().isUseIndividualFOW() || TabletopTool.isPersonalServer())) {
boolean isAllowed = TabletopTool.getPlayer().isGM() || !TabletopTool.getServerPolicy().useStrictTokenManagement();
String playerId = TabletopTool.getPlayer().getName();
T3Frame frame = TabletopTool.getFrame();
ZoneRenderer zr = frame.getZoneRenderer(getId());
ZoneView zoneView = zr.getZoneView();
ExposedAreaMetaData meta = null;
for (GUID guid : selectedToks) {
Token tok = getToken(guid);
if ((isAllowed || tok.isOwner(playerId)) && tok.getHasSight()) {
GUID tea = tok.getExposedAreaGUID();
meta = exposedAreaMeta.get(tea);
if (meta == null) {
meta = new ExposedAreaMetaData();
exposedAreaMeta.put(tea, meta);
}
meta.addToExposedAreaHistory(area);
}
}
// If 'meta' is not null, it means at least one token's TEA was modified so we need to flush the ZoneView
if (meta != null)
zoneView.flush();
} else {
// Not using IF so add the EA to the GEA instead of a TEA.
exposedArea.add(area);
}
fireModelChangeEvent(new ModelChangeEvent(this, Event.FOG_CHANGED));
}
/**
* Modifies the global exposed area (GEA) or token exposed by resetting it
* and then setting it to the contents of the passed in Area and firing a
* ModelChangeEvent.
*
* @param area
* @param selectedToks
*/
public void setFogArea(Area area, Set<GUID> selectedToks) {
if (area == null) {
return;
}
if (selectedToks != null && !selectedToks.isEmpty()) {
List<Token> allToks = new ArrayList<Token>();
for (GUID guid : selectedToks) {
allToks.add(getToken(guid));
}
for (Token tok : allToks) {
if (!tok.getHasSight()) {
continue;
}
ExposedAreaMetaData meta = exposedAreaMeta.get(tok.getExposedAreaGUID());
if (meta == null)
meta = new ExposedAreaMetaData();
meta.clearExposedAreaHistory();
meta.addToExposedAreaHistory(area);
exposedAreaMeta.put(tok.getExposedAreaGUID(), meta);
TabletopTool.getFrame().getZoneRenderer(this.getId()).getZoneView().flush(tok);
putToken(tok);
}
} else {
exposedArea.reset();
exposedArea.add(area);
}
fireModelChangeEvent(new ModelChangeEvent(this, Event.FOG_CHANGED));
}
public void hideArea(Area area, Set<GUID> selectedToks) {
if (area == null) {
return;
}
if (getVisionType() == VisionType.OFF) {
exposedArea.subtract(area);
}
if (selectedToks != null && !selectedToks.isEmpty() && TabletopTool.getServerPolicy().isUseIndividualFOW()) {
List<Token> allToks = new ArrayList<Token>();
for (GUID guid : selectedToks) {
allToks.add(getToken(guid));
}
for (Token tok : allToks) {
if (!AppUtil.playerOwns(tok)) {
continue;
}
if (!tok.getHasSight()) {
continue;
}
ExposedAreaMetaData meta = exposedAreaMeta.get(tok.getExposedAreaGUID());
if (meta == null)
meta = new ExposedAreaMetaData();
meta.removeExposedAreaHistory(area);
exposedAreaMeta.put(tok.getExposedAreaGUID(), meta);
TabletopTool.getFrame().getZoneRenderer(this.getId()).getZoneView().flush(tok);
putToken(tok);
}
} else {
exposedArea.subtract(area);
}
fireModelChangeEvent(new ModelChangeEvent(this, Event.FOG_CHANGED));
}
public long getCreationTime() {
return creationTime;
}
// FIXME This needs to take the current grid type into account, such as square or hex
public ZonePoint getNearestVertex(ZonePoint point) {
int gridx = (int) Math.round((point.x - grid.getOffsetX()) / grid.getCellWidth());
int gridy = (int) Math.round((point.y - grid.getOffsetY()) / grid.getCellHeight());
// System.out.println("gx:" + gridx + " zx:" + (gridx * grid.getCellWidth() + grid.getOffsetX()));
return new ZonePoint((int) (gridx * grid.getCellWidth() + grid.getOffsetX()), (int) (gridy * grid.getCellHeight() + grid.getOffsetY()));
}
/**
* Returns the Area of the exposed fog for the current tokens (as determined
* by view.getTokens()). This means if no tokens are current, the return
* value is the zone's global exposed fog area. If tokens are returned by
* getTokens(), their exposed areas are added to the zone's global area and
* the result is returned.
*
* @param view
* holds whether or not tokens are selected
* @return
*/
public Area getExposedArea(PlayerView view) {
Area combined = new Area(exposedArea);
List<Token> toks = view.getTokens();
// Don't need to worry about StrictTokenOwnership since the PlayerView only contains tokens we own by calling AppUtil.playerOwns()
if (toks == null || toks.isEmpty()) {
return combined;
}
for (Token tok : toks) {
// Don't need this IF statement; see com.t3.client.ui.zone.ZoneRenderer.getPlayerView(Role)
// if (!tok.getHasSight() || !AppUtil.playerOwns(tok)) {
// continue;
// }
ExposedAreaMetaData meta = exposedAreaMeta.get(tok.getExposedAreaGUID());
if (meta != null)
combined.add(meta.getExposedAreaHistory());
}
return combined;
}
/**
* This is the Global Exposed Area (GEA) discussed so much on the dev-team
* mailing list. :)
*
* @return Area object representing exposed fog area visible to all tokens
*/
public Area getExposedArea() {
return exposedArea;
}
public int getUnitsPerCell() {
return Math.max(unitsPerCell, 1);
}
public void setUnitsPerCell(int unitsPerCell) {
this.unitsPerCell = unitsPerCell;
}
public int getLargestZOrder() {
return tokenOrderedList.size() > 0 ? tokenOrderedList.get(tokenOrderedList.size() - 1).getZOrder() : 0;
}
public int getSmallestZOrder() {
return tokenOrderedList.size() > 0 ? tokenOrderedList.get(0).getZOrder() : 0;
}
///////////////////////////////////////////////////////////////////////////
// labels
///////////////////////////////////////////////////////////////////////////
public void putLabel(Label label) {
boolean newLabel = labels.containsKey(label.getId());
labels.put(label.getId(), label);
if (newLabel) {
fireModelChangeEvent(new ModelChangeEvent(this, Event.LABEL_ADDED, label));
} else {
fireModelChangeEvent(new ModelChangeEvent(this, Event.LABEL_CHANGED, label));
}
}
public List<Label> getLabels() {
return new ArrayList<Label>(this.labels.values());
}
public void removeLabel(GUID labelId) {
Label label = labels.remove(labelId);
if (label != null) {
fireModelChangeEvent(new ModelChangeEvent(this, Event.LABEL_REMOVED, label));
}
}
///////////////////////////////////////////////////////////////////////////
// drawables
///////////////////////////////////////////////////////////////////////////
public void addDrawable(DrawnElement drawnElement) {
switch (drawnElement.getDrawable().getLayer()) {
case OBJECT:
objectDrawables.add(drawnElement);
break;
case BACKGROUND:
backgroundDrawables.add(drawnElement);
break;
case GM:
gmDrawables.add(drawnElement);
break;
default:
drawables.add(drawnElement);
}
fireModelChangeEvent(new ModelChangeEvent(this, Event.DRAWABLE_ADDED, drawnElement));
}
public List<DrawnElement> getDrawnElements() {
return getDrawnElements(Zone.Layer.TOKEN);
}
public List<DrawnElement> getObjectDrawnElements() {
return getDrawnElements(Zone.Layer.OBJECT);
}
public List<DrawnElement> getGMDrawnElements() {
return getDrawnElements(Zone.Layer.GM);
}
public List<DrawnElement> getBackgroundDrawnElements() {
return getDrawnElements(Zone.Layer.BACKGROUND);
}
public List<DrawnElement> getDrawnElements(Zone.Layer layer) {
switch (layer) {
case OBJECT:
return objectDrawables;
case GM:
return gmDrawables;
case BACKGROUND:
return backgroundDrawables;
default:
return drawables;
}
}
public void removeDrawable(GUID drawableId) {
// Since we don't know anything about the drawable, look through all the layers
// Do we need to remove it from the Undo manager as well? Probably. Perhaps some
// UndoPerZone method that searches and deletes the drawable ID?
removeDrawable(drawables, drawableId);
removeDrawable(backgroundDrawables, drawableId);
removeDrawable(objectDrawables, drawableId);
removeDrawable(gmDrawables, drawableId);
}
private void removeDrawable(List<DrawnElement> drawableList, GUID drawableId) {
ListIterator<DrawnElement> i = drawableList.listIterator();
while (i.hasNext()) {
DrawnElement drawable = i.next();
if (drawable.getDrawable().getId().equals(drawableId)) {
i.remove();
fireModelChangeEvent(new ModelChangeEvent(this, Event.DRAWABLE_REMOVED, drawable));
return;
}
}
}
public void clearDrawables(List<DrawnElement> drawableList) {
ListIterator<DrawnElement> i = drawableList.listIterator();
while (i.hasNext()) {
DrawnElement drawable = i.next();
fireModelChangeEvent(new ModelChangeEvent(this, Event.DRAWABLE_REMOVED, drawable));
}
drawableList.clear();
undo.clear(); // clears the *entire* undo queue, but finer grained control isn't available
}
public void addDrawable(Pen pen, Drawable drawable) {
undo.addDrawable(pen, drawable);
}
public boolean canUndo() {
return undo.canUndo();
}
public void undoDrawable() {
undo.undo();
}
public boolean canRedo() {
return undo.canRedo();
}
public void redoDrawable() {
undo.redo();
}
///////////////////////////////////////////////////////////////////////////
// tokens
///////////////////////////////////////////////////////////////////////////
/**
* Adds the specified Token to this zone, accounting for updating the
* ordered list of tokens as well as firing the appropriate
* <code>ModelChangeEvent</code> (either <code>Event.TOKEN_ADDED</code> or
* <code>Event.TOKEN_CHANGED</code>).
*
* @param token
* the Token to be added to this zone
*/
public void putToken(Token token) {
boolean newToken = !tokenMap.containsKey(token.getId());
token.setZone(this);
tokenMap.put(token.getId(), token);
// LATER: optimize this
tokenOrderedList.remove(token);
tokenOrderedList.add(token);
Collections.sort(tokenOrderedList, TOKEN_Z_ORDER_COMPARATOR);
if (newToken) {
fireModelChangeEvent(new ModelChangeEvent(this, Event.TOKEN_ADDED, token));
} else {
fireModelChangeEvent(new ModelChangeEvent(this, Event.TOKEN_CHANGED, token));
}
}
/**
* Same as {@link #putToken(List)} but optimizes map updates by accepting a
* list of Tokens. Note that this method fires a single
* <code>ModelChangeEvent</code> using <code>Event.TOKEN_ADDED</code> and
* passes the list of added tokens as a parameter. Ditto for
* <code>Event.TOKEN_CHANGED</code>.
* <p>
* Not currently invoked by other code, but event handling changes for
* multiple tokens has been made. Marked as deprecated to prevent use until
* the rest of the integration is completed.
*
* @param tokens
* List of Tokens to be added to this zone
*/
@Deprecated
public void putTokens(List<Token> tokens) {
// System.out.println("putToken() called with list of " + tokens.size() + " tokens.");
Collection<Token> values = tokenMap.values();
List<Token> addedTokens = new LinkedList<Token>(tokens);
addedTokens.removeAll(values);
List<Token> changedTokens = new LinkedList<Token>(tokens);
changedTokens.retainAll(values);
for (Token t : tokens) {
tokenMap.put(t.getId(), t);
}
tokenOrderedList.removeAll(tokens);
tokenOrderedList.addAll(tokens);
Collections.sort(tokenOrderedList, TOKEN_Z_ORDER_COMPARATOR);
if (!addedTokens.isEmpty())
fireModelChangeEvent(new ModelChangeEvent(this, Event.TOKEN_ADDED, addedTokens));
if (!changedTokens.isEmpty())
fireModelChangeEvent(new ModelChangeEvent(this, Event.TOKEN_CHANGED, changedTokens));
}
public void removeToken(GUID id) {
Token token = tokenMap.remove(id);
if (token != null) {
tokenOrderedList.remove(token);
fireModelChangeEvent(new ModelChangeEvent(this, Event.TOKEN_REMOVED, token));
}
}
public Token getToken(GUID id) {
return tokenMap.get(id);
}
/**
* Returns the first token with a given name. The name is matched
* case-insensitively.
*/
public Token getTokenByName(String name) {
for (Token token : getAllTokens()) {
if (StringUtil.isEmpty(token.getName())) {
continue;
}
if (token.getName().equalsIgnoreCase(name)) {
return token;
}
}
return null;
}
/**
* Looks for the given identifier as a token name, token GM name, or GUID,
* in that order.
*
* @param identifier
* @return token that matches the identifier or <code>null</code>
*/
public Token resolveToken(String identifier) {
Token token = getTokenByName(identifier);
if (token == null)
token = getTokenByGMName(identifier);
if (token == null) {
try {
token = getToken(GUID.valueOf(identifier));
} catch (Exception e) {
// indication of not a GUID, OK to ignore
}
}
return token;
}
/**
* Returns the first token with a given GM name. The name is matched
* case-insensitively.
*/
public Token getTokenByGMName(String name) {
for (Token token : getAllTokens()) {
if (StringUtil.isEmpty(token.getGMName())) {
continue;
}
if (token.getGMName().equalsIgnoreCase(name)) {
return token;
}
}
return null;
}
public List<DrawnElement> getAllDrawnElements() {
List<DrawnElement> list = new ArrayList<DrawnElement>();
list.addAll(getDrawnElements());
list.addAll(getObjectDrawnElements());
list.addAll(getBackgroundDrawnElements());
list.addAll(getGMDrawnElements());
return list;
}
public int getTokenCount() {
return tokenOrderedList.size();
}
public List<Token> getAllTokens() {
return Collections.unmodifiableList(new ArrayList<Token>(tokenOrderedList));
}
public Set<MD5Key> getAllAssetIds() {
Set<MD5Key> idSet = new HashSet<MD5Key>();
// Zone
if (getBackgroundPaint() instanceof DrawableTexturePaint) {
idSet.add(((DrawableTexturePaint) getBackgroundPaint()).getAssetId());
}
idSet.add(getMapAssetId());
if (getFogPaint() instanceof DrawableTexturePaint) {
idSet.add(((DrawableTexturePaint) getFogPaint()).getAssetId());
}
// Tokens
for (Token token : getAllTokens()) {
idSet.addAll(token.getAllImageAssets());
}
// Painted textures
for (DrawnElement drawn : getAllDrawnElements()) {
DrawablePaint paint = drawn.getPen().getPaint();
if (paint instanceof DrawableTexturePaint) {
idSet.add(((DrawableTexturePaint) paint).getAssetId());
}
paint = drawn.getPen().getBackgroundPaint();
if (paint instanceof DrawableTexturePaint) {
idSet.add(((DrawableTexturePaint) paint).getAssetId());
}
}
// It's easier to just remove null at the end than to do a is-null check on each asset
idSet.remove(null);
return idSet;
}
public List<Token> getTokensFiltered(TokenFilter filter) {
List<Token> l=new ArrayList<Token>(tokenOrderedList.size());
for(Token t:tokenOrderedList)
if(filter.filter(t))
l.add(t);
return Collections.unmodifiableList(l);
}
public interface TokenFilter {
boolean filter(Token t);
}
/**
* This is the list of non-stamp tokens, both pc and npc
*/
public List<Token> getTokens() {
return getTokensFiltered(new TokenFilter() {
@Override
public boolean filter(Token t) {
return !t.isStamp();
}
});
}
public List<Token> getStampTokens() {
return getTokensFiltered(new TokenFilter() {
@Override
public boolean filter(Token t) {
return t.isObjectStamp();
}
});
}
public List<Token> getPlayerTokens() {
return getTokensFiltered(new TokenFilter() {
@Override
public boolean filter(Token t) {
return t.getType() == Token.Type.PC;
}
});
}
public List<Token> getPlayerOwnedTokensWithSight(Player p) {
return getTokensFiltered(new TokenFilter() {
@Override
public boolean filter(Token t) {
return t.getType() == Token.Type.PC && t.getHasSight() && AppUtil.playerOwns(t);
}
});
}
public List<Token> getBackgroundStamps() {
return getTokensFiltered(new TokenFilter() {
@Override
public boolean filter(Token t) {
return t.isBackgroundStamp();
}
});
}
public List<Token> getGMStamps() {
return getTokensFiltered(new TokenFilter() {
@Override
public boolean filter(Token t) {
return t.isGMStamp();
}
});
}
public int findFreeNumber(String tokenBaseName, boolean checkDm) {
if (tokenNumberCache == null) {
tokenNumberCache = new HashMap<String, Integer>();
}
Integer _lastUsed = tokenNumberCache.get(tokenBaseName);
int lastUsed;
if (_lastUsed == null) {
lastUsed = 0;
} else {
lastUsed = _lastUsed;
}
boolean repeat = true;
while (repeat) {
lastUsed++;
repeat = false;
if (checkDm) {
Token token = getTokenByGMName(Integer.toString(lastUsed));
if (token != null) {
repeat = true;
}
}
if (!repeat && tokenBaseName != null) {
String name = tokenBaseName + " " + lastUsed;
Token token = getTokenByName(name);
if (token != null) {
repeat = true;
}
}
}
tokenNumberCache.put(tokenBaseName, lastUsed);
return lastUsed;
}
public static final Comparator<Token> TOKEN_Z_ORDER_COMPARATOR = new TokenZOrderComparator();
public static class TokenZOrderComparator implements Comparator<Token> {
@Override
public int compare(Token o1, Token o2) {
int lval = o1.getZOrder();
int rval = o2.getZOrder();
if (lval == rval) {
return o1.getId().compareTo(o2.getId());
} else {
return lval - rval;
}
}
}
/** @return Getter for initiativeList */
public InitiativeList getInitiativeList() {
return initiativeList;
}
/**
* @param initiativeList
* Setter for the initiativeList
*/
public void setInitiativeList(InitiativeList initiativeList) {
this.initiativeList = initiativeList;
fireModelChangeEvent(new ModelChangeEvent(this, Event.INITIATIVE_LIST_CHANGED));
}
public void optimize() {
log.debug("Optimizing Map " + getName());
TabletopTool.getFrame().setStatusMessage(I18N.getText("Zone.status.optimizing", getName()));
collapseDrawables();
}
/**
* Clear out any drawables that are hidden/erased. This is an optimization
* step that should only happen when you can't undo your changes and
* re-expose a drawable, typically at load.
*/
private void collapseDrawables() {
collapseDrawableLayer(drawables);
collapseDrawableLayer(gmDrawables);
collapseDrawableLayer(objectDrawables);
collapseDrawableLayer(backgroundDrawables);
}
private void collapseDrawableLayer(List<DrawnElement> layer) {
if (layer.isEmpty()) {
return;
}
Area area = new Area();
List<DrawnElement> list = new ArrayList<DrawnElement>(layer);
Collections.reverse(list);
int eraserCount = 0;
for (ListIterator<DrawnElement> drawnIter = list.listIterator(); drawnIter.hasNext();) {
DrawnElement drawn = drawnIter.next();
// Are we covered ourselves ?
Area drawnArea = drawn.getDrawable().getArea();
if (drawnArea == null) {
continue;
}
// The following is over-zealous optimization. Lines (1-dimensional) should be kept.
// (Does drawable cover area? If not, get rid of it.)
// if (drawnArea.isEmpty()) {
// drawnIter.remove();
// continue;
// }
// if (GraphicsUtil.contains(area, drawnArea)) { // Too expensive
if (area.contains(drawnArea.getBounds())) { // Not as accurate, but faster
drawnIter.remove();
continue;
}
// Are we possibly covering something up?
if (drawn.getPen().isEraser() && (drawn.getPen().getBackgroundMode() == Pen.MODE_SOLID)) {
area.add(drawnArea);
eraserCount++;
continue;
}
}
// Now use the new list
layer.clear();
// If the number of elements is greater than the number of erasables, keep them all.
if (list.size() > eraserCount) {
layer.addAll(list);
Collections.reverse(layer);
}
}
////
// Backward compatibility
@Override
protected Object readResolve() {
super.readResolve();
// 1.3b76 -> 1.3b77
// adding the exposed area for Individual FOW
if (exposedAreaMeta == null) {
exposedAreaMeta = new HashMap<GUID, ExposedAreaMetaData>();
}
// 1.3b70 -> 1.3b71
// These two variables were added
if (drawBoard == false) {
// this should check the file version, not the value
drawBoard = true;
}
if (boardPosition == null) {
boardPosition = new Point(0, 0);
}
Zone.Layer.TOKEN.setEnabled(true);
Zone.Layer.GM.setEnabled(true);
Zone.Layer.OBJECT.setEnabled(true);
Zone.Layer.BACKGROUND.setEnabled(true);
// 1.3b47 -> 1.3b48
if (visionType == null) {
if (getTokensFiltered(new TokenFilter() {
@Override
public boolean filter(Token t) {
return t.hasLightSources();
}
}).size() > 0) {
visionType = VisionType.NIGHT;
} else if (topology != null && !topology.isEmpty()) {
visionType = VisionType.DAY;
} else {
visionType = VisionType.OFF;
}
}
// Look for the bizarre z-ordering disappearing trick
boolean foundZero = false;
boolean fixZOrder = false;
for (Token token : tokenOrderedList) {
if (token.getZOrder() == 0) {
if (foundZero) {
fixZOrder = true;
break;
}
foundZero = true;
}
}
if (fixZOrder) {
int z = 0;
for (Token token : tokenOrderedList) {
token.setZOrder(z++);
}
}
// Transient "undo" field added in 1.3.b88
// This will be true; it's just in case we decide to make it persistent in the future
if (undo == null) {
undo = new UndoPerZone(this);
}
return this;
}
public Map<GUID, ExposedAreaMetaData> getExposedAreaMetaData() {
if (exposedAreaMeta == null) {
exposedAreaMeta = new HashMap<GUID, ExposedAreaMetaData>();
}
return exposedAreaMeta;
}
/**
* Find the area of this map which has been exposed to the given token GUID.
*
* @param tokenExposedAreaGUID
* token whose exposed area should be returned
* @return area of fog cleared away for/by this token
*/
public ExposedAreaMetaData getExposedAreaMetaData(GUID tokenExposedAreaGUID) {
ExposedAreaMetaData meta = exposedAreaMeta.get(tokenExposedAreaGUID);
if (meta != null) {
return meta;
}
meta = new ExposedAreaMetaData();
exposedAreaMeta.put(tokenExposedAreaGUID, meta);
return meta;
}
public void setExposedAreaMetaData(GUID tokenExposedAreaGUID, ExposedAreaMetaData meta) {
if (exposedAreaMeta == null) {
exposedAreaMeta = new HashMap<GUID, ExposedAreaMetaData>();
}
exposedAreaMeta.put(tokenExposedAreaGUID, meta);
fireModelChangeEvent(new ModelChangeEvent(this, Event.FOG_CHANGED));
}
}