Cloud map is used for both land and water. Foreground now uses seeded random number generator for reproducibility of results.master
@@ -44,10 +44,11 @@ This project is made from [Next](https://nextjs.org). Development practices for | |||
Biome map obtained from [Minecraft Earth Map](https://earth.motfe.net/tiles-biomes/). | |||
Land elevation, bathymetry, and real-color maps obtained from [Visible Earth](https://visibleearth.nasa.gov/). | |||
Water mask map obtained from [Shaded Relief](https://www.shadedrelief.com/natural3/pages/extra.html). | |||
Uses [D3](https://d3js.org/) (d3-geo) for map projections. Super Mario is a property of Nintendo. | |||
Water mask map obtained from [Shaded Relief](https://www.shadedrelief.com/natural3/pages/extra.html). Cloud map obtained | |||
from [Wikimedia Commons](https://commons.wikimedia.org/wiki/File:Asia_Cloud_Map.jpg). Uses [D3](https://d3js.org/) | |||
(d3-geo) for map projections. Super Mario is a property of Nintendo. | |||
[Super Mario War](http://supermariowar.supersanctuary.net/) is a fangame inspired by Super Mario games. | |||
## Samples | |||
![Larger Sample Output](./docs/img/sample-output-2.png) | |||
![Larger Sample Output](./docs/img/sample-output-2.png) |
@@ -11,6 +11,7 @@ | |||
}, | |||
"dependencies": { | |||
"@theoryofnekomata/formxtra": "^0.2.3", | |||
"chance": "^1.1.8", | |||
"d3": "^7.3.0", | |||
"d3-geo": "^3.0.1", | |||
"d3-geo-polygon": "^1.12.1", | |||
@@ -22,6 +23,7 @@ | |||
"ts-node": "^10.7.0" | |||
}, | |||
"devDependencies": { | |||
"@types/chance": "^1.1.3", | |||
"@types/d3": "^7.1.0", | |||
"@types/d3-geo": "^3.0.2", | |||
"@types/node": "17.0.21", | |||
@@ -1,7 +1,9 @@ | |||
import ColorService, {ColorServiceImpl} from './Color.service'; | |||
import {LandType} from '../../utils/types'; | |||
import {Biome, LandType} from '../../utils/types'; | |||
export default interface BiomeService { | |||
getBiome(x: number, y: number, width: number): Biome | |||
getBiomeByIndex(index: number): Biome | |||
checkBiomeLandType(x: number, y: number, width: number): LandType | |||
isMountainBiome(x: number, y: number, width: number): boolean | |||
isHillBiome(x: number, y: number, width: number): boolean | |||
@@ -15,13 +17,20 @@ export class BiomeServiceImpl implements BiomeService { | |||
this.colorService = new ColorServiceImpl() | |||
} | |||
checkBiomeLandType(x: number, y: number, width: number): LandType { | |||
const dataIndex = ((y * width) + x) * 4; | |||
switch (this.colorService.rgb( | |||
getBiome(x: number, y: number, width: number): Biome { | |||
return this.getBiomeByIndex(((y * width) + x) * 4); | |||
} | |||
getBiomeByIndex(dataIndex: number): Biome { | |||
return this.colorService.rgb( | |||
this.biomePngData[dataIndex], | |||
this.biomePngData[dataIndex + 1], | |||
this.biomePngData[dataIndex + 2], | |||
)) { | |||
); | |||
} | |||
checkBiomeLandType(x: number, y: number, width: number): LandType { | |||
switch (this.getBiome(x, y, width)) { | |||
case 0xfa9418: | |||
case 0xffbc40: | |||
return LandType.DESERT; | |||
@@ -75,12 +84,7 @@ export class BiomeServiceImpl implements BiomeService { | |||
} | |||
isMountainBiome(x: number, y: number, width: number): boolean { | |||
const dataIndex = ((y * width) + x) * 4; | |||
switch (this.colorService.rgb( | |||
(this.biomePngData[dataIndex]), | |||
(this.biomePngData[dataIndex + 1]), | |||
(this.biomePngData[dataIndex + 2]), | |||
)) { | |||
switch (this.getBiome(x, y, width)) { | |||
case 0x606060: | |||
case 0xa0a0a0: | |||
case 0x507050: | |||
@@ -95,17 +99,14 @@ export class BiomeServiceImpl implements BiomeService { | |||
} | |||
isHillBiome(x: number, y: number, width: number): boolean { | |||
const dataIndex = ((y * width) + x) * 4; | |||
switch (this.colorService.rgb( | |||
(this.biomePngData[dataIndex]), | |||
(this.biomePngData[dataIndex + 1]), | |||
(this.biomePngData[dataIndex + 2]), | |||
)) { | |||
switch (this.getBiome(x, y, width)) { | |||
case 0xffffff: | |||
case 0xd25f12: | |||
case 0x163933: | |||
case 0x2c4205: | |||
case 0x454f3e: | |||
case 0x687942: | |||
case Biome.SAVANNA: | |||
return true | |||
default: | |||
break | |||
@@ -115,16 +116,16 @@ export class BiomeServiceImpl implements BiomeService { | |||
} | |||
isWoodedBiome(x: number, y: number, width: number): boolean { | |||
const dataIndex = ((y * width) + x) * 4; | |||
switch (this.colorService.rgb( | |||
(this.biomePngData[dataIndex]), | |||
(this.biomePngData[dataIndex + 1]), | |||
(this.biomePngData[dataIndex + 2]), | |||
)) { | |||
switch (this.getBiome(x, y, width)) { | |||
case 0x0b6659: | |||
case 0x8db360: | |||
case 0x056621: | |||
case 0x537b09: | |||
case 0x596651: | |||
case 0x2d8e49: | |||
case 0x628b17: | |||
case Biome.DESERT: | |||
case Biome.GIANT_TREE_TAIGA: | |||
return true; | |||
default: | |||
break | |||
@@ -1,5 +1,5 @@ | |||
import {PNG} from 'pngjs'; | |||
import {Projection} from '../../utils/types'; | |||
import {Bounds, Projection} from '../../utils/types'; | |||
import * as d3geo from 'd3-geo'; | |||
type ProjectionData = { | |||
@@ -8,7 +8,7 @@ type ProjectionData = { | |||
} | |||
export default interface ProjectionService { | |||
project(equiImage: PNG, projection: Projection): PNG | null | |||
project(equiImage: PNG, projection: Projection, bounds: Bounds): PNG | null | |||
} | |||
export class ProjectionServiceImpl implements ProjectionService { | |||
@@ -33,14 +33,14 @@ export class ProjectionServiceImpl implements ProjectionService { | |||
private readonly referenceWidth = 960 as const; | |||
project(equiImage: PNG, projection: Projection): PNG | null { | |||
project(equiImage: PNG, projection: Projection, bounds: Bounds): PNG | null { | |||
if (projection === Projection.EQUIRECTANGULAR) { | |||
return equiImage; | |||
} | |||
const { [projection]: currentProjectionData } = this.projections | |||
const baseProjection = currentProjectionData.fn(); | |||
const modifiedProjection = baseProjection.center([0, 0]); | |||
const modifiedProjection = baseProjection.center([(bounds[0][0] + bounds[1][0]) / 2, (bounds[0][1] + bounds[1][1]) / 2]); | |||
const { invert } = modifiedProjection; | |||
if (!invert) { | |||
return null; | |||
@@ -1,5 +1,7 @@ | |||
import BiomeService, {BiomeServiceImpl} from './Biome.service'; | |||
import {LandType, WaterType} from '../../utils/types'; | |||
import {Biome, LandType, WaterType} from '../../utils/types'; | |||
import Chance from 'chance'; | |||
import ColorService, {ColorServiceImpl} from './Color.service'; | |||
export type NeighborData = { | |||
nw: boolean, | |||
@@ -20,6 +22,9 @@ export default interface TileService { | |||
} | |||
export class TileServiceImpl implements TileService { | |||
private readonly rng: Chance.Chance; | |||
private readonly colorService: ColorService; | |||
private index(x: number, y: number, width: number) { | |||
return ((y * width) + x) * 4; | |||
} | |||
@@ -199,27 +204,91 @@ export class TileServiceImpl implements TileService { | |||
switch (landType) { | |||
case LandType.DESERT: | |||
case LandType.MESA: | |||
if (!this.rng.bool({ likelihood: 80 })) { | |||
return 0 | |||
} | |||
return 796; | |||
case LandType.ICE: | |||
if (!this.rng.bool({ likelihood: 20 })) { | |||
return 0 | |||
} | |||
return 799; | |||
case LandType.FOREST: | |||
case LandType.PLAINS: | |||
default: | |||
return 797; | |||
break | |||
} | |||
if (!this.rng.bool({ likelihood: 80 })) { | |||
return 0 | |||
} | |||
return 797; | |||
} | |||
private getHillForegroundTileType(landType: LandType): number { | |||
private getHillForegroundTileType(landType: LandType, index: number): number { | |||
if (landType === LandType.ICE) { | |||
if (!this.rng.bool({ likelihood: 30 })) { | |||
return 0 | |||
} | |||
return 903 | |||
} | |||
switch(this.biomeService.getBiomeByIndex(index)) { | |||
case Biome.SAVANNA: | |||
if (!this.rng.bool({ likelihood: 30 })) { | |||
return 0 | |||
} | |||
return this.rng.bool() ? 901 : 902; | |||
default: | |||
break; | |||
} | |||
if (!this.rng.bool({ likelihood: 70 })) { | |||
return 0 | |||
} | |||
return 901 | |||
} | |||
private getWoodedForegroundTileType(landType: LandType): number { | |||
private getWoodedForegroundTileType(landType: LandType, index: number): number { | |||
if (landType === LandType.DESERT) { | |||
if (this.rng.bool({ likelihood: 5 })) { | |||
return 904; | |||
} | |||
return 0; | |||
} | |||
if (landType === LandType.ICE) { | |||
if (!this.rng.bool({ likelihood: 30 })) { | |||
return 0 | |||
} | |||
return 906 | |||
} | |||
switch (this.biomeService.getBiomeByIndex(index)) { | |||
case Biome.JUNGLE: | |||
case Biome.JUNGLE_EDGE: | |||
if (!this.rng.bool({ likelihood: 60 })) { | |||
return 0 | |||
} | |||
return 904; | |||
case Biome.FOREST: | |||
if (!this.rng.bool({ likelihood: 60 })) { | |||
return 0 | |||
} | |||
return 907; | |||
case Biome.TAIGA: | |||
case Biome.GIANT_TREE_TAIGA: | |||
if (!this.rng.bool({ likelihood: 20 })) { | |||
return 0 | |||
} | |||
return 908; | |||
default: | |||
break; | |||
} | |||
if (!this.rng.bool({ likelihood: 50 })) { | |||
return 0 | |||
} | |||
return 905 | |||
} | |||
@@ -228,8 +297,11 @@ export class TileServiceImpl implements TileService { | |||
private readonly elevaionPngData: Buffer, | |||
private readonly waterMaskPngData: Buffer, | |||
private readonly bathymetryPngData: Buffer, | |||
private readonly cloudsPngData: Buffer, | |||
) { | |||
this.biomeService = new BiomeServiceImpl(biomePngData) | |||
this.colorService = new ColorServiceImpl() | |||
this.rng = Chance(69420); | |||
} | |||
determineBackgroundTileType(x: number, y: number, width: number, height: number): number { | |||
@@ -239,9 +311,14 @@ export class TileServiceImpl implements TileService { | |||
} | |||
private determineLandForegroundTileType(x: number, y: number, dx: number): number { | |||
const i = this.index(x, y, dx); | |||
if (this.cloudsPngData[i] >= 0x20 && this.rng.bool({ likelihood: 10 })) { | |||
return 790; | |||
} | |||
const landType = this.biomeService.checkBiomeLandType(x, y, dx); | |||
const i = this.index(x, y, dx); | |||
const isMountain = this.elevaionPngData[i] >= 0x40 || this.biomeService.isMountainBiome( | |||
x, | |||
y, | |||
@@ -252,17 +329,23 @@ export class TileServiceImpl implements TileService { | |||
} | |||
if (this.biomeService.isHillBiome(x, y, dx)) { | |||
return this.getHillForegroundTileType(landType) | |||
return this.getHillForegroundTileType(landType, i); | |||
} | |||
if (this.biomeService.isWoodedBiome(x, y, dx)) { | |||
return this.getWoodedForegroundTileType(landType); | |||
return this.getWoodedForegroundTileType(landType, i); | |||
} | |||
return 0; | |||
} | |||
private determineWaterForegroundTileType(x: number, y: number, dx: number): number { | |||
const i = this.index(x, y, dx); | |||
if (this.cloudsPngData[i] >= 0x20 && this.rng.bool({ likelihood: 20 })) { | |||
return 790; | |||
} | |||
return 0; | |||
} | |||
@@ -5,7 +5,13 @@ import {NumericInput} from '../NumericInput'; | |||
import styles from '../../styles/components/GenerateMapForm/index.module.css'; | |||
import {ActionButton} from '../ActionButton'; | |||
export const GenerateMapForm: VFC = () => { | |||
type GenerateMapFormProps = { | |||
disabled?: boolean; | |||
} | |||
export const GenerateMapForm: VFC<GenerateMapFormProps> = ({ | |||
disabled = false, | |||
}) => { | |||
return ( | |||
<div | |||
className={styles.base} | |||
@@ -15,6 +21,7 @@ export const GenerateMapForm: VFC = () => { | |||
> | |||
<fieldset | |||
className={styles.fieldset} | |||
disabled={disabled} | |||
> | |||
<legend> | |||
Base | |||
@@ -150,6 +157,7 @@ export const GenerateMapForm: VFC = () => { | |||
> | |||
<fieldset | |||
className={styles.fieldset} | |||
disabled={disabled} | |||
> | |||
<legend> | |||
Cities | |||
@@ -162,6 +170,7 @@ export const GenerateMapForm: VFC = () => { | |||
> | |||
<fieldset | |||
className={styles.fieldset} | |||
disabled={disabled} | |||
> | |||
<legend> | |||
Map | |||
@@ -206,6 +215,7 @@ export const GenerateMapForm: VFC = () => { | |||
> | |||
<ActionButton | |||
block | |||
disabled={disabled} | |||
> | |||
Generate Map | |||
</ActionButton> | |||
@@ -1,15 +1,16 @@ | |||
import {NextApiHandler} from 'next'; | |||
import * as fs from 'fs/promises'; | |||
import {PNG} from 'pngjs'; | |||
import {MapType, Projection, WorldData} from '../../../utils/types'; | |||
import {Bounds, MapType, Projection, WorldData} from '../../../utils/types'; | |||
import {Stats} from 'fs'; | |||
import sharp from 'sharp'; | |||
import ProjectionService, {ProjectionServiceImpl} from '../../../backend/services/Projection.service'; | |||
import {FileSystemServiceImpl} from '../../../backend/services/FileSystem.service'; | |||
import TileService, {TileServiceImpl} from '../../../backend/services/Tile.service'; | |||
const generateProjectedBaseData = async (t: MapType, p: Projection) => { | |||
const destPath = `public/generated/base/${p}-${t}.png`; | |||
const stringifyBounds = (bounds: Bounds) => bounds.map(b => b.map(c => Math.floor(c).toString()).join('_')).join('_'); | |||
const generateProjectedBaseData = async (t: MapType, p: Projection, bounds: Bounds) => { | |||
const destPath = `public/generated/base/${p}_${t}_${stringifyBounds(bounds)}.png`; | |||
let stat: Stats | undefined; | |||
let shouldGenerateFile = false; | |||
try { | |||
@@ -27,7 +28,7 @@ const generateProjectedBaseData = async (t: MapType, p: Projection) => { | |||
const inputBuffer = await fs.readFile(`src/assets/data/000/${t}.png`) | |||
const inputPng = PNG.sync.read(inputBuffer); | |||
const projectionService: ProjectionService = new ProjectionServiceImpl() | |||
const outputPng = projectionService.project(inputPng, p) as PNG; | |||
const outputPng = projectionService.project(inputPng, p, bounds) as PNG; | |||
const outputBuffer = PNG.sync.write(outputPng); | |||
await fs.writeFile(destPath, outputBuffer); | |||
} | |||
@@ -39,6 +40,7 @@ const getResizeKernel = (t: MapType) => { | |||
switch (t) { | |||
case MapType.BIOME: | |||
case MapType.BATHYMETRY: | |||
case MapType.CLOUDS: | |||
return 'nearest'; | |||
case MapType.WATER_MASK: | |||
return 'lanczos2'; | |||
@@ -48,9 +50,9 @@ const getResizeKernel = (t: MapType) => { | |||
return 'cubic'; | |||
} | |||
const resizeBaseData = async (t: MapType, p: Projection, size: number, isWidthDimension: boolean) => { | |||
const destPath = `public/generated/resized/${isWidthDimension ? 'w' : 'h'}-${size}-${p}-${t}.png`; | |||
let resizeChain = sharp(`public/generated/base/${p}-${t}.png`) | |||
const resizeBaseData = async (t: MapType, p: Projection, bounds: Bounds, size: number, isWidthDimension: boolean) => { | |||
const destPath = `public/generated/resized/${isWidthDimension ? 'w' : 'h'}_${size}_${p}_${t}_${stringifyBounds(bounds)}.png`; | |||
let resizeChain = sharp(`public/generated/base/${p}_${t}_${stringifyBounds(bounds)}.png`) | |||
.resize({ | |||
[isWidthDimension ? 'width' : 'height']: size, | |||
kernel: getResizeKernel(t), | |||
@@ -68,18 +70,18 @@ const resizeBaseData = async (t: MapType, p: Projection, size: number, isWidthDi | |||
return destPath; | |||
} | |||
const generateWorldData = async (p: Projection, size: number, isWidthDimension: boolean) => { | |||
const generateWorldData = async (p: Projection, bounds: Bounds, size: number, isWidthDimension: boolean) => { | |||
await FileSystemServiceImpl.ensureDirectory('public/generated/resized'); | |||
const tempResizedFiles = await Promise.all( | |||
Object | |||
.values(MapType) | |||
.map(async (t: MapType) => resizeBaseData(t, p, size, isWidthDimension)) | |||
.map(async (t: MapType) => resizeBaseData(t, p, bounds, size, isWidthDimension)) | |||
); | |||
const mapTypeBuffers = await Promise.all(Object.values(MapType).map(async (t: MapType) => { | |||
return { | |||
mapType: t, | |||
buffer: await fs.readFile(`public/generated/resized/${isWidthDimension ? 'w' : 'h'}-${size}-${p}-${t}.png`), | |||
buffer: await fs.readFile(`public/generated/resized/${isWidthDimension ? 'w' : 'h'}_${size}_${p}_${t}_${stringifyBounds(bounds)}.png`), | |||
}; | |||
})) | |||
@@ -116,7 +118,8 @@ const generateWorldData = async (p: Projection, size: number, isWidthDimension: | |||
data[MapType.BIOME].data, | |||
data[MapType.ELEVATION].data, | |||
data[MapType.WATER_MASK].data, | |||
data[MapType.BATHYMETRY].data | |||
data[MapType.BATHYMETRY].data, | |||
data[MapType.CLOUDS].data, | |||
) | |||
let i = 0; | |||
@@ -165,21 +168,24 @@ const generateHandler: NextApiHandler = async (req, res) => { | |||
projection = Projection.EQUIRECTANGULAR, | |||
width, | |||
height, | |||
bounds = '-180,90;180,-90', | |||
bounds: rawBounds = '-180,90;180,-90', | |||
} = req.query | |||
const { size, isWidthDimension } = determineDimension( | |||
Array.isArray(width) ? width[0] : width, | |||
Array.isArray(height) ? height[0] : height, | |||
); | |||
const bounds = rawBounds as string; | |||
const boundsParsed = bounds.split(';').map((b) => b.split(',').map(c => Number(c))) as Bounds; | |||
await ensureDirectories(); | |||
const baseDataImageUrls = await Promise.all( | |||
Object | |||
.values(MapType) | |||
.map(async (t: MapType) => generateProjectedBaseData(t, projection as Projection)) | |||
.map(async (t: MapType) => generateProjectedBaseData(t, projection as Projection, boundsParsed)) | |||
); | |||
const worldData = await generateWorldData(projection as Projection, size, isWidthDimension); | |||
const worldData = await generateWorldData(projection as Projection, boundsParsed, size, isWidthDimension); | |||
res.json({ | |||
baseDataImageUrls, | |||
@@ -18,22 +18,6 @@ import WORLD_FOREGROUND from '../assets/gfx/world_foreground.png'; | |||
import {DropdownSelect} from '../components/DropdownSelect'; | |||
import {ActionButton} from '../components/ActionButton'; | |||
const IMAGE_URLS_INDEX = [ | |||
MapType.BIOME, | |||
MapType.ELEVATION, | |||
MapType.WATER_MASK, | |||
MapType.BATHYMETRY, | |||
MapType.REAL_COLOR, | |||
] | |||
const IMAGE_STACKING = [ | |||
MapType.WATER_MASK, | |||
MapType.BATHYMETRY, | |||
MapType.ELEVATION, | |||
MapType.REAL_COLOR, | |||
MapType.BIOME, | |||
] | |||
const loadImage = async (src: string, processTransparency = false) => { | |||
return new Promise<HTMLImageElement>((resolve, reject) => { | |||
const originalImage = new Image() | |||
@@ -235,8 +219,9 @@ ${new Array(worldData.height).fill(new Array(worldData.width).fill('0').join(',' | |||
const IndexPage: NextPage = () => { | |||
const [data, setData] = useState<Data>() | |||
const [loading, setLoading] = useState(false) | |||
const [previewMapOpacity, setPreviewMapOpacity] = useState<Record<MapType, number>>(() => { | |||
return IMAGE_URLS_INDEX.reduce( | |||
return Object.values(MapType).reduce( | |||
(theValue, mapType) => { | |||
return { | |||
...theValue, | |||
@@ -252,11 +237,11 @@ const IndexPage: NextPage = () => { | |||
const scrollRef = useRef(false) | |||
const handleSubmit: FormEventHandler<HTMLFormElement> = async (e) => { | |||
e.preventDefault() | |||
const form = e.target as HTMLFormElement | |||
const values = getFormValues(form) | |||
const values = getFormValues(e.currentTarget) | |||
const url = new URL('/api/generate', 'http://localhost:3000'); | |||
const search = new URLSearchParams(values) | |||
url.search = search.toString() | |||
setLoading(true) | |||
const response = await fetch(url.toString(), { | |||
method: 'GET', | |||
headers: { | |||
@@ -267,6 +252,7 @@ const IndexPage: NextPage = () => { | |||
if (response.ok) { | |||
setData(responseData) | |||
} | |||
setLoading(false) | |||
} | |||
const saveScreenshot: MouseEventHandler = async (e) => { | |||
@@ -373,7 +359,7 @@ const IndexPage: NextPage = () => { | |||
className={styles.form} | |||
onSubmit={handleSubmit} | |||
> | |||
<GenerateMapForm /> | |||
<GenerateMapForm disabled={loading} /> | |||
</form> | |||
</div> | |||
<div | |||
@@ -389,10 +375,10 @@ const IndexPage: NextPage = () => { | |||
ref={baseMapScrollRef} | |||
> | |||
{ | |||
IMAGE_STACKING.map((v: MapType) => ( | |||
Object.values(MapType).map((v: MapType, index: number) => ( | |||
<img | |||
key={v} | |||
src={data.baseDataImageUrls[IMAGE_URLS_INDEX.indexOf(v)]} | |||
src={data.baseDataImageUrls[index]} | |||
alt={v} | |||
style={{ | |||
opacity: previewMapOpacity[v], | |||
@@ -406,7 +392,7 @@ const IndexPage: NextPage = () => { | |||
className={styles.mapForm} | |||
> | |||
{ | |||
IMAGE_STACKING.map((v: MapType) => ( | |||
Object.values(MapType).map((v: MapType) => ( | |||
<div | |||
key={v} | |||
> | |||
@@ -6,10 +6,11 @@ export enum Projection { | |||
} | |||
export enum MapType { | |||
BIOME = 'biomes', | |||
ELEVATION = 'elevation', | |||
WATER_MASK = 'water-mask', | |||
BATHYMETRY = 'bathymetry', | |||
ELEVATION = 'elevation', | |||
CLOUDS = 'clouds', | |||
BIOME = 'biomes', | |||
REAL_COLOR = 'real-color', | |||
} | |||
@@ -45,3 +46,47 @@ export enum LandType { | |||
BEACH = 5, | |||
MUD = 6, | |||
} | |||
export type Coords = [number, number]; | |||
export type Bounds = [Coords, Coords]; | |||
export enum Biome { | |||
OCEAN = 0x000070, | |||
PLAINS = 0x8db360, | |||
DESERT = 0xfa9418, | |||
MOUNTAINS = 0x606060, | |||
FOREST = 0x056621, | |||
TAIGA = 0x0b6659, | |||
SWAMP = 0x07f9b2, | |||
RIVER = 0x0000ff, | |||
SNOWY_TUNDRA = 0xffffff, | |||
SNOWY_MOUNTAINS = 0xa0a0a0, | |||
BEACH = 0xfade55, | |||
DESERT_HILLS = 0xd25f12, | |||
TAIGA_HILLS = 0x163933, | |||
JUNGLE = 0x537b09, | |||
JUNGLE_HILLS = 0x2c4205, | |||
JUNGLE_EDGE = 0x628b17, | |||
SNOWY_BEACH = 0xfaf0c0, | |||
GIANT_TREE_TAIGA = 0x596651, | |||
GIANT_TREE_TAIGA_HILLS = 0x454f3e, | |||
WOODED_MOUNTAINS = 0x507050, | |||
SAVANNA = 0xbdb25f, | |||
SAVANNA_PLATEAU = 0xa79d64, | |||
BADLANDS = 0xd94515, | |||
BADLANDS_PLATEAU = 0xca8c65, | |||
WARM_OCEAN = 0x0000ac, | |||
LUKEWARM_OCEAN = 0x000090, | |||
COLD_OCEAN = 0x202070, | |||
DEEP_WARM_OCEAN = 0x000050, | |||
DEEP_LUKEWARM_OCEAN = 0x000040, | |||
DEEP_COLD_OCEAN = 0x202038, | |||
DEEP_FROZEN_OCEAN = 0x404090, | |||
SUNFLOWER_PLAINS = 0xb5db88, | |||
DESERT_LAKES = 0xffbc40, | |||
FLOWER_FOREST = 0x2d8e49, | |||
TAIGA_MOUNTAINS = 0x338e81, | |||
MODIFIED_JUNGLE_EDGE = 0x8ab33f, | |||
DARK_FOREST_HILLS = 0x687942, | |||
SNOWY_TAIGA_MOUNTAINS = 0x597d72, | |||
} |
@@ -176,6 +176,11 @@ | |||
resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.2.tgz#423c77877d0569db20e1fc80885ac4118314010e" | |||
integrity sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA== | |||
"@types/chance@^1.1.3": | |||
version "1.1.3" | |||
resolved "https://registry.yarnpkg.com/@types/chance/-/chance-1.1.3.tgz#d19fe9391288d60fdccd87632bfc9ab2b4523fea" | |||
integrity sha512-X6c6ghhe4/sQh4XzcZWSFaTAUOda38GQHmq9BUanYkOE/EO7ZrkazwKmtsj3xzTjkLWmwULE++23g3d3CCWaWw== | |||
"@types/d3-array@*": | |||
version "3.0.2" | |||
resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-3.0.2.tgz#71c35bca8366a40d1b8fce9279afa4a77fb0065d" | |||
@@ -668,6 +673,11 @@ chalk@^4.0.0: | |||
ansi-styles "^4.1.0" | |||
supports-color "^7.1.0" | |||
chance@^1.1.8: | |||
version "1.1.8" | |||
resolved "https://registry.yarnpkg.com/chance/-/chance-1.1.8.tgz#5d6c2b78c9170bf6eb9df7acdda04363085be909" | |||
integrity sha512-v7fi5Hj2VbR6dJEGRWLmJBA83LJMS47pkAbmROFxHWd9qmE1esHRZW8Clf1Fhzr3rjxnNZVCjOEv/ivFxeIMtg== | |||
chownr@^1.1.1: | |||
version "1.1.4" | |||
resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" | |||