/* * 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(); } } }