/* * $Id$ * * Copyright (c) 2004 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.Graphics; import java.awt.Point; import java.awt.Rectangle; import java.awt.Shape; import java.awt.Window; import java.awt.geom.AffineTransform; import java.awt.geom.Point2D; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import javax.swing.Box; import javax.swing.BoxLayout; import javax.swing.JPanel; import javax.swing.KeyStroke; import javax.swing.SwingUtilities; import VASSAL.build.GameModule; import VASSAL.build.module.GlobalOptions; import VASSAL.build.module.Map; import VASSAL.build.module.documentation.HelpFile; import VASSAL.build.module.map.MovementReporter; import VASSAL.build.module.map.boardPicker.Board; import VASSAL.command.Command; import VASSAL.command.NullCommand; import VASSAL.configure.BooleanConfigurer; import VASSAL.configure.FormattedExpressionConfigurer; import VASSAL.configure.NamedHotKeyConfigurer; import VASSAL.configure.StringConfigurer; import VASSAL.i18n.PieceI18nData; import VASSAL.i18n.TranslatablePiece; import VASSAL.tools.FormattedString; import VASSAL.tools.NamedKeyStroke; import VASSAL.tools.SequenceEncoder; /** * Give a piece a command that moves it a fixed amount in a particular * direction, optionally tracking the current rotation of the piece. */ public class Translate extends Decorator implements TranslatablePiece { private static final String _0 = "0"; public static final String ID = "translate;"; protected KeyCommand[] commands; protected String commandName; protected NamedKeyStroke keyCommand; protected FormattedString xDist = new FormattedString(""); protected FormattedString xIndex = new FormattedString(""); protected FormattedString xOffset = new FormattedString(""); protected FormattedString yDist = new FormattedString(""); protected FormattedString yIndex = new FormattedString(""); protected FormattedString yOffset = new FormattedString(""); protected String description; protected boolean moveStack; protected KeyCommand moveCommand; protected static MoveExecuter mover; public Translate() { this(ID + "Move Forward", null); } public Translate(String type, GamePiece inner) { mySetType(type); setInner(inner); } public String getDescription() { String d = "Move fixed distance"; if (description.length() > 0) { d += " - " + description; } return d; } public void mySetType(String type) { type = type.substring(ID.length()); SequenceEncoder.Decoder st = new SequenceEncoder.Decoder(type, ';'); commandName = st.nextToken("Move Forward"); keyCommand = st.nextNamedKeyStroke('M'); xDist.setFormat(st.nextToken(_0)); yDist.setFormat(st.nextToken("60")); moveStack = st.nextBoolean(true); xIndex.setFormat(st.nextToken(_0)); yIndex.setFormat(st.nextToken(_0)); xOffset.setFormat(st.nextToken(_0)); yOffset.setFormat(st.nextToken(_0)); description = st.nextToken(""); commands = null; } protected KeyCommand[] myGetKeyCommands() { if (commands == null) { moveCommand = new KeyCommand(commandName, keyCommand, Decorator.getOutermost(this), this); if (commandName.length() > 0 && keyCommand != null && !keyCommand.isNull()) { commands = new KeyCommand[]{moveCommand}; } else { commands = new KeyCommand[0]; } } moveCommand.setEnabled(getMap() != null); return commands; } public String myGetState() { return ""; } public String myGetType() { SequenceEncoder se = new SequenceEncoder(';'); se.append(commandName) .append(keyCommand) .append(xDist.getFormat()) .append(yDist.getFormat()) .append(moveStack) .append(xIndex.getFormat()) .append(yIndex.getFormat()) .append(xOffset.getFormat()) .append(yOffset.getFormat()) .append(description); return ID + se.getValue(); } public Command keyEvent(KeyStroke stroke) { myGetKeyCommands(); if (moveCommand.matches(stroke)) { // Delay the execution of the inner piece's key event until this piece has moved return myKeyEvent(stroke); } else { return super.keyEvent(stroke); } } public Command myKeyEvent(KeyStroke stroke) { myGetKeyCommands(); Command c = null; if (moveCommand.matches(stroke)) { setOldProperties(); if (mover == null) { mover = new MoveExecuter(); mover.setKeyEvent(stroke); SwingUtilities.invokeLater(mover); } GamePiece target = findTarget(stroke); if (target != null) { c = moveTarget(target); } mover.addKeyEventTarget(piece); // Return a non-null command to indicate that a change actually happened c = new NullCommand() { public boolean isNull() { return false; } }; } return c; } protected Command moveTarget(GamePiece target) { // Has this piece already got a move scheduled? If so, then we // need to use the endpoint of any existing moves as our // starting point. Point p = mover.getUpdatedPosition(target); // First move, so use the current location. if (p == null) { p = new Point(getPosition()); } // Perform the move fixed distance translate(p); // Handle rotation of the piece FreeRotator myRotation = (FreeRotator) Decorator.getDecorator(this, FreeRotator.class); if (myRotation != null) { Point2D myPosition = getPosition().getLocation(); Point2D p2d = p.getLocation(); p2d = AffineTransform.getRotateInstance(myRotation.getCumulativeAngleInRadians(), myPosition.getX(), myPosition.getY()).transform(p2d, null); p = new Point((int) p2d.getX(), (int) p2d.getY()); } // And snap to the grid if required. if (!Boolean.TRUE.equals(Decorator.getOutermost(this).getProperty(Properties.IGNORE_GRID))) { p = getMap().snapTo(p); } // Add to the list of scheduled moves mover.add(target.getMap(), target, p); return null; } protected void translate(Point p) { int x = 0; int y = 0; final GamePiece outer = Decorator.getOutermost(this); final Board b = outer.getMap().findBoard(p); final int Xdist = xDist.getTextAsInt(outer, "Xdistance", this); final int Xindex = xIndex.getTextAsInt(outer, "Xindex", this); final int Xoffset = xOffset.getTextAsInt(outer, "Xoffset", this); x = Xdist + Xindex * Xoffset; if (b != null) { x = (int)Math.round(b.getMagnification()*x); } final int Ydist = yDist.getTextAsInt(outer, "Ydistance", this); final int Yindex = yIndex.getTextAsInt(outer, "Yindex", this); final int Yoffset = yOffset.getTextAsInt(outer, "Yoffset", this); y = Ydist + Yindex * Yoffset; if (b != null) { y = (int)Math.round(b.getMagnification()*y); } p.translate(x, -y); } protected GamePiece findTarget(KeyStroke stroke) { final GamePiece outer = Decorator.getOutermost(this); GamePiece target = outer; if (moveStack && outer.getParent() != null && !outer.getParent().isExpanded()) { // Only move entire stack if this is the top piece // Otherwise moves the stack too far if the whole stack is multi-selected if (outer != outer.getParent().topPiece(GameModule.getUserId())) { target = null; } else { target = outer.getParent(); } } return target; } public void mySetState(String newState) { } public Rectangle boundingBox() { return getInner().boundingBox(); } public void draw(Graphics g, int x, int y, Component obs, double zoom) { getInner().draw(g, x, y, obs, zoom); } public String getName() { return getInner().getName(); } public Shape getShape() { return getInner().getShape(); } public PieceEditor getEditor() { return new Editor(this); } public HelpFile getHelpFile() { return HelpFile.getReferenceManualPage("Translate.htm"); } public PieceI18nData getI18nData() { return getI18nData(commandName, getCommandDescription(description, "Move Fixed Distance command")); } public static class Editor implements PieceEditor { private FormattedExpressionConfigurer xDist; private FormattedExpressionConfigurer yDist; private StringConfigurer name; private NamedHotKeyConfigurer key; private JPanel controls; private BooleanConfigurer moveStack; protected BooleanConfigurer advancedInput; protected FormattedExpressionConfigurer xIndexInput; protected FormattedExpressionConfigurer xOffsetInput; protected FormattedExpressionConfigurer yIndexInput; protected FormattedExpressionConfigurer yOffsetInput; protected StringConfigurer descInput; public Editor(Translate t) { controls = new JPanel(); controls.setLayout(new BoxLayout(controls, BoxLayout.Y_AXIS)); descInput = new StringConfigurer(null, "Description: ", t.description); controls.add(descInput.getControls()); name = new StringConfigurer(null, "Command Name: ", t.commandName); controls.add(name.getControls()); key = new NamedHotKeyConfigurer(null, "Keyboard shortcut: ", t.keyCommand); controls.add(key.getControls()); xDist = new FormattedExpressionConfigurer(null, "Distance to the right: ", t.xDist.getFormat(), t); controls.add(xDist.getControls()); yDist = new FormattedExpressionConfigurer(null, "Distance upwards: ", t.yDist.getFormat(), t); controls.add(yDist.getControls()); moveStack = new BooleanConfigurer(null, "Move entire stack?", Boolean.valueOf(t.moveStack)); controls.add(moveStack.getControls()); advancedInput = new BooleanConfigurer(null, "Advanced Options", false); advancedInput.addPropertyChangeListener(new PropertyChangeListener() { public void propertyChange(PropertyChangeEvent e) { updateAdvancedVisibility(); }}); controls.add(advancedInput.getControls()); Box b = Box.createHorizontalBox(); xIndexInput = new FormattedExpressionConfigurer(null, "Additional offset to the right: ", t.xIndex.getFormat(), t); b.add(xIndexInput.getControls()); xOffsetInput = new FormattedExpressionConfigurer(null, " times ", t.xOffset.getFormat(), t); b.add(xOffsetInput.getControls()); controls.add(b); b = Box.createHorizontalBox(); yIndexInput = new FormattedExpressionConfigurer(null, "Additional offset upwards: ", t.yIndex.getFormat(), t); b.add(yIndexInput.getControls()); yOffsetInput = new FormattedExpressionConfigurer(null, " times ", t.yOffset.getFormat(), t); b.add(yOffsetInput.getControls()); controls.add(b); updateAdvancedVisibility(); } private void updateAdvancedVisibility() { boolean visible = advancedInput.booleanValue().booleanValue(); xIndexInput.getControls().setVisible(visible); xOffsetInput.getControls().setVisible(visible); yIndexInput.getControls().setVisible(visible); yOffsetInput.getControls().setVisible(visible); Window w = SwingUtilities.getWindowAncestor(controls); if (w != null) { w.pack(); } } public Component getControls() { return controls; } public String getState() { return ""; } public String getType() { SequenceEncoder se = new SequenceEncoder(';'); se.append(name.getValueString()) .append(key.getValueString()) .append(xDist.getValueString()) .append(yDist.getValueString()) .append(moveStack.getValueString()) .append(xIndexInput.getValueString()) .append(yIndexInput.getValueString()) .append(xOffsetInput.getValueString()) .append(yOffsetInput.getValueString()) .append(descInput.getValueString()); return ID + se.getValue(); } } /** * Batches up all the movement commands resulting from a single KeyEvent * and executes them at once. Ensures that pieces that are moving won't * be merged with other moving pieces until they've been moved. */ public static class MoveExecuter implements Runnable { private List<Move> moves = new ArrayList<Move>(); private Set<GamePiece> pieces = new HashSet<GamePiece>(); private KeyStroke stroke; private List<GamePiece> innerPieces = new ArrayList<GamePiece>(); public void run() { mover = null; Command comm = new NullCommand(); for (final Move move : moves) { final Map.Merger merger = new Map.Merger(move.map, move.pos, move.piece); DeckVisitor v = new DeckVisitor() { public Object visitDeck(Deck d) { return merger.visitDeck(d); } public Object visitStack(Stack s) { if (!pieces.contains(s) && move.map.getPieceCollection().canMerge(s, move.piece)) { return merger.visitStack(s); } else { return null; } } public Object visitDefault(GamePiece p) { if (!pieces.contains(p) && move.map.getPieceCollection().canMerge(p, move.piece)) { return merger.visitDefault(p); } else { return null; } } }; DeckVisitorDispatcher dispatch = new DeckVisitorDispatcher(v); Command c = move.map.apply(dispatch); if (c == null) { c = move.map.placeAt(move.piece, move.pos); // Apply Auto-move key if (move.map.getMoveKey() != null) { c.append(Decorator.getOutermost(move.piece) .keyEvent(move.map.getMoveKey())); } } comm.append(c); if (move.piece.getMap() == move.map) { move.map.ensureVisible(move.map.selectionBoundsOf(move.piece)); } pieces.remove(move.piece); move.map.repaint(); } MovementReporter r = new MovementReporter(comm); if (GlobalOptions.getInstance().autoReportEnabled()) { Command reportCommand = r.getReportCommand(); if (reportCommand != null) { reportCommand.execute(); } comm.append(reportCommand); } comm.append(r.markMovedPieces()); if (stroke != null) { for (GamePiece gamePiece : innerPieces) { comm.append(gamePiece.keyEvent(stroke)); } } GameModule.getGameModule().sendAndLog(comm); } public void add(Map map, GamePiece piece, Point pos) { moves.add(new Move(map, piece, pos)); pieces.add(piece); } public void addKeyEventTarget(GamePiece piece) { innerPieces.add(piece); } public void setKeyEvent(KeyStroke stroke) { this.stroke = stroke; } /** * Return the updated position of a piece that has a move * calculation recorded * * @param target piece to check * @return updated position */ public Point getUpdatedPosition(GamePiece target) { Point p = null; for (Move move : moves) { if (move.piece == target) { p = move.pos; } } return p; } private static class Move { private Map map; private GamePiece piece; private Point pos; public Move(Map map, GamePiece piece, Point pos) { this.map = map; this.piece = piece; this.pos = pos; } } } }