/* * $Id$ * * Copyright (c) 2005 by Scott Giese, Rodney Kinney, Brent Easton * * 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.AlphaComposite; import java.awt.Color; import java.awt.Component; import java.awt.Composite; import java.awt.Frame; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Point; import java.awt.Rectangle; import java.awt.Shape; import java.awt.Window; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.geom.AffineTransform; import java.awt.geom.Area; import java.awt.geom.Ellipse2D; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import javax.swing.Box; import javax.swing.BoxLayout; import javax.swing.JButton; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JSeparator; import javax.swing.JTextField; import javax.swing.KeyStroke; import javax.swing.SwingUtilities; import VASSAL.build.module.Map; import VASSAL.build.module.documentation.HelpFile; import VASSAL.build.module.map.MapShader; import VASSAL.build.module.map.boardPicker.Board; import VASSAL.build.module.map.boardPicker.board.GeometricGrid; import VASSAL.build.module.map.boardPicker.board.MapGrid; import VASSAL.command.ChangeTracker; import VASSAL.command.Command; import VASSAL.configure.BooleanConfigurer; import VASSAL.configure.ChooseComponentDialog; import VASSAL.configure.ColorConfigurer; import VASSAL.configure.IntConfigurer; import VASSAL.configure.NamedHotKeyConfigurer; import VASSAL.configure.StringConfigurer; import VASSAL.i18n.PieceI18nData; import VASSAL.i18n.Resources; import VASSAL.i18n.TranslatablePiece; import VASSAL.tools.NamedKeyStroke; import VASSAL.tools.SequenceEncoder; /** * @author Scott Giese sgiese@sprintmail.com * * Displays a transparency surrounding the GamePiece which represents the Area of Effect of the GamePiece */ public class AreaOfEffect extends Decorator implements TranslatablePiece, MapShader.ShadedPiece { public static final String ID = "AreaOfEffect;"; protected static final Color defaultTransparencyColor = Color.GRAY; protected static final float defaultTransparencyLevel = 0.3F; protected static final int defaultRadius = 1; protected Color transparencyColor; protected float transparencyLevel; protected int radius; protected boolean alwaysActive; protected boolean active; protected String activateCommand; protected NamedKeyStroke activateKey; protected KeyCommand[] commands; protected String mapShaderName; protected MapShader shader; protected KeyCommand keyCommand; protected boolean fixedRadius = true; protected String radiusMarker = ""; protected String description = ""; public AreaOfEffect() { this(ID + ColorConfigurer.colorToString(defaultTransparencyColor), null); } public AreaOfEffect(String type, GamePiece inner) { mySetType(type); setInner(inner); } public String getDescription() { String d = "Area Of Effect"; if (description.length() > 0) { d += " - " + description; } return d; } public String myGetType() { final SequenceEncoder se = new SequenceEncoder(';'); se.append(transparencyColor); se.append((int) (transparencyLevel * 100)); se.append(radius); se.append(alwaysActive); se.append(activateCommand); se.append(activateKey); se.append(mapShaderName == null ? "" : mapShaderName); se.append(fixedRadius); se.append(radiusMarker); se.append(description); return ID + se.getValue(); } public void mySetType(String type) { final SequenceEncoder.Decoder st = new SequenceEncoder.Decoder(type, ';'); st.nextToken(); // Discard ID transparencyColor = st.nextColor(defaultTransparencyColor); transparencyLevel = st.nextInt((int) (defaultTransparencyLevel * 100)) / 100.0F; radius = st.nextInt(defaultRadius); alwaysActive = st.nextBoolean(true); activateCommand = st.nextToken("Show Area"); activateKey = st.nextNamedKeyStroke(null); keyCommand = new KeyCommand(activateCommand, activateKey, Decorator.getOutermost(this), this); mapShaderName = st.nextToken(""); if (mapShaderName.length() == 0) { mapShaderName = null; } fixedRadius = st.nextBoolean(true); radiusMarker = st.nextToken(""); description = st.nextToken(""); shader = null; commands = null; } // State does not change during the game public String myGetState() { return alwaysActive ? "" : String.valueOf(active); } // State does not change during the game public void mySetState(String newState) { if (!alwaysActive) { active = "true".equals(newState); } } public Rectangle boundingBox() { // TODO: Need the context of the parent Component, because the transparency is only drawn // on a Map.View object. Because this context is not known, the bounding box returned by // this method does not encompass the bounds of the transparency. The result of this is // that portions of the transparency will not be drawn after scrolling the Map window. return piece.boundingBox(); } public Shape getShape() { return piece.getShape(); } public String getName() { return piece.getName(); } public void draw(Graphics g, int x, int y, Component obs, double zoom) { if ((alwaysActive || active) && mapShaderName == null) { // The transparency is only drawn on a Map.View component. Only the // GamePiece is drawn within other windows (Counter Palette, etc.). if (obs instanceof Map.View && getMap() != null) { final Graphics2D g2d = (Graphics2D) g; final Color oldColor = g2d.getColor(); g2d.setColor(transparencyColor); final Composite oldComposite = g2d.getComposite(); g2d.setComposite(AlphaComposite.getInstance( AlphaComposite.SRC_OVER, transparencyLevel)); Area a = getArea(); if (zoom != 1.0) { a = new Area(AffineTransform.getScaleInstance(zoom,zoom) .createTransformedShape(a)); } g2d.fill(a); g2d.setColor(oldColor); g2d.setComposite(oldComposite); } } // Draw the GamePiece piece.draw(g, x, y, obs, zoom); } protected Area getArea() { Area a; final Map map = getMap(); // Always draw the area centered on the piece's current position // (For instance, don't draw it at an offset if it's in an expanded stack) final Point mapPosition = getPosition(); final int myRadius = getRadius(); final Board board = map.findBoard(mapPosition); final MapGrid grid = board == null ? null : board.getGrid(); if (grid instanceof GeometricGrid) { final GeometricGrid gGrid = (GeometricGrid) grid; final Rectangle boardBounds = board.bounds(); final Point boardPosition = new Point( mapPosition.x-boardBounds.x, mapPosition.y-boardBounds.y); a = gGrid.getGridShape(boardPosition, myRadius); // In board co-ords final AffineTransform t = AffineTransform.getTranslateInstance( boardBounds.x, boardBounds.y); // Translate back to map co-ords final double mag = board.getMagnification(); if (mag != 1.0) { t.translate(boardPosition.x, boardPosition.y); t.scale(mag, mag); t.translate(-boardPosition.x, -boardPosition.y); } a = a.createTransformedArea(t); } else { a = new Area( new Ellipse2D.Double(mapPosition.x - myRadius, mapPosition.y - myRadius, myRadius * 2, myRadius * 2)); } return a; } protected int getRadius() { if (fixedRadius) { return radius; } else { final String r = (String) Decorator.getOutermost(this).getProperty(radiusMarker); try { return Integer.parseInt(r); } catch (NumberFormatException e) { reportDataError(this, Resources.getString("Error.non_number_error"), "radius["+radiusMarker+"]="+r, e); return 0; } } } // No hot-keys protected KeyCommand[] myGetKeyCommands() { if (commands == null) { if (alwaysActive || activateCommand.length() == 0) { commands = new KeyCommand[0]; } else { commands = new KeyCommand[]{keyCommand}; } } return commands; } // No hot-keys public Command myKeyEvent(KeyStroke stroke) { Command c = null; myGetKeyCommands(); if (!alwaysActive && keyCommand.matches(stroke)) { final ChangeTracker t = new ChangeTracker(this); active = !active; c = t.getChangeCommand(); } return c; } public HelpFile getHelpFile() { return HelpFile.getReferenceManualPage("AreaOfEffect.htm"); } public PieceEditor getEditor() { return new TraitEditor(this); } public Area getArea(MapShader shader) { Area a = null; final MapShader.ShadedPiece shaded = (MapShader.ShadedPiece) Decorator.getDecorator(piece,MapShader.ShadedPiece.class); if (shaded != null) { a = shaded.getArea(shader); } if (alwaysActive || active) { if (shader.getConfigureName().equals(mapShaderName)) { Area myArea = getArea(); if (a == null) { a = myArea; } else { a.add(myArea); } } } return a; } protected static class TraitEditor implements PieceEditor { protected JPanel panel; protected ColorConfigurer transparencyColorValue; protected IntConfigurer transparencyValue; protected IntConfigurer radiusValue; protected BooleanConfigurer alwaysActive; protected StringConfigurer activateCommand; protected NamedHotKeyConfigurer activateKey; protected BooleanConfigurer useMapShader; protected BooleanConfigurer fixedRadius; protected StringConfigurer radiusMarker; protected StringConfigurer descConfig; protected Box selectShader; protected String mapShaderId; protected TraitEditor(AreaOfEffect trait) { panel = new JPanel(); panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); panel.add(new JLabel("Contributed by Scott Giese (sgiese@sprintmail.com)", JLabel.CENTER)); panel.add(new JSeparator()); panel.add(new JLabel(" ")); descConfig = new StringConfigurer(null, "Description: ", trait.description); panel.add(descConfig.getControls()); useMapShader = new BooleanConfigurer(null, "Use Map Shading?", trait.mapShaderName != null); mapShaderId = trait.mapShaderName; panel.add(useMapShader.getControls()); selectShader = Box.createHorizontalBox(); panel.add(selectShader); final JLabel l = new JLabel("Map Shading: "); selectShader.add(l); final JTextField tf = new JTextField(12); tf.setEditable(false); selectShader.add(tf); tf.setText(trait.mapShaderName); final JButton b = new JButton("Select"); selectShader.add(b); b.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { ChooseComponentDialog d = new ChooseComponentDialog((Frame) SwingUtilities.getAncestorOfClass(Frame.class, panel), MapShader.class); d.setVisible(true); if (d.getTarget() != null) { mapShaderId = d.getTarget().getConfigureName(); tf.setText(mapShaderId); } else { mapShaderId = null; tf.setText(""); } } }); transparencyColorValue = new ColorConfigurer(null, "Fill Color: ", trait.transparencyColor); panel.add(transparencyColorValue.getControls()); transparencyValue = new IntConfigurer(null, "Opacity (%): ", (int) (trait.transparencyLevel * 100)); panel.add(transparencyValue.getControls()); fixedRadius = new BooleanConfigurer(null, "Fixed Radius?", Boolean.valueOf(trait.fixedRadius)); fixedRadius.addPropertyChangeListener(new PropertyChangeListener() { public void propertyChange(PropertyChangeEvent evt) { updateRangeVisibility(); } }); panel.add(fixedRadius.getControls()); radiusValue = new IntConfigurer(null, "Radius: ", trait.radius); panel.add(radiusValue.getControls()); radiusMarker = new StringConfigurer(null, "Radius Marker: ", trait.radiusMarker); panel.add(radiusMarker.getControls()); alwaysActive = new BooleanConfigurer(null, "Always visible?", trait.alwaysActive ? Boolean.TRUE : Boolean.FALSE); activateCommand = new StringConfigurer(null, "Toggle visible command: ", trait.activateCommand); activateKey = new NamedHotKeyConfigurer(null, "Toggle visible keyboard shortcut: ", trait.activateKey); updateRangeVisibility(); alwaysActive.addPropertyChangeListener(new PropertyChangeListener() { public void propertyChange(PropertyChangeEvent evt) { updateCommandVisibility(); } }); updateCommandVisibility(); useMapShader.addPropertyChangeListener(new PropertyChangeListener() { public void propertyChange(PropertyChangeEvent evt) { updateFillVisibility(); } }); updateFillVisibility(); panel.add(alwaysActive.getControls()); panel.add(activateCommand.getControls()); panel.add(activateKey.getControls()); } protected void updateFillVisibility() { final boolean useShader = Boolean.TRUE.equals(useMapShader.getValue()); transparencyColorValue.getControls().setVisible(!useShader); transparencyValue.getControls().setVisible(!useShader); selectShader.setVisible(useShader); repack(); } protected void updateRangeVisibility() { final boolean fixedRange = fixedRadius.booleanValue().booleanValue(); radiusValue.getControls().setVisible(fixedRange); radiusMarker.getControls().setVisible(!fixedRange); repack(); } protected void updateCommandVisibility() { final boolean alwaysActiveSelected = Boolean.TRUE.equals(alwaysActive.getValue()); activateCommand.getControls().setVisible(!alwaysActiveSelected); activateKey.getControls().setVisible(!alwaysActiveSelected); repack(); } protected void repack() { final Window w = SwingUtilities.getWindowAncestor(alwaysActive.getControls()); if (w != null) { w.pack(); } } public Component getControls() { return panel; } public String getState() { return "false"; } public String getType() { final boolean alwaysActiveSelected = Boolean.TRUE.equals(alwaysActive.getValue()); final SequenceEncoder se = new SequenceEncoder(';'); se.append(transparencyColorValue.getValueString()); se.append(transparencyValue.getValueString()); se.append(radiusValue.getValueString()); se.append(alwaysActiveSelected); se.append(activateCommand.getValueString()); se.append(activateKey.getValueString()); if (Boolean.TRUE.equals(useMapShader.getValue()) && mapShaderId != null) { se.append(mapShaderId); } else { se.append(""); } se.append(fixedRadius.getValueString()); se.append(radiusMarker.getValueString()); se.append(descConfig.getValueString()); return AreaOfEffect.ID + se.getValue(); } } public PieceI18nData getI18nData() { return getI18nData(activateCommand, getCommandDescription(description, "Toggle Visible command")); } }