490 lines
16 KiB
HTML
490 lines
16 KiB
HTML
|
<html>
|
||
|
<head>
|
||
|
<title>ProcGen Island</title>
|
||
|
</head>
|
||
|
<body>
|
||
|
<script>
|
||
|
"use strict";
|
||
|
(() => {
|
||
|
// lib/prng.ts
|
||
|
var UINT_MAX = 4294967295;
|
||
|
function mulberry32(state) {
|
||
|
return function () {
|
||
|
let t = (state += 1831565813);
|
||
|
t = Math.imul(t ^ (t >>> 15), t | 1);
|
||
|
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
||
|
return (t ^ (t >>> 14)) >>> 0;
|
||
|
};
|
||
|
}
|
||
|
|
||
|
// island/data.ts
|
||
|
var WATER = 0;
|
||
|
var BEACH = 1;
|
||
|
var LIGHT_FOREST = 2;
|
||
|
var DENSE_FOREST = 3;
|
||
|
var MOUNTAIN = 4;
|
||
|
var ICECAP = 7;
|
||
|
|
||
|
// island/generators.ts
|
||
|
var SMALL_MOUNTAIN = (islands, basePos) => () => {
|
||
|
const islandTiles = islands.floodSearch(
|
||
|
basePos,
|
||
|
(tile) => tile > WATER
|
||
|
);
|
||
|
const edgeTiles = islandTiles.filter(
|
||
|
(pos) => islands.data[pos] <= BEACH
|
||
|
);
|
||
|
islands.dropWithin(edgeTiles);
|
||
|
const mountainTiles = islands.floodSearch(
|
||
|
basePos,
|
||
|
(tile) => tile > MOUNTAIN
|
||
|
);
|
||
|
islands.dropWithin(mountainTiles);
|
||
|
return true;
|
||
|
};
|
||
|
var BIG_MOUNTAIN = (islands, basePos) => () => {
|
||
|
const mountainTiles = islands.floodSearch(
|
||
|
basePos,
|
||
|
(tile) => tile > MOUNTAIN
|
||
|
);
|
||
|
islands.dropWithin(mountainTiles);
|
||
|
return mountainTiles.some((pos) => islands.data[pos] == ICECAP);
|
||
|
};
|
||
|
var SMALL_BEACH = (islands, basePos) => () => {
|
||
|
const islandTiles = islands.floodSearch(
|
||
|
basePos,
|
||
|
(tile) => tile > WATER
|
||
|
);
|
||
|
const shoreTiles = islandTiles.filter(
|
||
|
(pos) => islands.data[pos] <= WATER
|
||
|
);
|
||
|
islands.dropWithin(shoreTiles);
|
||
|
return true;
|
||
|
};
|
||
|
var BIG_BEACH = (islands, basePos) => {
|
||
|
let dropped = 0;
|
||
|
return () => {
|
||
|
const islandTiles = islands.floodSearch(
|
||
|
basePos,
|
||
|
(tile) => tile > WATER
|
||
|
);
|
||
|
const shoreTiles = islandTiles.filter(
|
||
|
(pos) => islands.data[pos] <= WATER
|
||
|
);
|
||
|
islands.dropWithin(shoreTiles);
|
||
|
dropped++;
|
||
|
const landTiles = islandTiles.filter(
|
||
|
(pos) => islands.data[pos] > WATER
|
||
|
);
|
||
|
return landTiles.length > dropped;
|
||
|
};
|
||
|
};
|
||
|
var SCATTERED_FOREST = (islands, basePos) => () => {
|
||
|
const islandTiles = islands.floodSearch(
|
||
|
basePos,
|
||
|
(tile) => tile > WATER
|
||
|
);
|
||
|
const shoreTiles = islandTiles.filter(
|
||
|
(pos) => islands.data[pos] <= WATER
|
||
|
);
|
||
|
islands.dropWithin(shoreTiles);
|
||
|
const beachTiles = islandTiles.filter(
|
||
|
(pos) => islands.data[pos] == BEACH
|
||
|
);
|
||
|
islands.dropWithin(beachTiles);
|
||
|
islands.dropWithin(beachTiles);
|
||
|
const forestLobe = islands.floodSearch(
|
||
|
basePos,
|
||
|
(tile) => tile > WATER
|
||
|
);
|
||
|
const forestTiles = forestLobe.filter(
|
||
|
(pos) => islands.data[pos] == LIGHT_FOREST
|
||
|
);
|
||
|
islands.dropWithin(forestTiles);
|
||
|
return true;
|
||
|
};
|
||
|
var CONTIGUOUS_FOREST = (islands, basePos) => () => {
|
||
|
const islandTiles = islands.floodSearch(
|
||
|
basePos,
|
||
|
(tile) => tile > WATER
|
||
|
);
|
||
|
const shoreTiles = islandTiles.filter(
|
||
|
(pos) => islands.data[pos] <= WATER
|
||
|
);
|
||
|
islands.dropWithin(shoreTiles);
|
||
|
const beachTiles = islandTiles.filter(
|
||
|
(pos) => islands.data[pos] == BEACH
|
||
|
);
|
||
|
islands.dropWithin(beachTiles);
|
||
|
const forestLobe = islands.floodSearch(
|
||
|
basePos,
|
||
|
(tile) => tile > BEACH
|
||
|
);
|
||
|
const forestTiles = forestLobe.filter(
|
||
|
(pos) => islands.data[pos] == LIGHT_FOREST
|
||
|
);
|
||
|
islands.dropWithin(forestTiles);
|
||
|
islands.dropWithin(forestTiles);
|
||
|
return true;
|
||
|
};
|
||
|
var HILLY_FOREST = (islands, basePos) => () => {
|
||
|
const islandTiles = islands.floodSearch(
|
||
|
basePos,
|
||
|
(tile) => tile > WATER
|
||
|
);
|
||
|
const shoreTiles = islandTiles.filter(
|
||
|
(pos) => islands.data[pos] <= WATER
|
||
|
);
|
||
|
islands.dropWithin(shoreTiles);
|
||
|
const beachTiles = islandTiles.filter(
|
||
|
(pos) => islands.data[pos] == BEACH
|
||
|
);
|
||
|
islands.dropWithin(beachTiles);
|
||
|
const centralForest = islands.floodSearch(
|
||
|
basePos,
|
||
|
(tile) => tile > BEACH
|
||
|
);
|
||
|
islands.dropWithin(centralForest);
|
||
|
const edgeTiles = centralForest.filter(
|
||
|
(pos) => islands.data[pos] == LIGHT_FOREST
|
||
|
);
|
||
|
islands.dropWithin(edgeTiles);
|
||
|
const hillTiles = centralForest.filter(
|
||
|
(pos) => islands.data[pos] >= MOUNTAIN
|
||
|
);
|
||
|
return hillTiles.length > 10;
|
||
|
};
|
||
|
var ERODED_BEACH = (islands, basePos) => () => {
|
||
|
const islandTiles = islands.floodSearch(
|
||
|
basePos,
|
||
|
(tile) => tile > WATER
|
||
|
);
|
||
|
const shoreTiles = islandTiles.filter(
|
||
|
(pos) => islands.data[pos] <= WATER
|
||
|
);
|
||
|
islands.dropWithin(shoreTiles);
|
||
|
if (islandTiles.length > 1) {
|
||
|
const erodePos = islandTiles[islands.rng() % islandTiles.length];
|
||
|
islands.data[erodePos] = Math.max(
|
||
|
islands.data[erodePos] - 1,
|
||
|
WATER
|
||
|
);
|
||
|
}
|
||
|
return true;
|
||
|
};
|
||
|
var NO_ISLAND = (islands, basePos) => () => {
|
||
|
return true;
|
||
|
};
|
||
|
var SINKHOLE_BURNOUT = 1500;
|
||
|
var SINKHOLE = (islands, basePos) => {
|
||
|
let ticks = 0;
|
||
|
return () => {
|
||
|
if (ticks++ < SINKHOLE_BURNOUT) {
|
||
|
const sunk = islands.floodSearch(basePos, (tile) => tile < 0);
|
||
|
islands.sinkhole(islands.choose(sunk));
|
||
|
}
|
||
|
return true;
|
||
|
};
|
||
|
};
|
||
|
var WIDE_SINKHOLE = (islands, basePos) => {
|
||
|
let ticks = 0;
|
||
|
return () => {
|
||
|
if (ticks++ < SINKHOLE_BURNOUT) {
|
||
|
const sunk = islands.floodSearch(basePos, (tile) => tile < 0);
|
||
|
const sunkEdge = sunk.filter((pos) => islands.data[pos] >= WATER);
|
||
|
if (sunkEdge.length > 0) {
|
||
|
islands.sinkhole(islands.choose(sunkEdge));
|
||
|
} else {
|
||
|
islands.sinkhole(islands.choose(sunk));
|
||
|
}
|
||
|
}
|
||
|
return true;
|
||
|
};
|
||
|
};
|
||
|
var BIG_ISLANDS = [BIG_MOUNTAIN, BIG_BEACH, HILLY_FOREST];
|
||
|
var ROCKY_ISLANDS = [SMALL_MOUNTAIN, BIG_MOUNTAIN, HILLY_FOREST];
|
||
|
var GREEN_ISLANDS = [SCATTERED_FOREST, CONTIGUOUS_FOREST, HILLY_FOREST];
|
||
|
var SMALL_ISLANDS = [SMALL_BEACH, SCATTERED_FOREST, ERODED_BEACH];
|
||
|
var ALL_ISLANDS = [
|
||
|
SMALL_MOUNTAIN,
|
||
|
BIG_MOUNTAIN,
|
||
|
SMALL_BEACH,
|
||
|
BIG_BEACH,
|
||
|
SCATTERED_FOREST,
|
||
|
CONTIGUOUS_FOREST,
|
||
|
HILLY_FOREST,
|
||
|
ERODED_BEACH,
|
||
|
];
|
||
|
var VOIDS = [NO_ISLAND, SINKHOLE, WIDE_SINKHOLE];
|
||
|
|
||
|
// island/grid.ts
|
||
|
var IslandGrid = class {
|
||
|
constructor(width, height, seed) {
|
||
|
this.width = width;
|
||
|
this.height = height;
|
||
|
this.data = Array(width * height).fill(WATER);
|
||
|
this.rng = mulberry32(seed);
|
||
|
const islandBag = this.shuffle([
|
||
|
this.choose(BIG_ISLANDS),
|
||
|
this.choose(ROCKY_ISLANDS),
|
||
|
this.choose(GREEN_ISLANDS),
|
||
|
this.choose(SMALL_ISLANDS),
|
||
|
this.choose(SMALL_ISLANDS),
|
||
|
this.choose(ALL_ISLANDS),
|
||
|
this.choose(ALL_ISLANDS),
|
||
|
this.choose(VOIDS),
|
||
|
this.choose(VOIDS),
|
||
|
this.choose(VOIDS),
|
||
|
this.choose(VOIDS),
|
||
|
NO_ISLAND,
|
||
|
NO_ISLAND,
|
||
|
]);
|
||
|
const islandCount = islandBag.length;
|
||
|
const spacing = (Math.PI * 2) / islandCount;
|
||
|
const rootX = width / 2;
|
||
|
const rootY = height / 2;
|
||
|
const xScale = width / 4;
|
||
|
const yScale = height / 4;
|
||
|
for (let i = 0; i < islandCount; i++) {
|
||
|
const rScale = (this.rng() / UINT_MAX) * 2 - 1;
|
||
|
const y = rootY + Math.sin(spacing * i) * yScale * rScale;
|
||
|
const x = rootX + Math.cos(spacing * i) * xScale * rScale;
|
||
|
this.generators.push(islandBag[i](this, this.xy(x | 0, y | 0)));
|
||
|
}
|
||
|
}
|
||
|
data;
|
||
|
rng;
|
||
|
generators = [];
|
||
|
done = false;
|
||
|
xy(x, y) {
|
||
|
return (
|
||
|
(((x % this.width) + this.width) % this.width) +
|
||
|
this.width * (((y % this.height) + this.height) % this.height)
|
||
|
);
|
||
|
}
|
||
|
floodSearch(startPos, shouldExpand) {
|
||
|
const len = this.data.length;
|
||
|
const width = this.width;
|
||
|
const seen = new Uint8Array(len);
|
||
|
const hitPositions = [];
|
||
|
function enqueue(pos) {
|
||
|
if (!seen[pos]) {
|
||
|
hitPositions.push(pos);
|
||
|
seen[pos] = 1;
|
||
|
}
|
||
|
}
|
||
|
enqueue(startPos);
|
||
|
for (let i = 0; i < hitPositions.length; i++) {
|
||
|
const pos = hitPositions[i];
|
||
|
if (shouldExpand(this.data[pos])) {
|
||
|
enqueue((pos - width) % len);
|
||
|
enqueue((pos - 1) % len);
|
||
|
enqueue((pos + 1) % len);
|
||
|
enqueue((pos + width) % len);
|
||
|
}
|
||
|
}
|
||
|
return hitPositions;
|
||
|
}
|
||
|
drop(pos) {
|
||
|
const lowerNeighbors = [];
|
||
|
const check = (adjPos) => {
|
||
|
if (this.data[adjPos] < this.data[pos]) {
|
||
|
lowerNeighbors.push(adjPos);
|
||
|
}
|
||
|
};
|
||
|
this.forCardinals(pos, check);
|
||
|
if (lowerNeighbors.length > 0) {
|
||
|
const downhill =
|
||
|
lowerNeighbors[this.rng() % lowerNeighbors.length];
|
||
|
return this.drop(downhill);
|
||
|
}
|
||
|
this.forDiagonals(pos, check);
|
||
|
if (lowerNeighbors.length > 0) {
|
||
|
const downhill =
|
||
|
lowerNeighbors[this.rng() % lowerNeighbors.length];
|
||
|
return this.drop(downhill);
|
||
|
}
|
||
|
++this.data[pos];
|
||
|
}
|
||
|
sinkhole(pos) {
|
||
|
const higherNeighbors = [];
|
||
|
this.forNeighbors(pos, (adjPos) => {
|
||
|
if (this.data[adjPos] > this.data[pos]) {
|
||
|
higherNeighbors.push(adjPos);
|
||
|
}
|
||
|
});
|
||
|
if (higherNeighbors.length > 0) {
|
||
|
const uphill =
|
||
|
higherNeighbors[this.rng() % higherNeighbors.length];
|
||
|
return this.sinkhole(uphill);
|
||
|
}
|
||
|
this.data[pos] = Math.max(this.data[pos] - 1, -3);
|
||
|
}
|
||
|
forCardinals(pos, action) {
|
||
|
action((pos - this.width) % this.data.length);
|
||
|
action((pos - 1) % this.data.length);
|
||
|
action((pos + 1) % this.data.length);
|
||
|
action((pos + this.width) % this.data.length);
|
||
|
}
|
||
|
forDiagonals(pos, action) {
|
||
|
action((pos - this.width - 1) % this.data.length);
|
||
|
action((pos - this.width + 1) % this.data.length);
|
||
|
action((pos + this.width - 1) % this.data.length);
|
||
|
action((pos + this.width + 1) % this.data.length);
|
||
|
}
|
||
|
forNeighbors(pos, action) {
|
||
|
this.forCardinals(pos, action);
|
||
|
this.forDiagonals(pos, action);
|
||
|
}
|
||
|
deepenWater() {
|
||
|
for (let i = 0; i < this.data.length; i++) {
|
||
|
if (this.data[i] == WATER) {
|
||
|
let isShore = false;
|
||
|
this.forNeighbors(i, (adjPos) => {
|
||
|
if (this.data[adjPos] > WATER) {
|
||
|
isShore = true;
|
||
|
}
|
||
|
});
|
||
|
if (!isShore) {
|
||
|
this.data[i] = WATER - 1;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
choose(list) {
|
||
|
if (list.length == 0) {
|
||
|
throw new Error("Picking from empty list");
|
||
|
}
|
||
|
return list[this.rng() % list.length];
|
||
|
}
|
||
|
shuffle(list) {
|
||
|
const shuffled = list.slice();
|
||
|
for (let i = 0; i < shuffled.length - 1; i++) {
|
||
|
const swapIndex = (this.rng() % (shuffled.length - i)) + i;
|
||
|
[shuffled[i], shuffled[swapIndex]] = [
|
||
|
shuffled[swapIndex],
|
||
|
shuffled[i],
|
||
|
];
|
||
|
}
|
||
|
return shuffled;
|
||
|
}
|
||
|
dropWithin(tiles) {
|
||
|
if (tiles.length > 0) {
|
||
|
this.drop(this.choose(tiles));
|
||
|
}
|
||
|
}
|
||
|
step() {
|
||
|
this.done = this.generators
|
||
|
.map((generator) => generator())
|
||
|
.every((done) => done);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
// island/render.ts
|
||
|
function renderIslands(islands, cx) {
|
||
|
for (let y = 0; y < islands.height; y++) {
|
||
|
for (let x = 0; x < islands.width; x++) {
|
||
|
const tile = islands.data[islands.xy(x, y)];
|
||
|
switch (tile) {
|
||
|
case -3:
|
||
|
case -2:
|
||
|
case -1:
|
||
|
cx.fillStyle = "#000088";
|
||
|
break;
|
||
|
case WATER:
|
||
|
cx.fillStyle = "blue";
|
||
|
break;
|
||
|
case BEACH:
|
||
|
cx.fillStyle = "yellow";
|
||
|
break;
|
||
|
case LIGHT_FOREST:
|
||
|
cx.fillStyle = "#00ff00";
|
||
|
break;
|
||
|
case DENSE_FOREST:
|
||
|
cx.fillStyle = "#008800";
|
||
|
break;
|
||
|
case MOUNTAIN:
|
||
|
case MOUNTAIN + 1:
|
||
|
case MOUNTAIN + 2:
|
||
|
cx.fillStyle = "#666666";
|
||
|
break;
|
||
|
default:
|
||
|
cx.fillStyle = "#88aaff";
|
||
|
break;
|
||
|
}
|
||
|
cx.fillRect(x, y, 1, 1);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// lib/html.ts
|
||
|
function h(name, props, ...children) {
|
||
|
const element = Object.assign(document.createElement(name), props);
|
||
|
element.append(...children);
|
||
|
return element;
|
||
|
}
|
||
|
function canvas2d(props) {
|
||
|
const canvas = h("canvas", props);
|
||
|
const cx = canvas.getContext("2d");
|
||
|
if (!cx) {
|
||
|
throw new Error("2D rendering context not supported");
|
||
|
}
|
||
|
return [canvas, cx];
|
||
|
}
|
||
|
|
||
|
// island.ts
|
||
|
var BLOWUP = 4;
|
||
|
var WIDTH = 240;
|
||
|
var HEIGHT = 135;
|
||
|
var DEFAULT_SEED = 128;
|
||
|
function IslandApplet() {
|
||
|
let timerId;
|
||
|
let ticks = 0;
|
||
|
let islands = new IslandGrid(WIDTH, HEIGHT, DEFAULT_SEED);
|
||
|
const [canvas, cx] = canvas2d({
|
||
|
width: WIDTH * BLOWUP,
|
||
|
height: HEIGHT * BLOWUP,
|
||
|
});
|
||
|
cx.scale(BLOWUP, BLOWUP);
|
||
|
const seedInput = h("input", {
|
||
|
type: "number",
|
||
|
valueAsNumber: DEFAULT_SEED,
|
||
|
});
|
||
|
const seedLabel = h("label", {}, "Seed:", seedInput);
|
||
|
const generateButton = h(
|
||
|
"button",
|
||
|
{
|
||
|
onclick: () => {
|
||
|
clearInterval(timerId);
|
||
|
ticks = 0;
|
||
|
islands = new IslandGrid(
|
||
|
WIDTH,
|
||
|
HEIGHT,
|
||
|
seedInput.valueAsNumber
|
||
|
);
|
||
|
timerId = setInterval(function tick() {
|
||
|
islands.step();
|
||
|
islands.step();
|
||
|
islands.step();
|
||
|
ticks += 3;
|
||
|
if (islands.done) {
|
||
|
clearInterval(timerId);
|
||
|
islands.deepenWater();
|
||
|
}
|
||
|
renderIslands(islands, cx);
|
||
|
}, 1e3 / 30);
|
||
|
},
|
||
|
},
|
||
|
"Generate"
|
||
|
);
|
||
|
renderIslands(islands, cx);
|
||
|
return [canvas, seedLabel, generateButton];
|
||
|
}
|
||
|
globalThis.IslandApplet = IslandApplet;
|
||
|
})();
|
||
|
|
||
|
document.body.append(...IslandApplet());
|
||
|
</script>
|
||
|
</body>
|
||
|
</html>
|