Skip to content

Commit

Permalink
Feature/5 puddles can overflow (#10)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
IR0NSIGHT authored Aug 13, 2023
1 parent 289ad6d commit f99a9a4
Show file tree
Hide file tree
Showing 19 changed files with 815 additions and 310 deletions.
25 changes: 25 additions & 0 deletions .github/workflows/unittest.yml
Original file line number Diff line number Diff line change
@@ -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

4 changes: 4 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
module.exports = {
preset: 'ts-jest',
// verbose: true,
testEnvironment: 'node',
// ... other Jest configuration options
collectCoverage: true,
coverageDirectory: 'coverage',
coverageReporters: ["json", "html"],
};
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
3 changes: 3 additions & 0 deletions src/SeenSet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ export type SeenSet = {
hasNot: (p: Point) => boolean;
};

export type SeenSetReadOnly = Omit<SeenSet,"add">


export const makeSet = (): SeenSet => {
//@ts-ignore
const seenSet: any = new java.util.HashSet<string>();
Expand Down
19 changes: 19 additions & 0 deletions src/__mocks__/SeenSet.ts
Original file line number Diff line number Diff line change
@@ -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<SeenSet,"add">


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))
}
};
26 changes: 16 additions & 10 deletions src/applyRiver.ts
Original file line number Diff line number Diff line change
@@ -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);
Expand Down
9 changes: 3 additions & 6 deletions src/global.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};

61 changes: 26 additions & 35 deletions src/header.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
104 changes: 78 additions & 26 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -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
Expand All @@ -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 =" +
Expand Down
25 changes: 25 additions & 0 deletions src/pathing/postprocessing.test.ts
Original file line number Diff line number Diff line change
@@ -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);
})
})
Loading

0 comments on commit f99a9a4

Please sign in to comment.