// Make a map out of a voronoi graph // Original Author: amitp@cs.stanford.edu // License: MIT package com.bioxx.jmapgen; import java.awt.Rectangle; import java.util.*; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.nbt.NBTTagList; import net.minecraft.util.EnumFacing; import net.minecraft.util.math.BlockPos; import com.bioxx.jmapgen.IslandParameters.Feature; import com.bioxx.jmapgen.attributes.*; import com.bioxx.jmapgen.com.nodename.delaunay.DelaunayUtil; import com.bioxx.jmapgen.com.nodename.delaunay.Voronoi; import com.bioxx.jmapgen.com.nodename.geom.LineSegment; import com.bioxx.jmapgen.dungeon.Dungeon; import com.bioxx.jmapgen.graph.*; import com.bioxx.jmapgen.graph.Center.HexDirection; import com.bioxx.jmapgen.graph.Center.Marker; import com.bioxx.jmapgen.pathfinding.PathFinder; import com.bioxx.jmapgen.processing.AnimalProcessor; import com.bioxx.jmapgen.processing.CaveProcessor; import com.bioxx.jmapgen.processing.OreProcessor; import com.bioxx.tfc2.TFC; import com.bioxx.tfc2.api.util.Helper; public class IslandMap { public int NUM_POINTS = 4096*4; public int NUM_POINTS_SQ = (int) Math.sqrt(NUM_POINTS); boolean builtVoronoi = false; // Passed in by the caller: public int SIZE; protected IslandParameters islandParams; protected IslandData islandData; // Island details are controlled by this random generator. The // initial map upon loading is always deterministic, but // subsequent maps reset this random number generator with a // random seed. public Random mapRandom = new Random(); // These store the graph data public Vector<Point> points; // Only useful during map construction public Vector<Center> centers; public Vector<Corner> corners; public Vector<Edge> edges; public Vector<River> rivers; public Vector<Lake> lakes; public long seed; public PathFinder pathfinder; public Vector<Dungeon> dungeons; private CaveProcessor caves; private OreProcessor ores; private AnimalProcessor animalProc; public IslandMap(int size, long s) { SIZE = size; seed = s; points = new Vector<Point>(); edges = new Vector<Edge>(); centers = new Vector<Center>(); corners = new Vector<Corner>(); lakes = new Vector<Lake>(); rivers = new Vector<River>(); pathfinder = new PathFinder(this); caves = new CaveProcessor(this); ores = new OreProcessor(this); animalProc = new AnimalProcessor(this); dungeons = new Vector<Dungeon>(); } public void newIsland(IslandParameters is) { islandParams = is; mapRandom.setSeed(seed); NUM_POINTS = is.SIZE*4; NUM_POINTS_SQ = (int) Math.sqrt(NUM_POINTS); is.createShape(seed); islandData = new IslandData(is); } public IslandParameters getParams() { return this.islandParams; } public IslandData getIslandData() { return this.islandData; } public void generateFake() { points.clear(); edges.clear(); centers.clear(); corners.clear(); lakes.clear(); rivers.clear(); pathfinder = new PathFinder(this); caves = new CaveProcessor(this); ores = new OreProcessor(this); animalProc = new AnimalProcessor(this); dungeons.clear(); points = this.generateHexagon(SIZE); Rectangle R = new Rectangle(); R.setFrame(0, 0, SIZE, SIZE); //System.out.println("Starting Creating map Voronoi..."); Voronoi voronoi = new Voronoi(points, R); //System.out.println("Finished Creating map Voronoi..."); buildGraph(points, voronoi); } public void generateFull() { points.clear(); edges.clear(); centers.clear(); corners.clear(); lakes.clear(); rivers.clear(); pathfinder = new PathFinder(this); caves = new CaveProcessor(this); ores = new OreProcessor(this); animalProc = new AnimalProcessor(this); dungeons.clear(); points = this.generateHexagon(SIZE); Rectangle R = new Rectangle(); R.setFrame(0, 0, SIZE, SIZE); //System.out.println("Starting Creating map Voronoi..."); Voronoi voronoi = new Voronoi(points, R); //System.out.println("Finished Creating map Voronoi..."); buildGraph(points, voronoi); // Determine the elevations and water at Voronoi corners. int borderCount = assignCornerElevations(); //If there is too much land on the borders then toss this island and start fresh if(borderCount > 20) { seed += 1234567; newIsland(islandParams); //resetMap(); generateFull(); TFC.log.debug("Reset Map: Centers-" + centers.size()); return; } // Determine polygon and corner type: ocean, coast, land. assignOceanCoastAndLand(); redistributeElevations(landCorners(corners)); // Assign elevations to non-land corners for(Iterator<Corner> i = corners.iterator(); i.hasNext();) { Corner q = (Corner)i.next(); if (q.hasMarker(Marker.Ocean) || q.hasMarker(Marker.Coast)) { q.elevation = 0.0; } } sortClockwise(); if(!this.islandParams.hasFeature(Feature.NoLand)) { // Polygon elevations are the average of their corners assignPolygonElevations(); //if(!this.islandParams.hasFeature(Feature.Desert)) assignLakeElevations(lakeCenters(centers)); // Determine downslope paths. calculateDownslopesCenter(); createVolcano(getCentersAbove(getLandCenters(), 0.4)); createValleys(getCentersAbove(0.4)); createCanyons(); calculateDownslopesCenter(); createGorges(); // Determine downslope paths. calculateDownslopesCenter(); // Create rivers. //if(!this.getParams().hasFeature(Feature.Desert)) createRivers(getCentersAbove(0.25)); assignSlopedNoise(); assignHillyNoise(); createSpires(); calculateDownslopesCenter(); } else { // Assign elevations to non-land corners for(Iterator<Center> i = centers.iterator(); i.hasNext();) { Center q = (Center)i.next(); q.setMarkers(Marker.Ocean, Marker.Water); q.setElevation(0); } } assignMoisture(); redistributeMoisture(getLandCenters()); assignMoisturePostRedist(); setupBiomeInfo(); caves.generate(); ores.generate(); //Generate Dungeons if(!this.getParams().hasFeature(Feature.NoLand)) { createPortals(); /*Dungeon d = new Dungeon(); Vector<Center> dungeonCenters = this.getCentersAbove(0.4); d.generate(seed, dungeonCenters.get(this.mapRandom.nextInt(dungeonCenters.size()))); dungeons.add(d);*/ } animalProc.generate(); } public Center getPortalForFacing(EnumFacing facing) { for(Center c : centers) { if(c.hasAttribute(Attribute.Portal) && ((PortalAttribute)c.getAttribute(Attribute.Portal)).direction == facing) return c; } return null; } private void createSpires() { if(!this.getParams().hasFeature(Feature.Spires)) return; Vector<Center> land = this.getLandCenters(); Vector<Center> starts = new Vector<Center>(); for(int i = 0; i < 100; i++) { Center c = land.get(this.mapRandom.nextInt(land.size())); if(c.hasAttribute(Attribute.River) || c.hasAnyMarkersOf(Marker.Water, Marker.Pond, Marker.Coast)) { i--; continue; } //c.setElevation(Math.min(c.getElevation()+this.convertMCToHeight(mapRandom.nextInt(15) + 35), 1.0)); c.setMarkers(Marker.Spire); } } private void createCanyons() { if(!this.islandParams.hasFeature(Feature.Canyons)) return; Vector<Center> highCenters = getCentersAbove(0.8); Vector<Center> startCenters = new Vector<Center>(); if(highCenters.size() == 0) return; int maxCanyons = 5; for(int i = 0; i < maxCanyons; i++) { Center start = null; boolean found = false; int count = 0; while(found == false && count < 100) { found = true; start = highCenters.get(mapRandom.nextInt(highCenters.size())); if(start.hasAttribute(Attribute.Canyon) || startCenters.contains(start)) found = false; for(Center c : startCenters) { if(start.point.distance(c.point) < 200) found = false; } count++; } startCenters.add(start); Vector<GenericNode> canyon = new Vector<GenericNode>(); GenericNode curNode = new GenericNode(start); GenericNode nextNode = null; double minElevation = curNode.getCenter().getElevation(); while(curNode != null) { canyon.add(curNode); nextNode = getCanyonNextNode(curNode); curNode.setDown(nextNode); if(nextNode != null) { nextNode.setUp(curNode); nextNode.nodeNum = curNode.nodeNum + 1; } minElevation = curNode.getCenter().getElevation(); //Dont manipulate curNode after this, it may be null. curNode = nextNode; } //System.out.println("Canyon " +i + ": " + start.point.x + "," + start.point.y); //First we process the middle centers themselves for(GenericNode gn : canyon) { Center curCenter = gn.getCenter(); double elevMult = Math.min(0.5, gn.nodeNum* 0.05); if(!curCenter.hasAttribute(Attribute.Canyon)) curCenter.setElevation(Math.max(minElevation, gn.getCenter().getElevation()*(1-elevMult))); //Create a canyon attribute for the node center CanyonAttribute a = new CanyonAttribute(Attribute.Canyon, gn.nodeNum); //set the down center in the canyon attribute to the down node for this node if(gn.getDown() != null) a.setDown(gn.getDown().getCenter()); if(gn.getUp() != null) a.setUp(gn.getUp().getCenter()); curCenter.addAttribute(a); } //Next we go back through and process the neighbor centers for(GenericNode gn : canyon) { for(Center n : gn.getCenter().neighbors) { //If this center already has a canyon attribute than it must have already been processed. if(!n.hasAttribute(Attribute.Canyon) && !n.hasMarker(Marker.Water)) { CanyonAttribute c = new CanyonAttribute(Attribute.Canyon, gn.nodeNum); c.setDown(gn.getCenter()); if(n.addAttribute(c)) n.setElevation(Math.max(minElevation, gn.getCenter().getElevation())); } } } } } private GenericNode getCanyonNextNode(GenericNode curNode) { if(curNode.getCenter().downslope == null || curNode.getCenter().downslope.hasMarker(Marker.Water)) return null; RandomCollection<Center> possibles = new RandomCollection<Center>(this.mapRandom); for(Center n : curNode.getCenter().neighbors) { if(curNode.getUp() != null && n == curNode.getUp().getCenter()) continue; if(n.hasAttribute(Attribute.Canyon)) return null; if(n.hasMarker(Marker.Water)) { return null; } else if(n.getElevation() < curNode.getCenter().getElevation()) possibles.add(0.5, n); else possibles.add(0.1, n); } if(possibles.size() > 0) return new GenericNode(possibles.next()); return null; } private void createVolcano(Vector<Center> candidates) { if(!this.islandParams.hasFeature(Feature.Volcano) || candidates.size() == 0) return; Center mid = candidates.get(mapRandom.nextInt(candidates.size())); System.out.println("Volcano: X" + mid.point.x + " Z"+ mid.point.y); Vector<Center> caldera = new Vector<Center>(); caldera.add(mid); caldera.addAll(mid.neighbors); for(Center c : caldera) { c.resetMarkers(); c.setMarkers(Marker.Lava, Marker.Volcano); c.setElevation(0.80); } LinkedList<Center> queue = new LinkedList<Center>(); double elev = 0.03; double baseDist = mid.point.distanceSq(mid.neighbors.get(0).point); for(Center c : caldera) { for(Center n : c.neighbors) { if(!n.hasAnyMarkersOf(Marker.Volcano)) queue.add(n); } } Center qc; double dist = 0; while(!queue.isEmpty()) { qc = queue.pop(); if(!qc.hasAnyMarkersOf(Marker.Volcano)) { dist = mid.point.distanceSq(qc.point); dist /= baseDist; if(qc.getElevation() < 1-dist*elev) { qc.setMarkers(Marker.Volcano); qc.setElevation(1-dist*elev); if(qc.getElevation() < -1 || qc.getElevation() > 1) System.out.println(qc.getElevation()); queue.addAll(qc.neighbors); } else { } } } } private void createValleys(Vector<Center> candidates) { if(!this.islandParams.hasFeature(Feature.Valleys)) return; int totalValleys = 1+mapRandom.nextInt(5); for(int count = 0; count < totalValleys; count++) { int minSize = 20+mapRandom.nextInt(30); Center mid = candidates.get(mapRandom.nextInt(candidates.size())); LinkedList<Center> valleyQueue = new LinkedList<Center>(); Vector<Center> valleyFinal = new Vector<Center>(); Vector<Lake> lakesToDrop = new Vector<Lake>(); if(mid.hasMarker(Marker.Water)) continue; valleyFinal.add(mid); valleyQueue.addAll(mid.neighbors); double minElevation = Float.MAX_VALUE; while(!valleyQueue.isEmpty()) { Center c = valleyQueue.pop(); //Make sure that we aren't readding a center that is already in the valley. if(valleyFinal.contains(c)) continue; if(valleyFinal.size() <= minSize || mapRandom.nextInt(1+valleyFinal.size()-minSize) == 0 ) { //If we hit a lake center, then we just drop the entire lake into the valley. if(c.hasMarker(Marker.Water) && !c.hasMarker(Marker.Ocean)) { Lake l = centerInExistingLake(c); if(l != null && !lakesToDrop.contains(l)) { lakesToDrop.add(l); } } else if(c.hasMarker(Marker.Ocean)) continue; valleyFinal.add(c); if(c.elevation < minElevation) minElevation = c.elevation; for(Center n : c.neighbors) { if(!valleyQueue.contains(n) && !valleyFinal.contains(n)) valleyQueue.add(n); } } } if(valleyFinal.size() >= minSize) { //System.out.println("Valley: X" + mid.point.x + " Z"+ mid.point.y); for(Center n : valleyFinal) { n.elevation = minElevation*0.8 + (-convertMCToHeight(2) + mapRandom.nextDouble()*convertMCToHeight(5));//Math.max(minElevation, n.elevation*0.8); n.setMarkers(Marker.Valley); } for(Lake l : lakesToDrop) { for(Center c : l.centers) { c.elevation = minElevation*0.79; } } } } } /** * This method chooses a random hex and raises it by a random amount, no higher than the highest hex within 4 hexes. * All neighboring hexes are also elevated to a lesser degree. */ private void assignHillyNoise() { for(Iterator<Center> centerIter = centers.iterator(); centerIter.hasNext();) { Center center = (Center)centerIter.next(); //10% change of any hex being selected as long as it is not a water or canyon hex, and does not contain a river. if(!center.hasAttribute(Attribute.Canyon) && !center.hasAttribute(Attribute.Gorge) && !center.hasMarker(Marker.Coast) && this.mapRandom.nextInt(100) < 10 && !center.hasMarker(Marker.Water) && center.getAttribute(Attribute.River) == null) { Center highest = this.getHighestNeighbor(center); highest = this.getHighestNeighbor(highest); highest = this.getHighestNeighbor(highest); highest = this.getHighestNeighbor(highest); double diff = highest.elevation - center.elevation; double mult = 0.5; if(center.hasMarker(Marker.Valley)) mult = 0.1; center.elevation += diff * (mult + mult*mapRandom.nextDouble()); if(center.elevation <= 0) return; for(Iterator<Center> centerIter2 = center.neighbors.iterator(); centerIter2.hasNext();) { Center center2 = (Center)centerIter2.next(); if(!center2.hasMarker(Marker.Lava) && !center2.hasAttribute(Attribute.Gorge) && !center2.hasMarker(Marker.Coast) && center2.getAttribute(Attribute.River) == null && !center2.hasMarker(Marker.Water)) { center2.elevation += Math.max(0, (center.elevation - center2.elevation)*mapRandom.nextDouble()); if(center2.elevation <= 0) return; } } } } } /** * This Method Adds some noise to the world by perturbing random hexes between the lowest and highest adjacent hexes. */ private void assignSlopedNoise() { for(Iterator<Center> centerIter = centers.iterator(); centerIter.hasNext();) { Center center = (Center)centerIter.next(); if(!center.hasAttribute(Attribute.Gorge) && !center.hasMarker(Marker.Coast) && !center.hasMarker(Marker.Water) && !center.hasAttribute(Attribute.River)) { boolean nearWater = false; for(Iterator<Center> centerIter2 = center.neighbors.iterator(); centerIter2.hasNext();) { Center center2 = (Center)centerIter2.next(); if(center2.hasMarker(Marker.Water)) nearWater = true; } if(!center.hasMarker(Marker.Lava) && !nearWater && this.mapRandom.nextInt(100) < 50) { Center lowest = getLowestNeighbor(center); Center highest = getHighestNeighbor(center); if(this.mapRandom.nextInt(100) < 70) center.elevation -= mapRandom.nextDouble() * (center.elevation - lowest.elevation); else center.elevation += mapRandom.nextDouble() * (center.elevation - highest.elevation); center.elevation = Math.min(Math.max(0, center.elevation), 1.0); if(center.elevation <= 0) return; } } } } private void createPortals() { Vector<Center> low = this.getCentersBelow(0.3, false); Center temp = low.get(this.mapRandom.nextInt(low.size())); while(true) { if(temp.point.y < 2048 && temp.point.x > 256 && temp.point.x < 2304) { PortalAttribute pa = new PortalAttribute(Helper.combineCoords(this.islandParams.getXCoord(), this.islandParams.getZCoord()-1), EnumFacing.NORTH); temp.addAttribute(pa); break; } temp = low.get(this.mapRandom.nextInt(low.size())); } while(true) { if(temp.point.y > 2048 && temp.point.x > 256 && temp.point.x < 2304) { PortalAttribute pa = new PortalAttribute(Helper.combineCoords(this.islandParams.getXCoord(), this.islandParams.getZCoord()-1), EnumFacing.SOUTH); temp.addAttribute(pa); break; } temp = low.get(this.mapRandom.nextInt(low.size())); } while(true) { if(temp.point.x > 2048 && temp.point.y > 256 && temp.point.y < 2304) { PortalAttribute pa = new PortalAttribute(Helper.combineCoords(this.islandParams.getXCoord(), this.islandParams.getZCoord()-1), EnumFacing.WEST); temp.addAttribute(pa); break; } temp = low.get(this.mapRandom.nextInt(low.size())); } while(true) { if(temp.point.x < 2048 && temp.point.y > 256 && temp.point.y < 2304) { PortalAttribute pa = new PortalAttribute(Helper.combineCoords(this.islandParams.getXCoord(), this.islandParams.getZCoord()-1), EnumFacing.WEST); temp.addAttribute(pa); break; } temp = low.get(this.mapRandom.nextInt(low.size())); } } public Center getHighestNeighbor(Center c) { Center highest = c; for(Iterator<Center> centerIter2 = c.neighbors.iterator(); centerIter2.hasNext();) { Center center2 = (Center)centerIter2.next(); if(highest == null || center2.elevation > highest.elevation) highest = center2; } RiverAttribute attrib = ((RiverAttribute)c.getAttribute(Attribute.River)); if(attrib != null && attrib.upriver != null) { highest = getLowestFromGroup(attrib.upriver); } return highest; } public Center getLowestNeighbor(Center c) { Center lowest = c; for(Iterator<Center> centerIter2 = c.neighbors.iterator(); centerIter2.hasNext();) { Center center2 = (Center)centerIter2.next(); if(lowest == null || center2.elevation < lowest.elevation) lowest = center2; } RiverAttribute attrib = ((RiverAttribute)c.getAttribute(Attribute.River)); if(attrib != null && attrib.getDownRiver() != null) lowest = attrib.getDownRiver(); return lowest; } private Center getLowestFromGroup(Vector<Center> group) { Center lowest = group.get(0); for(Iterator<Center> centerIter2 = group.iterator(); centerIter2.hasNext();) { Center center2 = (Center)centerIter2.next(); if(lowest == null || center2.elevation < lowest.elevation) lowest = center2; } return lowest; } private Center getHighestFromGroup(Vector<Center> group) { Center highest = group.get(0); for(Iterator<Center> centerIter2 = group.iterator(); centerIter2.hasNext();) { Center center2 = (Center)centerIter2.next(); if(highest == null || center2.elevation > highest.elevation) highest = center2; } return highest; } private void sortClockwise() { for(Iterator<Center> centerIter = centers.iterator(); centerIter.hasNext();) { Center center = (Center)centerIter.next(); Vector<Corner> sortedCorners = new Vector<Corner>(); Vector<Center> sortedNeighbors = new Vector<Center>(); Point zeroPoint = new Point(center.point.x, center.point.y+1); //Sort neighbors clockwise for(Iterator<Corner> iter = center.corners.iterator(); iter.hasNext();) { Corner c = (Corner)iter.next(); if(sortedCorners.size() == 0) sortedCorners.add(c); else { boolean found = false; for(int i = 0; i < sortedCorners.size(); i++) { Corner c1 = sortedCorners.get(i); double c1angle = Math.atan2((c1.point.y - zeroPoint.y) , (c1.point.x - zeroPoint.x)); double c2angle = Math.atan2((c.point.y - zeroPoint.y) , (c.point.x - zeroPoint.x)); if(c2angle < c1angle) { sortedCorners.add(i, c); found = true; break; } } if(!found) sortedCorners.add(c); } } //Sort neighbors clockwise for(Iterator<Center> iter = center.neighbors.iterator(); iter.hasNext();) { Center c = (Center)iter.next(); if(sortedNeighbors.size() == 0) sortedNeighbors.add(c); else { boolean found = false; for(int i = 0; i < sortedNeighbors.size(); i++) { Center c1 = sortedNeighbors.get(i); double c1angle = Math.atan2((c1.point.y - zeroPoint.y) , (c1.point.x - zeroPoint.x)); double c2angle = Math.atan2((c.point.y - zeroPoint.y) , (c.point.x - zeroPoint.x)); if(c2angle < c1angle) { sortedNeighbors.add(i, c); found = true; break; } } if(!found) sortedNeighbors.add(c); } } center.neighbors = sortedNeighbors; center.corners = sortedCorners; } } private void setupBiomeInfo() { double edgeDistance = 0.10; double min = this.SIZE * edgeDistance; double max = this.SIZE * (1 -edgeDistance); for(Iterator<Center> centerIter = centers.iterator(); centerIter.hasNext();) { Center center = (Center)centerIter.next(); //Assign biome information to each hex center.biome = getBiome(center); //If this hex is near the map border we want to count the number of hexes in the connected island. //If there are too few then we will delete this tiny island to make the islands look better if(!center.hasMarker(Marker.Water) && (center.point.x < min || center.point.x > max || center.point.y < min || center.point.y > max)) { Vector<Center> island = countIsland(center, 25); if(island != null && island.size() > 0) { for(Center n : island) { n.setMarkers(Marker.Water, Marker.Ocean); n.biome = BiomeType.OCEAN; } } } if(center.hasMarker(Marker.CoastWater)) center.elevation = -0.01 - mapRandom.nextDouble()*0.03; else if(center.hasMarker(Marker.Ocean)) center.elevation = -0.1 - mapRandom.nextDouble()*0.25; } } /** * @return May return null if the island is too big. */ public Vector<Center> countIsland(Center start, int maxSize) { Vector<Center> outList = new Vector<Center>(); LinkedList<Center> checkList = new LinkedList<Center>(); outList.add(start); checkList.add(start); while(checkList.size() > 0) { Center c = checkList.pollFirst(); for(Center n : c.neighbors) { if(!checkList.contains(n) && !outList.contains(n) && !n.hasMarker(Marker.Water)) { outList.add(n); checkList.addLast(n); } } if(outList.size() >= maxSize) return null; } return outList; } public Vector<Point> generateHexagon(int size) { Vector<Point> points = new Vector<Point>(); int N = (int) Math.sqrt(NUM_POINTS); double xC, yC; for (int x = 0; x < N; x++) { for (int y = 0; y < N; y++) { xC = (0.5 + x) / N * (size); yC = (0.25 + 0.5 * x % 2 + y) / N * (size); points.add(new Point(xC, yC)); } } return points; } /** * Create an array of corners that are on land only, for use by * algorithms that work only on land. We return an array instead * of a vector because the redistribution algorithms want to sort * this array using Array.sortOn. */ public Vector<Corner> landCorners(Vector<Corner> corners) { Corner q; Vector<Corner> locations = new Vector<Corner>(); for (int i = 0; i < corners.size(); i++) { q = corners.get(i); if (!q.hasMarker(Marker.Ocean) && !q.hasMarker(Marker.Coast)) { locations.add(q); } } return locations; } public Vector<Center> landCenters(Vector<Center> centers) { Center q; Vector<Center> locations = new Vector<Center>(); for (int i = 0; i < centers.size(); i++) { q = centers.get(i); if (!q.hasMarker(Marker.Ocean) && !q.hasMarker(Marker.Coast)) { locations.add(q); } } return locations; } public Vector<Center> lakeCenters(Vector<Center> centers2) { Center q; Vector<Center> locations = new Vector<Center>(); for (int i = 0; i < centers2.size(); i++) { q = centers2.get(i); if (!q.hasMarker(Marker.Ocean) && q.hasMarker(Marker.Water)) { locations.add(q); } } return locations; } /** // Build graph data structure in 'edges', 'centers', 'corners', // based on information in the Voronoi results: point.neighbors // will be a list of neighboring points of the same type (corner // or center); point.edges will be a list of edges that include // that point. Each edge connects to four points: the Voronoi edge // edge.{v0,v1} and its dual Delaunay triangle edge edge.{d0,d1}. // For boundary polygons, the Delaunay edge will have one null // point, and the Voronoi edge may be null. */ public void buildGraph(Vector<Point> points, Voronoi voronoi) { Center p; Corner q; Point point; Point other; Vector<com.bioxx.jmapgen.com.nodename.delaunay.Edge> libedges = voronoi.getEdges(); HashMap<Point, Center> centerLookup = new HashMap<Point, Center>(); //System.out.println("Starting buildGraph..."); // Build Center objects for each of the points, and a lookup map // to find those Center objects again as we build the graph //System.out.println("Building Centers from " + points.size() + " total Points"); for(int i = 0; i < points.size(); i++) { point = points.get(i); p = new Center(); p.index = centers.size(); p.point = point; centers.add(p); centerLookup.put(point, p); } // Workaround for Voronoi lib bug: we need to call region() // before Edges or neighboringSites are available for(int i = 0; i < centers.size(); i++) { p = centers.get(i); voronoi.region(p.point); } // The Voronoi library generates multiple Point objects for // corners, and we need to canonicalize to one Corner object. // To make lookup fast, we keep an array of Points, bucketed by // x value, and then we only have to look at other Points in // nearby buckets. When we fail to find one, we'll create a new // Corner object. Vector<Vector<Corner>> _cornerMap = new Vector<Vector<Corner>>(); _cornerMap.setSize((int)SIZE); for(int i = 0; i < libedges.size(); i++) { com.bioxx.jmapgen.com.nodename.delaunay.Edge libedge = libedges.get(i); LineSegment dedge = libedge.delaunayLine(); LineSegment vedge = libedge.voronoiEdge(); // Fill the graph data. Make an Edge object corresponding to // the edge from the voronoi library. Edge edge = new Edge(); edge.index = edges.size(); edges.add(edge); edge.midpoint = vedge.p0 != null && vedge.p1 != null ? Point.interpolate(vedge.p0, vedge.p1, 0.5) : null; Corner c0 = makeCorner(vedge.p0, _cornerMap); Corner c1 = makeCorner(vedge.p1, _cornerMap); edge.setVoronoiEdge(c0, c1); edge.dCenter0 = centerLookup.get(dedge.p0); edge.dCenter1 = centerLookup.get(dedge.p1); // Centers point to edges. Corners point to edges. if (edge.dCenter0 != null) { edge.dCenter0.borders.add(edge); } if (edge.dCenter1 != null) { edge.dCenter1.borders.add(edge); } if (edge.vCorner0 != null) { edge.vCorner0.protrudes.add(edge); } if (edge.vCorner1 != null) { edge.vCorner1.protrudes.add(edge); } // Centers point to centers. if (edge.dCenter0 != null && edge.dCenter1 != null) { addToCenterList(edge.dCenter0.neighbors, edge.dCenter1); addToCenterList(edge.dCenter1.neighbors, edge.dCenter0); } // Centers point to corners if (edge.dCenter0 != null) { addToCornerList(edge.dCenter0.corners, edge.vCorner0); addToCornerList(edge.dCenter0.corners, edge.vCorner1); } if (edge.dCenter1 != null) { addToCornerList(edge.dCenter1.corners, edge.vCorner0); addToCornerList(edge.dCenter1.corners, edge.vCorner1); } // Corners point to centers if (edge.vCorner0 != null) { addToCenterList(edge.vCorner0.touches, edge.dCenter0); addToCenterList(edge.vCorner0.touches, edge.dCenter1); } if (edge.vCorner1 != null) { addToCenterList(edge.vCorner1.touches, edge.dCenter0); addToCenterList(edge.vCorner1.touches, edge.dCenter1); } } //System.out.println("Finished buildGraph..."); } @SuppressWarnings("unchecked") public Corner makeCorner(Point point, Vector<Vector<Corner>> _cornerMap) { Corner q; int bucket; if (point == null) return null; int minBucket = (int)(point.x) - 1; int maxBucket = (int)(point.x) + 1; for (bucket = minBucket; bucket <= maxBucket; bucket++) { Vector<Corner> cornermap = (Vector<Corner>) DelaunayUtil.getAtPosition(_cornerMap, bucket); for(int i = 0; cornermap != null && i < cornermap.size(); i++) { q = cornermap.get(i); double dx = point.x - q.point.x; double dy = point.y - q.point.y; double dxdy = dx*dx + dy*dy; if (dxdy < 1E-6) { return q; } } } bucket = (int)(point.x); if (_cornerMap.size() <= bucket || _cornerMap.get(bucket) == null) { DelaunayUtil.setAtPosition(_cornerMap, bucket, new Vector<Corner>()); } q = new Corner(); q.index = corners.size(); corners.add(q); q.point = point; if(point.x ==0 || point.x == SIZE || point.y == 0 || point.y == SIZE) q.setMarkers(Marker.Border); _cornerMap.get(bucket).add(q); return q; } void addToCornerList(Vector<Corner> v, Corner x) { if (x != null && !v.contains(x)) { v.add(x); } } void addToCenterList(Vector<Center> v, Center x) { if (x != null && v.indexOf(x) < 0) { v.add(x); } } /* Determine elevations and water at Voronoi corners. By // construction, we have no local minima. This is important for // the downslope vectors later, which are used in the river // construction algorithm. Also by construction, inlets/bays // push low elevation areas inland, which means many rivers end // up flowing out through them. Also by construction, lakes // often end up on river paths because they don't raise the // elevation as much as other terrain does.*/ private int assignCornerElevations() { Corner baseCorner, adjacentCorner; LinkedList<Corner> queue = new LinkedList<Corner>(); int numLandBorder = 0; /** * First we check each corner to see if it is land or water * */ for(Corner c : corners) { if(!inside(c.point)) { c.setMarkers(Marker.Water); } else if(c.hasMarker(Marker.Border)) numLandBorder++; if (c.hasMarker(Marker.Border)) { c.elevation = 0; queue.add(c); } } /** * Next we assign the borders to have 0 elevation and all other corners to have MAX_VALUE. We also add * the border points to a queue which contains all start points for elevation distribution. */ // Traverse the graph and assign elevations to each point. As we // move away from the map border, increase the elevations. This // guarantees that rivers always have a way down to the coast by // going downhill (no local minima). while (queue.size() > 0) { baseCorner = queue.pollFirst(); for(int i = 0; i < baseCorner.adjacent.size(); i++) { adjacentCorner = baseCorner.adjacent.get(i); if(!adjacentCorner.hasMarker(Marker.Border)) { // Every step up is epsilon over water or 1 over land. The // number doesn't matter because we'll rescale the // elevations later. double newElevation = 0.000000001 + baseCorner.elevation; if (!baseCorner.hasMarker(Marker.Water) && !adjacentCorner.hasMarker(Marker.Water)) { newElevation += 1; } // If this point changed, we'll add it to the queue so // that we can process its neighbors too. if (newElevation < adjacentCorner.elevation) { adjacentCorner.elevation = newElevation; queue.add(adjacentCorner); } } } } return numLandBorder; } private void resetMap() { for(Corner c : corners) { c.resetMarkers(); c.elevation = 0; } for(Center c : centers) { c.resetMarkers(); c.setElevation(0); } } public Vector<Corner> sortElevation(Vector<Corner> locations) { Vector<Corner> locationsOut = new Vector<Corner>(); for(Iterator<Corner> iter = locations.iterator(); iter.hasNext();) { Corner c = iter.next(); for(int o = 0; o < locationsOut.size(); o++) { Corner cOut = locationsOut.get(o); if(cOut.elevation < c.elevation) { locationsOut.add(o, c); if(cOut.elevation < 0) cOut.elevation = 0; break; } } } return locationsOut; } public Vector<Corner> sortMoisture(Vector<Corner> locations) { Vector<Corner> locationsOut = new Vector<Corner>(); for(Iterator<Corner> iter = locations.iterator(); iter.hasNext();) { Corner c = iter.next(); for(int o = 0; o < locationsOut.size(); o++) { Corner cOut = locationsOut.get(o); if(cOut.moisture < c.moisture) { locationsOut.add(o, c); break; } } } return locationsOut; } /* Change the overall distribution of elevations so that lower // elevations are more common than higher // elevations. Specifically, we want elevation X to have frequency // (1-X). To do this we will sort the corners, then set each // corner to its desired elevation.*/ public void redistributeElevations(Vector<Corner> locations) { // SCALE_FACTOR increases the mountain area. At 1.0 the maximum // elevation barely shows up on the map, so we set it to 1.1. double SCALE_FACTOR = 1.0; Collections.sort(locations, new CornerElevationSorter()); int locationsSize = locations.size(); Corner c; for (int i = 0; i < locationsSize; i++) { c = locations.get(i); double y = (double)i/(double)(locationsSize-1); double x = y; if(this.islandParams.hasFeature(Feature.SharperMountains) && y >= 0.05) { x = Math.pow(y, 2); } else if(this.islandParams.hasFeature(Feature.EvenSharperMountains) && y >= 0.05) { x = Math.pow(y, 3); } else { // Now we have to solve for x, given the known y. // * y = 1 - (1-x)^2 // * y = 1 - (1 - 2x + x^2) // * y = 2x - x^2 // * x^2 - 2x + y = 0 // From this we can use the quadratic equation to get: double sqrtScale = Math.sqrt(SCALE_FACTOR); double scale1Y = SCALE_FACTOR*(1-y); double sqrtscale1Y = Math.sqrt(scale1Y); x = sqrtScale - sqrtscale1Y; } if (x > 1.0) x = 1.0; c.elevation = x; if(!c.hasMarker(Marker.Water) && !c.isShoreline()) c.elevation +=0.01; } } // Change the overall distribution of moisture to be evenly distributed. public void redistributeMoisture(Vector<Center> locations) { int i; Collections.sort(locations, new MoistureComparator()); Center c1; for (i = 0; i < locations.size(); i++) { c1 = locations.get(i); float m = i/(float)(locations.size()); if(this.getParams().hasFeature(Feature.Desert)) m = m*0.25f; c1.setMoistureRaw(m); } } public void assignMoisture() { LinkedList<Center> queue = new LinkedList<Center>(); // Fresh water for(Center cr : centers) { RiverAttribute attrib = (RiverAttribute)cr.getAttribute(Attribute.River); if ((cr.hasMarker(Marker.Water) || (attrib != null && attrib.getRiver() > 0)) && !cr.hasMarker(Marker.Ocean)) { double rivermult = attrib != null ? attrib.getRiver() : 0; cr.setMoistureRaw((attrib != null && attrib.getRiver() > 0) ? Math.min(3.0, (0.1 * rivermult)) : 1.0); if(this.getParams().hasFeature(Feature.Desert)) cr.setMoistureRaw((Math.log10(cr.getMoistureRaw()*0.5)+2)/2); queue.push(cr); } else { cr.setMoistureRaw(0.0); } } //This controls how far the moisture level spreads from the moisture source. Lower values cause less overall island moisture. double moistureMult = (0.6 * this.islandParams.getIslandMoisture().getMoisture()); /*if(this.getParams().hasFeature(Feature.Desert)) moistureMult = (float) ((Math.log10(1)+1)/1);*/ while (queue.size() > 0) { Center q = queue.pop(); for(Center adjacent : q.neighbors) { double newMoisture = q.getMoistureRaw() * moistureMult; if (newMoisture > adjacent.getMoistureRaw()) { adjacent.setMoistureRaw(newMoisture); queue.push(adjacent); } } } // Salt water for(Center cr : centers) { if (cr.hasMarker(Marker.Ocean)) { cr.setMoistureRaw(1.0); } if (cr.hasMarker(Marker.Coast)) { cr.setMoistureRaw(Math.max(0.5, cr.getMoistureRaw())); } } } // Determine polygon and corner types: ocean, coast, land. public void assignOceanCoastAndLand() { // Compute polygon attributes 'ocean' and 'water' based on the // corner attributes. Count the water corners per // polygon. Oceans are all polygons connected to the edge of the // map. In the first pass, mark the edges of the map as ocean; // in the second pass, mark any water-containing polygon // connected an ocean as ocean. LinkedList<Center> queue = new LinkedList<Center>(); Center c = null, r = null; Corner q; int numWater; for(int i = 0; i < centers.size(); i++) { c = centers.get(i); numWater = 0; for(int j = 0; j < c.corners.size(); j++) { q = c.corners.get(j); if (q.hasMarker(Marker.Border)) { c.setMarkers(Marker.Border, Marker.Ocean); q.setMarkers(Marker.Water); queue.add(c); } if (q.hasMarker(Marker.Water)) { numWater += 1; } } if((c.hasMarker(Marker.Ocean) || numWater >= c.corners.size() * this.islandParams.lakeThreshold)) c.setMarkers(Marker.Water); } while (queue.size() > 0) { c = queue.pop(); for(int j = 0; j < c.neighbors.size(); j++) { r = c.neighbors.get(j); if (r.hasMarker(Marker.Water) && !r.hasMarker(Marker.Ocean)) { r.setMarkers(Marker.Ocean); queue.add(r); } } } int numOcean = 0; int numLand = 0; // Set the polygon attribute 'coast' based on its neighbors. If // it has at least one ocean and at least one land neighbor, // then this is a coastal polygon. for(int i = 0; i < centers.size(); i++) { c = centers.get(i); numOcean = 0; numLand = 0; for(int j = 0; j < c.neighbors.size(); j++) { r = c.neighbors.get(j); numOcean += (r.hasMarker(Marker.Ocean) ? 1 : 0); numLand += (!r.hasMarker(Marker.Water) ? 1 : 0); } if((numOcean > 0) && !c.hasMarker(Marker.Ocean)) c.setMarkers(Marker.Coast); if(c.hasMarker(Marker.Ocean) && (numLand > 0)) c.setMarkers(Marker.CoastWater); } // Set the corner attributes based on the computed polygon // attributes. If all polygons connected to this corner are // ocean, then it's ocean; if all are land, then it's land; // otherwise it's coast. for(int j = 0; j < corners.size(); j++) { q = corners.get(j); numOcean = 0; numLand = 0; for(int i = 0; i < q.touches.size(); i++) { c = q.touches.get(i); numOcean += (c.hasMarker(Marker.Ocean) ? 1 : 0); numLand += (!c.hasMarker(Marker.Water) ? 1 : 0); } if(numOcean == q.touches.size()) q.setMarkers(Marker.Ocean); if((numOcean > 0) && (numLand > 0)) q.setMarkers(Marker.Coast); if(q.hasMarker(Marker.Border) || ((numLand != q.touches.size()) && !q.hasMarker(Marker.Coast))) q.setMarkers(Marker.Water); } } // Polygon elevations are the average of the elevations of their corners. public void assignPolygonElevations() { Center p; Corner q; double sumElevation; for(int i = 0; i < centers.size(); i++) { p = centers.get(i); sumElevation = 0.0; for(int j = 0; j < p.corners.size(); j++) { q = p.corners.get(j); sumElevation += q.elevation; } p.elevation = sumElevation / p.corners.size(); //If we are generating cliffs then we multiply the elevation by .85 to keep it <= 1.0 and add 0.15 if(this.islandParams.hasFeature(Feature.Cliffs) && !p.hasMarker(Marker.Ocean) && !p.hasMarker(Marker.Coast) && p.elevation >= 0) p.elevation = Math.max((p.elevation * 0.85) + 0.15, 0.15); } } public void assignLakeElevations(Vector<Center> centers) { for(Center c : centers) { //if there are current no lakes, or the current center doesnt exist in any lakes already Lake exists = centerInExistingLake(c); if(lakes.isEmpty() || exists == null) { //default the lakeElevation 1 double lakeElev = 1; //Create a new lake Lake lake = new Lake(); //contains a list of centers that need to check outward to find the bounds of the lake. LinkedList<Center> centersToCheck = new LinkedList<Center>(); // add the current center to the centersToCheck list lake.addCenter(c); //Add the center to the queue for outward propagation centersToCheck.add(c); while (centersToCheck.size() > 0) { Center baseCenter = centersToCheck.pollFirst(); for(Center adj : baseCenter.neighbors) { if(!lake.hasCenter(adj) && adj.hasMarker(Marker.Water) && !adj.hasMarker(Marker.Ocean)) { lake.addCenter(adj); centersToCheck.add(adj); } } } lakes.add(lake); } } for(int lakeID = 0; lakeID < lakes.size(); lakeID++) { Lake lake = lakes.get(lakeID); lake.lakeID = lakeID; for(Center c : lake.centers) { c.elevation = lake.lowestCenter.elevation; LakeAttribute attrib = new LakeAttribute(Attribute.Lake); attrib.setLakeElev(lake.lowestCenter.elevation); attrib.setLakeID(lakeID); //Here we try to smooth the centers around lakes a bit for(Center n : c.neighbors) { if(n.hasAttribute(Attribute.Lake)) { LakeAttribute nAttrib = (LakeAttribute) n.getAttribute(Attribute.Lake); if(nAttrib.getBorderDistance() < attrib.getBorderDistance()) attrib.setBorderDistance(nAttrib.getBorderDistance() + 1); else if (nAttrib.getBorderDistance() > attrib.getBorderDistance()) nAttrib.setBorderDistance(attrib.getBorderDistance() + 1); } if(!n.hasMarker(Marker.Water)) { attrib.setBorderDistance(0); if(n.elevation < c.elevation)//Neighbor is lower than the lake n.elevation += (c.elevation - n.elevation)/2; else if(c.elevation < n.elevation)//Neighbor is higher than the lake n.elevation -= (n.elevation - c.elevation)/2; } } if(c.getElevation() < 0.1) attrib.setMarsh(true); c.addAttribute(attrib); if(attrib.getBorderDistance() > 0) { double total = c.getElevation() - (c.getElevation() * 0.9D); c.setElevation(c.getElevation() - (total * (attrib.getBorderDistance() / 10D))); } } } } private Lake centerInExistingLake(Center center) { for(Lake lake : lakes) { if(lake.hasCenter(center)) return lake; } return null; } public void calculateDownslopesCenter() { Center upCorner, tempCorner, downCorner; for(int j = 0; j < centers.size(); j++) { upCorner = centers.get(j); downCorner = upCorner; for(int i = 0; i < upCorner.neighbors.size(); i++) { tempCorner= upCorner.neighbors.get(i); if (convertHeightToMC(tempCorner.elevation) <= convertHeightToMC(downCorner.elevation)) { downCorner = tempCorner; } } upCorner.downslope = downCorner; } } public Vector<Center> getCentersAbove(double elev) { return getCentersAbove(centers, elev); } public Vector<Center> getCentersAbove(Vector<Center> inCenters, double elev) { Vector<Center> out = new Vector<Center>(); for(Center c : inCenters) { if (c.elevation >= elev) out.add(c); } return out; } public Vector<Center> getCentersBelow(Vector<Center> inCenters, double elev, boolean allowWater) { Vector<Center> out = new Vector<Center>(); for(Center c : inCenters) { if (c.elevation <= elev) { if(allowWater || !c.hasMarker(Marker.Water)) out.add(c); } } return out; } public Vector<Center> getCentersBelow(double elev, boolean allowWater) { return getCentersBelow(centers, elev, allowWater); } public Vector<Center> getLandCenters() { Vector<Center> out = new Vector<Center>(); for(Center c : centers) { if(!c.hasMarker(Marker.Water)) out.add(c); } return out; } public Vector<Center> getWaterCenters() { Vector<Center> out = new Vector<Center>(); for(Center c : centers) { if(c.hasMarker(Marker.Water)) out.add(c); } return out; } public Vector<Center> getRiverCenters() { Vector<Center> out = new Vector<Center>(); for(Center c : centers) { if(c.hasAttribute(Attribute.River)) out.add(c); } return out; } public Vector<Center> getOceanCenters() { Vector<Center> out = new Vector<Center>(); for(Center c : centers) { if(c.hasMarker(Marker.Ocean)) out.add(c); } return out; } public Vector<Center> getLakeCenters() { Vector<Center> out = new Vector<Center>(); for(Center c : centers) { if(c.biome == BiomeType.LAKE) out.add(c); } return out; } public Vector<Center> getMarshCenters() { Vector<Center> out = new Vector<Center>(); for(Center c : centers) { if(c.biome == BiomeType.MARSH) out.add(c); } return out; } private void createGorges() { if(!this.islandParams.hasFeature(Feature.Gorges)) return; Vector<Center> possibleStarts = new Vector<Center>(); Vector<Gorge> gorges = new Vector<Gorge>(); Gorge gorge = null; Vector<Center> highCenters = this.getCentersAbove(0.5); for(Center c : centers) { if(c.hasAttribute(Attribute.Canyon)) { CanyonAttribute a = (CanyonAttribute) c.getAttribute(Attribute.Canyon); if(a.isNode && a.nodeNum < 10) possibleStarts.add(c); } } for (int i = 0; i < 100; i++) { boolean flag = true; Center c = highCenters.get(mapRandom.nextInt(highCenters.size()-1)); for(Center n : c.neighbors) { if(possibleStarts.contains(n) || n.hasAttribute(Attribute.Canyon)) { flag = false; break; } } if(flag) possibleStarts.add(c); } int id = 0; for(Center c : possibleStarts) { if(c.hasMarker(Marker.Water)) continue; gorge = new Gorge(); GorgeNode curNode = new GorgeNode(c); GorgeNode nextNode = curNode; int count = 0; id++; while (true) { if (c == null || count > 250 || curNode == null || curNode.center.hasMarker(Marker.Water)) { break; } count++; //calculate the next node nextNode = getNextGorgeNode(curNode); if(nextNode != null) nextNode.setUp(curNode); //set the downriver center for this node to the next center curNode.setDown(nextNode); gorge.addNode(curNode); //set the current working center to our next node before starting over curNode = nextNode; } if(gorge != null && gorge.nodes.size() > 2) { gorges.add(gorge); for(GorgeNode cn : gorge.nodes) { double diff = cn.center.getElevation() - gorge.minElev; double elev = cn.center.getElevation(); if(!cn.center.hasAttribute(Attribute.Gorge)) { cn.center.setElevation(Math.max(gorge.minElev,cn.center.elevation - Math.min(diff * 0.5, 0.2))); if(cn.getUp() != null && cn.center.getElevation() > cn.getUp().center.getElevation()) { cn.center.setElevation(cn.getUp().center.getElevation()); } GorgeAttribute a = new GorgeAttribute(Attribute.Gorge); if(cn.getUp() != null) a.setUp(cn.getUp().center); if(cn.getDown() != null) a.setDown(cn.getDown().center); a.gorgeID = id; cn.center.addAttribute(a); } } } } } public GorgeNode getNextGorgeNode(GorgeNode cur) { RandomCollection<Center> possibles = new RandomCollection<Center>(this.mapRandom); //Go through each neighbor and find all possible hexes at the same elevation or lower for(Center n : cur.center.neighbors) { //If the elevations are the same or lower then this might be an ok location if(convertHeightToMC(n.elevation) < convertHeightToMC(cur.center.elevation)) { //If next to a gorge hex then we finish here if(n.hasAttribute(Attribute.Gorge)) return null; if(n.hasMarker(Marker.Ocean) || n.hasMarker(Marker.Water)) { return null; } //If the elevation is <= our current cell elevation then we allow this cell to be selected possibles.add(0.5, n); } else if(convertHeightToMC(n.elevation) == convertHeightToMC(cur.center.elevation)) { possibles.add(0.1, n); } } if(possibles.size() > 0) { return new GorgeNode(possibles.next()); } return null; } public void createRivers(Vector<Center> land) { Center c; Center prev; Vector<Center> possibleStarts = new Vector<Center>(); int starts = (int)Math.floor(100 * this.islandParams.getIslandMoisture().getMoisture()); if(this.islandParams.hasFeature(Feature.Gorges)) { starts = (int)Math.floor(15 * this.islandParams.getIslandMoisture().getMoisture()); } for (int i = 0; i < starts; i++) { c = land.get(mapRandom.nextInt(land.size()-1)); //We dont want rivers to start inside of valleys if(c.hasMarker(Marker.Valley)) continue; possibleStarts.add(c); } if(this.islandParams.hasFeature(Feature.Gorges)) { for(Center cn : centers) { if(cn.hasAttribute(Attribute.Gorge)) { if(((GorgeAttribute)cn.getAttribute(Attribute.Gorge)).getUp() == null && (mapRandom.nextDouble() > 0.25 || cn.hasAttribute(Attribute.Canyon))) { possibleStarts.add(cn); } } } } for (int i = 0; i < lakes.size(); i++) { possibleStarts.add(lakes.get(i).lowestCenter); for(Center cen : lakes.get(i).lowestCenter.neighbors) { if(cen.hasMarker(Marker.Water) && mapRandom.nextBoolean()) possibleStarts.add(cen); } } buildRiver(possibleStarts); } private void buildRiver(Vector<Center> possibleStarts) { Center c; for (int i = 0; i < possibleStarts.size(); i++) { c = possibleStarts.get(i); RiverAttribute cAttrib = ((RiverAttribute)c.getAttribute(Attribute.River)); if (c.hasMarker(Marker.Ocean) || c.elevation > 0.85 || (cAttrib != null && cAttrib.getRiver() > 0)) continue; River r = new River(); RiverNode curNode = new RiverNode(c); r.addNode(curNode); RiverNode nextNode = curNode; int count = 0; while (true) { if (c == null || c == c.downslope || count > 250 || (c.hasMarker(Marker.Water) && curNode != r.riverStart)) { break; } count++; curNode = nextNode; //calculate the next rivernode nextNode = getNextRiverNode(r, curNode); if(nextNode == null) break; RiverAttribute nextAttrib = ((RiverAttribute)nextNode.center.getAttribute(Attribute.River)); //set the downriver center for this node to the next center curNode.setDownRiver(nextNode.center); nextNode.setUpRiver(curNode.center); //add the next node to the river graph r.addNode(nextNode); //If the current hex is water then we exit early unless this is the first node in the river if((c.hasMarker(Marker.Water) && curNode != r.riverStart) && (curNode.downRiver == null || curNode.downRiver.hasMarker(Marker.Water))) break; //Keep track of the length of a river before it joins another river or reaches its end if(nextAttrib == null || nextAttrib.getRiver() == 0) r.lengthToMerge++; //set the current working center to our next node before starting over c = nextNode.center; } //If this river is long enough to be acceptable and it eventually empties into a water hex then we process the river into the map boolean isValid = false; //Is the riverstart valid if(r.riverStart != null && r.riverStart.center.hasMarker(Marker.Water) && r.nodes.lastElement().center.hasMarker(Marker.Water) && (r.riverStart != r.nodes.lastElement()) && r.nodes.lastElement().center.elevation < r.riverStart.center.elevation) isValid = true; if(r.lengthToMerge > 4 && r.nodes.lastElement().center.hasMarker(Marker.Water)) isValid = true; else isValid = false; RiverAttribute startAttrib = (RiverAttribute)r.riverStart.center.getAttribute(Attribute.River); if(r.riverStart == null || (startAttrib != null && startAttrib.getRiver() != 0) || r.nodes.size() < 4) isValid = false; if(isValid) { if(r.riverStart.center.hasMarker(Marker.Water) && this.centerInExistingLake(r.riverStart.center).centers.size() > 8) r.riverWidth = 4 - 3 * r.riverStart.center.elevation; else if(r.riverStart.center.hasAttribute(Attribute.Gorge)) r.riverWidth = 1; else r.riverWidth = 0.5; //Add this river to the river collection rivers.add(r); curNode = r.nodes.get(0); nextNode = curNode; boolean cancelRiver = false; //Propogate through each node in this river and setup RiverAttributes for each Center for (int j = 0; j <= r.nodes.size() && !cancelRiver; j++) { //On the first node, we add a small pond and make sure that this river does not start too close to another river. if(j == 0) { for(Center n :r.riverStart.center.neighbors) { if(n.getAttribute(Attribute.River) != null && ((RiverAttribute)n.getAttribute(Attribute.River)).getRiver() > 0) { rivers.remove(r); cancelRiver = true; break; } } r.riverStart.center.setMarkers(Marker.Pond); } else { if(j < r.nodes.size()) nextNode = r.nodes.get(j); else nextNode = null; //Sanity RiverAttribute riverAttrib = ((RiverAttribute)curNode.center.getAttribute(Attribute.River)); if(riverAttrib == null) { riverAttrib = new RiverAttribute(Attribute.River); curNode.center.addAttribute(riverAttrib); //curNode.center.setElevation(Math.max(curNode.center.getElevation() - this.convertMCToHeight(1), 0)); } riverAttrib.addRiver(r.riverWidth); riverAttrib.setRiverMidpoint(curNode.center.point); if(nextNode != null) { riverAttrib.setDownRiver(nextNode.center); if(nextNode.center.getElevation() > curNode.center.getElevation()) nextNode.center.setElevation(curNode.center.getElevation()); //Sanity RiverAttribute nextAttrib = ((RiverAttribute)nextNode.center.getAttribute(Attribute.River)); if(nextAttrib == null) { nextAttrib = new RiverAttribute(Attribute.River); nextNode.center.addAttribute(nextAttrib); } nextAttrib.addUpRiverCenter(curNode.center); curNode = nextNode; } } } } } //After the rivers are built, we traverse through them one more time to add some jitter to the straight rivers for(River river : rivers) { for(RiverNode rn : river.nodes) { RiverAttribute Attrib = ((RiverAttribute)rn.center.getAttribute(Attribute.River)); if(rn.upRiver != null && rn.downRiver != null && Attrib.upriver != null && Attrib.upriver.size() == 1) { HexDirection hd = rn.center.getDirection(rn.upRiver); HexDirection dn = rn.center.getDirection(rn.downRiver); if(hd.getOpposite() == dn) { double x = 0; double y = 0; if(hd == HexDirection.North || hd == HexDirection.South) if(((rn.center.index >> 1) & 1) > 0) {x = 6 - Attrib.getRiver();} else {x = -6 + Attrib.getRiver();} else if(hd == HexDirection.NorthEast || hd == HexDirection.SouthWest) { if(((rn.center.index >> 1) & 1) > 0) {y = -6 + Attrib.getRiver(); x = -6 + Attrib.getRiver();} else {y = 6 - Attrib.getRiver(); x = 6 - Attrib.getRiver();} } else if(hd == HexDirection.SouthEast || hd == HexDirection.NorthWest) { if(((rn.center.index >> 1) & 1) > 0) {y = -6 + Attrib.getRiver(); x = 6 - Attrib.getRiver();} else {y = 6 - Attrib.getRiver(); x = -6 + Attrib.getRiver();} } else { System.out.println("River:" + hd.toString() + " : " + dn.toString()); } Attrib.setRiverMidpoint(rn.center.point.plus(x, y)); } } } } } public RiverNode getNextRiverNode(River river, RiverNode curNode) { RiverAttribute curAttrib = (RiverAttribute)curNode.center.getAttribute(Attribute.River); Center next = (curAttrib != null ? curAttrib.getDownRiver() : null); if(next != null) return new RiverNode(next); RandomCollection<Center> possibles = new RandomCollection<Center>(this.mapRandom); //The river will attempt to meander if we aren't propagating down an existing river if(curAttrib == null || curAttrib.getRiver() == 0) { int curMCElev = convertHeightToMC(curNode.center.elevation); //Go through each neighbor and find all possible hexes at the same elevation or lower for(Center n : curNode.center.neighbors) { int nMCElev = convertHeightToMC(n.elevation); //Make sure that we aren't trying to flow backwards if the hexes are on the same level if(n == curNode.upRiver) continue; //We dont want our rivers to turn at very sharp angles so we check our previous node to make sure that it is not neighbors with this node if(n.neighbors.contains(curNode.upRiver)) continue; //If the elevations are the same or lower then this might be an ok location if(nMCElev <= curMCElev) { //If next to a water hex then we move to it instead of anything else if(n.hasMarker(Marker.Ocean) || n.hasMarker(Marker.Water)) { //Unless we are dealing with a lake tile and this is the first River node if(river.riverStart == curNode && !n.hasMarker(Marker.Ocean)) continue; return new RiverNode(n); } //If one of the neighbors is also a river then we want to join it if(n.getAttribute(Attribute.River) != null && ((RiverAttribute)n.getAttribute(Attribute.River)).getRiver() > 0) return new RiverNode(n); if(curNode.center.elevation - n.elevation > 0.06) return new RiverNode(n); //If the elevation is <= our current cell elevation then we allow this cell to be selected if(nMCElev == curMCElev) possibles.add(0.2, n); else if(nMCElev < curMCElev-5) possibles.add(1.0,n); else possibles.add(0.5,n); } } } if(possibles.size() > 0) { Center p = possibles.next(); return new RiverNode(p); } return null; } public int convertHeightToMC(double d) { return (int)Math.floor(this.islandParams.islandMaxHeight * d); } public double convertMCToHeight(int i) { return i/this.islandParams.islandMaxHeight; } public void assignMoisturePostRedist() { LinkedList<Center> queue = new LinkedList<Center>(); // Fresh water for(Center cr : centers) { if (cr.hasMarker(Marker.Coast)) { queue.push(cr); } } while (queue.size() > 0) { Center q = queue.pop(); for(Center adjacent : q.neighbors) { if(!adjacent.hasMarker(Marker.Ocean) && adjacent.getElevation() - q.getElevation() < 0.08) { double moistureMult = Math.max(1 - adjacent.getElevation() / 0.25, 0); double newMoisture = q.getMoistureRaw() * moistureMult; if (newMoisture > adjacent.getMoistureRaw()) { adjacent.setMoistureRaw(newMoisture); queue.push(adjacent); } } } } } /* Assign a biome type to each polygon. If it has // ocean/coast/water, then that's the biome; otherwise it depends // on low/high elevation and low/medium/high moisture. This is // roughly based on the Whittaker diagram but adapted to fit the // needs of the island map generator.*/ public BiomeType getBiome(Center p) { float m = p.getMoistureRaw(); m *= this.getParams().getIslandMoisture().getMoisture(); if (p.hasMarker(Marker.Ocean)) { return BiomeType.OCEAN; } else if (p.hasMarker(Marker.Water)) { if (p.elevation < 0.1) return BiomeType.MARSH; //if (p.elevation > 0.8) return BiomeType.ICE; return BiomeType.LAKE; } else if (p.hasMarker(Marker.Coast)) { return BiomeType.BEACH; } else if (p.elevation > 0.8) { if (m > 0.50) return BiomeType.SNOW; else if (m > 0.33) return BiomeType.TUNDRA; else if (m > 0.16) return BiomeType.BARE; else return BiomeType.SCORCHED; } else if (p.elevation > 0.6) { if (m > 0.66) return BiomeType.TAIGA; else if (m > 0.33) return BiomeType.SHRUBLAND; else return BiomeType.TEMPERATE_DESERT; } else if (p.elevation > 0.3) { if (m > 0.83) return BiomeType.TEMPERATE_RAIN_FOREST; else if (m > 0.50) return BiomeType.TEMPERATE_DECIDUOUS_FOREST; else if (m > 0.16) return BiomeType.GRASSLAND; else return BiomeType.TEMPERATE_DESERT; } else { if (m > 0.66) return BiomeType.TROPICAL_RAIN_FOREST; else if (m > 0.33) return BiomeType.TROPICAL_SEASONAL_FOREST; else if (m > 0.16) return BiomeType.GRASSLAND; else return BiomeType.SUBTROPICAL_DESERT; } } // Look up a Voronoi Edge object given two adjacent Voronoi // polygons, or two adjacent Voronoi corners public Edge lookupEdgeFromCenter(Center p, Center r) { for(int j = 0; j < p.borders.size(); j++) { Edge edge = p.borders.get(j); if (edge.dCenter0 == r || edge.dCenter1 == r) return edge; } return null; } public Edge lookupEdgeFromCorner(Corner q, Corner s) { for(int j = 0; j < q.protrudes.size(); j++) { Edge edge = q.protrudes.get(j); if (edge.vCorner0 == s || edge.vCorner1 == s) return edge; } return null; } // Determine whether a given point should be on the island or in the water. public Boolean inside(Point p) { return islandParams.insidePerlin(p); } double elevationBucket(Center p) { if (p.hasMarker(Marker.Ocean)) return -1; else return Math.floor(p.elevation*10); } /** * @param p This blockPos should be in World Coords */ public Center getClosestCenter(BlockPos p) { return getClosestCenter(new Point(p.getX() % 4096, p.getZ() % 4096)); } /** * @return nearest Center point for the containing hex */ public Center getClosestCenter(Point param) { Point p = param.toIslandCoord(); /*//First we place the point in a local grid between 0 and the map width p.x = p.x % SIZE; p.y = p.y % SIZE; //If the point has any negative numbers, we add the map width to make it positive and get the correct location if(p.x < 0) p.x += SIZE; if(p.y < 0) p.y += SIZE;*/ //Form the best guess coordinates int x = (int)Math.floor((p.x /(SIZE/NUM_POINTS_SQ))); int y = (int)Math.floor((p.y /(SIZE/NUM_POINTS_SQ))); if(NUM_POINTS_SQ*x+y >= centers.size()) return centers.get(0); Center orig = this.centers.get( NUM_POINTS_SQ*x+y); //Get the inCircle radius double r = 0; if(orig.corners.size() > 0) { r = Math.sqrt(3)/2*(orig.borders.get(0).midpoint.distanceSq(orig.point)); } Center bestGuess = orig; double dist = p.distanceSq(orig.point); //Perform a quick test to see if the point is within the inCircle. If it is then we can skip the rest of the method and return the Best Guess if(dist < r) return bestGuess; for (int i = 0; i < orig.neighbors.size(); i++) { Center guess = orig.neighbors.get(i); double newDist = p.distanceSq(guess.point); if(newDist < dist) { dist = newDist; bestGuess = guess; if(dist < r) return bestGuess; } for (int j = 0; j < guess.neighbors.size(); j++) { Center guess2 = guess.neighbors.get(j); double newDist2 = p.distanceSq(guess2.point); if(newDist2 < dist) { dist = newDist2; bestGuess = guess2; if(dist < r) return bestGuess; } } } return bestGuess; } public Corner getClosestCorner(Point p) { Corner closest = corners.get(0); double distance = p.distance(corners.get(0).point); for (int i = 1; i < corners.size(); i++) { double newDist = p.distance(corners.get(i).point); if(newDist < distance) { distance = newDist; closest = corners.get(i); } } return closest; } public void writeToNBT(NBTTagCompound nbt) { NBTTagList nList = new NBTTagList(); for(Center c : centers) { NBTTagCompound n = new NBTTagCompound(); c.writeToNBT(n); nList.appendTag(n); } nbt.setTag("centers", nList); nList = new NBTTagList(); for(Corner c : corners) { NBTTagCompound n = new NBTTagCompound(); c.writeToNBT(n); nList.appendTag(n); } nbt.setTag("corners", nList); nList = new NBTTagList(); for(Edge e : edges) { NBTTagCompound n = new NBTTagCompound(); e.writeToNBT(n); nList.appendTag(n); } nbt.setTag("edges", nList); nList = new NBTTagList(); for(Dungeon d : dungeons) { NBTTagCompound n = new NBTTagCompound(); d.writeToNBT(n); nList.appendTag(n); } nbt.setTag("dungeons", nList); NBTTagCompound dataNBT = new NBTTagCompound(); this.islandData.writeToNBT(dataNBT); nbt.setTag("data", dataNBT); } public void readFromNBT(NBTTagCompound nbt) { NBTTagList centerList = nbt.getTagList("centers", 10); NBTTagList cornerList = nbt.getTagList("corners", 10); NBTTagList edgeList = nbt.getTagList("edges", 10); Center c; centers.clear(); corners.clear(); edges.clear(); //First we create empty centers, corners, and edges that can be referenced from each other for(int i = 0; i < centerList.tagCount(); i++) { centers.add(new Center(i)); } for(int i = 0; i < cornerList.tagCount(); i++) { corners.add(new Corner(i)); } for(int i = 0; i < edgeList.tagCount(); i++) { edges.add(new Edge(i)); } for(int i = 0; i < centers.size(); i++) { c = centers.get(i); c.readFromNBT(centerList.getCompoundTagAt(i), this); //Rebuild the lake list if(c.hasAttribute(Attribute.Lake)) { int lakeID = ((LakeAttribute)c.getAttribute(Attribute.Lake)).getLakeID(); if(lakes.size() <= lakeID) lakes.setSize(lakeID+1); if(lakes.get(lakeID) == null) { lakes.set(lakeID, new Lake()); lakes.get(lakeID).lakeID = lakeID; } lakes.get(lakeID).addCenter(c); } } for(int i = 0; i < corners.size(); i++) { corners.get(i).readFromNBT(cornerList.getCompoundTagAt(i), this); } for(int i = 0; i < edges.size(); i++) { edges.get(i).readFromNBT(edgeList.getCompoundTagAt(i), this); } NBTTagList dungeonList = nbt.getTagList("dungeons", 10); for(int i = 0; i < dungeonList.tagCount(); i++) { Dungeon d = new Dungeon("generic", 0, 0, 0); d.readFromNBT(this, dungeonList.getCompoundTagAt(i)); dungeons.add(d); } this.islandData = new IslandData(this.getParams()); islandData.readFromNBT(nbt.getCompoundTag("data")); } }