@@ -0,0 +1,134 @@ | |||
import ColorService, {ColorServiceImpl} from './Color.service'; | |||
import {LandType} from '../../utils/types'; | |||
export default interface BiomeService { | |||
checkBiomeLandType(x: number, y: number, width: number): LandType | |||
isMountainBiome(x: number, y: number, width: number): boolean | |||
isHillBiome(x: number, y: number, width: number): boolean | |||
isWoodedBiome(x: number, y: number, width: number): boolean | |||
} | |||
export class BiomeServiceImpl implements BiomeService { | |||
private readonly colorService: ColorService | |||
constructor(private readonly biomePngData: Buffer) { | |||
this.colorService = new ColorServiceImpl() | |||
} | |||
checkBiomeLandType(x: number, y: number, width: number): LandType { | |||
const dataIndex = ((y * width) + x) * 4; | |||
switch (this.colorService.rgb( | |||
this.biomePngData[dataIndex], | |||
this.biomePngData[dataIndex + 1], | |||
this.biomePngData[dataIndex + 2], | |||
)) { | |||
case 0xfa9418: | |||
case 0xffbc40: | |||
return LandType.DESERT; | |||
case 0x606060: | |||
case 0xd25f12: | |||
case 0xd94515: | |||
case 0xca8c65: | |||
return LandType.MESA; | |||
case 0x056621: | |||
case 0x0b6659: | |||
case 0x537b09: | |||
case 0x2c4205: | |||
case 0x628b17: | |||
case 0x507050: | |||
case 0x2d8e49: | |||
case 0x8ab33f: | |||
case 0x687942: | |||
return LandType.FOREST; | |||
case 0x596651: | |||
case 0x454f3e: | |||
case 0x338e81: | |||
// return LandType.MUD; | |||
// case 0xfade55: | |||
// return LandType.BEACH; | |||
case 0xffffff: | |||
case 0xa0a0a0: | |||
case 0xfaf0c0: | |||
case 0x597d72: | |||
case 0x202070: | |||
case 0x202038: | |||
case 0x404090: | |||
case 0x163933: | |||
return LandType.ICE; | |||
case 0xfade55: | |||
case 0x8db360: | |||
case 0x07f9b2: | |||
case 0xbdb25f: | |||
case 0xa79d64: | |||
case 0xb5db88: | |||
case 0x000070: | |||
case 0x0000ff: | |||
case 0x0000ac: | |||
case 0x000090: | |||
case 0x000050: | |||
case 0x000040: | |||
default: | |||
break; | |||
} | |||
return LandType.PLAINS; | |||
} | |||
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]), | |||
)) { | |||
case 0x606060: | |||
case 0xa0a0a0: | |||
case 0x507050: | |||
case 0x338e81: | |||
case 0x597d72: | |||
return true | |||
default: | |||
break | |||
} | |||
return false | |||
} | |||
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]), | |||
)) { | |||
case 0xd25f12: | |||
case 0x163933: | |||
case 0x2c4205: | |||
case 0x454f3e: | |||
case 0x687942: | |||
return true | |||
default: | |||
break | |||
} | |||
return false | |||
} | |||
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]), | |||
)) { | |||
case 0x056621: | |||
case 0x537b09: | |||
case 0x596651: | |||
case 0x2d8e49: | |||
return true; | |||
default: | |||
break | |||
} | |||
return false; | |||
} | |||
} |
@@ -0,0 +1,9 @@ | |||
export default interface ColorService { | |||
rgb(r: number, g: number, b: number): number | |||
} | |||
export class ColorServiceImpl implements ColorService { | |||
rgb(r: number, g: number, b: number): number { | |||
return (r << 16) + (g << 8) + b | |||
} | |||
} |
@@ -3,11 +3,14 @@ import {Projection} from '../../utils/types'; | |||
import * as d3geo from 'd3-geo'; | |||
type ProjectionData = { | |||
fn: Function, | |||
fn: (...args: unknown[]) => d3geo.GeoProjection, | |||
bounds: (a: [number, number]) => [number, number], | |||
} | |||
export default interface ProjectionService {} | |||
export default interface ProjectionService { | |||
project(equiImage: PNG, projection: Projection): PNG | null | |||
} | |||
export class ProjectionServiceImpl implements ProjectionService { | |||
private readonly projections: Record<Projection, ProjectionData> = { | |||
[Projection.EQUIRECTANGULAR]: { | |||
@@ -26,20 +29,21 @@ export class ProjectionServiceImpl implements ProjectionService { | |||
fn: d3geo.geoConicConformal, | |||
bounds: (a) => a, | |||
}, | |||
} as const | |||
private readonly referenceWidth = 960 as const; | |||
project(equiImage: PNG, projection: Projection) { | |||
project(equiImage: PNG, projection: Projection): PNG | null { | |||
if (projection === Projection.EQUIRECTANGULAR) { | |||
return equiImage; | |||
} | |||
const { [projection]: currentProjectionData } = this.projections | |||
const { invert } = currentProjectionData.fn(); | |||
const baseProjection = currentProjectionData.fn(); | |||
const modifiedProjection = baseProjection.center([0, 0]); | |||
const { invert } = modifiedProjection; | |||
if (!invert) { | |||
return undefined; | |||
return null; | |||
} | |||
const { width: sx, height: sy, data: sourceData } = equiImage; | |||
@@ -0,0 +1,281 @@ | |||
import BiomeService, {BiomeServiceImpl} from './Biome.service'; | |||
import {LandType, WaterType} from '../../utils/types'; | |||
export type NeighborData = { | |||
nw: boolean, | |||
n: boolean, | |||
ne: boolean, | |||
w: boolean, | |||
i: number, | |||
e: boolean, | |||
sw: boolean, | |||
s: boolean, | |||
se: boolean, | |||
} | |||
export default interface TileService { | |||
determineBackgroundTileType(x: number, y: number, width: number, height: number): number | |||
determineForegroundTileType(tileType: number, x: number, y: number, dx: number): number; | |||
determineWaterBackgroundTileType(x: number, y: number, dx: number): number; | |||
} | |||
export class TileServiceImpl implements TileService { | |||
private index(x: number, y: number, width: number) { | |||
return ((y * width) + x) * 4; | |||
} | |||
private readonly landThreshold = 0x80 | |||
private readonly SHALLOW_WATER_THRESHOLD = 0xE0 | |||
private checkNeighboringLands(waterMaskPngData: Buffer, x: number, y: number, width: number, height: number): NeighborData { | |||
const ny = y === 0 ? 0 : y - 1; | |||
const wx = (x + width - 1) % width; | |||
const ex = (x + 1) % width ; | |||
const sy = y === height - 1 ? height - 1 : y + 1; | |||
return { | |||
nw: waterMaskPngData[this.index(wx, ny, width)] < this.landThreshold, | |||
n: waterMaskPngData[this.index(x, ny, width)] < this.landThreshold, | |||
ne: waterMaskPngData[this.index(ex, ny, width)] < this.landThreshold, | |||
w: waterMaskPngData[this.index(wx, y, width)] < this.landThreshold, | |||
i: waterMaskPngData[this.index(x, y, width)], | |||
e: waterMaskPngData[this.index(ex, y, width)] < this.landThreshold, | |||
sw: waterMaskPngData[this.index(wx, sy, width)] < this.landThreshold, | |||
s: waterMaskPngData[this.index(x, sy, width)] < this.landThreshold, | |||
se: waterMaskPngData[this.index(ex, sy, width)] < this.landThreshold, | |||
}; | |||
} | |||
private determineCoastalBackgroundTileType(landFlags: NeighborData) { | |||
let cursor = 0; | |||
if (landFlags.n) { | |||
cursor += 12; // 12 | |||
if (landFlags.e) { | |||
cursor += 12; // 24 | |||
if (landFlags.s) { | |||
cursor += 14; // 38 | |||
if (landFlags.w) { | |||
cursor -= 10; // 28 | |||
} | |||
} else if (landFlags.w) { | |||
cursor += 16; // 40 | |||
} else if (landFlags.sw) { | |||
cursor += 12; // 36 | |||
} | |||
} else if (landFlags.w) { | |||
cursor += 15; // 27 | |||
if (landFlags.s) { | |||
cursor += 12; // 39 | |||
} else if (landFlags.se) { | |||
cursor += 10; // 37 | |||
} | |||
} else if (landFlags.s) { | |||
cursor += 31; // 43 | |||
} else if (landFlags.se) { | |||
cursor += 8; // 20 | |||
if (landFlags.sw) { | |||
cursor += 12; // 32 | |||
} | |||
} else if (landFlags.sw) { | |||
cursor += 11; // 23 | |||
} | |||
} else if (landFlags.s) { | |||
cursor += 13; // 13 | |||
if (landFlags.e) { | |||
cursor += 12; // 25 | |||
if (landFlags.w) { | |||
cursor += 16; // 41 | |||
} else if (landFlags.nw) { | |||
cursor += 10; // 35 | |||
} | |||
} else if (landFlags.w) { | |||
cursor += 13; // 26 | |||
if (landFlags.ne) { | |||
cursor += 8; // 34 | |||
} | |||
} else if (landFlags.ne) { | |||
cursor += 8; // 21 | |||
if (landFlags.nw) { | |||
cursor += 20; // 33 | |||
} | |||
} else if (landFlags.nw) { | |||
cursor += 9; // 22 | |||
} | |||
} else if (landFlags.w) { | |||
cursor += 14; // 14 | |||
if (landFlags.e) { | |||
cursor += 28; // 42 | |||
} else if (landFlags.se) { | |||
cursor += 4; // 18 | |||
if (landFlags.ne) { | |||
cursor += 13; // 31 | |||
} | |||
} else if (landFlags.ne) { | |||
cursor += 5; // 19 | |||
} | |||
} else if (landFlags.e) { | |||
cursor += 15; // 15 | |||
if (landFlags.nw) { | |||
cursor += 1 // 16 | |||
if (landFlags.sw) { | |||
cursor += 14; // 30 | |||
} | |||
} else if (landFlags.sw) { | |||
cursor += 2; // 17 | |||
} | |||
} else if (landFlags.ne) { | |||
cursor += 2; // 2 | |||
if (landFlags.nw) { | |||
cursor += 4; // 6 | |||
if (landFlags.sw) { | |||
cursor += 42; // 48 | |||
if (landFlags.se) { | |||
cursor -= 4; // 44 | |||
} | |||
} else if (landFlags.se) { | |||
cursor += 39; // 45 | |||
} | |||
} else if (landFlags.se) { | |||
cursor += 7; // 9 | |||
if (landFlags.sw) { | |||
cursor += 37; // 46 | |||
} | |||
} else if (landFlags.sw) { | |||
cursor += 8; // 10 | |||
} | |||
} else if (landFlags.nw) { | |||
cursor += 3; // 3 | |||
if (landFlags.sw) { | |||
cursor += 5; // 8 | |||
if (landFlags.se) { | |||
cursor += 39; // 47 | |||
} | |||
} else if (landFlags.se) { | |||
cursor += 8; // 11 | |||
} | |||
} else if (landFlags.sw) { | |||
cursor += 4; // 4 | |||
if (landFlags.se) { | |||
cursor += 3; // 7 | |||
} | |||
} else if (landFlags.se) { | |||
cursor += 5; // 5 | |||
} | |||
return cursor; | |||
} | |||
private determineBaseBackgroundTileType(waterMaskPngData: Buffer, x: number, y: number, width: number, height: number): number { | |||
const landFlags = this.checkNeighboringLands(waterMaskPngData, x, y, width, height); | |||
const islandThreshold = 0xE0; | |||
if (landFlags.i < this.landThreshold) { | |||
return 1; // 1 | |||
} | |||
if ( | |||
landFlags.i < islandThreshold | |||
&& !( | |||
landFlags.nw | |||
|| landFlags.n | |||
|| landFlags.ne | |||
|| landFlags.w | |||
|| landFlags.e | |||
|| landFlags.sw | |||
|| landFlags.s | |||
|| landFlags.se | |||
) | |||
) { | |||
return 29; // 29 | |||
} | |||
return this.determineCoastalBackgroundTileType(landFlags) | |||
} | |||
private readonly biomeService: BiomeService | |||
private getMountainForegroundTileType(landType: LandType): number { | |||
switch (landType) { | |||
case LandType.DESERT: | |||
case LandType.MESA: | |||
return 796; | |||
case LandType.ICE: | |||
return 799; | |||
case LandType.FOREST: | |||
case LandType.PLAINS: | |||
default: | |||
return 797; | |||
} | |||
} | |||
private getHillForegroundTileType(landType: LandType): number { | |||
if (landType === LandType.ICE) { | |||
return 903 | |||
} | |||
return 901 | |||
} | |||
private getWoodedForegroundTileType(landType: LandType): number { | |||
if (landType === LandType.ICE) { | |||
return 906 | |||
} | |||
return 905 | |||
} | |||
constructor( | |||
biomePngData: Buffer, | |||
private readonly elevaionPngData: Buffer, | |||
private readonly waterMaskPngData: Buffer, | |||
private readonly bathymetryPngData: Buffer, | |||
) { | |||
this.biomeService = new BiomeServiceImpl(biomePngData) | |||
} | |||
determineBackgroundTileType(x: number, y: number, width: number, height: number): number { | |||
const landType = this.biomeService.checkBiomeLandType(x, y, width); | |||
const tileType = this.determineBaseBackgroundTileType(this.waterMaskPngData, x, y, width, height); | |||
return ((landType * 60) + tileType); | |||
} | |||
private determineLandForegroundTileType(x: number, y: number, dx: number): number { | |||
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, | |||
dx | |||
); | |||
if (isMountain) { | |||
return this.getMountainForegroundTileType(landType); | |||
} | |||
if (this.biomeService.isHillBiome(x, y, dx)) { | |||
return this.getHillForegroundTileType(landType) | |||
} | |||
if (this.biomeService.isWoodedBiome(x, y, dx)) { | |||
return this.getWoodedForegroundTileType(landType); | |||
} | |||
return 0; | |||
} | |||
private determineWaterForegroundTileType(x: number, y: number, dx: number): number { | |||
return 0; | |||
} | |||
determineWaterBackgroundTileType(x: number, y: number, dx: number): number { | |||
const i = this.index(x, y, dx); | |||
const isShallowWater = this.bathymetryPngData[i] >= this.SHALLOW_WATER_THRESHOLD; | |||
return isShallowWater ? WaterType.LIGHT : WaterType.DEFAULT; | |||
} | |||
determineForegroundTileType(tileType: number, x: number, y: number, dx: number): number { | |||
if (tileType % 60 === 1) { | |||
return this.determineLandForegroundTileType(x, y, dx); | |||
} | |||
return this.determineWaterForegroundTileType(x, y, dx); | |||
} | |||
} |
@@ -4,7 +4,6 @@ import {Checkbox} from '../Checkbox'; | |||
import {NumericInput} from '../NumericInput'; | |||
import styles from '../../styles/components/GenerateMapForm/index.module.css'; | |||
import {ActionButton} from '../ActionButton'; | |||
import {MapType} from '../../utils/types'; | |||
export const GenerateMapForm: VFC = () => { | |||
return ( | |||
@@ -1,11 +1,12 @@ | |||
import {NextApiHandler} from 'next'; | |||
import * as fs from 'fs/promises'; | |||
import {PNG} from 'pngjs'; | |||
import {MapType, Projection, WaterType, WorldData} from '../../../utils/types'; | |||
import {MapType, Projection, WorldData} from '../../../utils/types'; | |||
import {Stats} from 'fs'; | |||
import sharp from 'sharp'; | |||
import {ProjectionServiceImpl} from '../../../backend/services/Projection.service'; | |||
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`; | |||
@@ -25,7 +26,7 @@ const generateProjectedBaseData = async (t: MapType, p: Projection) => { | |||
if (shouldGenerateFile) { | |||
const inputBuffer = await fs.readFile(`src/assets/data/000/${t}.png`) | |||
const inputPng = PNG.sync.read(inputBuffer); | |||
const projectionService = new ProjectionServiceImpl() | |||
const projectionService: ProjectionService = new ProjectionServiceImpl() | |||
const outputPng = projectionService.project(inputPng, p) as PNG; | |||
const outputBuffer = PNG.sync.write(outputPng); | |||
await fs.writeFile(destPath, outputBuffer); | |||
@@ -64,307 +65,17 @@ const resizeBaseData = async (t: MapType, p: Projection, size: number, isWidthDi | |||
.toBuffer(); | |||
await fs.writeFile(destPath, buffer); | |||
return destPath.replace('public/', '/'); | |||
} | |||
enum LandType { | |||
MESA = 0, | |||
DESERT = 1, | |||
FOREST = 2, | |||
PLAINS = 3, | |||
ICE = 4, | |||
BEACH = 5, | |||
MUD = 6, | |||
} | |||
const rgb = (r: number, g: number, b: number) => { | |||
return (r << 16) + (g << 8) + b | |||
} | |||
const checkBiomeLandType = (biomePngData: Buffer, x: number, y: number, width: number) => { | |||
const dataIndex = ((y * width) + x) * 4; | |||
switch (rgb( | |||
biomePngData[dataIndex], | |||
biomePngData[dataIndex + 1], | |||
biomePngData[dataIndex + 2], | |||
)) { | |||
case 0xfa9418: | |||
case 0xffbc40: | |||
return LandType.DESERT; | |||
case 0x606060: | |||
case 0xd25f12: | |||
case 0xd94515: | |||
case 0xca8c65: | |||
return LandType.MESA; | |||
case 0x056621: | |||
case 0x0b6659: | |||
case 0x537b09: | |||
case 0x2c4205: | |||
case 0x628b17: | |||
case 0x507050: | |||
case 0x2d8e49: | |||
case 0x8ab33f: | |||
case 0x687942: | |||
return LandType.FOREST; | |||
case 0x596651: | |||
case 0x454f3e: | |||
case 0x338e81: | |||
// return LandType.MUD; | |||
// case 0xfade55: | |||
// return LandType.BEACH; | |||
case 0xffffff: | |||
case 0xa0a0a0: | |||
case 0xfaf0c0: | |||
case 0x597d72: | |||
case 0x202070: | |||
case 0x202038: | |||
case 0x404090: | |||
case 0x163933: | |||
return LandType.ICE; | |||
case 0xfade55: | |||
case 0x8db360: | |||
case 0x07f9b2: | |||
case 0xbdb25f: | |||
case 0xa79d64: | |||
case 0xb5db88: | |||
case 0x000070: | |||
case 0x0000ff: | |||
case 0x0000ac: | |||
case 0x000090: | |||
case 0x000050: | |||
case 0x000040: | |||
default: | |||
break; | |||
} | |||
return LandType.PLAINS; | |||
} | |||
const isMountainBiome = (biomePngData: Buffer, x: number, y: number, width: number) => { | |||
const dataIndex = ((y * width) + x) * 4; | |||
switch (rgb( | |||
(biomePngData[dataIndex]), | |||
(biomePngData[dataIndex + 1]), | |||
(biomePngData[dataIndex + 2]), | |||
)) { | |||
case 0x606060: | |||
case 0xa0a0a0: | |||
case 0x507050: | |||
case 0x338e81: | |||
case 0x597d72: | |||
return true | |||
default: | |||
break | |||
} | |||
return false | |||
} | |||
const isHillBiome = (biomePngData: Buffer, x: number, y: number, width: number) => { | |||
const dataIndex = ((y * width) + x) * 4; | |||
switch (rgb( | |||
(biomePngData[dataIndex]), | |||
(biomePngData[dataIndex + 1]), | |||
(biomePngData[dataIndex + 2]), | |||
)) { | |||
case 0xd25f12: | |||
case 0x163933: | |||
case 0x2c4205: | |||
case 0x454f3e: | |||
case 0x687942: | |||
return true | |||
default: | |||
break | |||
} | |||
return false | |||
} | |||
const isWoodedBiome = (biomePngData: Buffer, x: number, y: number, width: number) => { | |||
const dataIndex = ((y * width) + x) * 4; | |||
switch (rgb( | |||
(biomePngData[dataIndex]), | |||
(biomePngData[dataIndex + 1]), | |||
(biomePngData[dataIndex + 2]), | |||
)) { | |||
case 0x056621: | |||
case 0x537b09: | |||
case 0x596651: | |||
case 0x2d8e49: | |||
return true; | |||
default: | |||
break | |||
} | |||
return false; | |||
} | |||
const index = (x: number, y: number, width: number) => { | |||
return ((y * width) + x) * 4; | |||
} | |||
const landThreshold = 0x80 | |||
const SHALLOW_WATER_THRESHOLD = 0xE0 | |||
const checkNeighboringLands = (waterMaskPngData: Buffer, x: number, y: number, width: number, height: number) => { | |||
const ny = y === 0 ? 0 : y - 1; | |||
const wx = (x + width - 1) % width; | |||
const ex = (x + 1) % width ; | |||
const sy = y === height - 1 ? height - 1 : y + 1; | |||
return { | |||
nw: waterMaskPngData[index(wx, ny, width)] < landThreshold, | |||
n: waterMaskPngData[index(x, ny, width)] < landThreshold, | |||
ne: waterMaskPngData[index(ex, ny, width)] < landThreshold, | |||
w: waterMaskPngData[index(wx, y, width)] < landThreshold, | |||
i: waterMaskPngData[index(x, y, width)], | |||
e: waterMaskPngData[index(ex, y, width)] < landThreshold, | |||
sw: waterMaskPngData[index(wx, sy, width)] < landThreshold, | |||
s: waterMaskPngData[index(x, sy, width)] < landThreshold, | |||
se: waterMaskPngData[index(ex, sy, width)] < landThreshold, | |||
}; | |||
} | |||
const determineBackgroundTileType = (landFlags: ReturnType<typeof checkNeighboringLands>) => { | |||
const islandThreshold = 0xE0; | |||
if (landFlags.i < landThreshold) { | |||
return 1; // 1 | |||
} | |||
if ( | |||
landFlags.i < islandThreshold | |||
&& !( | |||
landFlags.nw | |||
|| landFlags.n | |||
|| landFlags.ne | |||
|| landFlags.w | |||
|| landFlags.e | |||
|| landFlags.sw | |||
|| landFlags.s | |||
|| landFlags.se | |||
) | |||
) { | |||
return 29; // 29 | |||
} | |||
let cursor = 0; | |||
if (landFlags.n) { | |||
cursor += 12; // 12 | |||
if (landFlags.e) { | |||
cursor += 12; // 24 | |||
if (landFlags.s) { | |||
cursor += 14; // 38 | |||
if (landFlags.w) { | |||
cursor -= 10; // 28 | |||
} | |||
} else if (landFlags.w) { | |||
cursor += 16; // 40 | |||
} else if (landFlags.sw) { | |||
cursor += 12; // 36 | |||
} | |||
} else if (landFlags.w) { | |||
cursor += 15; // 27 | |||
if (landFlags.s) { | |||
cursor += 12; // 39 | |||
} else if (landFlags.se) { | |||
cursor += 10; // 37 | |||
} | |||
} else if (landFlags.s) { | |||
cursor += 31; // 43 | |||
} else if (landFlags.se) { | |||
cursor += 8; // 20 | |||
if (landFlags.sw) { | |||
cursor += 12; // 32 | |||
} | |||
} else if (landFlags.sw) { | |||
cursor += 11; // 23 | |||
} | |||
} else if (landFlags.s) { | |||
cursor += 13; // 13 | |||
if (landFlags.e) { | |||
cursor += 12; // 25 | |||
if (landFlags.w) { | |||
cursor += 16; // 41 | |||
} else if (landFlags.nw) { | |||
cursor += 10; // 35 | |||
} | |||
} else if (landFlags.w) { | |||
cursor += 13; // 26 | |||
if (landFlags.ne) { | |||
cursor += 8; // 34 | |||
} | |||
} else if (landFlags.ne) { | |||
cursor += 8; // 21 | |||
if (landFlags.nw) { | |||
cursor += 20; // 33 | |||
} | |||
} else if (landFlags.nw) { | |||
cursor += 9; // 22 | |||
} | |||
} else if (landFlags.w) { | |||
cursor += 14; // 14 | |||
if (landFlags.e) { | |||
cursor += 28; // 42 | |||
} else if (landFlags.se) { | |||
cursor += 4; // 18 | |||
if (landFlags.ne) { | |||
cursor += 13; // 31 | |||
} | |||
} else if (landFlags.ne) { | |||
cursor += 5; // 19 | |||
} | |||
} else if (landFlags.e) { | |||
cursor += 15; // 15 | |||
if (landFlags.nw) { | |||
cursor += 1 // 16 | |||
if (landFlags.sw) { | |||
cursor += 14; // 30 | |||
} | |||
} else if (landFlags.sw) { | |||
cursor += 2; // 17 | |||
} | |||
} else if (landFlags.ne) { | |||
cursor += 2; // 2 | |||
if (landFlags.nw) { | |||
cursor += 4; // 6 | |||
if (landFlags.sw) { | |||
cursor += 42; // 48 | |||
if (landFlags.se) { | |||
cursor -= 4; // 44 | |||
} | |||
} else if (landFlags.se) { | |||
cursor += 39; // 45 | |||
} | |||
} else if (landFlags.se) { | |||
cursor += 7; // 9 | |||
if (landFlags.sw) { | |||
cursor += 37; // 46 | |||
} | |||
} else if (landFlags.sw) { | |||
cursor += 8; // 10 | |||
} | |||
} else if (landFlags.nw) { | |||
cursor += 3; // 3 | |||
if (landFlags.sw) { | |||
cursor += 5; // 8 | |||
if (landFlags.se) { | |||
cursor += 39; // 47 | |||
} | |||
} else if (landFlags.se) { | |||
cursor += 8; // 11 | |||
} | |||
} else if (landFlags.sw) { | |||
cursor += 4; // 4 | |||
if (landFlags.se) { | |||
cursor += 3; // 7 | |||
} | |||
} else if (landFlags.se) { | |||
cursor += 5; // 5 | |||
} | |||
return cursor; | |||
return destPath; | |||
} | |||
const generateWorldData = async (p: Projection, 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)) | |||
); | |||
const mapTypeBuffers = await Promise.all(Object.values(MapType).map(async (t: MapType) => { | |||
return { | |||
mapType: t, | |||
@@ -401,101 +112,73 @@ const generateWorldData = async (p: Projection, size: number, isWidthDimension: | |||
initialItems: [] as number[][], | |||
} | |||
const tileService: TileService = new TileServiceImpl( | |||
data[MapType.BIOME].data, | |||
data[MapType.ELEVATION].data, | |||
data[MapType.WATER_MASK].data, | |||
data[MapType.BATHYMETRY].data | |||
) | |||
let i = 0; | |||
for (let y = 0; y < dy; y += 1) { | |||
worldData.spriteWaterLayer[y] = []; | |||
worldData.spriteBackgroundLayer[y] = []; | |||
worldData.spriteForegroundLayer[y] = []; | |||
for (let x = 0; x < dx; x += 1) { | |||
const isShallowWater = data[MapType.BATHYMETRY].data[i] >= SHALLOW_WATER_THRESHOLD; | |||
worldData.spriteWaterLayer[y][x] = isShallowWater ? WaterType.LIGHT : WaterType.DEFAULT; | |||
worldData.spriteWaterLayer[y][x] = tileService.determineWaterBackgroundTileType(x, y, dx); | |||
const landType = checkBiomeLandType(data[MapType.BIOME].data, x, y, dx); | |||
const tileType = determineBackgroundTileType(checkNeighboringLands(data[MapType.WATER_MASK].data, x, y, dx, dy)); | |||
worldData.spriteBackgroundLayer[y][x] = ((landType * 60) + tileType); | |||
worldData.spriteForegroundLayer[y][x] = 0; | |||
if (tileType === 1) { | |||
const isMountain = data[MapType.ELEVATION].data[i] >= 0x40 || isMountainBiome( | |||
data[MapType.BIOME].data, | |||
x, | |||
y, | |||
dx | |||
); | |||
if (isMountain) { | |||
switch (landType) { | |||
case LandType.DESERT: | |||
case LandType.MESA: | |||
worldData.spriteForegroundLayer[y][x] = 796; | |||
break; | |||
case LandType.ICE: | |||
worldData.spriteForegroundLayer[y][x] = 799; | |||
break; | |||
case LandType.FOREST: | |||
case LandType.PLAINS: | |||
default: | |||
worldData.spriteForegroundLayer[y][x] = 797; | |||
break; | |||
} | |||
} | |||
const isHill = isHillBiome(data[MapType.BIOME].data, x, y, dx); | |||
if (isHill) { | |||
if (landType === LandType.ICE) { | |||
worldData.spriteForegroundLayer[y][x] = 903 | |||
} else { | |||
worldData.spriteForegroundLayer[y][x] = 901 | |||
} | |||
} | |||
const isWooded = isWoodedBiome(data[MapType.BIOME].data, x, y, dx); | |||
if (isWooded) { | |||
if (landType === LandType.ICE) { | |||
worldData.spriteForegroundLayer[y][x] = 906 | |||
} else { | |||
worldData.spriteForegroundLayer[y][x] = 905 | |||
} | |||
} | |||
} | |||
const tileType = tileService.determineBackgroundTileType(x, y, dx, dy); | |||
worldData.spriteBackgroundLayer[y][x] = tileType; | |||
worldData.spriteForegroundLayer[y][x] = tileService.determineForegroundTileType(tileType, x, y, dx); | |||
i += 4; | |||
} | |||
} | |||
await Promise.all(tempResizedFiles.map(f => fs.unlink(f))); | |||
await fs.rmdir('public/generated/resized'); | |||
return worldData; | |||
} | |||
const ensureDirectories = async () => { | |||
await FileSystemServiceImpl.ensureDirectory('public/generated'); | |||
await FileSystemServiceImpl.ensureDirectory('public/generated/base'); | |||
} | |||
const determineDimension = (width: string, height: string) => { | |||
if (width && Number.isFinite(Number(width))) { | |||
return { | |||
size: Number(width), | |||
isWidthDimension: true, | |||
} | |||
} | |||
if (height && Number.isFinite(Number(height))) { | |||
return { | |||
size: Number(height), | |||
isWidthDimension: false, | |||
} | |||
} | |||
throw new Error('Unspecified width or height'); | |||
} | |||
const generateHandler: NextApiHandler = async (req, res) => { | |||
const { | |||
projection = Projection.EQUIRECTANGULAR, | |||
width, | |||
height, | |||
bounds = '-180,90;180,-90', | |||
} = req.query | |||
let isWidthDimension: boolean; | |||
let size: number; | |||
if (width && Number.isFinite(Number(width))) { | |||
size = Number(width); | |||
isWidthDimension = true; | |||
} else if (height && Number.isFinite(Number(height))) { | |||
size = Number(height); | |||
isWidthDimension = false; | |||
} else { | |||
throw new Error('Unspecified width or height'); | |||
} | |||
const { size, isWidthDimension } = determineDimension( | |||
Array.isArray(width) ? width[0] : width, | |||
Array.isArray(height) ? height[0] : height, | |||
); | |||
await FileSystemServiceImpl.ensureDirectory('public/generated'); | |||
await FileSystemServiceImpl.ensureDirectory('public/generated/base'); | |||
await FileSystemServiceImpl.ensureDirectory('public/generated/resized'); | |||
await FileSystemServiceImpl.ensureDirectory('public/worlds'); | |||
await ensureDirectories(); | |||
const baseDataImageUrls = await Promise.all( | |||
Object | |||
.values(MapType) | |||
.map(async (t: MapType) => generateProjectedBaseData(t, projection as Projection)) | |||
); | |||
await Promise.all( | |||
Object | |||
.values(MapType) | |||
.map(async (t: MapType) => resizeBaseData(t, projection as Projection, size, isWidthDimension)) | |||
); | |||
const worldData = await generateWorldData(projection as Projection, size, isWidthDimension); | |||
res.json({ | |||
@@ -35,3 +35,13 @@ export enum WaterType { | |||
LIGHT = 5, | |||
LAVA = 6 | |||
} | |||
export enum LandType { | |||
MESA = 0, | |||
DESERT = 1, | |||
FOREST = 2, | |||
PLAINS = 3, | |||
ICE = 4, | |||
BEACH = 5, | |||
MUD = 6, | |||
} |