diff --git a/src/core/transformers/03_plastic/03_strataflux/corners.ts b/src/core/transformers/03_plastic/03_strataflux/corners.ts deleted file mode 100644 index 38fe8da..0000000 --- a/src/core/transformers/03_plastic/03_strataflux/corners.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { Mutable } from "../../../common"; -import { NSEW } from "../../../common/geometry"; -import { Grid, MutableGrid } from "../../../common/grid"; -import { filterTruthy } from "../../../common/utils"; -import { StrataformedCavern } from "../02_strataform"; -import { HEIGHT_MAX, HEIGHT_MIN } from "./base"; -import getEdges from "./edges"; - -// IDEA: Don't use corners; use HeightNodes which do not have the x and y coord -// Start by finding lakes and assign everywhere in the lake the same HeightNode -// object. This will have some performance benefit, but also it means neighbors -// can be added between non-adjacent corners. As long as the relationship is -// defined symmetrically, there should be no issue with these as the algorithm -// doesn't require the space be Euclidean. - -export type Corner = { - readonly target: number | undefined; - readonly x: number; - readonly y: number; - readonly neighbors: readonly { - readonly corner: Corner; - // The maximum upward slope moving from here to the neighbor. - readonly ascent: number; - // The maximum downward slope moving from here to the neighbor. - readonly descent: number; - }[]; - collapseQueued: boolean; - min: number; - max: number; - range: number; -}; - -export default function getCorners(cavern: StrataformedCavern): Grid { - const corners = new MutableGrid(); - for (let x = cavern.left; x <= cavern.right; x++) { - for (let y = cavern.top; y <= cavern.bottom; y++) { - const target = cavern.height.get(x, y); - // Corners on the border are adjusted to a height of 0 when loaded in Manic - // Miners. This means they should be compressed here, but this also gives - // strataflux a starting point for which corners have known heights. - const isBorder = ( - x === cavern.left || - x === cavern.right || - y === cavern.top || - y === cavern.bottom - ); - corners.set(x, y, { - target, - x, - y, - neighbors: [], - collapseQueued: isBorder, - min: isBorder ? 0 : HEIGHT_MIN, - max: isBorder ? 0 : HEIGHT_MAX, - range: isBorder ? 0 : HEIGHT_MAX - HEIGHT_MIN, - }) - } - } - // With the corner objects created, add all neighbors. - const {edgesH, edgesV} = getEdges(cavern); - corners.forEach((corner, x, y) => { - (corner.neighbors as Mutable).push( - ...filterTruthy(NSEW.map(([ox, oy]) => { - const c = corners.get(x + ox, y + oy); - if (!c) { - return undefined; - } - // Find the edge. Since these are indexed by the minimum, the edges in - // the positive direction are found at [x, y]. - const e = (oy === 0 ? edgesH : edgesV).get( - x + Math.min(ox, 0), - y + Math.min(oy, 0), - ); - if (!e) { - return undefined; - } - // Determine ascent and descent from the edge slopes. - return (ox + oy < 0) - ? {corner: c, ascent: e.backwardSlope, descent: e.forwardSlope} - : {corner: c, ascent: e.forwardSlope, descent: e.backwardSlope}; - })) - ); - }); - return corners; -} \ No newline at end of file diff --git a/src/core/transformers/03_plastic/03_strataflux/edges.ts b/src/core/transformers/03_plastic/03_strataflux/edges.ts index 2a706f9..785afc3 100644 --- a/src/core/transformers/03_plastic/03_strataflux/edges.ts +++ b/src/core/transformers/03_plastic/03_strataflux/edges.ts @@ -1,14 +1,88 @@ import { Mutable } from "../../../common"; import { Grid, MutableGrid } from "../../../common/grid"; -import { Tile } from "../../../models/tiles"; +import { pairEach } from "../../../common/utils"; import { StrataformedCavern } from "../02_strataform"; import { CORNER_OFFSETS } from "./base"; -export type Edge = { - // The maximum slope when going up toward the positive axis - readonly forwardSlope: number, - // The maximum slope when going up toward the negative axis - readonly backwardSlope: number, + +type Edge = { + readonly to: readonly [number, number]; + readonly ascent: number; + readonly descent: number; +} + +type EdgeData = { + readonly x1: number; + readonly y1: number; + readonly x2: number; + readonly y2: number; + forward: number; + backward: number; +} + +class EdgeMap { + private readonly data = new Map<`${number},${number},${number},${number}`, EdgeData>(); + + get(x1: number, y1: number, x2: number, y2: number): {ascent: number, descent: number} | undefined { + if (x1 < x2 || (x1 === x2 && y1 <= y2)) { + const r = this.data.get(`${x1},${y1},${x2},${y2}`); + return r && {ascent: r.forward, descent: r.backward}; + } + const r = this.data.get(`${x2},${y2},${x1},${y1}`); + return r && {ascent: r.backward, descent: r.forward}; + } + set(x1: number, y1: number, x2: number, y2: number, ascent: number, descent: number) { + if (x1 === x2 && y1 === y2) { + return; + } + if (x1 < x2 || (x1 === x2 && y1 < y2)) { + const r = this.data.get(`${x1},${y1},${x2},${y2}`); + if (r) { + r.forward = Math.min(r.forward, ascent); + r.backward = Math.min(r.backward, descent); + } else { + this.data.set(`${x1},${y1},${x2},${y2}`, { + x1, + y1, + x2, + y2, + forward: ascent, + backward: descent, + }); + } + } else { + const r = this.data.get(`${x2},${y2},${x1},${y1}`); + if (r) { + r.forward = Math.min(r.forward, descent); + r.backward = Math.min(r.backward, ascent); + } else { + this.data.set(`${x2},${y2},${x1},${y1}`, { + x1: x2, + y1: y2, + x2: x1, + y2: y1, + forward: descent, + backward: ascent, + }); + } + } + } + edges(): Grid { + const result = new MutableGrid; + function get(x: number, y: number) { + let r = result.get(x, y); + if (!r) { + r = []; + result.set(x, y, r); + } + return r; + } + this.data.forEach(({x1, y1, x2, y2, forward, backward}) => { + get(x1, y1).push({to: [x2, y2], ascent: forward, descent: backward}); + get(x2, y2).push({to: [x1, y1], ascent: backward, descent: forward}); + }); + return result; + } } // Each tile has a maximum slope its edges are allowed to have. @@ -46,69 +120,17 @@ function getTileSlopes(cavern: StrataformedCavern): Grid { return result; } -// Turns the given point into a "bowl" where this point and all points around -// it up to the given size must slope downward toward this point. -function bowlify( - x: number, - y: number, - size: number, - edgesH: MutableGrid>, - edgesV: MutableGrid>, -) { - for (let i = 0; i <= size; i++) { - for (let j = -i; j <= i; j++) { - edgesH.get(x + i, y + j)!.backwardSlope = 0; - edgesH.get(x - i - 1, y + j)!.forwardSlope = 0; - edgesV.get(x + j, y + i)!.backwardSlope = 0; - edgesV.get(x + j, y - i - 1)!.forwardSlope = 0; - } - } -} - -export default function getEdges(cavern: StrataformedCavern): {edgesH: Grid, edgesV: Grid} { - const tileSlopes = getTileSlopes(cavern); +export default function getEdgeMap(cavern: StrataformedCavern): EdgeMap { + const edges = new EdgeMap(); - const edgesH = new MutableGrid>(); - for (let x = cavern.left; x < cavern.right; x++) { - for (let y = cavern.top + 1; y < cavern.bottom; y++) { - const slope = Math.min( - tileSlopes.get(x, y - 1)!, - tileSlopes.get(x, y)!, - ); - edgesH.set(x, y, {forwardSlope: slope, backwardSlope: slope}); - } - } - const edgesV = new MutableGrid>(); - for (let x = cavern.left + 1; x < cavern.right; x++) { - for (let y = cavern.top; y < cavern.bottom; y++) { - const slope = Math.min( - tileSlopes.get(x - 1, y)!, - tileSlopes.get(x, y)!, - ); - edgesV.set(x, y, {forwardSlope: slope, backwardSlope: slope}); - } - } - - const bowls = new MutableGrid(); - cavern.tiles.forEach((tile, x, y) => { - let size; - if (tile === Tile.WATER) { - size = 1; - } else if (tile === Tile.LAVA) { - size = 1; - } else if (cavern.pearlInnerDex.get(x, y)?.some(p => cavern.plans[p].hasErosion)) { - size = 0; - } else { - return; - } - CORNER_OFFSETS.forEach(([ox, oy]) => { - bowls.set(x + ox, y + oy, Math.max( - size, - bowls.get(x + ox, y + oy) ?? 0), - ); - }); + getTileSlopes(cavern).forEach((slope, x1, y1) => { + const x2 = x1 + 1; + const y2 = y1 + 1; + edges.set(x1, y1, x2, y1, slope, slope); + edges.set(x1, y1, x1, y2, slope, slope); + edges.set(x2, y1, x2, y2, slope, slope); + edges.set(x1, y2, x2, y2, slope, slope); }); - bowls.forEach((size, x, y) => bowlify(x, y, size, edgesH, edgesV)); - return {edgesH, edgesV}; + return edges; } \ No newline at end of file diff --git a/src/core/transformers/03_plastic/03_strataflux/index.ts b/src/core/transformers/03_plastic/03_strataflux/index.ts index c45f2a5..de775c2 100644 --- a/src/core/transformers/03_plastic/03_strataflux/index.ts +++ b/src/core/transformers/03_plastic/03_strataflux/index.ts @@ -1,7 +1,7 @@ import { PseudorandomStream } from "../../../common"; import { Grid, MutableGrid } from "../../../common/grid"; import { StrataformedCavern } from "../02_strataform"; -import getCorners, { Corner } from "./corners"; +import getNodes, { HeightNode } from "./nodes"; // The "strataflux" algorithm is particularly complex mostly because it // involves many data structures and concepts that are not really used @@ -34,29 +34,29 @@ function superflat(cavern: StrataformedCavern): Grid { return result; } -const collapseQueueSort = ({ range: a }: Corner, { range: b }: Corner) => b - a; +const collapseQueueSort = ({ range: a }: HeightNode, { range: b }: HeightNode) => b - a; -function getRandomHeight(corner: Corner, rng: PseudorandomStream): number { - if (corner.min === corner.max) { - return corner.min; +function getRandomHeight(node: HeightNode, rng: PseudorandomStream): number { + if (node.min === node.max) { + return node.min; } const targetInRange = (() => { - if (corner.target === undefined) { + if (node.target === undefined) { return 0.5; } - if (corner.target <= corner.min) { + if (node.target <= node.min) { return 0; } - if (corner.target >= corner.max) { + if (node.target >= node.max) { return 1; } - return (corner.target - corner.min) / (corner.max - corner.min); + return (node.target - node.min) / (node.max - node.min); })(); return rng.betaInt({ a: 1 + 3 * targetInRange, b: 1 + 3 * (1 - targetInRange), - min: corner.min, - max: corner.max + 1, + min: node.min, + max: node.max + 1, }); } @@ -68,63 +68,65 @@ export default function strataflux( return { ...cavern, height: superflat(cavern) }; } - const corners = getCorners(cavern); + const nodes = getNodes(cavern); const height = new MutableGrid(); const rng = cavern.dice.height; // The collapse queue is a priority queue. The algorithm will always take the - // corner with the smallest possible range of values. - const collapseQueue: Corner[] = corners + // node with the smallest possible range of values. + const collapseQueue: HeightNode[] = nodes .map(c => c).filter(c => c.collapseQueued); - // Collapsing a corner means picking a specific height in range for that corner. + // Collapsing a node means picking a specific height in range for that node. const collapse = () => { - const corner = collapseQueue.pop()!; - const h = getRandomHeight(corner, rng); - height.set(corner.x, corner.y, h); - corner.min = h; - corner.max = h; - corner.range = 0; - return corner; + const node = collapseQueue.pop()!; + const h = getRandomHeight(node, rng); + for (let i = 0; i < node.corners.length; i++) { + height.set(...node.corners[i], h); + } + node.min = h; + node.max = h; + node.range = 0; + return node; }; // I think this algorithm is like O(n^4) where N is the size of the cavern, // so try to save some performance where possible. while (collapseQueue.length) { - // Take a corner off the collapse queue and collapse it. + // Take a node off the collapse queue and collapse it. // Then, put it on the spread queue. const spreadQueue = [collapse()]; while (spreadQueue.length) { - // Take a corner off the spread queue. - const corner = spreadQueue.shift()!; - // Spread to each of this corner's neighbors. - for (let i = 0; i < corner.neighbors.length; i++) { - const neighbor = corner.neighbors[i]; + // Take a node off the spread queue. + const node = spreadQueue.shift()!; + // Spread to each of this node's neighbors. + for (let i = 0; i < node.neighbors.length; i++) { + const neighbor = node.neighbors[i]; // If the neighbor is already collapsed, nothing to do. - if (!neighbor.corner.range) { + if (!neighbor.node.range) { continue; } // Pull the neighbor's minimum up and maximum down to fit within the // allowed ascent/descent slope for this edge. - neighbor.corner.min = Math.max( - neighbor.corner.min, - corner.min - neighbor.descent, + neighbor.node.min = Math.max( + neighbor.node.min, + node.min - neighbor.descent, ); - neighbor.corner.max = Math.min( - neighbor.corner.max, - corner.max + neighbor.ascent, + neighbor.node.max = Math.min( + neighbor.node.max, + node.max + neighbor.ascent, ); - const range = neighbor.corner.max - neighbor.corner.min; + const range = neighbor.node.max - neighbor.node.min; // If the neighbor's range has shrunk, - if (range < neighbor.corner.range) { - neighbor.corner.range = range; + if (range < neighbor.node.range) { + neighbor.node.range = range; // put it on the spread queue - spreadQueue.push(neighbor.corner); + spreadQueue.push(neighbor.node); // and put it on the collapse queue if it wasn't already. - if (!neighbor.corner.collapseQueued) { - neighbor.corner.collapseQueued = true; - collapseQueue.push(neighbor.corner); + if (!neighbor.node.collapseQueued) { + neighbor.node.collapseQueued = true; + collapseQueue.push(neighbor.node); } } } diff --git a/src/core/transformers/03_plastic/03_strataflux/nodes.ts b/src/core/transformers/03_plastic/03_strataflux/nodes.ts new file mode 100644 index 0000000..0e6789f --- /dev/null +++ b/src/core/transformers/03_plastic/03_strataflux/nodes.ts @@ -0,0 +1,137 @@ +import { Mutable } from "../../../common"; +import { NSEW, Point } from "../../../common/geometry"; +import { Grid, MutableGrid } from "../../../common/grid"; +import { Tile } from "../../../models/tiles"; +import { StrataformedCavern } from "../02_strataform"; +import { CORNER_OFFSETS, HEIGHT_MAX, HEIGHT_MIN } from "./base"; +import getEdgeMap from "./edges"; + +export type HeightNode = { + readonly target: number | undefined; + readonly neighbors: readonly { + readonly node: HeightNode; + // The maximum upward slope moving from here to the neighbor. + readonly ascent: number; + // The maximum downward slope moving from here to the neighbor. + readonly descent: number; + }[]; + readonly corners: Point[]; + collapseQueued: boolean; + min: number; + max: number; + range: number; +}; + +function getBowls(cavern: StrataformedCavern) { + const result = new MutableGrid(); + cavern.tiles.forEach((tile, x, y) => { + let size; + if (tile === Tile.WATER) { + size = 2; + } else if (tile === Tile.LAVA) { + size = 2; + } else if (cavern.pearlInnerDex.get(x, y)?.some(p => cavern.plans[p].hasErosion)) { + size = 0; + } else { + return; + } + CORNER_OFFSETS.forEach(([ox, oy]) => { + result.set(x + ox, y + oy, Math.max( + size, + result.get(x + ox, y + oy) ?? 0), + ); + }); + }); + return result; +} + +function mkNode(cavern: StrataformedCavern, x: number, y: number): HeightNode { + const target = cavern.height.get(x, y); + // Corners on the border are adjusted to a height of 0 when loaded in Manic + // Miners. This means they should be compressed here, but this also gives + // strataflux a starting point for which corners have known heights. + const isBorder = ( + x === cavern.left || + x === cavern.right || + y === cavern.top || + y === cavern.bottom + ); + return { + target, + neighbors: [], + corners: [[x, y]], + collapseQueued: isBorder, + min: isBorder ? 0 : HEIGHT_MIN, + max: isBorder ? 0 : HEIGHT_MAX, + range: isBorder ? 0 : HEIGHT_MAX - HEIGHT_MIN, + }; +} + +function getNodesForBowls(cavern: StrataformedCavern, bowls: Grid) { + const result = new MutableGrid(); + // oh hey look it's the same algorithm from discovery zones + const queue: { x: number; y: number; node: HeightNode | null }[] = + bowls.map((_, x, y) => ({ x, y, node: null })); + while (queue.length > 0) { + let { x, y, node } = queue.shift()!; + if (!result.get(x, y)) { + if (!node) { + node = mkNode(cavern, x, y); + } + result.set(x, y, node); + const neighbors = NSEW.map(([ox, oy]) => ({ + x: x + ox, + y: y + oy, + node, + })).filter(({x: x2, y: y2}) => bowls.get(x2, y2)); + queue.unshift(...neighbors); + } + } + return result; +} + +export default function getNodes(cavern: StrataformedCavern): Grid { + // First handle "bowls" - these are the places that need to be at a lower + // elevation than their surroundings. Adjacent bowls must be at the same + // height, so just use the same node for the whole contiguous bowl. + const bowls = getBowls(cavern); + const nodes = getNodesForBowls(cavern, bowls); + + // Now create the rest of the node objects. + for (let x = cavern.left; x <= cavern.right; x++) { + for (let y = cavern.top; y <= cavern.bottom; y++) { + const node = nodes.get(x, y) + if (node) { + node.corners.push([x, y]); + } else { + nodes.set(x, y, mkNode(cavern, x, y)); + } + } + } + + // Get the edge map with tile slopes precomputed. + const edges = getEdgeMap(cavern); + + // Add edges for bowls. + bowls.forEach((size, x, y) => { + const s = size + 1; + for (let ox = -s; ox <= s; ox++) { + for (let oy = -s; oy <= s; oy++) { + edges.set(x, y, x + ox, y + oy, Infinity, 0); + } + } + }) + + // Convert edges to neighbors, linking them together. + edges.edges().forEach((e, x1, y1) => { + const node = nodes.get(x1, y1); + if (node) { + const neighbors = e.map( + ({to, ascent, descent}) => ({node: nodes.get(...to), ascent, descent}) + ).filter(({node: n}) => n && n !== node) as HeightNode['neighbors']; + (node.neighbors as Mutable).push(...neighbors); + } + }); + + return nodes; +} \ No newline at end of file diff --git a/src/webui/components/map_preview/height.tsx b/src/webui/components/map_preview/height.tsx index 04bb316..add8947 100644 --- a/src/webui/components/map_preview/height.tsx +++ b/src/webui/components/map_preview/height.tsx @@ -17,18 +17,16 @@ function toColor(h: number) { export default function HeightPreview({ height }: { height: Grid }) { return ( - + {height.map((h, x, y) => { return ( - {h} - + ); })} diff --git a/src/webui/components/map_preview/tiles.tsx b/src/webui/components/map_preview/tiles.tsx index fa5e2c4..47a0d96 100644 --- a/src/webui/components/map_preview/tiles.tsx +++ b/src/webui/components/map_preview/tiles.tsx @@ -182,15 +182,15 @@ export default function TilesPreview({ } const boundsFill = getBoundsFill(mapOverlay); return ( - + {boundsFill && cavern.top && ( )} {cavern.tiles.map((t, x, y) => { @@ -204,10 +204,10 @@ export default function TilesPreview({ key={`${x},${y}`} className={styles.tile} fill={fill} - x={x * SCALE} - y={y * SCALE} - width={SCALE} - height={SCALE} + x={x} + y={y} + width={1} + height={1} > {title && {title}}