package de.lighti.components.player.statistics;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Point;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.logging.Logger;
import javax.swing.BorderFactory;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import de.lighti.io.ImageCache;
import de.lighti.model.Statics;
import de.lighti.model.game.Ability;
/**
* SkillTree component displays a hero build. It creates a column with a header icon
* for each different ability of a hero and then renders rows with markers
* representing the order the player has upgraded the respective skill.
* The colour scheme is loosely based opn Steam's default coulor scheme for hero builds.
* @author Tobias Mahlmann
*
*/
public class SkillTreecomponent extends JScrollPane {
/**
* Delegate component that takes care of the actual rendering. The outer component is just neded
* for layouting.
*
* @author Tobias Mahlmann
*
*/
private class SkillPanel extends JPanel {
/**
* Generated id.
*/
private static final long serialVersionUID = -9070781920194030418L;
private List<Ability> abilities;
private TreeMap<Long, String> abilityLog;
private final static int INDENT = 5;
private final int ICON_SIZE = 64;
private final int MARKER_SIZE = 32;
private final Color BACKGROUND_COLOR = new Color( 46, 47, 49 ); //Light gray
private final Color ACTIVE_MARKER_COLOR = new Color( 120, 36, 27 ); //Dark red
private final Color INACTIVE_MARKER_COLOR = new Color( 34, 34, 34 ); //Dark gray
private SkillPanel() {
super();
setBackground( BACKGROUND_COLOR );
setBorder( BorderFactory.createLineBorder( Color.BLACK ) );
}
@Override
protected void paintComponent( Graphics g ) {
super.paintComponent( g );
if (abilities != null && !abilities.isEmpty()) {
final FontMetrics metrics = getFontMetrics( getFont() );
final Map<String, Point> columns = new HashMap<String, Point>();
int x = INDENT;
int y = INDENT;
for (final Ability a : abilities) {
if (a != null) {
columns.put( a.getKey(), new Point( x, y ) );
try {
final BufferedImage image = ImageCache.getAbilityImage( a.getKey() );
if (image == null) {
g.drawString( a.getLocalisedName(), x, y );
x += metrics.stringWidth( a.getLocalisedName() ) + INDENT;
}
else {
g.drawImage( image, x, y, ICON_SIZE, ICON_SIZE, this );
x += ICON_SIZE + INDENT;
}
}
catch (final IOException e) {
LOGGER.warning( "Error loading image: " + e.getLocalizedMessage() );
g.drawString( a.getLocalisedName(), x, y );
x += metrics.stringWidth( a.getLocalisedName() ) + INDENT;
}
}
else {
g.drawString( Statics.UNKNOWN_ABILITY, x, y );
x += metrics.stringWidth( Statics.UNKNOWN_ABILITY ) + INDENT;
}
}
y += MARKER_SIZE / 2 + ICON_SIZE + 3 * INDENT;
int i = 1;
for (final String s : abilityLog.values()) {
for (final Map.Entry<String, Point> e : columns.entrySet()) {
if (e.getKey().equals( s )) {
//Draw active marker
x = (int) e.getValue().getX() + MARKER_SIZE + INDENT;
g.setColor( ACTIVE_MARKER_COLOR );
g.fillRect( x - MARKER_SIZE / 2, y - MARKER_SIZE / 2, MARKER_SIZE, MARKER_SIZE );
g.setColor( Color.WHITE );
final String number = Integer.toString( i );
final Rectangle2D bounds = metrics.getStringBounds( number, g );
g.drawString( number, x - (int) bounds.getWidth() / 2, y + (int) bounds.getHeight() / 2 );
}
else {
//Draw inactive marker
x = (int) e.getValue().getX() + MARKER_SIZE + INDENT;
g.setColor( INACTIVE_MARKER_COLOR );
g.fillRect( x - MARKER_SIZE / 2, y - MARKER_SIZE / 2, MARKER_SIZE, MARKER_SIZE );
}
}
y += MARKER_SIZE + INDENT;
i++;
}
}
}
/**
* Recalculate the size of the inner component based on the number of different abilities and
* how many level ups the hero had.
*/
private void recalculateBounds() {
if (abilities != null && !abilities.isEmpty()) {
int x = INDENT;
int y = INDENT + ICON_SIZE + 3 * INDENT;
//We never change the font during rendering, so we can use the component's metrics to calculate the bounds
final FontMetrics metrics = getFontMetrics( getFont() );
for (final Ability a : abilities) {
if (a != null) {
try {
final BufferedImage image = ImageCache.getAbilityImage( a.getKey() );
if (image == null) {
x += metrics.stringWidth( a.getLocalisedName() ) + INDENT;
}
else {
x += ICON_SIZE + INDENT;
}
}
catch (final IOException e) {
LOGGER.warning( "Error loading image: " + e.getLocalizedMessage() );
x += metrics.stringWidth( a.getLocalisedName() ) + INDENT;
}
}
else {
x += metrics.stringWidth( Statics.UNKNOWN_ABILITY ) + INDENT;
}
}
y += (MARKER_SIZE + INDENT) * abilityLog.size() + INDENT;
setPreferredSize( new Dimension( x, y ) );
setSize( new Dimension( x, y ) );
}
}
private void setAbilities( List<Ability> abilities ) {
if (abilities != this.abilities) {
this.abilities = abilities;
abilityLog = new TreeMap<Long, String>();
for (final Ability a : abilities) {
if (a != null) {
for (final Long l : a.getLevel().keySet()) {
if (a.getLevel().get( l ) > 0) {
abilityLog.put( l, a.getKey() );
}
}
}
}
recalculateBounds();
}
}
}
/**
* Generated id.
*/
private static final long serialVersionUID = -5620172809015835852L;
private final static Logger LOGGER = Logger.getLogger( SkillTreecomponent.class.getName() );
private final SkillPanel skillPanel;
/**
* Default constructor.
*/
public SkillTreecomponent() {
super();
final JPanel filler = new JPanel();
filler.setLayout( new FlowLayout() );
skillPanel = new SkillPanel();
filler.add( skillPanel );
setBorder( null );
setViewportView( filler );
}
/**
* Main update method for this component.
* @param abilities a ordered list of a hero abilities.
*/
public void setAbilities( List<Ability> abilities ) {
skillPanel.setAbilities( abilities );
}
}