package net.sf.colossus.gui;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.LayoutManager;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.geom.AffineTransform;
import java.awt.image.AffineTransformOp;
import java.awt.image.BufferedImage;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.ImageIcon;
import javax.swing.JPanel;
import net.sf.colossus.client.Client;
import net.sf.colossus.common.IOptions;
import net.sf.colossus.common.Options;
import net.sf.colossus.game.PlayerColor;
import net.sf.colossus.server.VariantSupport;
import net.sf.colossus.util.StaticResourceLoader;
import net.sf.colossus.variant.CreatureType;
/**
* Class Chit implements the GUI for a Titan chit representing
* either a character or a legion.
*
* TODO offer constructors using the Legion instead of strings
* TODO consider splitting into LegionChit and CreatureChit
* TODO Important: All creature (and marker) related Chits should get an
* option argument in order to be able to ask for options like
* "marker in players original color or now-owning-player's-color
* and "angel in traditionl blue or in actual players color".
*
* @author David Ripton
* @author Romain Dolbeau
*/
class Chit extends JPanel
{
private static final Logger LOGGER = Logger
.getLogger(Chit.class.getName());
private final Image bufferedImage;
private Image bufferedInvertedImage;
Rectangle rect;
final Client client; // may be null; set for some subclasses
final IOptions options;
/** Flag to mark chit as dead and paint it with an "X" through it. */
private boolean dead;
/** Flag to paint a border around the chit. */
private boolean border = true;
private Color borderColor = Color.black;
private final boolean playerColoredAngel;
/** Flag to paint the chit upside-down. */
protected final boolean inverted;
// Initialize early to avoid NullPointerException with GTK L&F
private final String id;
final static BasicStroke oneWide = new BasicStroke(1);
private final static BasicStroke threeWide = new BasicStroke(3);
/**
* Factory method for creatures, based on image names
* TODO try to get rid of the string bases Creature chits
* @param scale
* @param id
* @return The newly created CreatureChit
*/
public static Chit newCreatureChit(int scale, String id)
{
return new Chit(scale, id);
}
/**
* Factory method for creatures, based on CreatureType
* @param scale
* @param type
* @return The newly created CreatureChit
*/
public static Chit newCreatureChit(int scale, CreatureType type)
{
return new Chit(scale, type);
}
/**
* Factory method for creatures, based on markerId
* @param scale
* @param markerId
* @return The newly created MarkerChit
*/
public static Chit newDiceChit(int scale, String markerId)
{
return new Chit(scale, markerId);
}
/**
* Factory method for icons representing e.g. hazard effects
* @param scale
* @param id
* @return the created Chit
*/
public static Chit newSymbolChit(int scale, String id)
{
return new Chit(scale, id);
}
/**
* P l a i n C o n s t r u c t o r s
*
**/
// to be used mostly by the factory methods
Chit(int scale, String id)
{
this(scale, id, false, false);
}
Chit(int scale, CreatureType creatureType)
{
this(scale, creatureType.getName());
}
// No client, no options: use only for TerrainEffect symbol or dice!
Chit(int scale, String id, String[] overlays)
{
this(scale, id, false, false, false, overlays, null, null);
}
Chit(int scale, String id, boolean inverted, Client client)
{
this(scale, id, inverted, false, false, client);
}
Chit(int scale, String id, boolean inverted, boolean dubious)
{
this(scale, id, inverted, dubious, false, null);
}
Chit(int scale, String id, boolean inverted, boolean dubious,
boolean dubiousAsBlank, Client client)
{
this(scale, id, inverted, dubious, dubiousAsBlank, null, client,
client != null ? client.getOptions() : null);
}
// TODO this is a bit confusing: the id parameter can be either the name of
// a creature type or a markerId (maybe more? => Or a symbol for hazard effect,
// or some dice...).
// Good thing markerIds have no overlap with creature names
// (should perhaps use Marker instead plain Chit there?)
/**
* @param idPerhapsWithColor CreatureType id, markerId, or filename of
* some picture denoting some symbol (for HazardEffects).
* For Markers, Titans and Angels could be of form <id>-<color> and then
* they will be painted in that color (e.g. captured markers)
* @param options TODO
*/
private Chit(int scale, String idPerhapsWithColor, boolean inverted,
boolean dubious, boolean dubiousAsBlank, String[] overlays,
Client client, IOptions options)
{
// LayoutManager null - we want to place things ourselves
super((LayoutManager)null);
assert idPerhapsWithColor != null : "Each chit must have an ID set";
if (isMarkerId(idPerhapsWithColor))
{
this.id = idPerhapsWithColor.substring(0, 4);
}
else
{
this.id = idPerhapsWithColor;
}
this.inverted = inverted;
this.options = options;
// should not be needed any more
this.client = client;
// TODO don't get that yet. Refactor all the boolean options
// to get them via an IClient or something?
this.playerColoredAngel = true;
Point point = getLocation();
// Images are 60x60, so if scale is close to that, avoid
// rescaling.
if (scale > 50 && scale < 70)
{
scale = 60;
}
rect = new Rectangle(point.x, point.y, scale, scale);
setBounds(rect);
setBackground(Color.lightGray);
if (dubious && dubiousAsBlank)
{
String[] names = new String[1];
names[0] = "QuestionMarkMask";
bufferedImage = getImage(names, scale);
}
else if (VariantSupport.getCurrentVariant().isCreature(
idPerhapsWithColor))
{
CreatureType cre = VariantSupport.getCurrentVariant()
.getCreatureByName(idPerhapsWithColor);
String[] names = cre.getImageNames();
if (dubious)
{
String[] names2 = new String[names.length + 1];
for (int i = 0; i < names.length; i++)
{
names2[i] = names[i];
}
names2[names.length] = "QuestionMarkMask"
+ (cre.getBaseColor().equals("black") ? "Red" : "");
names = names2;
}
bufferedImage = getImage(names, scale);
}
else
{
if (idPerhapsWithColor.startsWith("Titan-"))
{
String[] filenames = new String[4 + (dubious ? 1 : 0)];
int power = getTitanPower();
String color = idPerhapsWithColor.split("-")[2] + "Colossus";
filenames[0] = "Plain" + "-" + color;
filenames[1] = "TitanMask";
filenames[2] = "Power-" + power + "-" + color;
int skill = (VariantSupport.getCurrentVariant()
.getCreatureByName("Titan")).getSkill();
filenames[3] = "Skill-" + skill + "-" + color;
if (dubious)
{
filenames[4] = "QuestionMarkMask"
+ (color.equals("BlackColossus") ? "Red" : "");
}
bufferedImage = getImage(filenames, scale);
}
else if (idPerhapsWithColor.startsWith("Angel-"))
{
String[] filenames = new String[5 + (dubious ? 1 : 0)];
int power = (VariantSupport.getCurrentVariant()
.getCreatureByName("Angel")).getPower();
String color = playerColoredAngel ? (idPerhapsWithColor
.split("-")[2] + "Colossus") : "giantBlue";
filenames[0] = "Plain" + "-" + color;
filenames[1] = "AngelMask";
filenames[2] = "Power-" + power + "-" + color;
int skill = (VariantSupport.getCurrentVariant()
.getCreatureByName("Angel")).getSkill();
filenames[3] = "Skill-" + skill + "-" + color;
filenames[4] = "Angel-Name-" + color;
if (dubious)
{
filenames[5] = "QuestionMarkMask"
+ (color.equals("BlackColossus") ? "Red" : "");
}
bufferedImage = getImage(filenames, scale);
}
else if (isMarkerId(idPerhapsWithColor))
{
// Legion marker
// May be of form "Bk03" or form "Bk03-Black"
String[] filenames = new String[2];
String colorName;
if (idPerhapsWithColor.length() >= 5)
{
colorName = idPerhapsWithColor.split("-")[1];
}
else
{
String shortColor = idPerhapsWithColor.substring(0, 2);
PlayerColor playerColor = PlayerColor
.getByShortName(shortColor);
colorName = playerColor.getName();
}
filenames[0] = "Plain" + "-" + colorName + "Colossus";
filenames[1] = idPerhapsWithColor.substring(0, 4);
bufferedImage = getImage(filenames, scale);
}
else
{
if (overlays == null)
{
bufferedImage = getImage(idPerhapsWithColor, scale);
}
else
{
String[] filenames = new String[overlays.length + 1];
filenames[0] = idPerhapsWithColor;
for (int i = 0; i < overlays.length; i++)
{
filenames[i + 1] = overlays[i];
}
bufferedImage = getImage(filenames, scale);
}
}
}
}
public static boolean isMarkerId(String id)
{
return (id.length() >= 4 && id.charAt(0) >= 'A' && id.charAt(0) <= 'Z'
&& id.charAt(1) >= 'a' && id.charAt(1) <= 'z'
&& id.charAt(2) >= '0' && id.charAt(2) <= '1'
&& id.charAt(3) >= '0' && id.charAt(3) <= '9');
}
public int getTitanPower()
{
if (!id.startsWith("Titan-"))
{
return -1;
}
String[] parts = id.split("-");
int power = Integer.parseInt(parts[1]);
return power;
}
private static Image getImage(String imageFilename, int scale)
{
ImageIcon tempIcon = null;
List<String> directories = VariantSupport.getImagesDirectoriesList();
tempIcon = StaticResourceLoader.getImageIcon(imageFilename,
directories, scale, scale);
if (tempIcon == null)
{
LOGGER.log(Level.SEVERE, "Couldn't get image :" + imageFilename);
throw new RuntimeException(
"Unable to retrieve image for filename '" + imageFilename
+ "'");
}
return tempIcon.getImage();
}
private static Image getImage(String[] imageFilenames, int scale)
{
List<String> directories = VariantSupport.getImagesDirectoriesList();
Image composite = StaticResourceLoader.getCompositeImage(
imageFilenames, directories, scale, scale);
return composite;
}
// TODO should become package private again one day.
// see isDead()
public String getId()
{
if (id == null)
{
// this should never happen, since id is initialized
// already from beginning on, still someone gets a NPE
// just for this id; perhaps due to using GTK L&F ?
LOGGER.log(Level.SEVERE, "Chit id is still null ?");
// id = "<notdefined?>";
}
return id;
}
@Override
public String toString()
{
return getId();
}
void rescale(int scale)
{
rect.width = scale;
rect.height = scale;
setBounds(rect);
}
@Override
public void paintComponent(Graphics g)
{
Graphics2D g2 = (Graphics2D)g;
super.paintComponent(g2);
Image image = bufferedImage;
// TODO options == null is just a quick hack to prevent NPEs in cases
// there would not be an options object.
if (inverted
&& (options == null || !options
.getOption(Options.doNotInvertDefender)))
{
if (bufferedInvertedImage == null)
{
int width = bufferedImage.getWidth(this);
int height = bufferedImage.getHeight(this);
BufferedImage bi = new BufferedImage(width, height,
BufferedImage.TYPE_INT_RGB);
Graphics2D biContext = bi.createGraphics();
biContext.drawImage(image, 0, 0, null);
double theta = Math.PI;
AffineTransform at = AffineTransform.getRotateInstance(theta,
width / 2.0, height / 2.0);
AffineTransformOp ato = new AffineTransformOp(at,
AffineTransformOp.TYPE_BILINEAR);
BufferedImage bi2 = ato.createCompatibleDestImage(bi, null);
bi2 = ato.filter(bi, bi2);
bufferedInvertedImage = bi2;
}
image = bufferedInvertedImage;
}
g2.drawImage(image, rect.x, rect.y, rect.width, rect.height, this);
if (isDead())
{
// Draw a triple-wide red X.
g2.setStroke(threeWide);
g2.setColor(Color.red);
g2.drawLine(rect.x, rect.y, rect.x + rect.width, rect.y
+ rect.height);
g2.drawLine(rect.x + rect.width, rect.y, rect.x, rect.y
+ rect.height);
g2.setStroke(oneWide);
}
if (border)
{
g2.setColor(borderColor);
Rectangle rect = getBounds();
g.drawRect(rect.x, rect.y, rect.width - 1, rect.height - 1);
}
}
@Override
public void setLocation(Point point)
{
rect.setLocation(point);
setBounds(rect);
}
@Override
public Rectangle getBounds()
{
return rect;
}
public Point getCenter()
{
return new Point(rect.x + rect.width / 2, rect.y + rect.height / 2);
}
@Override
public Dimension getPreferredSize()
{
return new Dimension(rect.width, rect.height);
}
@Override
public Dimension getMinimumSize()
{
return getPreferredSize();
}
@Override
public Dimension getMaximumSize()
{
return getPreferredSize();
}
boolean isDead()
{
return dead;
}
void setDead(boolean dead)
{
this.dead = dead;
repaint();
}
void toggleDead()
{
dead = !dead;
}
void setBorder(boolean border)
{
this.border = border;
}
void setBorderColor(Color borderColor)
{
this.borderColor = borderColor;
}
}