package squidpony.squidgrid.mapping;
import squidpony.annotation.Beta;
import squidpony.squidgrid.Direction;
import squidpony.squidmath.*;
import squidpony.squidmath.Noise.Noise3D;
import squidpony.squidmath.Noise.Noise4D;
/**
* Can be used to generate world maps with a wide variety of data, starting with height, temperature and moisture.
* From there, you can determine biome information in as much detail as your game needs, with a default implementation
* available that assigns a single biome to each cell based on heat/moisture. The maps this produces are valid
* for spherical or toroidal world projections, and will wrap from edge to opposite edge seamlessly thanks to a
* technique from the Accidental Noise Library ( https://www.gamedev.net/blog/33/entry-2138456-seamless-noise/ ) that
* involves getting a 2D slice of 4D Simplex noise. Because of how Simplex noise works, this also allows extremely high
* zoom levels as long as certain parameters are within reason. You can access the height map with the
* {@link #heightData} field, the heat map with the {@link #heatData} field, the moisture map with the
* {@link #moistureData} field, and a special map that stores ints representing the codes for various ranges of
* elevation (0 to 8 inclusive, with 0 the deepest ocean and 8 the highest mountains) with {@link #heightCodeData}. The
* last map should be noted as being the simplest way to find what is land and what is water; any height code 4 or
* greater is land, and any height code 3 or less is water. This can produce rivers, and keeps that information in a
* GreasedRegion (alongside a GreasedRegion containing lake positions) instead of in the other map data. This class does
* not use Coord at all, but if you want maps with width and/or height greater than 256, and you want to use the river
* or lake data as a Collection of Coord, then you should call {@link squidpony.squidmath.Coord#expandPoolTo(int, int)}
* with your width and height so the Coords remain safely pooled. If you're fine with keeping rivers and lakes as
* GreasedRegions and not requesting Coord values from them, then you don't need to do anything with Coord. Certain
* parts of this class are not necessary to generate, just in case you want river-less maps or something similar;
* setting {@link #generateRivers} to false will disable river generation (it defaults to true).
* <br>
* The main trade-off this makes to obtain better quality is reduced speed; generating a 512x512 map on a circa-2016
* laptop (i7-6700HQ processor at 2.6 GHz) takes about 1 second (about 1.15 seconds for an un-zoomed map, 0.95 or so
* seconds to increase zoom at double resolution). If you don't need a 512x512 map, this takes commensurately less time
* to generate less grid cells, with 64x64 maps generating faster than they can be accurately seen on the same hardware.
* River positions are produced using a different method, and do not involve the Simplex noise parts other than using
* the height map to determine flow. Zooming with rivers is tricky, and generally requires starting from the outermost
* zoom level and progressively enlarging and adding detail to all rivers as zoom increases on specified points.
*/
@Beta
public abstract class WorldMapGenerator {
public final int width, height;
public long seed, cachedState;
public StatefulRNG rng;
public boolean generateRivers = true;
public final double[][] heightData, heatData, moistureData;
public final GreasedRegion landData, riverData, lakeData,
partialRiverData, partialLakeData;
protected final GreasedRegion workingData;
public final int[][] heightCodeData;
public double waterModifier = 0.0, coolingModifier = 1.0,
minHeight = Double.POSITIVE_INFINITY, maxHeight = Double.NEGATIVE_INFINITY,
minHeightActual = Double.POSITIVE_INFINITY, maxHeightActual = Double.NEGATIVE_INFINITY,
minHeat = Double.POSITIVE_INFINITY, maxHeat = Double.NEGATIVE_INFINITY,
minWet = Double.POSITIVE_INFINITY, maxWet = Double.NEGATIVE_INFINITY;
public int zoom = 0;
protected IntVLA startCacheX = new IntVLA(8), startCacheY = new IntVLA(8);
public static final double
deepWaterLower = -1.0, deepWaterUpper = -0.7, // 0
mediumWaterLower = -0.7, mediumWaterUpper = -0.3, // 1
shallowWaterLower = -0.3, shallowWaterUpper = -0.1, // 2
coastalWaterLower = -0.1, coastalWaterUpper = 0.1, // 3
sandLower = 0.1, sandUpper = 0.18, // 4
grassLower = 0.18, grassUpper = 0.35, // 5
forestLower = 0.35, forestUpper = 0.6, // 6
rockLower = 0.6, rockUpper = 0.8, // 7
snowLower = 0.8, snowUpper = 1.0; // 8
public static final double[] lowers = {deepWaterLower, mediumWaterLower, shallowWaterLower, coastalWaterLower,
sandLower, grassLower, forestLower, rockLower, snowLower};
/**
* Constructs a WorldMapGenerator (this class is abstract, so you should typically call this from a subclass or as
* part of an anonymous class that implements {@link #regenerate(int, int, int, int, double, double, long)}).
* Always makes a 256x256 map. If you were using {@link WorldMapGenerator#WorldMapGenerator(long, int, int)}, then
* this would be the same as passing the parameters {@code 0x1337BABE1337D00DL, 256, 256}.
*/
public WorldMapGenerator()
{
this(0x1337BABE1337D00DL, 256, 256);
}
/**
* Constructs a WorldMapGenerator (this class is abstract, so you should typically call this from a subclass or as
* part of an anonymous class that implements {@link #regenerate(int, int, int, int, double, double, long)}).
* Takes only the width/height of the map. The initial seed is set to the same large long
* every time, and it's likely that you would set the seed when you call {@link #generate(long)}. The width and
* height of the map cannot be changed after the fact, but you can zoom in.
*
* @param mapWidth the width of the map(s) to generate; cannot be changed later
* @param mapHeight the height of the map(s) to generate; cannot be changed later
*/
public WorldMapGenerator(int mapWidth, int mapHeight)
{
this(0x1337BABE1337D00DL, mapWidth, mapHeight);
}
/**
* Constructs a WorldMapGenerator (this class is abstract, so you should typically call this from a subclass or as
* part of an anonymous class that implements {@link #regenerate(int, int, int, int, double, double, long)}).
* Takes an initial seed and the width/height of the map. The {@code initialSeed}
* parameter may or may not be used, since you can specify the seed to use when you call {@link #generate(long)}.
* The width and height of the map cannot be changed after the fact, but you can zoom in.
*
* @param initialSeed the seed for the StatefulRNG this uses; this may also be set per-call to generate
* @param mapWidth the width of the map(s) to generate; cannot be changed later
* @param mapHeight the height of the map(s) to generate; cannot be changed later
*/
public WorldMapGenerator(long initialSeed, int mapWidth, int mapHeight)
{
width = mapWidth;
height = mapHeight;
seed = initialSeed;
cachedState = ~initialSeed;
rng = new StatefulRNG(initialSeed);
heightData = new double[width][height];
heatData = new double[width][height];
moistureData = new double[width][height];
landData = new GreasedRegion(width, height);
riverData = new GreasedRegion(width, height);
lakeData = new GreasedRegion(width, height);
partialRiverData = new GreasedRegion(width, height);
partialLakeData = new GreasedRegion(width, height);
workingData = new GreasedRegion(width, height);
heightCodeData = new int[width][height];
}
/**
* Generates a world using a random RNG state and all parameters randomized.
* The worlds this produces will always have width and height as specified in the constructor (default 256x256).
* You can call {@link #zoomIn(int, int, int)} to double the resolution and center on the specified area, but the width
* and height of the 2D arrays this changed, such as {@link #heightData} and {@link #moistureData} will be the same.
*/
public void generate()
{
generate(rng.nextLong());
}
/**
* Generates a world using the specified RNG state as a long. Other parameters will be randomized, using the same
* RNG state to start with.
* The worlds this produces will always have width and height as specified in the constructor (default 256x256).
* You can call {@link #zoomIn(int, int, int)} to double the resolution and center on the specified area, but the width
* and height of the 2D arrays this changed, such as {@link #heightData} and {@link #moistureData} will be the same.
* @param state the state to give this generator's RNG; if the same as the last call, this will reuse data
*/
public void generate(long state) {
generate(-1.0, -1.0, state);
}
/**
* Generates a world using the specified RNG state as a long, with specific water and cooling modifiers that affect
* the land-water ratio and the average temperature, respectively.
* The worlds this produces will always have width and height as specified in the constructor (default 256x256).
* You can call {@link #zoomIn(int, int, int)} to double the resolution and center on the specified area, but the width
* and height of the 2D arrays this changed, such as {@link #heightData} and {@link #moistureData} will be the same.
* @param waterMod should be between 0.85 and 1.2; a random value will be used if this is negative
* @param coolMod should be between 0.85 and 1.4; a random value will be used if this is negative
* @param state the state to give this generator's RNG; if the same as the last call, this will reuse data
*/
public void generate(double waterMod, double coolMod, long state)
{
if(cachedState != state || waterMod != waterModifier || coolMod != coolingModifier)
{
seed = state;
zoom = 0;
startCacheX.clear();
startCacheY.clear();
startCacheX.add(0);
startCacheY.add(0);
}
regenerate(startCacheX.peek(), startCacheY.peek(),
(width >> zoom), (height >> zoom), waterMod, coolMod, state);
}
/**
* Halves the resolution of the map and doubles the area it covers; the 2D arrays this uses keep their sizes. This
* version of zoomOut always zooms out from the center of the currently used area.
* <br>
* Only has an effect if you have previously zoomed in using {@link #zoomIn(int, int, int)} or its overload.
*/
public void zoomOut()
{
zoomOut(1, width >> 1, height >> 1);
}
/**
* Halves the resolution of the map and doubles the area it covers repeatedly, halving {@code zoomAmount} times; the
* 2D arrays this uses keep their sizes. This version of zoomOut allows you to specify where the zoom should be
* centered, using the current coordinates (if the map size is 256x256, then coordinates should be between 0 and
* 255, and will refer to the currently used area and not necessarily the full world size).
* <br>
* Only has an effect if you have previously zoomed in using {@link #zoomIn(int, int, int)} or its overload.
* @param zoomCenterX the center X position to zoom out from; if too close to an edge, this will stop moving before it would extend past an edge
* @param zoomCenterY the center Y position to zoom out from; if too close to an edge, this will stop moving before it would extend past an edge
*/
public void zoomOut(int zoomAmount, int zoomCenterX, int zoomCenterY)
{
if(zoomAmount == 0) return;
if(zoomAmount < 0) {
zoomIn(-zoomAmount, zoomCenterX, zoomCenterY);
return;
}
if(zoom > 0)
{
if(seed != cachedState)
{
generate(rng.nextLong());
}
zoom -= zoomAmount;
startCacheX.pop();
startCacheY.pop();
startCacheX.add(Math.min(Math.max(startCacheX.pop() + (zoomCenterX >> zoom + 1) - (width >> zoom + 2),
0), width - (width >> zoom)));
startCacheY.add(Math.min(Math.max(startCacheY.pop() + (zoomCenterY >> zoom + 1) - (height >> zoom + 2),
0), height - (height >> zoom)));
regenerate(startCacheX.peek(), startCacheY.peek(),width >> zoom, height >> zoom,
waterModifier, coolingModifier, cachedState);
rng.setState(cachedState);
}
}
/**
* Doubles the resolution of the map and halves the area it covers; the 2D arrays this uses keep their sizes. This
* version of zoomIn always zooms in to the center of the currently used area.
* <br>
* Although there is no technical restriction on maximum zoom, zooming in more than 5 times (64x scale or greater)
* will make the map appear somewhat less realistic due to rounded shapes appearing more bubble-like and less like a
* normal landscape.
*/
public void zoomIn()
{
zoomIn(1, width >> 1, height >> 1);
}
/**
* Doubles the resolution of the map and halves the area it covers repeatedly, doubling {@code zoomAmount} times;
* the 2D arrays this uses keep their sizes. This version of zoomIn allows you to specify where the zoom should be
* centered, using the current coordinates (if the map size is 256x256, then coordinates should be between 0 and
* 255, and will refer to the currently used area and not necessarily the full world size).
* <br>
* Although there is no technical restriction on maximum zoom, zooming in more than 5 times (64x scale or greater)
* will make the map appear somewhat less realistic due to rounded shapes appearing more bubble-like and less like a
* normal landscape.
* @param zoomCenterX the center X position to zoom in to; if too close to an edge, this will stop moving before it would extend past an edge
* @param zoomCenterY the center Y position to zoom in to; if too close to an edge, this will stop moving before it would extend past an edge
*/
public void zoomIn(int zoomAmount, int zoomCenterX, int zoomCenterY)
{
if(zoomAmount == 0) return;
if(zoomAmount < 0)
{
zoomOut(-zoomAmount, zoomCenterX, zoomCenterY);
return;
}
if(seed != cachedState)
{
generate(rng.nextLong());
}
zoom += zoomAmount;
if(startCacheX.isEmpty())
{
startCacheX.add(0);
startCacheY.add(0);
}
else {
startCacheX.add(Math.min(Math.max(startCacheX.peek() + (zoomCenterX >> zoom - 1) - (width >> zoom + 1),
0), width - (width >> zoom)));
startCacheY.add(Math.min(Math.max(startCacheY.peek() + (zoomCenterY >> zoom - 1) - (height >> zoom + 1),
0), height - (height >> zoom)));
}
regenerate(startCacheX.peek(), startCacheY.peek(),width >> zoom, height >> zoom,
waterModifier, coolingModifier, cachedState);
rng.setState(cachedState);
}
protected abstract void regenerate(int startX, int startY, int usedWidth, int usedHeight,
double waterMod, double coolMod, long state);
public int codeHeight(final double high)
{
if(high < deepWaterUpper)
return 0;
if(high < mediumWaterUpper)
return 1;
if(high < shallowWaterUpper)
return 2;
if(high < coastalWaterUpper)
return 3;
if(high < sandUpper)
return 4;
if(high < grassUpper)
return 5;
if(high < forestUpper)
return 6;
if(high < rockUpper)
return 7;
return 8;
}
protected final int decodeX(final int coded)
{
return coded % width;
}
protected final int decodeY(final int coded)
{
return coded / width;
}
protected int wrapX(final int x) {
return (x + width) % width;
}
protected int wrapY(final int y) {
return (y + height) % height;
}
private static final Direction[] reuse = new Direction[6];
private void appendDirToShuffle(RNG rng) {
rng.randomPortion(Direction.CARDINALS, reuse);
reuse[rng.next(2)] = Direction.DIAGONALS[rng.next(2)];
reuse[4] = Direction.DIAGONALS[rng.next(2)];
reuse[5] = Direction.OUTWARDS[rng.next(3)];
}
protected void addRivers()
{
landData.refill(heightCodeData, 4, 999);
long rebuildState = rng.nextLong();
//workingData.allOn();
//.empty().insertRectangle(8, 8, width - 16, height - 16);
riverData.empty().refill(heightCodeData, 6, 100);
riverData.quasiRandomRegion(0.0066);
int[] starts = riverData.asTightEncoded();
int len = starts.length, currentPos, choice, adjX, adjY, currX, currY, tcx, tcy, stx, sty, sbx, sby;
riverData.clear();
lakeData.clear();
PER_RIVER:
for (int i = 0; i < len; i++) {
workingData.clear();
currentPos = starts[i];
stx = tcx = currX = decodeX(currentPos);
sty = tcy = currY = decodeY(currentPos);
while (true) {
double best = 999999;
choice = -1;
appendDirToShuffle(rng);
for (int d = 0; d < 5; d++) {
adjX = wrapX(currX + reuse[d].deltaX);
/*
if (adjX < 0 || adjX >= width)
{
if(rng.next(4) == 0)
riverData.or(workingData);
continue PER_RIVER;
}*/
adjY = wrapY(currY + reuse[d].deltaY);
if (heightData[adjX][adjY] < best && !workingData.contains(adjX, adjY)) {
best = heightData[adjX][adjY];
choice = d;
tcx = adjX;
tcy = adjY;
}
}
currX = tcx;
currY = tcy;
if (best >= heightData[stx][sty]) {
tcx = rng.next(2);
adjX = wrapX(currX + ((tcx & 1) << 1) - 1);
adjY = wrapY(currY + (tcx & 2) - 1);
lakeData.insert(currX, currY);
lakeData.insert(wrapX(currX+1), currY);
lakeData.insert(wrapX(currX-1), currY);
lakeData.insert(currX, wrapY(currY+1));
lakeData.insert(currX, wrapY(currY-1));
if(heightCodeData[adjX][adjY] <= 3) {
riverData.or(workingData);
continue PER_RIVER;
}
else if((heightData[adjX][adjY] -= 0.0002) < 0.0) {
if (rng.next(3) == 0)
riverData.or(workingData);
continue PER_RIVER;
}
tcx = rng.next(2);
adjX = wrapX(currX + ((tcx & 1) << 1) - 1);
adjY = wrapY(currY + (tcx & 2) - 1);
if(heightCodeData[adjX][adjY] <= 3) {
riverData.or(workingData);
continue PER_RIVER;
}
else if((heightData[adjX][adjY] -= 0.0002) < 0.0) {
if (rng.next(3) == 0)
riverData.or(workingData);
continue PER_RIVER;
}
}
if(choice != -1 && reuse[choice].isDiagonal())
{
tcx = wrapX(currX - reuse[choice].deltaX);
tcy = wrapY(currY - reuse[choice].deltaY);
if(heightData[tcx][currY] <= heightData[currX][tcy] && !workingData.contains(tcx, currY))
{
if(heightCodeData[tcx][currY] < 3 || riverData.contains(tcx, currY))
{
riverData.or(workingData);
continue PER_RIVER;
}
workingData.insert(tcx, currY);
}
else if(!workingData.contains(currX, tcy))
{
if(heightCodeData[currX][tcy] < 3 || riverData.contains(currX, tcy))
{
riverData.or(workingData);
continue PER_RIVER;
}
workingData.insert(currX, tcy);
}
}
if(heightCodeData[currX][currY] < 3 || riverData.contains(currX, currY))
{
riverData.or(workingData);
continue PER_RIVER;
}
workingData.insert(currX, currY);
}
}
GreasedRegion tempData = new GreasedRegion(width, height);
int riverCount = riverData.size() >> 4, currentMax = riverCount >> 3, idx = 0, prevChoice;
for (int h = 5; h < 9; h++) { //, currentMax += riverCount / 18
workingData.empty().refill(heightCodeData, h).and(riverData);
RIVER:
for (int j = 0; j < currentMax && idx < riverCount; j++) {
double vdc = VanDerCorputQRNG.weakDetermine(idx++), best = -999999;
currentPos = workingData.atFractionTight(vdc);
if(currentPos < 0)
break;
stx = sbx = tcx = currX = decodeX(currentPos);
sty = sby = tcy = currY = decodeY(currentPos);
appendDirToShuffle(rng);
choice = -1;
prevChoice = -1;
for (int d = 0; d < 5; d++) {
adjX = wrapX(currX + reuse[d].deltaX);
adjY = wrapY(currY + reuse[d].deltaY);
if (heightData[adjX][adjY] > best) {
best = heightData[adjX][adjY];
prevChoice = choice;
choice = d;
sbx = tcx;
sby = tcy;
tcx = adjX;
tcy = adjY;
}
}
currX = sbx;
currY = sby;
if (prevChoice != -1 && heightCodeData[currX][currY] >= 4) {
if (reuse[prevChoice].isDiagonal()) {
tcx = wrapX(currX - reuse[prevChoice].deltaX);
tcy = wrapY(currY - reuse[prevChoice].deltaY);
if (heightData[tcx][currY] <= heightData[currX][tcy]) {
if(heightCodeData[tcx][currY] < 3)
{
riverData.or(tempData);
continue;
}
tempData.insert(tcx, currY);
}
else
{
if(heightCodeData[currX][tcy] < 3)
{
riverData.or(tempData);
continue;
}
tempData.insert(currX, tcy);
}
}
if(heightCodeData[currX][currY] < 3)
{
riverData.or(tempData);
continue;
}
tempData.insert(currX, currY);
}
while (true) {
best = -999999;
appendDirToShuffle(rng);
choice = -1;
for (int d = 0; d < 6; d++) {
adjX = wrapX(currX + reuse[d].deltaX);
adjY = wrapY(currY + reuse[d].deltaY);
if (heightData[adjX][adjY] > best && !riverData.contains(adjX, adjY)) {
best = heightData[adjX][adjY];
choice = d;
sbx = adjX;
sby = adjY;
}
}
currX = sbx;
currY = sby;
if (choice != -1) {
if (reuse[choice].isDiagonal()) {
tcx = wrapX(currX - reuse[choice].deltaX);
tcy = wrapY(currY - reuse[choice].deltaY);
if (heightData[tcx][currY] <= heightData[currX][tcy]) {
if(heightCodeData[tcx][currY] < 3)
{
riverData.or(tempData);
continue RIVER;
}
tempData.insert(tcx, currY);
}
else
{
if(heightCodeData[currX][tcy] < 3)
{
riverData.or(tempData);
continue RIVER;
}
tempData.insert(currX, tcy);
}
}
if(heightCodeData[currX][currY] < 3)
{
riverData.or(tempData);
continue RIVER;
}
tempData.insert(currX, currY);
}
else
{
riverData.or(tempData);
tempData.clear();
continue RIVER;
}
if (best <= heightData[stx][sty] || heightData[currX][currY] > rng.nextDouble(280.0)) {
riverData.or(tempData);
tempData.clear();
if(heightCodeData[currX][currY] < 3)
continue RIVER;
lakeData.insert(currX, currY);
sbx = rng.next(8);
sbx &= sbx >>> 4;
if ((sbx & 1) == 0)
lakeData.insert(wrapX(currX + 1), currY);
if ((sbx & 2) == 0)
lakeData.insert(wrapX(currX - 1), currY);
if ((sbx & 4) == 0)
lakeData.insert(currX, wrapY(currY + 1));
if ((sbx & 8) == 0)
lakeData.insert(currX, wrapY(currY - 1));
sbx = rng.next(2);
lakeData.insert(wrapX(currX + (-(sbx & 1) | 1)), wrapY(currY + ((sbx & 2) - 1))); // random diagonal
lakeData.insert(currX, wrapY(currY + ((sbx & 2) - 1))); // ortho next to random diagonal
lakeData.insert(wrapX(currX + (-(sbx & 1) | 1)), currY); // ortho next to random diagonal
continue RIVER;
}
}
}
}
rng.setState(rebuildState);
}
/**
* A way to get biome information for the cells on a map when you only need a single value to describe a biome, such
* as "Grassland" or "TropicalRainforest".
* <br>
* To use: 1, Construct a SimpleBiomeMapper (constructor takes no arguments). 2, call
* {@link #makeBiomes(WorldMapGenerator)} with a WorldMapGenerator that has already produced at least one world map.
* 3, get biome codes from the {@link #biomeCodeData} field, where a code is an int that can be used as an index
* into the {@link #biomeTable} static field to get a String name for a biome type, or used with an alternate biome
* table of your design. Biome tables in this case are 54-element arrays organized into groups of 6 elements. Each
* group goes from the coldest temperature first to the warmest temperature last in the group. The first group of 6
* contains the dryest biomes, the next 6 are medium-dry, the next are slightly-dry, the next slightly-wet, then
* medium-wet, then wettest. After this first block of dry-to-wet groups, there is a group of 6 for coastlines, a
* group of 6 for rivers, and lastly a group for lakes. This also assigns moisture codes and heat codes from 0 to 5
* for each cell, which may be useful to simplify logic that deals with those factors.
*/
public static class SimpleBiomeMapper
{
/**
* The heat codes for the analyzed map, from 0 to 5 inclusive, with 0 coldest and 5 hottest.
*/
public int[][] heatCodeData,
/**
* The moisture codes for the analyzed map, from 0 to 5 inclusive, with 0 driest and 5 wettest.
*/
moistureCodeData,
/**
* The biome codes for the analyzed map, from 0 to 53 inclusive. You can use {@link #biomeTable} to look up
* String names for biomes, or construct your own table as you see fit (see docs in {@link SimpleBiomeMapper}).
*/
biomeCodeData;
public static final double
coldestValueLower = 0.0, coldestValueUpper = 0.15, // 0
colderValueLower = 0.15, colderValueUpper = 0.31, // 1
coldValueLower = 0.31, coldValueUpper = 0.5, // 2
warmValueLower = 0.5, warmValueUpper = 0.69, // 3
warmerValueLower = 0.69, warmerValueUpper = 0.85, // 4
warmestValueLower = 0.85, warmestValueUpper = 1.0, // 5
driestValueLower = 0.0, driestValueUpper = 0.27, // 0
drierValueLower = 0.27, drierValueUpper = 0.4, // 1
dryValueLower = 0.4, dryValueUpper = 0.6, // 2
wetValueLower = 0.6, wetValueUpper = 0.8, // 3
wetterValueLower = 0.8, wetterValueUpper = 0.9, // 4
wettestValueLower = 0.9, wettestValueUpper = 1.0; // 5
/**
* The default biome table to use with biome codes from {@link #biomeCodeData}. Biomes are assigned based on
* heat and moisture for the first 36 of 54 elements (coldest to warmest for each group of 6, with the first
* group as the dryest and the last group the wettest), then the next 6 are for coastlines (coldest to warmest),
* then rivers (coldest to warmest), then lakes (coldest to warmest).
*/
public static final String[] biomeTable = {
//COLDEST //COLDER //COLD //HOT //HOTTER //HOTTEST
"Ice", "Ice", "Grassland", "Desert", "Desert", "Desert", //DRYEST
"Ice", "Tundra", "Grassland", "Grassland", "Desert", "Desert", //DRYER
"Ice", "Tundra", "Woodland", "Woodland", "Savanna", "Desert", //DRY
"Ice", "Tundra", "SeasonalForest", "SeasonalForest", "Savanna", "Savanna", //WET
"Ice", "Tundra", "BorealForest", "TemperateRainforest", "TropicalRainforest", "Savanna", //WETTER
"Ice", "BorealForest", "BorealForest", "TemperateRainforest", "TropicalRainforest", "TropicalRainforest", //WETTEST
"Rocky", "Rocky", "Beach", "Beach", "Beach", "Beach", //COASTS
"Ice", "River", "River", "River", "River", "River", //RIVERS
"Ice", "River", "River", "River", "River", "River", //LAKES
};
/**
* Simple constructor; pretty much does nothing. Make sure to call {@link #makeBiomes(WorldMapGenerator)} before
* using fields like {@link #biomeCodeData}.
*/
public SimpleBiomeMapper()
{
heatCodeData = null;
moistureCodeData = null;
biomeCodeData = null;
}
/**
* Analyzes the last world produced by the given WorldMapGenerator and uses all of its generated information to
* assign biome codes for each cell (along with heat and moisture codes). After calling this, biome codes can be
* taken from {@link #biomeCodeData} and used as indices into {@link #biomeTable} or a custom biome table.
* @param world a WorldMapGenerator that should have generated at least one map; it may be at any zoom
*/
public void makeBiomes(WorldMapGenerator world) {
if(world == null || world.width <= 0 || world.height <= 0)
return;
if(heatCodeData == null || (heatCodeData.length != world.width || heatCodeData[0].length != world.height))
heatCodeData = new int[world.width][world.height];
if(moistureCodeData == null || (moistureCodeData.length != world.width || moistureCodeData[0].length != world.height))
moistureCodeData = new int[world.width][world.height];
if(biomeCodeData == null || (biomeCodeData.length != world.width || biomeCodeData[0].length != world.height))
biomeCodeData = new int[world.width][world.height];
final double i_hot = (world.maxHeat == world.minHeat) ? 1.0 : 1.0 / (world.maxHeat - world.minHeat);
for (int x = 0; x < world.width; x++) {
for (int y = 0; y < world.height; y++) {
final double hot = (world.heatData[x][y] - world.minHeat) * i_hot, moist = world.moistureData[x][y];
final int heightCode = world.heightCodeData[x][y];
int hc, mc;
boolean isLake = world.generateRivers && world.partialLakeData.contains(x, y) && heightCode >= 4,
isRiver = world.generateRivers && world.partialRiverData.contains(x, y) && heightCode >= 4;
if (moist > wetterValueUpper) {
mc = 5;
} else if (moist > wetValueUpper) {
mc = 4;
} else if (moist > dryValueUpper) {
mc = 3;
} else if (moist > drierValueUpper) {
mc = 2;
} else if (moist > driestValueUpper) {
mc = 1;
} else {
mc = 0;
}
if (hot > warmerValueUpper) {
hc = 5;
} else if (hot > warmValueUpper) {
hc = 4;
} else if (hot > coldValueUpper) {
hc = 3;
} else if (hot > colderValueUpper) {
hc = 2;
} else if (hot > coldestValueUpper) {
hc = 1;
} else {
hc = 0;
}
heatCodeData[x][y] = hc;
moistureCodeData[x][y] = mc;
biomeCodeData[x][y] = isLake ? hc + 48 : (isRiver ? hc + 42 : ((heightCode == 4) ? hc + 36 : hc + mc * 6));
}
}
}
}
/**
* A concrete implementation of {@link WorldMapGenerator} that tiles both east-to-west and north-to-south. It tends
* to not appear distorted like {@link SphereMap} does in some areas, even though this is inaccurate for a
* rectangular projection of a spherical world (that inaccuracy is likely what players expect in a map, though).
* <a href="http://squidpony.github.io/SquidLib/DetailedWorldMapRiverDemo.png" >Example map</a>.
*/
public static class TilingMap extends WorldMapGenerator {
protected static final double terrainFreq = 1.75, terrainRidgedFreq = 1.1, heatFreq = 5.05, moistureFreq = 5.2, otherFreq = 5.5;
private double minHeat0 = Double.POSITIVE_INFINITY, maxHeat0 = Double.NEGATIVE_INFINITY,
minHeat1 = Double.POSITIVE_INFINITY, maxHeat1 = Double.NEGATIVE_INFINITY,
minWet0 = Double.POSITIVE_INFINITY, maxWet0 = Double.NEGATIVE_INFINITY;
public final Noise4D terrain, terrainRidged, heat, moisture, otherRidged;
/**
* Constructs a concrete WorldMapGenerator for a map that can be used as a tiling, wrapping east-to-west as well
* as north-to-south. Always makes a 256x256 map.
* Uses SeededNoise as its noise generator, with 1.0 as the octave multiplier affecting detail.
* If you were using {@link squidpony.squidgrid.mapping.WorldMapGenerator.TilingMap#TilingMap(long, int, int, squidpony.squidmath.Noise.Noise4D, double)}, then this would be the
* same as passing the parameters {@code 0x1337BABE1337D00DL, 256, 256, SeededNoise.instance, 1.0}.
*/
public TilingMap() {
this(0x1337BABE1337D00DL, 256, 256, SeededNoise.instance, 1.0);
}
/**
* Constructs a concrete WorldMapGenerator for a map that can be used as a tiling, wrapping east-to-west as well
* as north-to-south.
* Takes only the width/height of the map. The initial seed is set to the same large long
* every time, and it's likely that you would set the seed when you call {@link #generate(long)}. The width and
* height of the map cannot be changed after the fact, but you can zoom in.
* Uses SeededNoise as its noise generator, with 1.0 as the octave multiplier affecting detail.
*
* @param mapWidth the width of the map(s) to generate; cannot be changed later
* @param mapHeight the height of the map(s) to generate; cannot be changed later
*/
public TilingMap(int mapWidth, int mapHeight) {
this(0x1337BABE1337D00DL, mapWidth, mapHeight, SeededNoise.instance, 1.0);
}
/**
* Constructs a concrete WorldMapGenerator for a map that can be used as a tiling, wrapping east-to-west as well
* as north-to-south.
* Takes an initial seed and the width/height of the map. The {@code initialSeed}
* parameter may or may not be used, since you can specify the seed to use when you call {@link #generate(long)}.
* The width and height of the map cannot be changed after the fact, but you can zoom in.
* Uses SeededNoise as its noise generator, with 1.0 as the octave multiplier affecting detail.
*
* @param initialSeed the seed for the StatefulRNG this uses; this may also be set per-call to generate
* @param mapWidth the width of the map(s) to generate; cannot be changed later
* @param mapHeight the height of the map(s) to generate; cannot be changed later
*/
public TilingMap(long initialSeed, int mapWidth, int mapHeight) {
this(initialSeed, mapWidth, mapHeight, SeededNoise.instance, 1.0);
}
/**
* Constructs a concrete WorldMapGenerator for a map that can be used as a tiling, wrapping east-to-west as well
* as north-to-south. Takes an initial seed, the width/height of the map, and a noise generator (a
* {@link Noise4D} implementation, which is usually {@link SeededNoise#instance}. The {@code initialSeed}
* parameter may or may not be used, since you can specify the seed to use when you call
* {@link #generate(long)}. The width and height of the map cannot be changed after the fact, but you can zoom
* in. Currently only SeededNoise makes sense to use as the value for {@code noiseGenerator}, and the seed it's
* constructed with doesn't matter because it will change the seed several times at different scales of noise
* (it's fine to use the static {@link SeededNoise#instance} because it has no changing state between runs of
* the program; it's effectively a constant). The detail level, which is the {@code octaveMultiplier} parameter
* that can be passed to another constructor, is always 1.0 with this constructor.
*
* @param initialSeed the seed for the StatefulRNG this uses; this may also be set per-call to generate
* @param mapWidth the width of the map(s) to generate; cannot be changed later
* @param mapHeight the height of the map(s) to generate; cannot be changed later
* @param noiseGenerator an instance of a noise generator capable of 4D noise, almost always {@link SeededNoise}
*/
public TilingMap(long initialSeed, int mapWidth, int mapHeight, final Noise4D noiseGenerator) {
this(initialSeed, mapWidth, mapHeight, noiseGenerator, 1.0);
}
/**
* Constructs a concrete WorldMapGenerator for a map that can be used as a tiling, wrapping east-to-west as well
* as north-to-south. Takes an initial seed, the width/height of the map, and parameters for noise
* generation (a {@link Noise4D} implementation, which is usually {@link SeededNoise#instance}, and a
* multiplier on how many octaves of noise to use, with 1.0 being normal (high) detail and higher multipliers
* producing even more detailed noise when zoomed-in). The {@code initialSeed} parameter may or may not be used,
* since you can specify the seed to use when you call {@link #generate(long)}. The width and height of the map
* cannot be changed after the fact, but you can zoom in. Currently only SeededNoise makes sense to use as the
* value for {@code noiseGenerator}, and the seed it's constructed with doesn't matter because it will change the
* seed several times at different scales of noise (it's fine to use the static {@link SeededNoise#instance} because
* it has no changing state between runs of the program; it's effectively a constant). The {@code octaveMultiplier}
* parameter should probably be no lower than 0.5, but can be arbitrarily high if you're willing to spend much more
* time on generating detail only noticeable at very high zoom; normally 1.0 is fine and may even be too high for
* maps that don't require zooming.
* @param initialSeed the seed for the StatefulRNG this uses; this may also be set per-call to generate
* @param mapWidth the width of the map(s) to generate; cannot be changed later
* @param mapHeight the height of the map(s) to generate; cannot be changed later
* @param noiseGenerator an instance of a noise generator capable of 4D noise, almost always {@link SeededNoise}
* @param octaveMultiplier used to adjust the level of detail, with 0.5 at the bare-minimum detail and 1.0 normal
*/
public TilingMap(long initialSeed, int mapWidth, int mapHeight, final Noise4D noiseGenerator, double octaveMultiplier) {
super(initialSeed, mapWidth, mapHeight);
terrain = new Noise.Layered4D(noiseGenerator, (int) (0.5 + octaveMultiplier * 8), terrainFreq);
terrainRidged = new Noise.Ridged4D(noiseGenerator, (int) (0.5 + octaveMultiplier * 10), terrainRidgedFreq);
heat = new Noise.Layered4D(noiseGenerator, (int) (0.5 + octaveMultiplier * 3), heatFreq);
moisture = new Noise.Layered4D(noiseGenerator, (int) (0.5 + octaveMultiplier * 4), moistureFreq);
otherRidged = new Noise.Ridged4D(noiseGenerator, (int) (0.5 + octaveMultiplier * 6), otherFreq);
}
protected void regenerate(int startX, int startY, int usedWidth, int usedHeight,
double waterMod, double coolMod, long state)
{
boolean fresh = false;
if(cachedState != state || waterMod != waterModifier || coolMod != coolingModifier)
{
minHeight = Double.POSITIVE_INFINITY;
maxHeight = Double.NEGATIVE_INFINITY;
minHeat0 = Double.POSITIVE_INFINITY;
maxHeat0 = Double.NEGATIVE_INFINITY;
minHeat1 = Double.POSITIVE_INFINITY;
maxHeat1 = Double.NEGATIVE_INFINITY;
minHeat = Double.POSITIVE_INFINITY;
maxHeat = Double.NEGATIVE_INFINITY;
minWet0 = Double.POSITIVE_INFINITY;
maxWet0 = Double.NEGATIVE_INFINITY;
minWet = Double.POSITIVE_INFINITY;
maxWet = Double.NEGATIVE_INFINITY;
cachedState = state;
fresh = true;
}
rng.setState(state);
int seedA = rng.nextInt(), seedB = rng.nextInt(), seedC = rng.nextInt(), t;
waterModifier = (waterMod <= 0) ? rng.nextDouble(0.25) + 0.89 : waterMod;
coolingModifier = (coolMod <= 0) ? rng.nextDouble(0.45) * (rng.nextDouble()-0.5) + 1.1 : coolMod;
double p, q,
ps, pc,
qs, qc,
h, temp,
i_w = 6.283185307179586 / width, i_h = 6.283185307179586 / height,
xPos = startX, yPos = startY, i_uw = usedWidth / (double)width, i_uh = usedHeight / (double)height;
double[] trigTable = new double[width << 1];
for (int x = 0; x < width; x++, xPos += i_uw) {
p = xPos * i_w;
trigTable[x<<1] = Math.sin(p);
trigTable[x<<1|1] = Math.cos(p);
}
for (int y = 0; y < height; y++, yPos += i_uh) {
q = yPos * i_h;
qs = Math.sin(q);
qc = Math.cos(q);
for (int x = 0, xt = 0; x < width; x++) {
ps = trigTable[xt++];//Math.sin(p);
pc = trigTable[xt++];//Math.cos(p);
h = terrain.getNoiseWithSeed(pc +
terrainRidged.getNoiseWithSeed(pc, ps, qc, qs, seedA + seedB),
ps, qc, qs, seedA);
h *= waterModifier;
heightData[x][y] = h;
heatData[x][y] = (p = heat.getNoiseWithSeed(pc, ps, qc
+ otherRidged.getNoiseWithSeed(pc, ps, qc, qs, seedB + seedC)
, qs, seedB));
moistureData[x][y] = (temp = moisture.getNoiseWithSeed(pc, ps, qc, qs
+ otherRidged.getNoiseWithSeed(pc, ps, qc, qs, seedC + seedA)
, seedC));
minHeightActual = Math.min(minHeightActual, h);
maxHeightActual = Math.max(maxHeightActual, h);
if(fresh) {
minHeight = Math.min(minHeight, h);
maxHeight = Math.max(maxHeight, h);
minHeat0 = Math.min(minHeat0, p);
maxHeat0 = Math.max(maxHeat0, p);
minWet0 = Math.min(minWet0, temp);
maxWet0 = Math.max(maxWet0, temp);
}
}
minHeightActual = Math.min(minHeightActual, minHeight);
maxHeightActual = Math.max(maxHeightActual, maxHeight);
}
double heightDiff = 2.0 / (maxHeightActual - minHeightActual),
heatDiff = 0.8 / (maxHeat0 - minHeat0),
wetDiff = 1.0 / (maxWet0 - minWet0),
hMod,
halfHeight = (height - 1) * 0.5, i_half = 1.0 / halfHeight;
double minHeightActual0 = minHeightActual;
double maxHeightActual0 = maxHeightActual;
yPos = startY;
ps = Double.POSITIVE_INFINITY;
pc = Double.NEGATIVE_INFINITY;
for (int y = 0; y < height; y++, yPos += i_uh) {
temp = Math.abs(yPos - halfHeight) * i_half;
temp *= (2.4 - temp);
temp = 2.2 - temp;
for (int x = 0; x < width; x++) {
heightData[x][y] = (h = (heightData[x][y] - minHeightActual) * heightDiff - 1.0);
minHeightActual0 = Math.min(minHeightActual0, h);
maxHeightActual0 = Math.max(maxHeightActual0, h);
heightCodeData[x][y] = (t = codeHeight(h));
hMod = 1.0;
switch (t) {
case 0:
case 1:
case 2:
case 3:
h = 0.4;
hMod = 0.2;
break;
case 6:
h = -0.1 * (h - forestLower - 0.08);
break;
case 7:
h *= -0.25;
break;
case 8:
h *= -0.4;
break;
default:
h *= 0.05;
}
heatData[x][y] = (h = (((heatData[x][y] - minHeat0) * heatDiff * hMod) + h + 0.6) * temp);
if (fresh) {
ps = Math.min(ps, h); //minHeat0
pc = Math.max(pc, h); //maxHeat0
}
}
}
if(fresh)
{
minHeat1 = ps;
maxHeat1 = pc;
}
heatDiff = coolingModifier / (maxHeat1 - minHeat1);
qs = Double.POSITIVE_INFINITY;
qc = Double.NEGATIVE_INFINITY;
ps = Double.POSITIVE_INFINITY;
pc = Double.NEGATIVE_INFINITY;
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
heatData[x][y] = (h = ((heatData[x][y] - minHeat1) * heatDiff));
moistureData[x][y] = (temp = (moistureData[x][y] - minWet0) * wetDiff);
if (fresh) {
qs = Math.min(qs, h);
qc = Math.max(qc, h);
ps = Math.min(ps, temp);
pc = Math.max(pc, temp);
}
}
}
if(fresh)
{
minHeat = qs;
maxHeat = qc;
minWet = ps;
maxWet = pc;
}
landData.refill(heightCodeData, 4, 999);
if(generateRivers) {
if (fresh) {
addRivers();
riverData.connect8way().thin().thin();
lakeData.connect8way().thin();
partialRiverData.remake(riverData);
partialLakeData.remake(lakeData);
} else {
partialRiverData.remake(riverData);
partialLakeData.remake(lakeData);
for (int i = 1; i <= zoom; i++) {
int stx = (startCacheX.get(i) - startCacheX.get(i - 1)) << (i - 1),
sty = (startCacheY.get(i) - startCacheY.get(i - 1)) << (i - 1);
if ((i & 3) == 3) {
partialRiverData.zoom(stx, sty).connect8way();
partialRiverData.or(workingData.remake(partialRiverData).fringe().quasiRandomRegion(0.4));
partialLakeData.zoom(stx, sty).connect8way();
partialLakeData.or(workingData.remake(partialLakeData).fringe().quasiRandomRegion(0.55));
} else {
partialRiverData.zoom(stx, sty).connect8way().thin();
partialRiverData.or(workingData.remake(partialRiverData).fringe().quasiRandomRegion(0.5));
partialLakeData.zoom(stx, sty).connect8way().thin();
partialLakeData.or(workingData.remake(partialLakeData).fringe().quasiRandomRegion(0.7));
}
}
}
}
}
}
/**
* A concrete implementation of {@link WorldMapGenerator} that distorts the map as it nears the poles, expanding the
* smaller-diameter latitude lines in extreme north and south regions so they take up the same space as the equator.
* This is ideal for projecting onto a 3D sphere, which could squash the poles to counteract the stretch this does.
* You might also want to produce an oval map that more-accurately represents the changes in the diameter of a
* latitude line on a spherical world; this could be done by using one of the maps this class makes and removing a
* portion of each non-equator row, arranging the removal so if the map is n units wide at the equator, the height
* should be n divided by {@link Math#PI}, and progressively more cells are removed from rows as you move away from
* the equator (down to empty space or 1 cell left at the poles).
* <a href="http://i.imgur.com/wth01QD.png" >Example map, showing distortion</a>
*/
public static class SphereMap extends WorldMapGenerator {
protected static final double terrainFreq = 2.5, terrainRidgedFreq = 1.3, heatFreq = 5.05, moistureFreq = 5.2, otherFreq = 5.5;
private double minHeat0 = Double.POSITIVE_INFINITY, maxHeat0 = Double.NEGATIVE_INFINITY,
minHeat1 = Double.POSITIVE_INFINITY, maxHeat1 = Double.NEGATIVE_INFINITY,
minWet0 = Double.POSITIVE_INFINITY, maxWet0 = Double.NEGATIVE_INFINITY;
public final Noise3D terrain, terrainRidged, heat, moisture, otherRidged;
/**
* Constructs a concrete WorldMapGenerator for a map that can be used to wrap a sphere (as with a texture on a
* 3D model), with seamless east-west wrapping, no north-south wrapping, and distortion that causes the poles to
* have significantly-exaggerated-in-size features while the equator is not distorted.
* Always makes a 256x256 map.
* Uses SeededNoise as its noise generator, with 1.0 as the octave multiplier affecting detail.
* If you were using {@link WorldMapGenerator.SphereMap#SphereMap(long, int, int, Noise3D, double)}, then this would be the
* same as passing the parameters {@code 0x1337BABE1337D00DL, 256, 256, SeededNoise.instance, 1.0}.
*/
public SphereMap() {
this(0x1337BABE1337D00DL, 256, 256, SeededNoise.instance, 1.0);
}
/**
* Constructs a concrete WorldMapGenerator for a map that can be used to wrap a sphere (as with a texture on a
* 3D model), with seamless east-west wrapping, no north-south wrapping, and distortion that causes the poles to
* have significantly-exaggerated-in-size features while the equator is not distorted.
* Takes only the width/height of the map. The initial seed is set to the same large long
* every time, and it's likely that you would set the seed when you call {@link #generate(long)}. The width and
* height of the map cannot be changed after the fact, but you can zoom in.
* Uses SeededNoise as its noise generator, with 1.0 as the octave multiplier affecting detail.
*
* @param mapWidth the width of the map(s) to generate; cannot be changed later
* @param mapHeight the height of the map(s) to generate; cannot be changed later
*/
public SphereMap(int mapWidth, int mapHeight) {
this(0x1337BABE1337D00DL, mapWidth, mapHeight, SeededNoise.instance, 1.0);
}
/**
* Constructs a concrete WorldMapGenerator for a map that can be used to wrap a sphere (as with a texture on a
* 3D model), with seamless east-west wrapping, no north-south wrapping, and distortion that causes the poles to
* have significantly-exaggerated-in-size features while the equator is not distorted.
* Takes an initial seed and the width/height of the map. The {@code initialSeed}
* parameter may or may not be used, since you can specify the seed to use when you call {@link #generate(long)}.
* The width and height of the map cannot be changed after the fact, but you can zoom in.
* Uses SeededNoise as its noise generator, with 1.0 as the octave multiplier affecting detail.
*
* @param initialSeed the seed for the StatefulRNG this uses; this may also be set per-call to generate
* @param mapWidth the width of the map(s) to generate; cannot be changed later
* @param mapHeight the height of the map(s) to generate; cannot be changed later
*/
public SphereMap(long initialSeed, int mapWidth, int mapHeight) {
this(initialSeed, mapWidth, mapHeight, SeededNoise.instance, 1.0);
}
/**
* Constructs a concrete WorldMapGenerator for a map that can be used to wrap a sphere (as with a texture on a
* 3D model), with seamless east-west wrapping, no north-south wrapping, and distortion that causes the poles to
* have significantly-exaggerated-in-size features while the equator is not distorted.
* Takes an initial seed, the width/height of the map, and a noise generator (a
* {@link Noise3D} implementation, which is usually {@link SeededNoise#instance}. The {@code initialSeed}
* parameter may or may not be used, since you can specify the seed to use when you call
* {@link #generate(long)}. The width and height of the map cannot be changed after the fact, but you can zoom
* in. Currently only SeededNoise makes sense to use as the value for {@code noiseGenerator}, and the seed it's
* constructed with doesn't matter because it will change the seed several times at different scales of noise
* (it's fine to use the static {@link SeededNoise#instance} because it has no changing state between runs of
* the program; it's effectively a constant). The detail level, which is the {@code octaveMultiplier} parameter
* that can be passed to another constructor, is always 1.0 with this constructor.
*
* @param initialSeed the seed for the StatefulRNG this uses; this may also be set per-call to generate
* @param mapWidth the width of the map(s) to generate; cannot be changed later
* @param mapHeight the height of the map(s) to generate; cannot be changed later
* @param noiseGenerator an instance of a noise generator capable of 3D noise, almost always {@link SeededNoise}
*/
public SphereMap(long initialSeed, int mapWidth, int mapHeight, final Noise3D noiseGenerator) {
this(initialSeed, mapWidth, mapHeight, noiseGenerator, 1.0);
}
/**
* Constructs a concrete WorldMapGenerator for a map that can be used to wrap a sphere (as with a texture on a
* 3D model), with seamless east-west wrapping, no north-south wrapping, and distortion that causes the poles to
* have significantly-exaggerated-in-size features while the equator is not distorted.
* Takes an initial seed, the width/height of the map, and parameters for noise
* generation (a {@link Noise3D} implementation, which is usually {@link SeededNoise#instance}, and a
* multiplier on how many octaves of noise to use, with 1.0 being normal (high) detail and higher multipliers
* producing even more detailed noise when zoomed-in). The {@code initialSeed} parameter may or may not be used,
* since you can specify the seed to use when you call {@link #generate(long)}. The width and height of the map
* cannot be changed after the fact, but you can zoom in. Currently only SeededNoise makes sense to use as the
* value for {@code noiseGenerator}, and the seed it's constructed with doesn't matter because it will change the
* seed several times at different scales of noise (it's fine to use the static {@link SeededNoise#instance} because
* it has no changing state between runs of the program; it's effectively a constant). The {@code octaveMultiplier}
* parameter should probably be no lower than 0.5, but can be arbitrarily high if you're willing to spend much more
* time on generating detail only noticeable at very high zoom; normally 1.0 is fine and may even be too high for
* maps that don't require zooming.
* @param initialSeed the seed for the StatefulRNG this uses; this may also be set per-call to generate
* @param mapWidth the width of the map(s) to generate; cannot be changed later
* @param mapHeight the height of the map(s) to generate; cannot be changed later
* @param noiseGenerator an instance of a noise generator capable of 3D noise, almost always {@link SeededNoise}
* @param octaveMultiplier used to adjust the level of detail, with 0.5 at the bare-minimum detail and 1.0 normal
*/
public SphereMap(long initialSeed, int mapWidth, int mapHeight, final Noise3D noiseGenerator, double octaveMultiplier) {
super(initialSeed, mapWidth, mapHeight);
terrain = new Noise.Layered3D(noiseGenerator, (int) (0.5 + octaveMultiplier * 8), terrainFreq);
terrainRidged = new Noise.Ridged3D(noiseGenerator, (int) (0.5 + octaveMultiplier * 10), terrainRidgedFreq);
heat = new Noise.Layered3D(noiseGenerator, (int) (0.5 + octaveMultiplier * 3), heatFreq);
moisture = new Noise.Layered3D(noiseGenerator, (int) (0.5 + octaveMultiplier * 4), moistureFreq);
otherRidged = new Noise.Ridged3D(noiseGenerator, (int) (0.5 + octaveMultiplier * 6), otherFreq);
}
protected int wrapY(final int y) {
return Math.max(0, Math.min(y, height - 1));
}
protected void regenerate(int startX, int startY, int usedWidth, int usedHeight,
double waterMod, double coolMod, long state)
{
boolean fresh = false;
if(cachedState != state || waterMod != waterModifier || coolMod != coolingModifier)
{
minHeight = Double.POSITIVE_INFINITY;
maxHeight = Double.NEGATIVE_INFINITY;
minHeat0 = Double.POSITIVE_INFINITY;
maxHeat0 = Double.NEGATIVE_INFINITY;
minHeat1 = Double.POSITIVE_INFINITY;
maxHeat1 = Double.NEGATIVE_INFINITY;
minHeat = Double.POSITIVE_INFINITY;
maxHeat = Double.NEGATIVE_INFINITY;
minWet0 = Double.POSITIVE_INFINITY;
maxWet0 = Double.NEGATIVE_INFINITY;
minWet = Double.POSITIVE_INFINITY;
maxWet = Double.NEGATIVE_INFINITY;
cachedState = state;
fresh = true;
}
rng.setState(state);
int seedA = rng.nextInt(), seedB = rng.nextInt(), seedC = rng.nextInt(), t;
waterModifier = (waterMod <= 0) ? rng.nextDouble(0.25) + 0.89 : waterMod;
coolingModifier = (coolMod <= 0) ? rng.nextDouble(0.45) * (rng.nextDouble()-0.5) + 1.1 : coolMod;
double p, q,
ps, pc,
qs, qc,
h, temp,
i_w = 6.283185307179586 / width, i_h = (3.141592653589793) / (height+2.0),
xPos = startX, yPos, i_uw = usedWidth / (double)width, i_uh = usedHeight / (height+2.0);
final double[] trigTable = new double[width << 1];
for (int x = 0; x < width; x++, xPos += i_uw) {
p = xPos * i_w;
trigTable[x<<1] = Math.sin(p);
trigTable[x<<1|1] = Math.cos(p);
}
yPos = startY + i_uh;
for (int y = 0; y < height; y++, yPos += i_uh) {
qs = -1.5707963267948966 + yPos * i_h;
qc = Math.cos(qs);
qs = Math.sin(qs);
//qs = Math.sin(qs);
for (int x = 0, xt = 0; x < width; x++) {
ps = trigTable[xt++] * qc;//Math.sin(p);
pc = trigTable[xt++] * qc;//Math.cos(p);
h = terrain.getNoiseWithSeed(pc +
terrainRidged.getNoiseWithSeed(pc, ps, qs,seedA + seedB),
ps, qs, seedA);
h *= waterModifier;
heightData[x][y] = h;
heatData[x][y] = (p = heat.getNoiseWithSeed(pc, ps
+ otherRidged.getNoiseWithSeed(pc, ps, qs,seedB + seedC)
, qs, seedB));
moistureData[x][y] = (temp = moisture.getNoiseWithSeed(pc, ps, qs
+ otherRidged.getNoiseWithSeed(pc, ps, qs, seedC + seedA)
, seedC));
minHeightActual = Math.min(minHeightActual, h);
maxHeightActual = Math.max(maxHeightActual, h);
if(fresh) {
minHeight = Math.min(minHeight, h);
maxHeight = Math.max(maxHeight, h);
minHeat0 = Math.min(minHeat0, p);
maxHeat0 = Math.max(maxHeat0, p);
minWet0 = Math.min(minWet0, temp);
maxWet0 = Math.max(maxWet0, temp);
}
}
minHeightActual = Math.min(minHeightActual, minHeight);
maxHeightActual = Math.max(maxHeightActual, maxHeight);
}
double heightDiff = 2.0 / (maxHeightActual - minHeightActual),
heatDiff = 0.8 / (maxHeat0 - minHeat0),
wetDiff = 1.0 / (maxWet0 - minWet0),
hMod,
halfHeight = (height - 1) * 0.5, i_half = 1.0 / halfHeight;
double minHeightActual0 = minHeightActual;
double maxHeightActual0 = maxHeightActual;
yPos = startY + i_uh;
ps = Double.POSITIVE_INFINITY;
pc = Double.NEGATIVE_INFINITY;
for (int y = 0; y < height; y++, yPos += i_uh) {
temp = Math.abs(yPos - halfHeight) * i_half;
temp *= (2.4 - temp);
temp = 2.2 - temp;
for (int x = 0; x < width; x++) {
heightData[x][y] = (h = (heightData[x][y] - minHeightActual) * heightDiff - 1.0);
minHeightActual0 = Math.min(minHeightActual0, h);
maxHeightActual0 = Math.max(maxHeightActual0, h);
heightCodeData[x][y] = (t = codeHeight(h));
hMod = 1.0;
switch (t) {
case 0:
case 1:
case 2:
case 3:
h = 0.4;
hMod = 0.2;
break;
case 6:
h = -0.1 * (h - forestLower - 0.08);
break;
case 7:
h *= -0.25;
break;
case 8:
h *= -0.4;
break;
default:
h *= 0.05;
}
heatData[x][y] = (h = (((heatData[x][y] - minHeat0) * heatDiff * hMod) + h + 0.6) * temp);
if (fresh) {
ps = Math.min(ps, h); //minHeat0
pc = Math.max(pc, h); //maxHeat0
}
}
}
if(fresh)
{
minHeat1 = ps;
maxHeat1 = pc;
}
heatDiff = coolingModifier / (maxHeat1 - minHeat1);
qs = Double.POSITIVE_INFINITY;
qc = Double.NEGATIVE_INFINITY;
ps = Double.POSITIVE_INFINITY;
pc = Double.NEGATIVE_INFINITY;
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
heatData[x][y] = (h = ((heatData[x][y] - minHeat1) * heatDiff));
moistureData[x][y] = (temp = (moistureData[x][y] - minWet0) * wetDiff);
if (fresh) {
qs = Math.min(qs, h);
qc = Math.max(qc, h);
ps = Math.min(ps, temp);
pc = Math.max(pc, temp);
}
}
}
if(fresh)
{
minHeat = qs;
maxHeat = qc;
minWet = ps;
maxWet = pc;
}
landData.refill(heightCodeData, 4, 999);
if(generateRivers) {
if (fresh) {
addRivers();
riverData.connect8way().thin().thin();
lakeData.connect8way().thin();
partialRiverData.remake(riverData);
partialLakeData.remake(lakeData);
} else {
partialRiverData.remake(riverData);
partialLakeData.remake(lakeData);
for (int i = 1; i <= zoom; i++) {
int stx = (startCacheX.get(i) - startCacheX.get(i - 1)) << (i - 1),
sty = (startCacheY.get(i) - startCacheY.get(i - 1)) << (i - 1);
if ((i & 3) == 3) {
partialRiverData.zoom(stx, sty).connect8way();
partialRiverData.or(workingData.remake(partialRiverData).fringe().quasiRandomRegion(0.4));
partialLakeData.zoom(stx, sty).connect8way();
partialLakeData.or(workingData.remake(partialLakeData).fringe().quasiRandomRegion(0.55));
} else {
partialRiverData.zoom(stx, sty).connect8way().thin();
partialRiverData.or(workingData.remake(partialRiverData).fringe().quasiRandomRegion(0.5));
partialLakeData.zoom(stx, sty).connect8way().thin();
partialLakeData.or(workingData.remake(partialLakeData).fringe().quasiRandomRegion(0.7));
}
}
}
}
}
}
}