GamePanelView.java
package org.microcol.gui.panelview;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Toolkit;
import java.awt.image.VolatileImage;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import javax.swing.JPanel;
import javax.swing.JViewport;
import javax.swing.Timer;
import org.microcol.gui.FpsCounter;
import org.microcol.gui.ImageProvider;
import org.microcol.gui.PathPlanning;
import org.microcol.gui.Point;
import org.microcol.gui.StepCounter;
import org.microcol.gui.event.GameController;
import org.microcol.gui.event.NextTurnController;
import org.microcol.model.Location;
import org.microcol.model.Model;
import org.microcol.model.Player;
import org.microcol.model.Ship;
import org.microcol.model.Terrain;
import com.google.common.base.Preconditions;
import com.google.inject.Inject;
public class GamePanelView extends JPanel implements GamePanelPresenter.Display {
/**
* Default serialVersionUID.
*/
private static final long serialVersionUID = 1L;
private final static int TILE_WIDTH_IN_PX = 35;
public final static int TOTAL_TILE_WIDTH_IN_PX = TILE_WIDTH_IN_PX;
private final ImageProvider imageProvider;
private VolatileImage dbImage;
private final Cursor gotoModeCursor;
private final GameController gameController;
private final PathPlanning pathPlanning;
private final VisualDebugInfo visualDebugInfo;
private Location gotoCursorTitle;
private Location cursorLocation;
private boolean gotoMode = false;
private WalkAnimator walkAnimator;
private final FpsCounter fpsCounter;
private boolean isGridShown;
private ScreenScrolling screenScrolling;
private final OneTurnMoveHighlighter oneTurnMoveHighlighter;
@Inject
public GamePanelView(final GameController gameController, final NextTurnController nextTurnController,
final PathPlanning pathPlanning, final ImageProvider imageProvider) {
this.gameController = Preconditions.checkNotNull(gameController);
this.pathPlanning = Preconditions.checkNotNull(pathPlanning);
this.imageProvider = Preconditions.checkNotNull(imageProvider);
this.visualDebugInfo = new VisualDebugInfo();
oneTurnMoveHighlighter = new OneTurnMoveHighlighter();
Toolkit toolkit = Toolkit.getDefaultToolkit();
gotoModeCursor = toolkit.createCustomCursor(imageProvider.getImage(ImageProvider.IMG_CURSOR_GOTO),
new java.awt.Point(1, 1), "gotoModeCursor");
final GamePanelView map = this;
nextTurnController.addListener(w -> map.repaint());
setAutoscrolls(true);
fpsCounter = new FpsCounter();
fpsCounter.start();
isGridShown = true;
timer = new Timer(1000 / DEFAULT_FRAME_PER_SECOND, event -> nextGameTick());
}
private final Timer timer;
/**
* Define how many times per second will be screen repainted (FPS). Real FPS
* will be always lover than this value. It's because not all
* {@link JComponent#repaint()} leads to real screen repaint.
*/
private final static int DEFAULT_FRAME_PER_SECOND = 50;
@Override
public void initGame(final boolean idGridShown) {
this.isGridShown = idGridShown;
if (!timer.isRunning()) {
timer.start();
}
}
@Override
public void stopTimer() {
fpsCounter.stop();
timer.stop();
}
/**
* Smallest game time interval. In ideal case it have time to draw world on
* screen.
*/
private void nextGameTick() {
if (screenScrolling != null && screenScrolling.isNextPointAvailable()) {
scrollToPoint(screenScrolling.getNextPoint());
}
repaint();
}
@Override
public void planScrollingAnimationToPoint(final Point targetPoint) {
screenScrolling = new ScreenScrolling(pathPlanning, getArea().getPointTopLeft(), targetPoint);
}
private void scrollToPoint(final Point point) {
final JViewport viewPort = (JViewport) getParent();
final Rectangle view = viewPort.getViewRect();
view.x = point.getX();
view.y = point.getY();
scrollRectToVisible(view);
}
@Override
public GamePanelView getGamePanelView() {
return this;
}
private VolatileImage prepareImage(final Area area) {
final Point p = Point.of(area.getWidth(), area.getHeight()).multiply(TILE_WIDTH_IN_PX);
return createVolatileImage(p.getX(), p.getY());
}
/**
* Call original paint with antialising on.
*/
@Override
public void paint(final Graphics g) {
super.paint(g);
final Graphics2D g2d = (Graphics2D) g;
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
final Area area = new Area((JViewport) this.getParent(), gameController.getModel().getMap());
if (dbImage == null) {
dbImage = prepareImage(area);
if (dbImage == null) {
/**
* This could happens when Graphics is not ready.
*/
return;
}
}
if (dbImage.validate(getGraphicsConfiguration()) == VolatileImage.IMAGE_INCOMPATIBLE) {
dbImage = prepareImage(area);
}
if (dbImage != null) {
final Graphics2D dbg = (Graphics2D) dbImage.getGraphics();
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF);
/**
* Following background drawing just verify that there are no
* uncovered pixels.
*/
// dbg.setColor(Color.YELLOW);
// dbg.fillRect(0, 0, getWidth(), getHeight());
paintTiles(dbg, area);
paintUnits(dbg, gameController.getModel(), area);
paintGrid(dbg, area);
paintCursor(dbg, area);
paintGoToPath(dbg, area);
paintMovingAnimation(dbg, area);
paintDebugInfo(dbg, visualDebugInfo, area);
final Point p = Point.of(area.getTopLeft().add(Location.of(-1, -1)));
g.drawImage(dbImage, p.getX(), p.getY(), null);
if (gameController.getModel().getCurrentPlayer().isComputer()) {
/**
* If move computer that make game field darker.
*/
g.drawImage(dbImage, p.getX(), p.getY(), null);
g.setColor(new Color(0, 0, 0, 64));
g.fillRect(p.getX(), p.getY(), dbImage.getWidth(this), dbImage.getHeight(this));
}
dbg.dispose();
}
// Sync the display on some systems.
// (on Linux, this fixes event queue problems)
Toolkit.getDefaultToolkit().sync();
fpsCounter.screenWasPainted();
}
private void paintMovingAnimation(final Graphics2D graphics, final Area area) {
if (walkAnimator != null && walkAnimator.getNextCoordinates() != null) {
if (area.isInArea(walkAnimator.getNextCoordinates())) {
final Point part = area.convert(walkAnimator.getNextCoordinates());
paintUnit(graphics, part, walkAnimator.getUnit());
graphics.drawImage(imageProvider.getImage(ImageProvider.IMG_TILE_MODE_GOTO),
part.getX() + TILE_WIDTH_IN_PX - 12, part.getY(), this);
}
if (walkAnimator.isNextAnimationLocationAvailable()) {
walkAnimator.countNextAnimationLocation();
}
}
}
/**
* Draw main game tiles.
*
* @param graphics
* required {@link Graphics2D}
*/
private void paintTiles(final Graphics2D graphics, final Area area) {
for (int i = area.getTopLeft().getX(); i <= area.getBottomRight().getX(); i++) {
for (int j = area.getTopLeft().getY(); j <= area.getBottomRight().getY(); j++) {
final Location location = Location.of(i, j);
final Point point = area.convert(location);
final Terrain terrain = gameController.getModel().getMap().getTerrainAt(location);
graphics.drawImage(imageProvider.getTerrainImage(terrain), point.getX(), point.getY(),
point.getX() + TILE_WIDTH_IN_PX, point.getY() + TILE_WIDTH_IN_PX, 0, 0, TILE_WIDTH_IN_PX,
TILE_WIDTH_IN_PX, this);
if (oneTurnMoveHighlighter.isItHighlighted(location)) {
graphics.setColor(new Color(255, 200, 255, 100));
graphics.fillRect(point.getX(), point.getY(), TILE_WIDTH_IN_PX, TILE_WIDTH_IN_PX);
}
}
}
}
/**
* Draw units.
* <p>
* Methods iterate through all location with ships, select first ship and
* draw it.
* </p>
*
* @param graphics
* required {@link Graphics2D}
* @param game
* required {@link Game}
*/
private void paintUnits(final Graphics2D graphics, final Model world, final Area area) {
final java.util.Map<Location, List<Ship>> ships = world.getShipsAt();
final java.util.Map<Location, List<Ship>> ships2 = ships.entrySet().stream()
.filter(e -> area.isInArea(e.getKey())).collect(Collectors.toMap(e -> e.getKey(), e -> e.getValue()));
ships2.forEach((location, list) -> {
final Ship ship = list.stream().findFirst().get();
final Point point = area.convert(location);
if (walkAnimator == null || !walkAnimator.isNextAnimationLocationAvailable()
|| !walkAnimator.getTo().equals(location)) {
paintUnit(graphics, point, ship);
}
});
}
private final static int FLAG_WIDTH = 7;
private final static int FLAG_HEIGHT = 12;
private void paintUnit(final Graphics2D graphics, final Point point, final Ship ship) {
Point p = point.add(2, 4);
graphics.drawImage(imageProvider.getShipImage(ship.getType()), p.getX(), p.getY(), this);
paintOwnersFlag(graphics, point.add(1, 5), ship.getOwner());
}
/**
* All units have flag containing color of owner. Method draw this flag.
*/
private void paintOwnersFlag(final Graphics2D graphics, final Point point, final Player player) {
graphics.setColor(Color.BLACK);
graphics.drawRect(point.getX(), point.getY(), FLAG_WIDTH, FLAG_HEIGHT);
// TODO JJ player's color should be property
if (player.isHuman()) {
graphics.setColor(Color.YELLOW);
} else {
switch (player.getName().hashCode() % 4) {
case 0:
graphics.setColor(Color.RED);
break;
case 1:
graphics.setColor(Color.GREEN);
break;
case 2:
graphics.setColor(Color.MAGENTA);
break;
case 3:
graphics.setColor(Color.BLUE);
break;
default:
graphics.setColor(Color.gray);
break;
}
}
graphics.fillRect(point.getX() + 1, point.getY() + 1, FLAG_WIDTH - 1, FLAG_HEIGHT - 1);
}
private void paintGrid(final Graphics2D graphics, final Area area) {
if (isGridShown) {
graphics.setColor(Color.LIGHT_GRAY);
graphics.setStroke(new BasicStroke(1));
for (int i = area.getTopLeft().getX(); i <= area.getBottomRight().getX(); i++) {
final Location l_1 = Location.of(i, area.getTopLeft().getY());
final Location l_2 = Location.of(i, area.getBottomRight().getY() + 1);
drawNetLine(graphics, area, l_1, l_2);
}
for (int j = area.getTopLeft().getY(); j <= area.getBottomRight().getY(); j++) {
final Location l_1 = Location.of(area.getTopLeft().getX(), j);
final Location l_2 = Location.of(area.getBottomRight().getX() + 1, j);
drawNetLine(graphics, area, l_1, l_2);
}
}
}
private void drawNetLine(final Graphics2D graphics, final Area area, final Location l_1, Location l_2) {
final Point p_1 = area.convert(l_1).add(-1, -1);
final Point p_2 = area.convert(l_2).add(-1, -1);
graphics.drawLine(p_1.getX(), p_1.getY(), p_2.getX(), p_2.getY());
}
private void paintCursor(final Graphics2D graphics, final Area area) {
if (cursorLocation != null) {
graphics.setColor(Color.RED);
graphics.setStroke(new BasicStroke(1));
paintCursor(graphics, area, cursorLocation);
}
}
/**
* Draw highlight of some tile.
*
* @param graphics
* required {@link Graphics2D}
* @param area
* required displayed area
* @param location
* required tiles where to draw cursor
*/
private void paintCursor(final Graphics2D graphics, final Area area, final Location location) {
final Point p = area.convert(location);
graphics.drawLine(p.getX(), p.getY(), p.getX() + TILE_WIDTH_IN_PX, p.getY());
graphics.drawLine(p.getX(), p.getY(), p.getX(), p.getY() + TILE_WIDTH_IN_PX);
graphics.drawLine(p.getX() + TILE_WIDTH_IN_PX, p.getY(), p.getX() + TILE_WIDTH_IN_PX,
p.getY() + TILE_WIDTH_IN_PX);
graphics.drawLine(p.getX(), p.getY() + TILE_WIDTH_IN_PX, p.getX() + TILE_WIDTH_IN_PX,
p.getY() + TILE_WIDTH_IN_PX);
}
/**
* When move mode is enabled mouse cursor is followed by highlighted path.
*
* @param graphics
* required {@link Graphics2D}
* @param area
* required displayed area
*/
private void paintGoToPath(final Graphics2D graphics, final Area area) {
if (gotoMode && gotoCursorTitle != null) {
graphics.setColor(Color.yellow);
graphics.setStroke(new BasicStroke(1));
paintCursor(graphics, area, gotoCursorTitle);
if (!cursorLocation.equals(gotoCursorTitle)) {
List<Point> steps = new ArrayList<>();
pathPlanning.paintPath(cursorLocation, gotoCursorTitle, location -> steps.add(area.convert(location)));
// TODO JJ get(0) could return different ship that is really
// moved
final Ship unit = gameController.getModel().getCurrentPlayer().getShipsAt(cursorLocation).get(0);
final StepCounter stepCounter = new StepCounter(5, unit.getAvailableMoves());
/**
* Here could be check if particular step in on screen, but draw
* few images outside screen is not big deal.
*/
steps.forEach(point -> paintStepsToTile(graphics, point, stepCounter));
}
}
}
/**
* Draw image on tile. Image is part of highlighted path.
*
* @param graphics
* required {@link Graphics2D}
* @param point
* required point where to draw image
*/
private void paintStepsToTile(final Graphics2D graphics, final Point point, final StepCounter stepCounter) {
graphics.drawImage(getImageFoStep(stepCounter.canMakeMoveInSameTurn(1)), point.getX(), point.getY(), this);
}
private Image getImageFoStep(final boolean normalStep) {
if (normalStep) {
return imageProvider.getImage(ImageProvider.IMG_ICON_STEPS_25x25);
} else {
return imageProvider.getImage(ImageProvider.IMG_ICON_STEPS_TURN_25x25);
}
}
private void paintDebugInfo(final Graphics2D graphics, final VisualDebugInfo visualDebugInfo, final Area area) {
visualDebugInfo.getLocations().stream().filter(location -> area.isInArea(location)).forEach(location -> {
final Point p = area.convert(location).add(10, 4);
graphics.setColor(Color.white);
graphics.fillRect(p.getX() + 1, p.getY() + 1, FLAG_WIDTH - 1, FLAG_HEIGHT - 1);
});
}
@Override
public void startMoveUnit(final Ship ship) {
oneTurnMoveHighlighter.setLocations(ship.getAvailableLocations());
}
@Override
public Dimension getPreferredSize() {
return getMinimumSize();
}
@Override
public Dimension getMinimumSize() {
/**
* Code solve state when component have to be painted and game doesn't
* exists. Because than graphics is not initialize and initGame can't
* prepare dbImage.
*/
if (gameController.getModel() == null) {
return new Dimension(100, 100);
} else {
return new Dimension(getGameMapWidth(), getGameMapHeight());
}
}
private int getGameMapWidth() {
return (gameController.getModel().getMap().getMaxX()) * TOTAL_TILE_WIDTH_IN_PX - 1;
}
private int getGameMapHeight() {
return (gameController.getModel().getMap().getMaxY()) * TOTAL_TILE_WIDTH_IN_PX - 1;
}
@Override
public Location getCursorLocation() {
return cursorLocation;
}
@Override
public void setCursorLocation(Location cursorTile) {
this.cursorLocation = cursorTile;
}
@Override
public void setCursorNormal() {
oneTurnMoveHighlighter.setLocations(null);
setCursor(Cursor.getDefaultCursor());
gotoMode = false;
}
@Override
public void setCursorGoto() {
setCursor(gotoModeCursor);
gotoMode = true;
}
@Override
public boolean isGotoMode() {
return gotoMode;
}
@Override
public Location getGotoCursorTitle() {
return gotoCursorTitle;
}
@Override
public void setGotoCursorTitle(final Location gotoCursorTitle) {
this.gotoCursorTitle = gotoCursorTitle;
}
@Override
public void setWalkAnimator(final WalkAnimator walkAnimator) {
this.walkAnimator = walkAnimator;
}
@Override
public WalkAnimator getWalkAnimator() {
return walkAnimator;
}
@Override
public void setGridShown(final boolean isGridShown) {
this.isGridShown = isGridShown;
}
public void onViewPortResize() {
dbImage = null;
}
@Override
public Area getArea() {
return new Area((JViewport) getParent(), gameController.getModel().getMap());
}
@Override
public VisualDebugInfo getVisualDebugInfo() {
return visualDebugInfo;
}
}