From f99a9a45a70a2ceb0af6962af98705778d7a0aab Mon Sep 17 00:00:00 2001 From: IR0NSIGHT Date: Sun, 13 Aug 2023 20:18:56 +0200 Subject: [PATCH] Feature/5 puddles can overflow (#10) * make findClosestDrop start with list instead of single point * small addition to averagePoint test * refactor puddle code, clean * use sensible puddle size and annotate with color (Debugging) * refactor puddle.ts surface collection, broken rn * proper flooding re-achieved * ponds find escape point * refactor findPondOutflow * rivers can overflow puddles trying to reach level 62 and then stop * add seenset to current river+pond generation * add ignoreSet to puddle surface collection that is passed down from river pathing * fix seenSet issue in layer generation * fix backflow into existing ponds? * refactor puddle layer generation trying to ignore existing ponds * add coverage report to jest config * add unit test with z mock * clean up tests that mock dimension * refactor pathToDrop and test * add move-across-flat unit test for river pathToDrop add move-across-flat unit test for river pathToDrop * add more test to pathToDrop * add first simple unit tests for pond escaping * fix river pathing test * allow escaping ponds deeper than 15 blocks (256 new value) * add (still failing) test for river flooding puddles and puddles swalling eachother * finally fixes ponds eating up eachother while flowing upwards * program respects user defined max surface for ponds * prepare unit test for finding escape-to-pond and refactor river findClosesDrop * add river running from pond bottom to escape point * catch undefined pondBottom-to-escape path * remvoe debug loging * remove obsolete function * github workflow for jest test * apply rivers only on non-puddle-points, add dirty fix to embedded ponds having different waterlevels * optimize inital search for starting points. ignore tiles without annotation * add regression test for path-to-escape bug * fix rounding error bug where a point thinks its a pond-bottom but isnt part of that pond surface * apply pond even if no espcae point was found * refactor user parameters to simply quickly prototying on map --- .github/workflows/unittest.yml | 25 +++ jest.config.js | 4 + package.json | 2 +- src/SeenSet.ts | 3 + src/__mocks__/SeenSet.ts | 19 ++ src/applyRiver.ts | 26 ++- src/global.d.ts | 9 +- src/header.js | 61 +++-- src/index.ts | 104 ++++++--- src/pathing/postprocessing.test.ts | 25 +++ src/pathing/postprocessing.ts | 20 ++ src/pathing/river.test.ts | 270 +++++++++++++++++++++++ src/pathing/river.ts | 176 +++++++++++++++ src/pathing/river_testIfDownhill.test.ts | 19 ++ src/puddle.ts | 179 ++++++++++----- src/river.test.ts | 31 --- src/river.ts | 135 ------------ src/terrain.ts | 14 +- tsconfig.json | 3 +- 19 files changed, 815 insertions(+), 310 deletions(-) create mode 100644 .github/workflows/unittest.yml create mode 100644 src/__mocks__/SeenSet.ts create mode 100644 src/pathing/postprocessing.test.ts create mode 100644 src/pathing/postprocessing.ts create mode 100644 src/pathing/river.test.ts create mode 100644 src/pathing/river.ts create mode 100644 src/pathing/river_testIfDownhill.test.ts delete mode 100644 src/river.test.ts delete mode 100644 src/river.ts diff --git a/.github/workflows/unittest.yml b/.github/workflows/unittest.yml new file mode 100644 index 0000000..4a1bc05 --- /dev/null +++ b/.github/workflows/unittest.yml @@ -0,0 +1,25 @@ +name: Unit Test + +on: push + +permissions: + contents: write + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Install Node.js and NPM + uses: actions/setup-node@v3 + with: + node-version: "latest" + + - name: "install dependencies" + run: npm ci + + - name: full build and deploy + run: npm run test + diff --git a/jest.config.js b/jest.config.js index b89e2f2..9699f8f 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,5 +1,9 @@ module.exports = { preset: 'ts-jest', + // verbose: true, testEnvironment: 'node', // ... other Jest configuration options + collectCoverage: true, + coverageDirectory: 'coverage', + coverageReporters: ["json", "html"], }; \ No newline at end of file diff --git a/package.json b/package.json index 3e5c3a7..d34bd2c 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "", "main": "Puddler.js", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", + "test": "npx jest src/", "compile": "tsc -p ./tsconfig.json", "insertHeader": "bash ./shellscripts/insertHeader.sh", "deploy": "npm run build && npm run insertHeader src/header.js dist/Puddler.js", diff --git a/src/SeenSet.ts b/src/SeenSet.ts index abd5dd1..3b9bdf7 100644 --- a/src/SeenSet.ts +++ b/src/SeenSet.ts @@ -6,6 +6,9 @@ export type SeenSet = { hasNot: (p: Point) => boolean; }; +export type SeenSetReadOnly = Omit + + export const makeSet = (): SeenSet => { //@ts-ignore const seenSet: any = new java.util.HashSet(); diff --git a/src/__mocks__/SeenSet.ts b/src/__mocks__/SeenSet.ts new file mode 100644 index 0000000..4680799 --- /dev/null +++ b/src/__mocks__/SeenSet.ts @@ -0,0 +1,19 @@ +import { point as Point } from "../point"; + +export type SeenSet = { + add: (p: Point) => void; + has: (p: Point) => boolean; + hasNot: (p: Point) => boolean; +}; + +export type SeenSetReadOnly = Omit + + +export const makeSet = (): SeenSet => { + const set = new Set(); + return { + add: (point) => set.add(JSON.stringify(point)), + has: (point) => set.has(JSON.stringify(point)), + hasNot: (point) => !set.has(JSON.stringify(point)) + } +}; diff --git a/src/applyRiver.ts b/src/applyRiver.ts index cd9b82e..33c00e3 100644 --- a/src/applyRiver.ts +++ b/src/applyRiver.ts @@ -1,25 +1,31 @@ import {point} from "./point"; -import {minFilter} from "./river"; import {isWater, setWaterLevel, setZ} from "./terrain"; +import {minFilter} from "./pathing/postprocessing"; +import {Puddle} from "./puddle"; +import {SeenSetReadOnly} from "./SeenSet"; + export type RiverExportTarget = { - waterlevel: number|undefined, - terrainDepth: number|undefined, - annotationColor: AnnotationLayer|undefined, + annotationColor: AnnotationLayer | undefined, applyRivers: boolean } type AnnotationLayer = number; -export const applyRiverToTerrain = (river: point[], target: RiverExportTarget): void => { - river +export const applyRiverToTerrain = (waterSystem: { + river: point[], + ponds: Puddle[], +}, target: RiverExportTarget, globalPondSurface: SeenSetReadOnly): void => { + + waterSystem.river + .filter(globalPondSurface.hasNot) //river point is not part of a pond .filter((a) => !isWater(a)) .map(minFilter) .forEach((a) => { - if (target.terrainDepth !== undefined && target.applyRivers) - setZ(a.point, a.z - target.terrainDepth) + if ( target.applyRivers) + setZ(a.point, a.z - 1) - if (target.waterlevel !== undefined && target.applyRivers) - setWaterLevel(a.point, a.z - target.waterlevel) + if (target.applyRivers) + setWaterLevel(a.point, a.z ) if (target.annotationColor !== undefined) dimension.setLayerValueAt(org.pepsoft.worldpainter.layers.Annotations.INSTANCE, a.point.x, a.point.y, target.annotationColor); diff --git a/src/global.d.ts b/src/global.d.ts index 15f98c6..fbbaa6d 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -5,14 +5,11 @@ declare const dimension: import('./worldpainterStubs/typescript/Dimension').Dime declare const org: any; //org.pepsoft java package declare const params: { maxSurface: number; - minDepth: number; minRiverLength: number; blocksPerRiver: number; floodPuddles: boolean; applyRivers: boolean; - exportRiverToAnnotation: number; - exportRiverWaterDepth: number; - exportRiverTerrainDepth: number; - - exportPuddleToAnnotation: number; + annotateAll: boolean; + waterLevel: number; }; + diff --git a/src/header.js b/src/header.js index 238fe2a..7db9e9d 100644 --- a/src/header.js +++ b/src/header.js @@ -3,52 +3,43 @@ //script.param.maxSurface.type=integer //script.param.maxSurface.description=maximum surface area of ponds //script.param.maxSurface.optional=false -//script.param.maxSurface.default=500 - -//script.param.minDepth.type=integer -//script.param.minDepth.description=minimal depth of ponds -//script.param.minDepth.optional=false -//script.param.minDepth.default=2 +//script.param.maxSurface.default=10000 +//script.param.maxSurface.displayName=Max Puddle Surface //script.param.minRiverLength.type=integer -//script.param.minRiverLength.description=minimal length of river +//script.param.minRiverLength.description=minimal length of river in blocks //script.param.minRiverLength.optional=false //script.param.minRiverLength.default=50 +//script.param.minRiverLength.displayName=Minimal River Length //script.param.blocksPerRiver.type=integer -//script.param.blocksPerRiver.description=one river every x blocks +//script.param.blocksPerRiver.description=spawn one river every x blocks //script.param.blocksPerRiver.optional=false -//script.param.blocksPerRiver.default=100 +//script.param.blocksPerRiver.default=1000 +//script.param.blocksPerRiver.displayName=Spawn Probability //script.param.floodPuddles.type=boolean -//script.param.floodPuddles.description=generate puddles on map. +//script.param.floodPuddles.description=flood ponds with water on map. //script.param.floodPuddles.optional=false -//script.param.floodPuddles.default=true +//script.param.floodPuddles.default=false +//script.param.floodPuddles.displayName=Flood Puddles + //script.param.applyRivers.type=boolean -//script.param.applyRivers.description=generate rivers on map. +//script.param.applyRivers.description=generate rivers as water on map. //script.param.applyRivers.optional=false -//script.param.applyRivers.default=true - -//script.param.exportRiverToAnnotation.type=integer -//script.param.exportRiverToAnnotation.description=Annotation color to export rivers to. -1 to disable. -//script.param.exportRiverToAnnotation.optional=false -//script.param.exportRiverToAnnotation.default=-1 - -//script.param.exportPuddleToAnnotation.type=integer -//script.param.exportPuddleToAnnotation.description=Annotation color to export puddles to. -1 to disable. -//script.param.exportPuddleToAnnotation.optional=false -//script.param.exportPuddleToAnnotation.default=-1 - - - -//script.param.exportRiverWaterDepth.type=integer -//script.param.exportRiverWaterDepth.description=Depth below original terrain level to export waterlevel to. -1 to disable. -//script.param.exportRiverWaterDepth.optional=false -//script.param.exportRiverWaterDepth.default=0 - -//script.param.exportRiverTerrainDepth.type=integer -//script.param.exportRiverTerrainDepth.description=Depth below original terrain level to export river bottom to. Should be higher than waterDepth to have effect -1 to disable. -//script.param.exportRiverTerrainDepth.optional=false -//script.param.exportRiverTerrainDepth.default=1 +//script.param.applyRivers.default=false +//script.param.applyRivers.displayName=Apply Rivers + +//script.param.annotateAll.type=boolean +//script.param.annotateAll.description=use annotations instead of water for river and puddle +//script.param.annotateAll.optional=false +//script.param.annotateAll.default=true +//script.param.annotateAll.displayName=Apply as Annotations + +//script.param.waterLevel.type=integer +//script.param.waterLevel.description=Water level of ocean. rivers will stop if they fall below that level +//script.param.waterLevel.optional=false +//script.param.waterLevel.default=62 +//script.param.waterLevel.displayName=Ocean Water Level diff --git a/src/index.ts b/src/index.ts index a8f3268..45202a8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,24 +3,21 @@ import {timer} from "./Timer"; import {applyRiverToTerrain, RiverExportTarget} from "./applyRiver"; import {log} from "./log"; import {mapDimensions, point} from "./point"; -import {capRiverWithPond, PuddleExportTarget} from "./puddle"; -import {capRiverStart, pathRiverFrom} from "./river"; +import {applyPuddleToMap, Puddle, PuddleExportTarget} from "./puddle"; +import {annotationColor, capRiverStart, pathRiverFrom} from "./pathing/river"; + const main = () => { const { maxSurface, - minDepth, minRiverLength, blocksPerRiver, floodPuddles, applyRivers, - exportRiverToAnnotation, - exportRiverWaterDepth, - exportRiverTerrainDepth, - exportPuddleToAnnotation + annotateAll } = params; - if (!floodPuddles && !applyRivers && exportRiverToAnnotation < 0 && exportPuddleToAnnotation < 0) { + if (!floodPuddles && !applyRivers && !annotateAll) { log("ERROR: the script will have NO EFFECT with the current settings!\nmust make/annotate puddle and/or river for script to have any effect."); return; } @@ -36,15 +33,41 @@ const main = () => { return dimension.getLayerValueAt(annotations, p.x, p.y) == 9; }; - for (let x = dims.start.x; x < dims.end.x; x++) { - for (let y = dims.start.y; y < dims.end.y; y++) { - const point = { x: x, y: y }; - if (isCyanAnnotated(point)) { - startPoints.push(point); - } + type Tile = any + const TILE_SIZE_BITS = 7; + const SHIFT_AMOUNT = 1 << TILE_SIZE_BITS; // Equivalent to 128 + + //collect all tiles + const tiles: Tile[] = [] + for (let x = dims.start.x>>TILE_SIZE_BITS; x < dims.end.x>>TILE_SIZE_BITS; x++) { + for (let y = dims.start.y>>TILE_SIZE_BITS; y < dims.end.y>>TILE_SIZE_BITS; y++) { + tiles.push(dimension.getTile(x, y)) } } + + const annotatedTiles = tiles.filter((t) => t.hasLayer(annotations)) + .map(tile => { + const start: point = {x: (tile.getX() << TILE_SIZE_BITS), y: (tile.y << TILE_SIZE_BITS)}; + return { + start: start, + end: {x: start.x + SHIFT_AMOUNT, y: start.y + SHIFT_AMOUNT}, + } + }) + log("annotated tiles: " + annotatedTiles.length); + annotatedTiles.forEach((tile) => { + for (let x = tile.start.x; x < tile.end.x; x++) { + for (let y = tile.start.y; y < tile.end.y; y++) { + const point = {x: x, y: y}; + if (isCyanAnnotated(point)) { + startPoints.push(point); + } + } + } + } + ); + + const passRandom = (p: point, chance: number): boolean => { const seed = p.x * p.y + p.x; //@ts-ignore @@ -60,34 +83,63 @@ const main = () => { log("total possible starts: " + startPoints.length); const filter = (p: point) => passRandom(p, 1 / blocksPerRiver); let rivers = startPoints.filter(filter).map((start) => { - return pathRiverFrom(start, allRiverPoints); + return pathRiverFrom(start, allRiverPoints, { maxSurface: maxSurface}); }); const exportTargetPuddle: PuddleExportTarget = { - annotationColor: (params.exportPuddleToAnnotation < 0) ? undefined : params.exportPuddleToAnnotation, + annotationColor: !annotateAll ? undefined : annotationColor.PURPLE, flood: floodPuddles, } const exportTargetRiver: RiverExportTarget = { - annotationColor: (params.exportRiverToAnnotation < 0) ? undefined : params.exportRiverToAnnotation, - terrainDepth: (params.exportRiverTerrainDepth < 0) ? undefined : params.exportRiverTerrainDepth, - waterlevel: (params.exportRiverWaterDepth < 0) ? undefined : params.exportRiverWaterDepth, + annotationColor: !annotateAll ? undefined : annotationColor.ORANGE, applyRivers: applyRivers } const longRivers = rivers - .map((a) => capRiverStart(a, 10)) - .filter((r) => r.length > minRiverLength); + .map((a) => ({ + ...a, + river: capRiverStart(a.river, 10) + })) + .filter((r) => r.river.length > minRiverLength) + log("export target river: " + JSON.stringify(exportTargetRiver)); - longRivers.forEach(r => applyRiverToTerrain(r, exportTargetRiver)); + log("export target puddle: " + JSON.stringify(exportTargetPuddle)); - rivers.forEach((riverPath) => { - capRiverWithPond(riverPath, maxSurface, minDepth, exportTargetPuddle); - }); + + const globalPonds = makeSet(); + longRivers.forEach( + r => r.ponds.forEach + (p => p.pondSurface.forEach(globalPonds.add))); + + longRivers.forEach(r => applyRiverToTerrain(r, exportTargetRiver, globalPonds)); + //longRivers.forEach(r => r.ponds.forEach(p => applyPuddleToMap(p.pondSurface, p.waterLevel, exportTargetPuddle))) + + let allPonds: Puddle[] = []; + longRivers.map(r => r.ponds) + .forEach(p => allPonds.push(...p)) + allPonds.sort((a, b) => b.pondSurface.length - a.pondSurface.length) + + const processedPondSurface = makeSet() + for (let pond of allPonds) { + //find out if this pond is embedded in another pond + let embeddedPond = false; + for (let surfacePoint of pond.pondSurface) { + if (processedPondSurface.has(surfacePoint)) { + embeddedPond = true + break; + } + } + if (embeddedPond) + continue; + + pond.pondSurface.forEach(processedPondSurface.add) + applyPuddleToMap(pond.pondSurface, pond.waterLevel, exportTargetPuddle) + } let totalLength = 0; - longRivers.forEach((r) => (totalLength += r.length)); + longRivers.forEach((r) => (totalLength += r.river.length)); //collect puddles log( "script too =" + diff --git a/src/pathing/postprocessing.test.ts b/src/pathing/postprocessing.test.ts new file mode 100644 index 0000000..ea92776 --- /dev/null +++ b/src/pathing/postprocessing.test.ts @@ -0,0 +1,25 @@ +// Your tests that use the getZ function +import {minFilter} from "./postprocessing"; +import {point} from "../point"; + +describe('min filter', () => { + beforeAll(() => { + (global as any).dimension = { + getLowestX: () => 0, + getLowestY: () => 0, + getHighestX: () => 10, + getHighestY: () => 10, + getHeightAt: (x: number, y: number) => x + } + }) + + afterAll(() => { + (global as any).dimension = undefined; + }); + + test('min filter returns min z neighbour', () => { + const point: point = {x: 5, y: 5}; + const minNeighbour = minFilter(point) + expect(minNeighbour.z).toEqual(4); + }) +}) diff --git a/src/pathing/postprocessing.ts b/src/pathing/postprocessing.ts new file mode 100644 index 0000000..d3763fe --- /dev/null +++ b/src/pathing/postprocessing.ts @@ -0,0 +1,20 @@ +import {getNeighbourPoints, point} from "../point"; +import {getZ} from "../terrain"; + +/** + * will get the z of the lowest neighbour of the riverpoint (which is the point after the point in the river) + * @param p + * @param i + * @param river point array that flows from high to low + * @returns river with z values + */ +export const minFilter = ( + p: point, +): { point: point; z: number } => { + const neiZs = getNeighbourPoints(p).map((a) => getZ(a, true)); + const minNeighbourNonRiver = Math.min.apply(Math, neiZs); + return { + point: p, + z: minNeighbourNonRiver, + }; +}; \ No newline at end of file diff --git a/src/pathing/river.test.ts b/src/pathing/river.test.ts new file mode 100644 index 0000000..d8c1304 --- /dev/null +++ b/src/pathing/river.test.ts @@ -0,0 +1,270 @@ +import {averagePoint, findClosestDrop, insertInSortedQueue, pathRiverFrom, squaredDistanceBetweenPoints} from "./river"; +import {parentedPoint, point} from "../point"; +import {makeSet} from '../SeenSet'; +import {getZ} from "../terrain"; +import {findPondOutflow} from "../puddle"; + +// Replace the original module with the mock implementation +jest.mock('../SeenSet'); + +describe('helper function river path', () => { + test("distance calculation easy", () => { + const pointA = {x: 0, y: 0}; + const pointB = {x: 0, y: 10}; + expect(squaredDistanceBetweenPoints(pointA, pointB)).toBe(10 * 10); + }) + + test("distance calculation easy", () => { + const pointA = {x: 10, y: 10}; + const pointB = {x: 20, y: 20}; + expect(squaredDistanceBetweenPoints(pointA, pointB)).toBe(10 * 10 + 10*10); + }) + + test("sorted queue", () => { + const pointA = {point: {x: 0, y: 0}, parent: undefined, distance: 0}; + const pointB = {point: {x: 0, y: 0}, parent: undefined, distance: 50}; + const pointC = {point: {x: 0, y: 0}, parent: undefined, distance: 100}; + + const queue: parentedPoint[] = []; + insertInSortedQueue(queue, pointC); + expect(queue).toEqual([pointC]); + insertInSortedQueue(queue, pointA); + expect(queue).toEqual([pointA, pointC]); + insertInSortedQueue(queue, pointB); + expect(queue).toEqual([pointA, pointB, pointC]); + }) + + test("average point", () => { + const pointA = {x: 0, y: 0}; + const pointB = {x: 10, y: 10}; + const pointC = {x: 20, y: -10}; + + expect(averagePoint([pointA])).toEqual(pointA); + const avg = averagePoint([pointA, pointB]); + expect(avg).toEqual({x: 5, y: 5}); + const avg1 = averagePoint([pointA, pointB, pointC]); + expect(avg1).toEqual({x: 10, y: 0}) + }) + + +}); + +describe("river pathing", () => { + beforeEach(() => { + (global as any).dimension = { + getLowestX: () => 0, + getLowestY: () => 0, + getHighestX: () => 1, //chunk, times 128 + getHighestY: () => 1, + getHeightAt: (x: number, y: number) => x, + getWaterLevelAt: (x: number, y: number) => -1, + setWaterLevelAt: (x: number, y: number, waterLevel: number) => { + } + }; + (global as any).print = (s: string) => console.log(s); + (global as any).params = { + waterLevel: 62, + } + }) + + afterEach(() => { + (global as any).dimension = undefined; + (global as any).print = undefined; + }); + + test("path to drop follows easy downhill", () => { + const path = findClosestDrop({x: 5, y: 5}, getZ({x: 5, y: 5})); + expect(path).toBeDefined() + expect(path![0].point).toEqual({x: 4, y: 5}) + expect(path!.length).toBe(1) + }) + + test("path to drop can cross flat area", () => { + //mock: area is flat, at (2,5) is a drop + (global as any).dimension.getHeightAt = (x: number, y: number) => { + return (x == 2 && y == 5) ? 0 : 42 + } + const path = findClosestDrop({x: 5, y: 5}, getZ({x: 5, y: 5})); + expect(path).toBeDefined() + expect(path!.length ).toEqual(3) + expect(path![path!.length - 1].point).toEqual({x: 2, y: 5}) + }) + + test("path to drop can fail if no drop", () => { + //mock: area is flat, starts in drop + expect((global as any).dimension.getHeightAt).toBeDefined(); + (global as any).dimension.getHeightAt = (x: number, y: number) => { + return (x == 5 && y == 5) ? 0 : 42 + } + const path = findClosestDrop({x: 5, y: 5}, getZ({x: 5, y: 5})); + expect(path).toBeUndefined(); + }) + + test("path to drop can not walk uphill to drop", () => { + //mock: area is flat, starts in drop + (global as any).dimension.getHeightAt = (x: number, y: number) => { + if (x == 5 && y == 5) return 10; + if (x == 0 && y == 0) return 0; //existing drop but not reachable without going uphill + return 42 + } + const path = findClosestDrop({x: 5, y: 5}, getZ({x: 5, y: 5})); + expect(path).toBeUndefined(); + }) + + test("river paths downhill and stops at waterlevel", () => { + (global as any).dimension.getHeightAt = (x: number, y: number) => { + if (x == 0) + return 61; //terrain is one below waterlevel => is ocean + return x + 62 + } + (global as any).params.waterLevel = 62; + const {river, ponds} = pathRiverFrom({x: 5, y: 5}, makeSet(), {maxSurface: 1000000}) + expect(river).toBeDefined() + expect(river).toEqual([ + {"x": 5, "y": 5}, + {"x": 4, "y": 5}, + {"x": 3, "y": 5}, + {"x": 2, "y": 5}, + {"x": 1, "y": 5}, + {"x": 0, "y": 5}]) + expect(ponds.length).toEqual(0) + }) + + test("findPondOutflow escapes simple pond", () => { + //mock: area is flat, starts in drop + (global as any).dimension.getHeightAt = (x: number, y: number) => { + if (x == 5 && (y == 5 ||y == 6)) return 100; + if (x == 0 && y == 5) return 0; //existing drop but not reachable without going uphill + return 110 + } + const start = {x: 5, y: 5}; + + const { pondSurface, waterLevel, depth, escapePoint} = findPondOutflow([start], 1000000, makeSet()) + expect(pondSurface.length).toEqual(2) + expect(waterLevel).toEqual(110) + expect(depth).toEqual(10) + expect(escapePoint).toEqual({x: 0, y: 5}) + }) + + test("findPondOutflow escapes deep pond", () => { + //mock: area is flat, starts in drop + (global as any).dimension.getHeightAt = (x: number, y: number) => { + if (x == 5 && (y == 5 ||y == 6)) return 100; + if (x == 0 && y == 5) return 0; //existing drop but not reachable without going uphill + return 200 + } + const start = {x: 5, y: 5}; + + const { pondSurface, waterLevel, depth, escapePoint} = findPondOutflow([start], 1000000, makeSet()) + expect(pondSurface.length).toEqual(2) + expect(waterLevel).toEqual(200) + expect(depth).toEqual(100) + expect(escapePoint).toEqual({x: 0, y: 5}) + }) + + test("findPondOutflow doesnt walk uphill", () => { + //mock: area is flat, starts in drop + (global as any).dimension.getHeightAt = (x: number, y: number) => { + if (x == 5 && (y == 5 ||y == 6)) return 100; //first and start pond + if (x == 5 && (y == 1 ||y == 2)) return 100; //second pond to traverse + if (x == 0 && y == 5) return 0; //existing drop but not reachable without going uphill + return 110 + } + const start = {x: 5, y: 5}; + + const {pondSurface, waterLevel, depth, escapePoint} = findPondOutflow([start], 1000000, makeSet()) + expect(pondSurface.length).toEqual(2) + expect(waterLevel).toEqual(110) + expect(depth).toEqual(10) + expect(escapePoint).toEqual({x: 5, y: 2}) + }) + + + test("river escapes pond twice, second pond swallows first pond", () => { + //mock: area is flat, starts in drop + (global as any).dimension.getHeightAt = (x: number, y: number) => { + if (x == 1 && y == 1) return 100; //first and start pond + if (x == 3 && y == 3) return 100; //second pond + if (x == 8 && y == 8) return 0; //final pond + + if (x >= 5 || y >= 5) return 200; //higher land everywhere except 0..5 => 0..5 will be the swallowing pond + return 110 + } + const start = {x: 0, y: 0}; + + const {river, ponds} = pathRiverFrom(start, makeSet(), {maxSurface: 1000000}) + expect(river).toBeDefined() + expect(river[0]).toEqual(start) + expect(river[river.length - 1]).toEqual({x: 8, y: 8}) + expect(ponds.length).toEqual(2) + expect(ponds[0].pondSurface).toEqual([{x: 1, y: 1}]) + expect(ponds[0].escapePoint).toEqual({x: 3, y: 3}) + + expect(ponds[1].pondSurface[0]).toEqual({x: 3, y: 3}) + expect(ponds[1].escapePoint).toEqual({x: 8, y: 8}) + + const comparePoints = (a: point, b: point): number => { + if (a.x !== b.x) { + return a.x - b.x; + } + + return a.y - b.y; + }; + const pondIdeal: point[] = [] + for (let x = 0; x < 5; x++) + for (let y = 0; y < 5; y++) + pondIdeal.push({x: x, y: y}) + + expect(pondIdeal.sort(comparePoints)).toEqual(ponds[1].pondSurface.sort(comparePoints)) + + }) + + test("river escapes pond and connects escape to pond", () => { + //mock: area is flat, starts in drop + (global as any).dimension.getHeightAt = (x: number, y: number) => { + if (x == 5 && y == 5) return 100; + if (x == 0 && y == 5) return 0; //dropout + return 110 + } + const start = {x: 8, y: 5}; + + const {river, ponds} = pathRiverFrom(start, makeSet(), {maxSurface: 1000000}) + expect(river).toEqual([ + {x: 8, y: 5}, + {x: 7, y: 5}, + {x: 6, y: 5}, + {x: 5, y: 5}, //pond bottom + {x: 4, y: 5}, + {x: 3, y: 5}, + {x: 2, y: 5}, + {x: 1, y: 5}, + {x: 0, y: 5} //escape point + ]) + }) + + test("regression: river was unable to path to escape point", () => { + //build after a real life map where i encountered the bug + //mock: area is flat, starts in drop + (global as any).dimension.getHeightAt = (x: number, y: number) => { + if (x == 10 && y == 5) return 88; + if (x <= 5) return 88; //dropout + return 89 + } + const start = {x: 15, y: 5}; + + const {river, ponds} = pathRiverFrom(start, makeSet(), {maxSurface: 1000000}) + expect(river).toEqual([ + {x: 15, y: 5}, + {x: 14, y: 5}, + {x: 13, y: 5}, + {x: 12, y: 5}, + {x: 11, y: 5}, + {x: 10, y: 5}, //pond bottom + {x: 9, y: 5}, + {x: 8, y: 5}, + {x: 7, y: 5}, + {x: 6, y: 5}, + {x: 5, y: 5}, //escape point + ]) + }) +}) \ No newline at end of file diff --git a/src/pathing/river.ts b/src/pathing/river.ts new file mode 100644 index 0000000..3e84df0 --- /dev/null +++ b/src/pathing/river.ts @@ -0,0 +1,176 @@ +import {makeSet, SeenSet} from "../SeenSet"; +import {addPoints, getNeighbourPoints, parentedPoint, parentedToList, point,} from "../point"; +import {getZ, isWater, markPos} from "../terrain"; +import {annotateAll, findPondOutflow, PondGenerationParams} from "../puddle"; +import {log} from "../log"; + +export const testIfDownhill = (path: point[]) => { + for (let i = 0; i < path.length - 1; i++) { + const current = getZ(path[i], true); + const next = getZ(path[i + 1], true) + if (next > current) + return false; + } + return true; +} +export const annotationColor = { + PURPLE: 10, + ORANGE: 2, + YELLOW: 5, + RED: 14 +}; + +/** + * start a new river path at this position + * @param pos + * @param rivers + * @param pondParams + */ +export const pathRiverFrom = (pos: point, rivers: SeenSet, pondParams: PondGenerationParams): {river: point[], ponds: {pondSurface: point[], waterLevel: number, depth: number, escapePoint: point | undefined}[] } => { + const path: parentedPoint[] = [{point: pos, parent: undefined, distance: -1}]; + let safetyIt = 0; + let current = pos; + let riverMerged = false; + const thisRiverSet = makeSet(); + const puddleDebugSet = makeSet(); + const ponds = []; + + while (safetyIt < 1000) { + safetyIt++; + if (getZ(current) < params.waterLevel) //base water level reached + break; + + let pathToDrop = findClosestDrop(current, getZ(current)); + + if (pathToDrop == undefined) { + //abort if closestDrop coulndt find anything + const pond = findPondOutflow([current], pondParams.maxSurface, puddleDebugSet) + //applyPuddleToMap(pond.pondSurface, pond.waterLevel, {annotationColor: undefined, flood: true}); + ponds.push(pond); + if (pond.escapePoint !== undefined) { + pond.pondSurface.forEach(puddleDebugSet.add); + + const escapePoint: parentedPoint = {point: pond.escapePoint!, parent: path[path.length - 1], distance: -1} + + const thisPond = makeSet(); + pond.pondSurface.forEach(thisPond.add); + //connect pond to escape + const pathEscapeToPond = findClosestDrop( + escapePoint.point, + pond.waterLevel, + (p) => thisPond.has(p), //we found a connection to the pond surface! + (p: point) => getZ(p, true) <= pond.waterLevel + ) + if (pathEscapeToPond == undefined) { + log("ERROR: couldnt find path to escape point: " + JSON.stringify(escapePoint.point)) + break; + } + pathToDrop = pathEscapeToPond.reverse(); + pathToDrop.shift(); //remove connectionpoint on pond surface + pathToDrop.push(escapePoint); + } else { + break; + } + } + + + + //add found path to riverpoint list, until water/river is reached + for (let point of pathToDrop) { + if (rivers.has(point.point)) { + riverMerged = true; + break; + } + + if (isWater(point.point)) { + break; + } + path.push(point); + thisRiverSet.add(point.point); + // rivers.add(point.point); + } + if (riverMerged) break; + //end of path is droppoint + current = pathToDrop[pathToDrop.length - 1].point; + } + path.forEach((p) => rivers.add(p.point)); + return { river: path.map((a) => a.point), ponds: ponds}; +}; + + +export const squaredDistanceBetweenPoints = (a: point, b: point) => { + const diff = {x: a.x-b.x, y: a.y-b.y}; + return diff.x*diff.x + diff.y*diff.y; +} + + +export const insertInSortedQueue = (sortedQueue: parentedPoint[], point: parentedPoint): void => { + let i = 0; + for (let iteratorPoint of sortedQueue) { + if (iteratorPoint.distance > point.distance) break; + i++; + } + sortedQueue.splice(i, 0, point); +} + + +export const averagePoint = (points: point[]): point => { + const sum = points.reduce((a, b) => addPoints(a, b), {x: 0, y: 0}); + return {x: sum.x / points.length, y: sum.y / points.length}; +} + +/** + * find the point closest to pos thats at least one block lower + * @param startingPoint + * @param posZ + * @param isDrop + * @param isValidNeighbour + * @returns path to this point with drop being the last + */ +export function findClosestDrop( + startingPoint: point, + posZ: number, + isDrop: (p: point) => boolean = (p) => getZ(next.point, true) < Math.round(posZ), + isValidNeighbour: (p: point) => boolean = (p) => getZ(p, true) <= posZ +): parentedPoint[]|undefined { + const seenSet: SeenSet = makeSet(); + + const queue: parentedPoint[] = []; + queue.push({ point: startingPoint, parent: undefined, distance: 0 }) + seenSet.add(startingPoint); + let next: parentedPoint; + let safetyIterator = 0; + let searchCenter: point = startingPoint + + while (queue.length != 0 && safetyIterator < 50000) { + next = queue.shift() as parentedPoint; + if (isDrop(next.point)) { + const path = parentedToList(next, []).reverse(); + //path starts with startingPoint, which is not wanted + path.shift(); + return path; + } + + let neighbours = getNeighbourPoints(next.point).filter(seenSet.hasNot); + const trueLowerNeighbours = neighbours.filter(n => getZ(n, true) < Math.round(posZ)); + if (trueLowerNeighbours.length == 0) { + //no lower neighbour, river is in flat area => sort by tinyest height difference + neighbours = neighbours.sort((a, b) => { + const aZ = getZ(a, false); + const bZ = getZ(b, false); + return aZ - bZ; + }); + } + + neighbours.filter(isValidNeighbour).forEach((n) => { + seenSet.add(n); + insertInSortedQueue(queue, {point: n, parent: next, distance: squaredDistanceBetweenPoints(n, searchCenter)}); + }); + safetyIterator++; + } + return undefined; +} + +export const capRiverStart = (river: point[], slice: number) => { + return river.slice(slice); +}; diff --git a/src/pathing/river_testIfDownhill.test.ts b/src/pathing/river_testIfDownhill.test.ts new file mode 100644 index 0000000..251ef71 --- /dev/null +++ b/src/pathing/river_testIfDownhill.test.ts @@ -0,0 +1,19 @@ +// Your tests that use the getZ function +import * as terrainModule from '../terrain'; +import {point} from "../point"; +import {testIfDownhill} from "./river"; + +jest.mock('../terrain'); +const mockedGetZ = jest.fn((pos: point, floor: boolean) => { + return pos.x; +}); +(terrainModule as any).getZ = mockedGetZ; + +test('river-never-running-uphill assertion works', () => { + + + const riverPath: point[] = [{x: 3, y: -1},{x: 3, y: -1},{x: 2, y: -1},{x: 1, y: -1}]; + expect(testIfDownhill(riverPath)).toEqual(true); + riverPath.push({x:2, y: -1}) + expect(testIfDownhill(riverPath)).toEqual(false); +}); diff --git a/src/puddle.ts b/src/puddle.ts index 8f555bb..81ca582 100644 --- a/src/puddle.ts +++ b/src/puddle.ts @@ -1,31 +1,51 @@ import {makeQueue, queue} from "./PointQueue"; -import {makeSet, SeenSet} from "./SeenSet"; +import {makeSet, SeenSetReadOnly} from "./SeenSet"; import {getNeighbourPoints, point,} from "./point"; -import {floodToLevel, getZ, isWater} from "./terrain"; +import {floodToLevel, getZ, markPos} from "./terrain"; +import {log} from "./log"; +import {annotationColor} from "./pathing/river"; export type PuddleExportTarget = { flood: boolean, - annotationColor: number|undefined + annotationColor: number | undefined } -export const capRiverWithPond = (river: point[], maxSurface: number, minDepth: number, target: PuddleExportTarget) => { - if (river.length > 0) { - const riverEnd = river[river.length - 1]; - if (!isWater(riverEnd)) { - const layers = collectPuddleLayers(riverEnd, 6, maxSurface); - if (layers.length < minDepth) return; - const bottomZ = getZ(riverEnd, true); - layers.forEach((l: point[], idx: number) => { - if (target.flood) - floodToLevel(l, bottomZ + layers.length - 1); - - if (target.annotationColor !== undefined) { - l.forEach((p: point) => { - dimension.setLayerValueAt(org.pepsoft.worldpainter.layers.Annotations.INSTANCE, p.x, p.y, target.annotationColor!); - }); - } - }); - } - } + +export type Puddle = { pondSurface: point[], waterLevel: number, depth: number, escapePoint: point | undefined } + +export const annotateAll = (points: point[], annotationColor: number) => { + points.forEach((p: point) => { + dimension.setLayerValueAt(org.pepsoft.worldpainter.layers.Annotations.INSTANCE, p.x, p.y, annotationColor); + }) +} + +export const applyPuddleToMap = (puddleSurface: point[], waterLevel: number, target: PuddleExportTarget) => { + if (target.flood) + floodToLevel(puddleSurface, waterLevel); + + if (target.annotationColor !== undefined) + annotateAll(puddleSurface, target.annotationColor!) + +} + +/** + * + * @param startPos starting positions for search. index zero will be considered bottom height of pond. should all be same height + * @param maxSurface + * @param ignoreAsEscape ignore these points trying to escape. will still be part of surface. + * @returns true if river is finishing in pond or existing waterbody + */ +export const findPondOutflow = (startPos: point[], maxSurface: number, ignoreAsEscape: SeenSetReadOnly): Puddle => { + const {layers, escapePoint} = collectPuddleLayers(startPos, maxSurface, ignoreAsEscape); + + const surfacePoints: point[] = [] + layers.forEach((layer) => surfacePoints.push(...layer)); + + return { + pondSurface: surfacePoints, + waterLevel: getZ(startPos[0], true) + layers.length, + depth: layers.length, + escapePoint: escapePoint + }; } /** @@ -33,63 +53,89 @@ export const capRiverWithPond = (river: point[], maxSurface: number, minDepth: n * @param start * @param maxLayers * @param maxSurface + * @param ignoreSet will not be mutated. ignore these points when collecting layers. are considered "invalid neighbours" */ export const collectPuddleLayers = ( - start: point, - maxLayers: number, - maxSurface: number -): point[][] => { - let level = getZ(start, true); - const maxLevel = level + maxLayers; - + start: point[], + maxSurface: number, + ignoreSet: SeenSetReadOnly, +): { layers: point[][], totalSurface: number, escapePoint: point | undefined } => { + if (start.length == 0) + throw new Error("collectPuddleLayers: start array is empty"); + + let level = getZ(start[0], true); + const internalSeenSet = makeSet(); //iterators - let nextLevelOpen = [start]; + let open = start; let totalSurface = 0; - const seenSet = makeSet(); const surfaceLayers: point[][] = []; - - for (let level = getZ(start, true); level <= maxLevel; level++) { - if (nextLevelOpen.length == 0) break; - const remainingSurfaceBlocks = maxSurface - totalSurface; + let escapePoint = undefined + for (let i = 0; i < 256; level++, i++) { + if (open.length == 0) { + break; + } //collect surface BLOCKS - const { surface, border } = collectSurfaceAndBorder( - nextLevelOpen, - seenSet, - level, - maxSurface, //equally distributed by level, stop earlier - (p: point) => false, - (p: point) => getZ(p, true) - ); + const {surface, border, earlyPoint, exceeded} = collectSurfaceAndBorder( + open, //starting points for surface collection + internalSeenSet, + maxSurface, //equally distributed by level, stop earlier + (p: point) => ignoreSet.hasNot(p) && getZ(p, true) < level, + (p: point) => getZ(p, true) <= level + )!; + + if (earlyPoint !== undefined) { + escapePoint = earlyPoint; + break; + } //stop if total surface would be exceeded - if (totalSurface + surface.length > maxSurface) break; + if (exceeded || totalSurface + surface.length > maxSurface) { + log(`total surface exceeded at additional ${surface.length} + existing ${totalSurface}, stop collecting layers`); + // annotateAll(surface, 10) + //markPos(surface[0],10) + // surfaceLayers.forEach((layer) => annotateAll(layer, 13)) + + break; + } - //add surface to list of layers surfaceLayers.push(surface); + surface.forEach(internalSeenSet.add); totalSurface += surface.length; //prepare next run - nextLevelOpen = border; + open = border; } - - return surfaceLayers; + return {layers: surfaceLayers, totalSurface: totalSurface, escapePoint: escapePoint}; }; -export const collectSurfaceAndBorder = ( - openArr: point[], - seenSet: SeenSet, - level: number, + +export type PondGenerationParams = { maxSurface: number, - failEarly: (p: point) => boolean, - getZ: (p: point) => number -): { surface: point[]; border: point[] } => { +} +/** + * collect points that are valid surface blocks into the surface list. + * collect direct neighbours of surface that are not valid surface blocks into the border list. + * runs until its finds no more connected surface blocks or maxSurface is exceeded + * @param openArr starting points in queue. not guaranteed to go into surface. + * @param ignoreSet ignore these points when testing returnEarly. readonly. + * @param maxSurface return undefine if maxSurface is exceeded + * @param returnEarly stop if this returns true for a point, return what was collected + * @param isValidSurface boolean function to split blocks into surface and border + */ +export const collectSurfaceAndBorder = ( + openArr: point[], + ignoreSet: SeenSetReadOnly, + maxSurface: number, + returnEarly: (p: point) => boolean, + isValidSurface: (p: point) => boolean, +): { surface: point[]; border: point[], earlyPoint: point | undefined, exceeded: boolean } => { + const seenSet = makeSet(); //use next level open that was collected before const surface: queue = makeQueue(); const border: queue = makeQueue(); - const isAtOrBelowSurfaceLevel = (p: point) => getZ(p) <= level; //prepare open queue const open = makeQueue(); @@ -97,23 +143,32 @@ export const collectSurfaceAndBorder = ( openArr.forEach(seenSet.add); let i = 0; + let exceeded = false; + let earlyPoint: point | undefined = undefined; while (!open.isEmpty()) { i++; const currentPoint = open.pop(); + if (i > maxSurface) { + exceeded = true; + break; + } - if (i > maxSurface || failEarly(currentPoint)) { - return { surface: [], border: [] }; + if (returnEarly(currentPoint)) { + earlyPoint = currentPoint; + break; } - if (isAtOrBelowSurfaceLevel(currentPoint)) surface.push(currentPoint); + + if (isValidSurface(currentPoint)) + surface.push(currentPoint); else { border.push(currentPoint); continue; } const ns = getNeighbourPoints(currentPoint); - const newNeighbors = ns.filter(seenSet.hasNot); + const newNeighbors = ns.filter(seenSet.hasNot).filter(ignoreSet.hasNot) //mark as seen newNeighbors.forEach((a) => { seenSet.add(a); @@ -123,6 +178,8 @@ export const collectSurfaceAndBorder = ( return { surface: surface.toArray(), - border: border.toArray(), //todo: maybe return the blocks below surface too and let wrapper decide how to handle? + border: border.toArray(), + earlyPoint: earlyPoint, + exceeded: exceeded }; }; diff --git a/src/river.test.ts b/src/river.test.ts deleted file mode 100644 index 42eb47c..0000000 --- a/src/river.test.ts +++ /dev/null @@ -1,31 +0,0 @@ -import {insertInSortedQueue, squaredDistanceBetweenPoints} from "./river"; -import {parentedPoint} from "./point"; - -describe('helper function river path', () => { - test("distance calculation easy", () => { - const pointA = {x: 0, y: 0}; - const pointB = {x: 0, y: 10}; - expect(squaredDistanceBetweenPoints(pointA, pointB)).toBe(10 * 10); - }) - - test("distance calculation easy", () => { - const pointA = {x: 10, y: 10}; - const pointB = {x: 20, y: 20}; - expect(squaredDistanceBetweenPoints(pointA, pointB)).toBe(10 * 10 + 10*10); - }) - - test("sorted queue", () => { - const pointA = {point: {x: 0, y: 0}, parent: undefined, distance: 0}; - const pointB = {point: {x: 0, y: 0}, parent: undefined, distance: 50}; - const pointC = {point: {x: 0, y: 0}, parent: undefined, distance: 100}; - - const queue: parentedPoint[] = []; - insertInSortedQueue(queue, pointC); - expect(queue).toEqual([pointC]); - insertInSortedQueue(queue, pointA); - expect(queue).toEqual([pointA, pointC]); - insertInSortedQueue(queue, pointB); - expect(queue).toEqual([pointA, pointB, pointC]); - }) - -}); \ No newline at end of file diff --git a/src/river.ts b/src/river.ts deleted file mode 100644 index ae668dc..0000000 --- a/src/river.ts +++ /dev/null @@ -1,135 +0,0 @@ -import { SeenSet, makeSet } from "./SeenSet"; -import { log } from "./log"; -import { - point, - parentedPoint, - getNeighbourPoints, - parentedToList, -} from "./point"; -import { getZ, isWater, markPos } from "./terrain"; - -/** - * start a new river path at this position - * @param pos - */ -export const pathRiverFrom = (pos: point, rivers: SeenSet): point[] => { - const path: parentedPoint[] = [{ point: pos, parent: undefined, distance: 0 }]; - let i = 0; - let current = pos; - let waterReached = false; - while (i < 1000) { - i++; - const pathToDrop = findClosestDrop(current, getZ(current)); - if (pathToDrop.length == 0) - //abort if closestDrop coulndt find anything - break; - for (let point of pathToDrop) { - if (isWater(point.point) || rivers.has(point.point)) { - if (rivers.has(point.point)) waterReached = true; - break; - } - path.push(point); - // rivers.add(point.point); - } - if (waterReached) break; - //end of path is droppoint - current = pathToDrop[pathToDrop.length - 1].point; - } - log( - "river stopped at " + - JSON.stringify(path[path.length - 1].point) + - " water reached: " + - waterReached - ); - path.forEach((p) => rivers.add(p.point)); - return path.map((a) => a.point); -}; - - -export const squaredDistanceBetweenPoints = (a: point, b: point) => { - const diff = {x: a.x-b.x, y: a.y-b.y}; - return diff.x*diff.x + diff.y*diff.y; -} - -export const insertInSortedQueue = (sortedQueue: parentedPoint[], point: parentedPoint): void => { - let i = 0; - for (let iteratorPoint of sortedQueue) { - if (iteratorPoint.distance > point.distance) break; - i++; - } - sortedQueue.splice(i, 0, point); -} - -/** - * find the point closest to pos thats at least one block lower - * @param pos - * @param posZ - * @returns path to this point from pos where pos is the first entry, drop the last - */ -export function findClosestDrop( - pos: point, - posZ: number, -): parentedPoint[] { - const seenSet: SeenSet = makeSet(); - - const queue: parentedPoint[] = [{ point: pos, parent: undefined, distance: 0 }]; - let next: parentedPoint; - let safetyIterator = 0; - seenSet.add(pos); - while (queue.length != 0 && safetyIterator < 10000) { - next = queue.shift() as parentedPoint; - - //abort condition - if (getZ(next.point, true) < Math.round(posZ)) { - const path = parentedToList(next, []).reverse(); - return path; - } - - let neighbours = getNeighbourPoints(next.point).filter(seenSet.hasNot); - const trueLowerNeighbours = neighbours.filter(n => getZ(n, true) < Math.round(posZ)); - if (trueLowerNeighbours.length == 0) { - //no lower neighbour, river is in flat area => sort by tinyest height difference - neighbours = neighbours.sort((a, b) => { - const aZ = getZ(a, false); - const bZ = getZ(b, false); - return aZ-bZ; - }); - - // markPos(next.point, 3) - } - - neighbours.forEach(function (n) { - const lower = getZ(n, false) <= posZ; - if (lower) { - //unknown point - seenSet.add(n); - insertInSortedQueue(queue, { point: n, parent: next, distance: squaredDistanceBetweenPoints(n, pos) }); - } else { - } - }); - safetyIterator++; - } - return []; -} - -export const capRiverStart = (river: point[], slice: number) => { - return river.slice(slice); -}; - -/** - * will get the z of the lowest neighbour of the riverpoint (which is the point after the point in the river) - * @param river point array that flows from high to low - * @returns river with z values - */ -export const minFilter = ( - p: point, - i: number, - river: point[] -): { point: point; z: number } => { - const neiZs = getNeighbourPoints(p).map((a) => getZ(a, true)); - const minNeighbourNonRiver = Math.min.apply(Math, neiZs); - return { - point: p, - z: minNeighbourNonRiver, - }; -}; diff --git a/src/terrain.ts b/src/terrain.ts index 6456f94..89af690 100644 --- a/src/terrain.ts +++ b/src/terrain.ts @@ -1,8 +1,9 @@ -import { point } from "./point"; +import {point} from "./point"; +import {annotateAll} from "./puddle"; -export function getZ(pos: point, floor?: boolean): number { +export function getZ(pos: point, round: boolean = true): number { const z = dimension.getHeightAt(pos.x, pos.y); - return floor ? Math.round(z) : z; + return round ? Math.round(z) : z; } export const setWaterLevel = (p: point, z: number): void => { @@ -20,7 +21,12 @@ export function getTerrainById(terrainId: number) { } export function markPos(pos: point, id: number) { - dimension.setTerrainAt(pos.x, pos.y, getTerrainById(id)); + const points = [] + for (var i = -4; i <= 4; i++) { + points.push({x :pos.x + i, y: pos.y - i}); + points.push({x :pos.x + i, y: pos.y + i}); + } + annotateAll(points, id); } /** diff --git a/tsconfig.json b/tsconfig.json index e787fcd..8c983e9 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -18,7 +18,8 @@ "exclude": [ "node_modules", "**/*.spec.ts", - // "**/*.test.ts" + "**/*.test.ts", + "src/__mocks__/*" ] } \ No newline at end of file