package org.erikaredmark.monkeyshines.editor;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.erikaredmark.monkeyshines.GameConstants;
import org.erikaredmark.monkeyshines.LevelScreen;
import org.erikaredmark.monkeyshines.World;
/**
*
* static utility class that, given a world, outputs a rasterised .png file, at full size, of every screen in the world
* in correct orientation. This effectively makes it easy to view a map in its entirety.
* <p/>
* Rasterisation starts at some screen id, and a 640x400 image is rendered for each screen, connected at the expected locations. However, due
* to bonus worlds, other unconnected screens may exists. Multiple rasterisations are needed for each unconnected set (client sends in
* the starting screen id, and only connected screens from there are rendered). Under typical circumstances, rendering starting at
* screen 1000 and the bonus screen should be enough to render every screen in the level. Any other screens, if they exist, would be
* otherwise unaccessible anyway.
*
* @author Erika Redmark
*
*/
public final class MapGenerator {
private MapGenerator() { }
public static BufferedImage generateMap(World world, int screenStart) {
// Take a listing of all screens. Starting at screenStart, look up, right, down, and left. We
// only capture all connecting screens.
Deque<LevelScreen> walkthrough = new ArrayDeque<>();
List<LevelScreen> drawThese = new ArrayList<>();
Set<Integer> alreadyLooked = new HashSet<>();
walkthrough.push(world.getScreenByID(screenStart) );
while (!(walkthrough.isEmpty() ) ) {
LevelScreen next = walkthrough.pop();
int nextId = next.getId();
alreadyLooked.add(nextId);
drawThese.add(next);
// Add to deque all four directions, checking the set to make sure we don't
// backtrack
int[] directions = new int[] {
nextId + 100,
nextId + 1,
nextId - 100,
nextId - 1
};
for (int dir : directions) {
if ( !(alreadyLooked.contains(dir) )
&& world.screenIdExists(dir) ) {
LevelScreen dirScreen = world.getScreenByID(dir);
walkthrough.push(dirScreen);
}
}
}
// First, we need to resolve the ids to logical width/height indexes. Screen ids have an ambiguity: for example,
// 1158 may be left of 1200 or right of 1100. We assume the last two digits of 50 as a cutaway point (similar to
// our original-level import functionality) for image exporting. This should handle MOST cases.
List<IdResolved> lvlsFormed = new ArrayList<>();
int maxWidthIndex = Integer.MIN_VALUE;
int maxHeightIndex = Integer.MIN_VALUE;
int minWidthIndex = Integer.MAX_VALUE;
int minHeightIndex = Integer.MAX_VALUE;
for (LevelScreen lvl : drawThese ) {
int id = lvl.getId();
// Abs important: - indexed screens will mess up the baseline 50 size usage.
int lastTwoDigits = Math.abs(id % 100);
int exceptLastTwoDigits = id / 100;
int widthIndex;
int heightIndex;
// Important: Negative ids invert the logic: So 1000 + 1 is 1001 to the right, but
// -1000 + 1 is -999... also to the right.
if (lastTwoDigits > 50) {
// Negatives require inverting width logic
// +/- on height to correct for the fact that 999 to 1000 and -1000 to -999 are on the same 'height'
// even though 9 != 10
if (id < 0) {
widthIndex = 100 - lastTwoDigits;
heightIndex = exceptLastTwoDigits - 1;
} else {
widthIndex = lastTwoDigits - 100;
heightIndex = exceptLastTwoDigits + 1;
}
} else {
if (id < 0) {
widthIndex = -lastTwoDigits;
} else {
widthIndex = lastTwoDigits;
}
heightIndex = exceptLastTwoDigits;
}
lvlsFormed.add(new IdResolved(id, widthIndex, heightIndex) );
if (widthIndex > maxWidthIndex) {
maxWidthIndex = widthIndex;
}
if (widthIndex < minWidthIndex) {
minWidthIndex = widthIndex;
}
if (heightIndex > maxHeightIndex) {
maxHeightIndex = heightIndex;
}
if (heightIndex < minHeightIndex) {
minHeightIndex = heightIndex;
}
}
// Postcondition: We have each level paired with a width/height index that unambigiously can be used to place it into a grid. We
// have calculated the min/max of these indexes and can now find the size of the image and draw onto it.
// +1 to correct for length since these are indexes. Subtract 1 to get unitIndex
int unitWidth = Math.abs(maxWidthIndex - minWidthIndex) + 1;
int unitHeight = Math.abs(maxHeightIndex - minHeightIndex) + 1;
Map<Integer, LevelScreen> allLvlScreens = world.getLevelScreens();
BufferedImage map = new BufferedImage(unitWidth * GameConstants.SCREEN_WIDTH, unitHeight * GameConstants.SCREEN_HEIGHT, BufferedImage.TYPE_INT_RGB);
Graphics2D g2d = map.createGraphics();
try {
for (IdResolved nextId : lvlsFormed) {
// resolve non-zero based indexes to 0 based
int drawX = (nextId.widthIndex + (-minWidthIndex) ) * GameConstants.SCREEN_WIDTH;
// Height index needs to be inverted
int normalisedHeight = nextId.heightIndex + (-minHeightIndex);
int drawY = ( ( (unitHeight - 1) - normalisedHeight) * GameConstants.SCREEN_HEIGHT);
LevelScreen actualScreen = allLvlScreens.get(nextId.id);
g2d.translate(drawX, drawY);
actualScreen.paint(g2d);
g2d.translate(-drawX, -drawY);
}
} finally {
g2d.dispose();
}
return map;
}
// Represents a level id with it's width/height index set to a non-ambigious value for ease in creating the maps.
private static final class IdResolved {
public final int id, widthIndex, heightIndex;
IdResolved(int id, int widthIndex, int heightIndex) {
this.id = id;
this.widthIndex = widthIndex;
this.heightIndex = heightIndex;
}
}
}