/* * $Id$ * * Copyright (c) 2000-2003 by Rodney Kinney * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License (LGPL) as published by the Free Software Foundation. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public * License along with this library; if not, copies are available * at http://www.opensource.org. */ package VASSAL.counters; import java.awt.Component; import java.awt.Font; import java.awt.Graphics; import java.awt.Image; import java.awt.Point; import java.awt.Rectangle; import java.awt.Shape; import java.awt.event.InputEvent; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import javax.swing.Box; import javax.swing.BoxLayout; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JPopupMenu; import javax.swing.JTextField; import javax.swing.KeyStroke; import VASSAL.build.GameModule; import VASSAL.build.module.Chatter; import VASSAL.build.module.GlobalOptions; import VASSAL.build.module.Map; import VASSAL.build.module.documentation.HelpFile; import VASSAL.build.module.map.boardPicker.Board; import VASSAL.build.module.map.boardPicker.board.mapgrid.Zone; import VASSAL.build.module.properties.PropertyNameSource; import VASSAL.command.AddPiece; import VASSAL.command.ChangePiece; import VASSAL.command.Command; import VASSAL.command.RemovePiece; import VASSAL.i18n.Localization; import VASSAL.i18n.PieceI18nData; import VASSAL.i18n.Resources; import VASSAL.i18n.TranslatablePiece; import VASSAL.tools.SequenceEncoder; import VASSAL.tools.image.ImageUtils; import VASSAL.tools.imageop.ScaledImagePainter; /** * Basic class for representing a physical component of the game Can be a counter, a card, or an overlay */ public class BasicPiece implements TranslatablePiece, StateMergeable, PropertyNameSource { public static final String ID = "piece;"; private static Highlighter highlighter; /** * Return information about the current location of the piece through getProperty(): * * LocationName - Current Location Name of piece as displayed in Chat Window CurrentX - Current X position CurrentY - * Current Y position CurrentMap - Current Map name or "" if not on a map CurrentBoard - Current Board name or "" if * not on a map CurrentZone - If the current map has a multi-zoned grid, then return the name of the Zone the piece is * in, or "" if the piece is not in any zone, or not on a map */ public static final String LOCATION_NAME = "LocationName"; public static final String CURRENT_MAP = "CurrentMap"; public static final String CURRENT_BOARD = "CurrentBoard"; public static final String CURRENT_ZONE = "CurrentZone"; public static final String CURRENT_X = "CurrentX"; public static final String CURRENT_Y = "CurrentY"; public static final String OLD_LOCATION_NAME = "OldLocationName"; public static final String OLD_MAP = "OldMap"; public static final String OLD_BOARD = "OldBoard"; public static final String OLD_ZONE = "OldZone"; public static final String OLD_X = "OldX"; public static final String OLD_Y = "OldY"; public static final String BASIC_NAME = "BasicName"; public static final String PIECE_NAME = "PieceName"; public static final String DECK_NAME = "DeckName"; public static final String DECK_POSITION = "DeckPosition"; public static Font POPUP_MENU_FONT = new Font("Dialog", 0, 11); protected JPopupMenu popup; protected Rectangle imageBounds; protected ScaledImagePainter imagePainter = new ScaledImagePainter(); private Map map; private KeyCommand[] commands; private Stack parent; private Point pos = new Point(0, 0); private String id; private java.util.Map<Object, Object> props; /** @deprecated Moved into own traits, retained for backward compatibility */ @Deprecated private char cloneKey; /** @deprecated Moved into own traits, retained for backward compatibility */ @Deprecated private char deleteKey; /** * @deprecated Replaced by * @{link #srcOp}. */ @Deprecated protected Image image; protected String imageName; private String commonName; public BasicPiece() { this(ID + ";;;;"); } public BasicPiece(String type) { mySetType(type); } public void mySetType(String type) { final SequenceEncoder.Decoder st = new SequenceEncoder.Decoder(type, ';'); st.nextToken(); cloneKey = st.nextChar('\0'); deleteKey = st.nextChar('\0'); imageName = st.nextToken(); commonName = st.nextToken(); imagePainter.setImageName(imageName); commands = null; } public String getType() { final SequenceEncoder se = new SequenceEncoder(cloneKey > 0 ? String.valueOf(cloneKey) : "", ';'); return ID + se.append(deleteKey > 0 ? String.valueOf(deleteKey) : "") .append(imageName) .append(commonName).getValue(); } public void setMap(Map map) { if (map != this.map) { commands = null; this.map = map; } } public Map getMap() { return getParent() == null ? map : getParent().getMap(); } public Object getProperty(Object key) { if (BASIC_NAME.equals(key)) { return getName(); } else return getPublicProperty(key); } /* * Properties visible in a masked unit */ public Object getPublicProperty(Object key) { if (Properties.KEY_COMMANDS.equals(key)) { return getKeyCommands(); } else if (LOCATION_NAME.equals(key)) { return getMap() == null ? "" : getMap().locationName(getPosition()); } else if (PIECE_NAME.equals(key)) { return Decorator.getOutermost(this).getName(); } else if (CURRENT_MAP.equals(key)) { return getMap() == null ? "" : getMap().getConfigureName(); } else if (DECK_NAME.equals(key)) { return getParent() instanceof Deck ? ((Deck) getParent()).getDeckName() : ""; } else if (DECK_POSITION.equals(key)) { if (getParent() instanceof Deck) { final Deck deck = (Deck) getParent(); final int size = deck.getPieceCount(); final int pos = deck.indexOf(Decorator.getOutermost(this)); return String.valueOf(size - pos + 1); } else { return "0"; } } else if (CURRENT_BOARD.equals(key)) { if (getMap() != null) { final Board b = getMap().findBoard(getPosition()); if (b != null) { return b.getName(); } } return ""; } else if (CURRENT_ZONE.equals(key)) { if (getMap() != null) { final Zone z = getMap().findZone(getPosition()); if (z != null) { return z.getName(); } } return ""; } else if (CURRENT_X.equals(key)) { return String.valueOf(getPosition().x); } else if (CURRENT_Y.equals(key)) { return String.valueOf(getPosition().y); } else if (Properties.VISIBLE_STATE.equals(key)) { return ""; } Object prop = props == null ? null : props.get(key); if (prop == null) { final Map map = getMap(); final Zone zone = (map == null ? null : map.findZone(getPosition())); if (zone != null) { prop = zone.getProperty(key); } else if (map != null) { prop = map.getProperty(key); } else { prop = GameModule.getGameModule().getProperty(key); } } return prop; } public Object getLocalizedProperty(Object key) { if (BASIC_NAME.equals(key)) { return getLocalizedName(); } else { return getLocalizedPublicProperty(key); } } /* * Properties visible in a masked unit */ public Object getLocalizedPublicProperty(Object key) { if (Properties.KEY_COMMANDS.equals(key)) { return getProperty(key); } else if (LOCATION_NAME.equals(key)) { return getMap() == null ? "" : getMap().localizedLocationName(getPosition()); } else if (PIECE_NAME.equals(key)) { return Decorator.getOutermost(this).getName(); } else if (BASIC_NAME.equals(key)) { return getLocalizedName(); } else if (CURRENT_MAP.equals(key)) { return getMap() == null ? "" : getMap().getLocalizedConfigureName(); } else if (DECK_NAME.equals(key)) { return getProperty(key); } else if (DECK_POSITION.equals(key)) { if (getParent() instanceof Deck) { final Deck deck = (Deck) getParent(); final int size = deck.getPieceCount(); final int pos = deck.indexOf(Decorator.getOutermost(this)); return String.valueOf(size - pos); } else { return "0"; } } else if (CURRENT_BOARD.equals(key)) { if (getMap() != null) { final Board b = getMap().findBoard(getPosition()); if (b != null) { return b.getLocalizedName(); } } return ""; } else if (CURRENT_ZONE.equals(key)) { if (getMap() != null) { final Zone z = getMap().findZone(getPosition()); if (z != null) { return z.getLocalizedName(); } } return ""; } else if (CURRENT_X.equals(key)) { return getProperty(key); } else if (CURRENT_Y.equals(key)) { return getProperty(key); } else if (Properties.VISIBLE_STATE.equals(key)) { return getProperty(key); } Object prop = props == null ? null : props.get(key); if (prop == null) { final Map map = getMap(); final Zone zone = (map == null ? null : map.findZone(getPosition())); if (zone != null) { prop = zone.getLocalizedProperty(key); } else if (map != null) { prop = map.getLocalizedProperty(key); } else { prop = GameModule.getGameModule().getLocalizedProperty(key); } } return prop; } public void setProperty(Object key, Object val) { if (props == null) { props = new HashMap<Object, Object>(); } if (val == null) { props.remove(key); } else { props.put(key, val); } } protected Object prefsValue(String s) { return GameModule.getGameModule().getPrefs().getValue(s); } public void draw(Graphics g, int x, int y, Component obs, double zoom) { if (imageBounds == null) { imageBounds = boundingBox(); } imagePainter.draw(g, x + (int) (zoom * imageBounds.x), y + (int) (zoom * imageBounds.y), zoom, obs); } protected KeyCommand[] getKeyCommands() { if (commands == null) { final ArrayList<KeyCommand> l = new ArrayList<KeyCommand>(); final GamePiece target = Decorator.getOutermost(this); if (cloneKey > 0) { l.add(new KeyCommand("Clone", KeyStroke.getKeyStroke(cloneKey, InputEvent.CTRL_MASK), target)); } if (deleteKey > 0) { l.add(new KeyCommand("Delete", KeyStroke.getKeyStroke(deleteKey, InputEvent.CTRL_MASK), target)); } commands = l.toArray(new KeyCommand[l.size()]); } final GamePiece outer = Decorator.getOutermost(this); boolean canAdjustPosition = outer.getMap() != null && outer.getParent() != null && outer.getParent().topPiece() != getParent().bottomPiece(); enableCommand("Move up", canAdjustPosition); enableCommand("Move down", canAdjustPosition); enableCommand("Move to top", canAdjustPosition); enableCommand("Move to bottom", canAdjustPosition); enableCommand("Clone", outer.getMap() != null); enableCommand("Delete", outer.getMap() != null); return commands; } private void enableCommand(String name, boolean enable) { for (int i = 0; i < commands.length; ++i) { if (name.equals(commands[i].getName())) { commands[i].setEnabled(enable); } } } private boolean isEnabled(KeyStroke stroke) { if (stroke == null) { return false; } for (int i = 0; i < commands.length; ++i) { if (stroke.equals(commands[i].getKeyStroke())) { return commands[i].isEnabled(); } } return true; } public Point getPosition() { return getParent() == null ? new Point(pos) : getParent().getPosition(); } public void setPosition(Point p) { if (getMap() != null && getParent() == null) { getMap().repaint(getMap().boundingBoxOf(Decorator.getOutermost(this))); } pos = p; if (getMap() != null && getParent() == null) { getMap().repaint(getMap().boundingBoxOf(Decorator.getOutermost(this))); } } public Stack getParent() { return parent; } public void setParent(Stack s) { parent = s; } public Rectangle boundingBox() { if (imageBounds == null) { imageBounds = ImageUtils.getBounds(imagePainter.getImageSize()); } return new Rectangle(imageBounds); } public Shape getShape() { return boundingBox(); } public boolean equals(GamePiece c) { return c == this; } public String getName() { return commonName; } public String getLocalizedName() { final String key = TranslatablePiece.PREFIX + getName(); return Localization.getInstance().translate(key, getName()); } public Command keyEvent(KeyStroke stroke) { getKeyCommands(); if (!isEnabled(stroke)) { return null; } Command comm = null; final GamePiece outer = Decorator.getOutermost(this); if (KeyStroke.getKeyStroke(cloneKey, InputEvent.CTRL_MASK).equals(stroke)) { final GamePiece newPiece = ((AddPiece) GameModule.getGameModule().decode(GameModule.getGameModule().encode(new AddPiece(outer)))).getTarget(); newPiece.setId(null); GameModule.getGameModule().getGameState().addPiece(newPiece); newPiece.setState(outer.getState()); comm = new AddPiece(newPiece); if (getMap() != null) { comm.append(getMap().placeOrMerge(newPiece, getPosition())); KeyBuffer.getBuffer().remove(outer); KeyBuffer.getBuffer().add(newPiece); if (GlobalOptions.getInstance().autoReportEnabled() && !Boolean.TRUE.equals(outer.getProperty(Properties.INVISIBLE_TO_OTHERS))) { String s = "* " + outer.getLocalizedName(); final String loc = getMap().locationName(outer.getPosition()); if (loc != null) { s += " cloned in " + loc + " * "; } else { s += "cloned *"; } final Command report = new Chatter.DisplayText(GameModule.getGameModule().getChatter(), s); report.execute(); comm = comm.append(report); } } } else if (KeyStroke.getKeyStroke(deleteKey, InputEvent.CTRL_MASK).equals(stroke)) { comm = new RemovePiece(outer); if (getMap() != null && GlobalOptions.getInstance().autoReportEnabled() && !Boolean.TRUE.equals(outer.getProperty(Properties.INVISIBLE_TO_OTHERS))) { String s = "* " + outer.getLocalizedName(); final String loc = getMap().locationName(outer.getPosition()); if (loc != null) { s += " deleted from " + loc + " * "; } else { s += " deleted *"; } final Command report = new Chatter.DisplayText(GameModule.getGameModule().getChatter(), s); comm = comm.append(report); } comm.execute(); } else if (getMap() != null && stroke.equals(getMap().getStackMetrics().getMoveUpKey())) { if (parent != null) { final String oldState = parent.getState(); final int index = parent.indexOf(outer); if (index < parent.getPieceCount() - 1) { parent.insert(outer, index + 1); comm = new ChangePiece(parent.getId(), oldState, parent.getState()); } else { getMap().getPieceCollection().moveToFront(parent); } } else { getMap().getPieceCollection().moveToFront(outer); } } else if (getMap() != null && stroke.equals(getMap().getStackMetrics().getMoveDownKey())) { if (parent != null) { final String oldState = parent.getState(); final int index = parent.indexOf(outer); if (index > 0) { parent.insert(outer, index - 1); comm = new ChangePiece(parent.getId(), oldState, parent.getState()); } else { getMap().getPieceCollection().moveToBack(parent); } } else { getMap().getPieceCollection().moveToBack(outer); } } else if (getMap() != null && stroke.equals(getMap().getStackMetrics().getMoveTopKey())) { parent = outer.getParent(); if (parent != null) { final String oldState = parent.getState(); if (parent.indexOf(outer) < parent.getPieceCount() - 1) { parent.insert(outer, parent.getPieceCount() - 1); comm = new ChangePiece(parent.getId(), oldState, parent.getState()); } else { getMap().getPieceCollection().moveToFront(parent); } } else { getMap().getPieceCollection().moveToFront(outer); } } else if (getMap() != null && stroke.equals(getMap().getStackMetrics().getMoveBottomKey())) { parent = getParent(); if (parent != null) { final String oldState = parent.getState(); if (parent.indexOf(outer) > 0) { parent.insert(outer, 0); comm = new ChangePiece(parent.getId(), oldState, parent.getState()); } else { getMap().getPieceCollection().moveToBack(parent); } } else { getMap().getPieceCollection().moveToBack(outer); } } return comm; } public String getState() { final SequenceEncoder se = new SequenceEncoder(';'); final String mapName = map == null ? "null" : map.getIdentifier(); se.append(mapName); final Point p = getPosition(); se.append(p.x).append(p.y); se.append(getGpId()); return se.getValue(); } public void setState(String s) { final GamePiece outer = Decorator.getOutermost(this); final Map oldMap = getMap(); final SequenceEncoder.Decoder st = new SequenceEncoder.Decoder(s, ';'); final String mapId = st.nextToken(); Map newMap = null; if (!"null".equals(mapId)) { newMap = Map.getMapById(mapId); if (newMap == null) { Decorator.reportDataError(this, Resources.getString("Error.not_found", "Map"), "mapId="+mapId); } } final Point newPos = new Point(st.nextInt(0), st.nextInt(0)); setPosition(newPos); if (newMap != oldMap) { if (newMap != null) { // This will remove from oldMap // and set the map to newMap newMap.addPiece(outer); } else if (oldMap != null) { oldMap.removePiece(outer); setMap(null); } else { setMap(null); } } setGpId(st.nextToken("")); } public void mergeState(String newState, String oldState) { if (!newState.equals(oldState)) { setState(newState); } } public String getId() { return id; } public void setId(String id) { this.id = id; } /** * Get the Highlighter instance for drawing selected pieces. Note that since this is a static method, all pieces in a * module will always use the same Highlighter */ public static Highlighter getHighlighter() { if (highlighter == null) { highlighter = new ColoredBorder(); } return highlighter; } /** * Set the Highlighter for all pieces */ public static void setHighlighter(Highlighter h) { highlighter = h; } public String getDescription() { return "Basic Piece"; } public String getGpId() { String id = (String) getProperty(Properties.PIECE_ID); return id == null ? "" : id; } public void setGpId(String id) { setProperty(Properties.PIECE_ID, id == null ? "" : id); } public HelpFile getHelpFile() { return HelpFile.getReferenceManualPage("BasicPiece.htm"); } public PieceEditor getEditor() { return new Ed(this); } private static class Ed implements PieceEditor { private JPanel panel; private KeySpecifier cloneKeyInput; private KeySpecifier deleteKeyInput; private JTextField pieceName; private ImagePicker picker; private String state; private Ed(BasicPiece p) { state = p.getState(); initComponents(p); } private void initComponents(BasicPiece p) { panel = new JPanel(); panel.setLayout(new BoxLayout(panel, BoxLayout.X_AXIS)); picker = new ImagePicker(); picker.setImageName(p.imageName); panel.add(picker); cloneKeyInput = new KeySpecifier(p.cloneKey); deleteKeyInput = new KeySpecifier(p.deleteKey); pieceName = new JTextField(12); pieceName.setText(p.commonName); pieceName.setMaximumSize(pieceName.getPreferredSize()); Box col = Box.createVerticalBox(); Box row = Box.createHorizontalBox(); row.add(new JLabel("Name: ")); row.add(pieceName); col.add(row); if (p.cloneKey != 0) { row = Box.createHorizontalBox(); row.add(new JLabel("To Clone: ")); row.add(cloneKeyInput); col.add(row); } if (p.deleteKey != 0) { row = Box.createHorizontalBox(); row.add(new JLabel("To Delete: ")); row.add(deleteKeyInput); col.add(row); } panel.add(col); } public void reset(BasicPiece p) { } public Component getControls() { return panel; } public String getState() { return state; } public String getType() { final SequenceEncoder se = new SequenceEncoder(cloneKeyInput.getKey(), ';'); final String type = se.append(deleteKeyInput.getKey()).append(picker.getImageName()).append(pieceName.getText()).getValue(); return BasicPiece.ID + type; } } public String toString() { return super.toString() + "[name=" + getName() + ",type=" + getType() + ",state=" + getState() + "]"; } public PieceI18nData getI18nData() { final PieceI18nData data = new PieceI18nData(this); data.add(commonName, "Basic piece name"); return data; } /** * Return Property names exposed by this trait */ public List<String> getPropertyNames() { ArrayList<String> l = new ArrayList<String>(); l.add(LOCATION_NAME); l.add(CURRENT_MAP); l.add(CURRENT_BOARD); l.add(CURRENT_ZONE); l.add(CURRENT_X); l.add(CURRENT_Y); l.add(OLD_LOCATION_NAME); l.add(OLD_MAP); l.add(OLD_BOARD); l.add(OLD_ZONE); l.add(OLD_X); l.add(OLD_Y); l.add(BASIC_NAME); l.add(PIECE_NAME); l.add(DECK_NAME); return l; } }