/*
* UFO Saved Game Editor
* Copyright (C) 2010 Christopher Davoren
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package net.rubikscomplex.ufosge.gui;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.geom.AffineTransform;
import java.awt.image.AffineTransformOp;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.Iterator;
import javax.swing.JComponent;
import javax.swing.JMenu;
import javax.swing.JMenuItem;
import javax.swing.JPopupMenu;
import javax.swing.JSeparator;
import javax.swing.event.EventListenerList;
import net.rubikscomplex.ufosge.data.Base;
import net.rubikscomplex.ufosge.data.SavedGame;
import net.rubikscomplex.ufosge.gui.event.BaseLayoutEvent;
import net.rubikscomplex.ufosge.gui.event.BaseLayoutListener;
import net.rubikscomplex.ufosge.resources.BaseBit;
import net.rubikscomplex.ufosge.resources.BigLetter;
import net.rubikscomplex.ufosge.util.Util;
/**
*
* @author Chris Davoren
*/
public class BaseFacilityEditor extends JComponent implements MouseListener, MouseMotionListener, ActionListener {
public static final int DEFAULT_SIZE = Base.BASE_SIZE * BaseBit.WIDTH;
protected SavedGame game;
protected Base base;
public byte[][] fCache;
public byte[][] dlCache;
protected int scaleFactor;
protected int cellSize;
protected BufferedImage floorImage;
protected BufferedImage facilityImage;
protected BufferedImage overlayImage;
protected BufferedImage compositedImage;
protected BufferedImage[] internalImageCache;
protected BufferedImage[] externalImageCache;
protected BufferedImage[] overlayImageCache;
protected EventListenerList layoutListenerList;
protected int selx, sely;
protected int hix, hiy;
protected JPopupMenu popupMenu;
protected JMenuItem fcMenuItem;
protected ArrayList<JMenuItem> facilityMenuItems;
protected int lastPopupX, lastPopupY;
public BaseFacilityEditor(SavedGame game, Base base, int scaleFactor) {
super();
this.game = game;
this.base = base;
fCache = new byte[Base.BASE_SIZE][Base.BASE_SIZE];
dlCache = new byte[Base.BASE_SIZE][Base.BASE_SIZE];
layoutListenerList = new EventListenerList();
selx = 0;
sely = 0;
hix = -1;
hiy = -1;
this.scaleFactor = scaleFactor;
cellSize = (DEFAULT_SIZE / Base.BASE_SIZE) * scaleFactor;
floorImage = new BufferedImage(DEFAULT_SIZE * scaleFactor, DEFAULT_SIZE * scaleFactor, BufferedImage.TYPE_INT_ARGB);
facilityImage = new BufferedImage(DEFAULT_SIZE * scaleFactor, DEFAULT_SIZE * scaleFactor, BufferedImage.TYPE_INT_ARGB);
overlayImage = new BufferedImage(DEFAULT_SIZE * scaleFactor, DEFAULT_SIZE * scaleFactor, BufferedImage.TYPE_INT_ARGB);
compositedImage = new BufferedImage(DEFAULT_SIZE * scaleFactor, DEFAULT_SIZE * scaleFactor, BufferedImage.TYPE_INT_ARGB);
BufferedImage floorTileRescaled = scaleImage(BaseBit.getBaseBit(BaseBit.INDEX_FLOOR).image, scaleFactor);
Graphics2D fg = floorImage.createGraphics();
for(int i = 0; i < Base.BASE_SIZE * Base.BASE_SIZE; i++) {
fg.drawImage(floorTileRescaled, (i % Base.BASE_SIZE) * cellSize, (i / Base.BASE_SIZE) * cellSize, null);
}
internalImageCache = new BufferedImage[Base.NUM_FACILITY_TYPES];
externalImageCache = new BufferedImage[Base.NUM_FACILITY_TYPES];
overlayImageCache = new BufferedImage[Base.NUM_FACILITY_TYPES];
for(int i = 0; i < Base.NUM_FACILITY_TYPES; i++) {
BufferedImage cacheImage = new BufferedImage(cellSize, cellSize, BufferedImage.TYPE_INT_ARGB);
Graphics2D cig = cacheImage.createGraphics();
Util.clearImage(cacheImage);
int internalImageIndex = BaseBit.getInternalImageIndex(i);
if (internalImageIndex != -1) {
cig.drawImage(scaleImage(BaseBit.getBaseBit(internalImageIndex).image, scaleFactor), 0, 0, null);
}
internalImageCache[i] = cacheImage;
cacheImage = new BufferedImage(cellSize, cellSize, BufferedImage.TYPE_INT_ARGB);
Util.clearImage(cacheImage);
cig = cacheImage.createGraphics();
int externalImageIndex = BaseBit.getExternalImageIndex(i);
if (externalImageIndex != -1) {
cig.drawImage(scaleImage(BaseBit.getBaseBit(externalImageIndex).image, scaleFactor), 0, 0, null);
}
externalImageCache[i] = cacheImage;
cacheImage = new BufferedImage(cellSize, cellSize, BufferedImage.TYPE_INT_ARGB);
Util.clearImage(cacheImage);
cig = cacheImage.createGraphics();
int overlayImageIndex = BaseBit.getOverlayImageIndex(i);
if (overlayImageIndex != -1) {
cig.drawImage(scaleImage(BaseBit.getBaseBit(overlayImageIndex).image, scaleFactor), 0, 0, null);
}
overlayImageCache[i] = cacheImage;
}
popupMenu = new JPopupMenu("Edit Cell");
fcMenuItem = new JMenuItem("Finish Construction", KeyEvent.VK_F);
fcMenuItem.addActionListener(this);
JMenu setFacilityMenu = new JMenu("Set Facility...");
setFacilityMenu.setMnemonic(KeyEvent.VK_S);
popupMenu.add(setFacilityMenu);
popupMenu.add(new JSeparator());
popupMenu.add(fcMenuItem);
facilityMenuItems = new ArrayList<JMenuItem>();
ArrayList<String> facilityNames = Base.getFacilityNameList(true, true);
Iterator<String> fni = facilityNames.iterator();
while(fni.hasNext()) {
JMenuItem facilityMenuItem = new JMenuItem(fni.next());
facilityMenuItems.add(facilityMenuItem);
setFacilityMenu.add(facilityMenuItem);
facilityMenuItem.addActionListener(this);
}
updateCache();
updateImage();
// updateSelection(-1, -1);
addMouseListener(this);
addMouseMotionListener(this);
setPreferredSize(new Dimension(DEFAULT_SIZE * scaleFactor, DEFAULT_SIZE * scaleFactor));
setMinimumSize(getPreferredSize());
setMaximumSize(getPreferredSize());
}
public void updateCache() {
for(int i = 0; i < Base.BASE_SIZE; i++) {
for(int j = 0; j < Base.BASE_SIZE; j++) {
fCache[i][j] = base.facilities[i][j];
dlCache[i][j] = base.daysLeft[i][j];
}
}
}
public void update() {
for(int i = 0; i < Base.BASE_SIZE; i++) {
for(int j = 0; j < Base.BASE_SIZE; j++) {
base.facilities[i][j] = fCache[i][j];
base.daysLeft[i][j] = dlCache[i][j];
}
}
}
public void setSelectedFacility(byte facilityType, byte daysLeft) {
int selx = getSelX();
int sely = getSelY();
Base.setFacilityType(selx, sely, facilityType, fCache, dlCache);
Base.setDaysLeft(selx, sely, daysLeft, fCache, dlCache);
updateImage();
repaint();
}
protected BufferedImage scaleImage(BufferedImage src, int scaleFactor) {
int width = src.getWidth();
int height = src.getHeight();
BufferedImage dst = new BufferedImage(width * scaleFactor, height * scaleFactor, src.getType());
AffineTransform at = new AffineTransform(scaleFactor, 0.0, 0.0, scaleFactor, 0.0, 0.0);
AffineTransformOp ato = new AffineTransformOp(at, AffineTransformOp.TYPE_NEAREST_NEIGHBOR);
ato.filter(src, dst);
return dst;
}
public void updateSelection(int oldSelx, int oldSely) {
Graphics2D cg = compositedImage.createGraphics();
cg.setColor(Color.WHITE);
cg.setXORMode(Color.BLACK);
cg.setStroke(new BasicStroke(scaleFactor + 1));
if (oldSelx != -1 && oldSely != -1) {
drawRect(cg, oldSelx, oldSely);
}
if (selx != -1 && sely != -1) {
drawRect(cg, selx, sely);
}
repaint();
}
public void updateHighlight(int oldHix, int oldHiy) {
// System.out.println("Update highlight.");
Graphics2D cg = compositedImage.createGraphics();
cg.setColor(Color.WHITE);
cg.setXORMode(Color.RED);
cg.setStroke(new BasicStroke(scaleFactor + 1));
if (oldHix != -1 && oldHiy != -1) {
drawRect(cg, oldHix, oldHiy);
}
if (hix != -1 && hiy != -1) {
drawRect(cg, hix, hiy);
}
repaint();
}
void drawRect(Graphics2D g, int posx, int posy) {
// System.out.println("DrawRect: " + posx + ", " + posy);
posx = realSelX(posx, posy);
posy = realSelY(posx, posy);
if (fCache[posy][posx] != Base.FACILITY_HANGAR_TL) {
g.drawRect(posx * cellSize, posy * cellSize, cellSize, cellSize);
}
else {
g.drawRect(posx * cellSize , posy * cellSize, cellSize * 2, cellSize * 2);
}
}
public void updateImage() {
Graphics2D cg = compositedImage.createGraphics();
updateFacilityImage();
updateOverlayImage();
cg.drawImage(floorImage, 0, 0, null);
cg.drawImage(facilityImage, 0, 0, null);
cg.drawImage(overlayImage, 0, 0, null);
updateSelection(-1, -1);
updateHighlight(-1, -1);
}
protected void updateFacilityImage() {
Util.clearImage(facilityImage);
Graphics2D fg = facilityImage.createGraphics();
for(int i = 0; i < Base.BASE_SIZE; i++) {
for(int j = 0; j < Base.BASE_SIZE; j++) {
if (fCache[i][j] == Base.FACILITY_EMPTY) {
continue;
}
/* Show external image if not under construction */
if (dlCache[i][j] == 0) {
fg.drawImage(externalImageCache[fCache[i][j]], j * cellSize, i * cellSize, null);
}
/* Show vertical connector provided neither facility is under construction and we are not inside a hangar */
if (i > 0 && fCache[i - 1][j] != Base.FACILITY_EMPTY && dlCache[i - 1][j] == 0 && dlCache[i][j] == 0 && !Base.isHangar(fCache[i][j], Base.HF_BOTTOM)) {
fg.drawImage(scaleImage(BaseBit.getBaseBit(BaseBit.INDEX_VERTICAL_CONNECTOR).image, scaleFactor), j * cellSize, (i * cellSize) - (16 * scaleFactor), null);
}
/* Show horizontal connector provided neither facility is under construction and we are not inside a hangar */
if (j > 0 && fCache[i][j - 1] != Base.FACILITY_EMPTY && dlCache[i][j - 1] == 0 && dlCache[i][j] == 0 && !Base.isHangar(fCache[i][j], Base.HF_RIGHT)) {
fg.drawImage(scaleImage(BaseBit.getBaseBit(BaseBit.INDEX_HORIZONTAL_CONNECTOR).image, scaleFactor), (j * cellSize) - (16 * scaleFactor), i * cellSize, null);
}
/* Show internal image if not empty and not a hangar */
if (fCache[i][j] != Base.FACILITY_EMPTY && !Base.isHangar(fCache[i][j])) {
fg.drawImage(internalImageCache[fCache[i][j]], j * cellSize, i * cellSize, null);
}
}
}
}
protected void updateOverlayImage() {
Util.clearImage(overlayImage);
Graphics2D og = overlayImage.createGraphics();
for(int i = 0; i < Base.BASE_SIZE; i++) {
for(int j = 0; j < Base.BASE_SIZE; j++) {
if (dlCache[i][j] > 0) {
og.drawImage(overlayImageCache[fCache[i][j]], j * cellSize, i * cellSize, null);
if (fCache[i][j] != Base.FACILITY_HANGAR_TR && fCache[i][j] != Base.FACILITY_HANGAR_BL && fCache[i][j] != Base.FACILITY_HANGAR_BR) {
BufferedImage daysLeftImage = scaleImage(BigLetter.makeYellowWordImage(new Integer(dlCache[i][j]).toString()), scaleFactor);
/* The following code centers the 'days left' text on the facility image.
* It is interesting to note that the actual game uses constant offsets
* instead - i.e. it does not take into account the height or width of
* the text for display.
*/
int centerx, centery;
// System.out.println("daysLeftImage: " + daysLeftImage.getWidth() + ", " + daysLeftImage.getHeight());
if (fCache[i][j] != Base.FACILITY_HANGAR_TL) {
centerx = (j * cellSize) + (cellSize / 2) - (daysLeftImage.getWidth() / 2) - scaleFactor;
// centerx = (j * cellSize) + (daysLeftImage.getWidth() / 2) - scaleFactor;
centery = (i * cellSize) + (cellSize / 2) - (daysLeftImage.getHeight() / 2);
}
else {
centerx = ((j+1) * cellSize) - (daysLeftImage.getWidth() / 2) - scaleFactor;
centery = ((i+1) * cellSize) - (daysLeftImage.getHeight() / 2);
}
og.drawImage(daysLeftImage, centerx, centery, null);
}
}
}
}
}
public void mouseClicked(MouseEvent e) {
}
protected int realSelX(int selx, int sely) {
if (fCache[sely][selx] == Base.FACILITY_HANGAR_TR || fCache[sely][selx] == Base.FACILITY_HANGAR_BR) {
selx--;
}
return selx;
}
protected int realSelY(int selx, int sely) {
if (fCache[sely][selx] == Base.FACILITY_HANGAR_BL || fCache[sely][selx] == Base.FACILITY_HANGAR_BR) {
sely--;
}
return sely;
}
public void mouseEntered(MouseEvent e) {
mouseMoved(e);
}
public void mouseExited(MouseEvent e) {
int oldHix = hix;
int oldHiy = hiy;
hix = -1;
hiy = -1;
updateHighlight(oldHix, oldHiy);
}
public void mousePressed(MouseEvent e) {
if(e.getButton() == MouseEvent.BUTTON1) {
int oldSelx = selx;
int oldSely = sely;
selx = e.getX() / cellSize;
sely = e.getY() / cellSize;
if (selx != oldSelx || sely != oldSely) {
// System.out.println("Updating selection from: (" + oldSelx + ", " + oldSely + ") to (" + selx + ", " + sely +")");
updateSelection(oldSelx, oldSely);
fireBaseLayoutEvent();
}
}
if(e.isPopupTrigger()) {
showPopupMenu(e);
}
}
public void mouseReleased(MouseEvent e) {
if (e.isPopupTrigger()) {
showPopupMenu(e);
}
}
public void mouseDragged(MouseEvent e) {
}
public void showPopupMenu(MouseEvent e) {
lastPopupX = e.getX();
lastPopupY = e.getY();
Point selectedCell = calcRealSelection(lastPopupX, lastPopupY);
fcMenuItem.setEnabled(dlCache[selectedCell.y][selectedCell.x] > 0);
Iterator<JMenuItem> jmii = facilityMenuItems.iterator();
while(jmii.hasNext()) {
JMenuItem jmi = jmii.next();
if (jmi.getText().equals(Base.facilityToString(Base.FACILITY_HANGAR_TL))) {
jmi.setEnabled(selectedCell.x < Base.BASE_SIZE - 1 && selectedCell.y < Base.BASE_SIZE - 1);
}
}
popupMenu.show(this, e.getX(), e.getY());
}
public void mouseMoved(MouseEvent e) {
if (e.getX() / cellSize >= Base.BASE_SIZE || e.getY() / cellSize >= Base.BASE_SIZE) {
System.out.println("Ignored: " + e.getX() + ", " + e.getY());
return;
}
// System.out.println("Mouse moved: " + e.getX() + " / " + e.getY());
int oldHix = hix;
int oldHiy = hiy;
hix = e.getX() / cellSize;
hiy = e.getY() / cellSize;
if (hix != oldHix || hiy != oldHiy) {
// System.out.println("Updating highlight cell to: (" + hix + ", " + hiy + ")");
updateHighlight(oldHix, oldHiy);
}
}
@Override
public void paint(Graphics g) {
Graphics2D g2 = (Graphics2D)g;
g2.drawImage(compositedImage, 0, 0, null);
}
public void addBaseLayoutEventListener(BaseLayoutListener listener) {
layoutListenerList.add(BaseLayoutListener.class, listener);
}
public void removeBaseLayoutEventListener(BaseLayoutListener listener) {
layoutListenerList.remove(BaseLayoutListener.class, listener);
}
public int getSelX() {
return realSelX(selx, sely);
}
public int getSelY() {
return realSelY(selx, sely);
}
public void fireBaseLayoutEvent() {
Object[] listeners = layoutListenerList.getListenerList();
for(int i = 0; i < listeners.length; i += 2) {
// System.out.println(listeners[i]);
if (listeners[i] == BaseLayoutListener.class) {
((BaseLayoutListener)listeners[i + 1]).baseLayoutPerformed(new BaseLayoutEvent(this, realSelX(selx, sely), realSelY(selx, sely)));
}
}
}
public Point calcRealSelection(int x, int y) {
int cellx = x / cellSize;
int celly = y / cellSize;
return new Point(realSelX(cellx, celly), realSelY(cellx, celly));
}
@Override
public void actionPerformed(ActionEvent e) {
String actionCommand = e.getActionCommand();
Point selectedCell = calcRealSelection(lastPopupX, lastPopupY);
if(actionCommand.equals("Finish construction")) {
Base.setDaysLeft(selectedCell.x, selectedCell.y, (byte)0, fCache, dlCache);
updateImage();
repaint();
fireBaseLayoutEvent();
}
else {
byte facilityType = Base.stringToFacility(actionCommand);
Base.setFacilityType(selectedCell.x, selectedCell.y, facilityType, fCache, dlCache);
updateImage();
repaint();
fireBaseLayoutEvent();
}
}
}