782 lines
20 KiB
TypeScript
782 lines
20 KiB
TypeScript
import { Architecture, LoadedNewMap } from "./newmap.ts";
|
|
import { Grid, Point, Rect, Size } from "./engine/datatypes.ts";
|
|
import { choose, shuffle } from "./utils.ts";
|
|
import { standardVaultTemplates, VaultTemplate } from "./vaulttemplate.ts";
|
|
import { ALL_STATS } from "./datatypes.ts";
|
|
import {
|
|
BreakableBlockPickup,
|
|
ExperiencePickupCallbacks,
|
|
LadderPickup,
|
|
LockPickup,
|
|
StatPickupCallbacks,
|
|
ThrallItemPickup,
|
|
ThrallPickup,
|
|
} from "./pickups.ts";
|
|
import { getPlayerProgress } from "./playerprogress.ts";
|
|
import { ItemStage } from "./thralls.ts";
|
|
import { Microtheme } from "./colors.ts";
|
|
|
|
const WIDTH = 19;
|
|
const HEIGHT = 19;
|
|
|
|
const MIN_VAULTS = 1;
|
|
const MAX_VAULTS = 2;
|
|
const NUM_VAULT_TRIES = 90;
|
|
const NUM_ROOM_TRIES = 90;
|
|
const NUM_STAIRCASE_TRIES = 90;
|
|
const NUM_STAIRCASES_DESIRED = 3;
|
|
const NUM_ROOMS_DESIRED = 1;
|
|
|
|
const EXTRA_CONNECTOR_CHANCE = 0.15;
|
|
const WINDING_PERCENT = 50;
|
|
|
|
// This is an implementation of Nystrom's algorithm:
|
|
// https://journal.stuffwithstuff.com/2014/12/21/rooms-and-mazes/
|
|
class Knife {
|
|
#map: LoadedNewMap;
|
|
#region: number;
|
|
#regions: Grid<number | null>;
|
|
#sealedWalls: Grid<boolean>;
|
|
|
|
constructor(
|
|
map: LoadedNewMap,
|
|
regions: Grid<number | null>,
|
|
sealedWalls: Grid<boolean>,
|
|
) {
|
|
this.#map = map;
|
|
this.#region = -1;
|
|
this.#regions = regions;
|
|
this.#sealedWalls = sealedWalls;
|
|
}
|
|
|
|
get map(): LoadedNewMap {
|
|
return this.#map;
|
|
}
|
|
|
|
get region(): number {
|
|
return this.#region;
|
|
}
|
|
|
|
get regions(): Grid<number | null> {
|
|
return this.#regions;
|
|
}
|
|
|
|
get sealedWalls(): Grid<boolean> {
|
|
return this.#sealedWalls;
|
|
}
|
|
|
|
startRegion() {
|
|
this.#region += 1;
|
|
}
|
|
|
|
carve(point: Point, theme?: Microtheme | null, label?: string) {
|
|
this.#regions.set(point, this.#region);
|
|
this.map.get(point).architecture = Architecture.Floor;
|
|
this.map.get(point).microtheme = theme ?? null;
|
|
this.map.get(point).zoneLabel = label ?? null;
|
|
}
|
|
|
|
carveRoom(
|
|
room: Rect,
|
|
protect?: boolean,
|
|
theme?: Microtheme | null,
|
|
label?: string,
|
|
) {
|
|
for (let y = room.top.y; y < room.top.y + room.size.h; y++) {
|
|
for (let x = room.top.x; x < room.top.x + room.size.w; x++) {
|
|
this.carve(new Point(x, y), theme, label);
|
|
}
|
|
}
|
|
|
|
if (protect ?? false) {
|
|
for (let y = room.top.y - 1; y < room.top.y + room.size.h + 1; y++) {
|
|
for (let x = room.top.x - 1; x < room.top.x + room.size.w + 1; x++) {
|
|
this.#sealedWalls.set(new Point(x, y), true);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
showDebug(merged: Record<number, number>) {
|
|
if (true) {
|
|
let out = "";
|
|
let errors: string[] = [];
|
|
const size = this.#regions.size;
|
|
for (let y = 0; y < size.h; y++) {
|
|
for (let x = 0; x < size.w; x++) {
|
|
const loc = new Point(x, y);
|
|
out += (() => {
|
|
if (this.#map.get(loc).architecture == Architecture.Wall) {
|
|
return this.#sealedWalls.get(loc) ? "◘" : "█";
|
|
}
|
|
let r = this.#regions.get(loc);
|
|
if (typeof r === "number") {
|
|
const resolved = merged[r];
|
|
if (typeof resolved === "number") {
|
|
r = resolved;
|
|
} else {
|
|
errors.push(`${loc} is region ${r}, not found in merged`);
|
|
}
|
|
if (r < 0) {
|
|
return "!";
|
|
}
|
|
// 0...9 and lowercase
|
|
if (r < 36) {
|
|
return r.toString(36);
|
|
}
|
|
// uppercase
|
|
r -= 26;
|
|
if (r < 36) {
|
|
return r.toString(36).toUpperCase();
|
|
}
|
|
// Greek lowercase
|
|
r -= 36;
|
|
if (r < 25) {
|
|
return String.fromCodePoint(r + 0x3b1);
|
|
}
|
|
// Greek uppercase (there is a hole at 0x3a2)
|
|
r -= 25;
|
|
if (r < 17) {
|
|
return String.fromCodePoint(r + 0x391);
|
|
}
|
|
r -= 17;
|
|
if (r < 7) {
|
|
return String.fromCodePoint(r + 0x3a3);
|
|
}
|
|
// Hebrew
|
|
r -= 7;
|
|
if (r < 27) {
|
|
return String.fromCodePoint(r + 0x5d0);
|
|
}
|
|
// give up
|
|
return "?";
|
|
}
|
|
return "."; // room without region
|
|
})();
|
|
}
|
|
out += "\n";
|
|
}
|
|
console.log(out);
|
|
if (errors.length > 0) {
|
|
console.log(`uh-oh: \n\t${errors.join("\n\t")}`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
export function generateMap(): LoadedNewMap {
|
|
for (let i = 0; i < 1000; i++) {
|
|
try {
|
|
return tryGenerateMap(standardVaultTemplates);
|
|
} catch (e) {
|
|
if (e instanceof TryAgainException) {
|
|
continue;
|
|
}
|
|
if (e instanceof BadMapError) {
|
|
console.log(`Bad map generated: ${e.message}:`);
|
|
showDebug(e.badMap);
|
|
// continue;
|
|
}
|
|
throw e;
|
|
}
|
|
}
|
|
throw new Error("couldn't generate map in 1000 attempts");
|
|
}
|
|
export function tryGenerateMap(vaultTemplates: VaultTemplate[]): LoadedNewMap {
|
|
let width = WIDTH;
|
|
let height = HEIGHT;
|
|
if (width % 2 == 0 || height % 2 == 0) {
|
|
throw new Error("map bounds must be odd-sized");
|
|
}
|
|
|
|
let grid = new LoadedNewMap("generated", new Size(width, height));
|
|
|
|
let regions: Grid<number | null> = new Grid(grid.size, () => null);
|
|
let sealedWalls: Grid<boolean> = new Grid(grid.size, () => false);
|
|
let knife = new Knife(grid, regions, sealedWalls);
|
|
|
|
let rooms = addRooms(knife, vaultTemplates);
|
|
showDebug(grid);
|
|
|
|
for (let y = 1; y < grid.size.h; y += 2) {
|
|
for (let x = 1; x < grid.size.w; x += 2) {
|
|
let pos = new Point(x, y);
|
|
if (grid.get(pos).architecture == Architecture.Wall) {
|
|
growMaze(knife, pos);
|
|
}
|
|
}
|
|
}
|
|
|
|
showDebug(grid);
|
|
|
|
connectRegions(knife);
|
|
removeDeadEnds(knife);
|
|
|
|
for (let r of rooms.values()) {
|
|
decorateRoom(grid, r);
|
|
}
|
|
|
|
showDebug(grid);
|
|
|
|
return grid;
|
|
}
|
|
|
|
class RoomChain {
|
|
#size: Size;
|
|
rooms: Rect[];
|
|
|
|
constructor(size: Size) {
|
|
this.#size = size;
|
|
this.rooms = [];
|
|
}
|
|
|
|
reserve(width: number, height: number): Rect | null {
|
|
let x = randrange(0, Math.floor((this.#size.w - width) / 2)) * 2 + 1;
|
|
let y = randrange(0, Math.floor((this.#size.h - height) / 2)) * 2 + 1;
|
|
|
|
let room = new Rect(new Point(x, y), new Size(width, height));
|
|
|
|
for (let other of this.rooms.values()) {
|
|
if (room.overlaps(other)) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
this.rooms.push(room);
|
|
return room;
|
|
}
|
|
}
|
|
|
|
function addRooms(knife: Knife, vaultTemplates: VaultTemplate[]): Rect[] {
|
|
vaultTemplates = [...vaultTemplates]; // so we can mutate it
|
|
shuffle(vaultTemplates);
|
|
let chain = new RoomChain(knife.map.size);
|
|
let nVaults = 0;
|
|
let nVaultsDesired = randrange(MIN_VAULTS, MAX_VAULTS + 1);
|
|
|
|
for (
|
|
let i = 0;
|
|
vaultTemplates.length > 0 &&
|
|
nVaults < nVaultsDesired &&
|
|
i < NUM_VAULT_TRIES;
|
|
i += 1
|
|
) {
|
|
let width = 7;
|
|
let height = 7;
|
|
|
|
let room = chain.reserve(width, height);
|
|
|
|
if (!room) {
|
|
continue;
|
|
}
|
|
|
|
nVaults += 1;
|
|
carveVault(knife, room, vaultTemplates.pop()!);
|
|
}
|
|
|
|
// staircases
|
|
let nStaircases = 0;
|
|
let nStaircasesDesired = NUM_STAIRCASES_DESIRED;
|
|
for (
|
|
let i = 0;
|
|
nStaircases < nStaircasesDesired && i < NUM_STAIRCASE_TRIES;
|
|
i += 1
|
|
) {
|
|
let width = 3;
|
|
let height = 3;
|
|
|
|
let room = chain.reserve(width, height);
|
|
if (!room) {
|
|
continue;
|
|
}
|
|
nStaircases += 1;
|
|
carveStaircase(knife, room, nStaircases - 1);
|
|
}
|
|
|
|
if (nStaircases == 0) {
|
|
throw new TryAgainException("couldn't make any staircases");
|
|
}
|
|
|
|
// rooms
|
|
let nRooms = 0;
|
|
let nRoomsDesired = NUM_ROOMS_DESIRED;
|
|
for (let i = 0; nRooms < nRoomsDesired && i < NUM_ROOM_TRIES; i += 1) {
|
|
let [width, height] = choose([
|
|
[3, 5],
|
|
[5, 3],
|
|
]);
|
|
|
|
let room = chain.reserve(width, height);
|
|
|
|
if (!room) {
|
|
continue;
|
|
}
|
|
nRooms += 1;
|
|
|
|
carveRoom(knife, room);
|
|
}
|
|
return chain.rooms;
|
|
}
|
|
|
|
function carveVault(knife: Knife, room: Rect, vaultTemplate: VaultTemplate) {
|
|
if (room.size.w != 7 || room.size.h != 7) {
|
|
throw new Error("room must be 7x7");
|
|
}
|
|
|
|
let quad0 = new Rect(room.top, new Size(3, 3));
|
|
let quad1 = new Rect(room.top.offset(new Point(4, 0)), new Size(3, 3));
|
|
let quad2 = new Rect(room.top.offset(new Point(4, 4)), new Size(3, 3));
|
|
let quad3 = new Rect(room.top.offset(new Point(0, 4)), new Size(3, 3));
|
|
|
|
let [a, b, c, d] = choose([
|
|
[quad0, quad1, quad2, quad3],
|
|
[quad1, quad2, quad3, quad0],
|
|
[quad2, quad3, quad0, quad1],
|
|
[quad3, quad0, quad1, quad2],
|
|
[quad3, quad2, quad1, quad0],
|
|
[quad2, quad1, quad0, quad3],
|
|
[quad1, quad0, quad3, quad2],
|
|
[quad0, quad3, quad2, quad1],
|
|
]);
|
|
|
|
let ab = mergeRects(a, b);
|
|
|
|
knife.startRegion();
|
|
knife.carveRoom(
|
|
ab,
|
|
false,
|
|
vaultTemplate.microtheme(),
|
|
vaultTemplate.roomLabels.hall,
|
|
);
|
|
knife.carveRoom(
|
|
c,
|
|
true,
|
|
vaultTemplate.microtheme(),
|
|
vaultTemplate.roomLabels.backroom,
|
|
);
|
|
knife.carveRoom(
|
|
d,
|
|
true,
|
|
vaultTemplate.microtheme(),
|
|
vaultTemplate.roomLabels.closet,
|
|
);
|
|
|
|
// now place standard pickups
|
|
for (let dy = 0; dy < ab.size.h; dy++) {
|
|
for (let dx = 0; dx < ab.size.w; dx++) {
|
|
// this is a ratio of 14 to 6
|
|
let xy = ab.top.offset(new Point(dx, dy));
|
|
let stat = vaultTemplate.stats.secondary;
|
|
if (dx == 0 || dy == 0 || dx == ab.size.w - 1 || dy == ab.size.h - 1) {
|
|
stat = vaultTemplate.stats.primary;
|
|
}
|
|
if (!(a.contains(xy) || b.contains(xy))) {
|
|
stat = vaultTemplate.stats.secondary;
|
|
}
|
|
knife.map.get(xy).pickup = new BreakableBlockPickup(
|
|
new StatPickupCallbacks(stat),
|
|
);
|
|
}
|
|
}
|
|
|
|
for (let dy = 0; dy < c.size.h; dy++) {
|
|
for (let dx = 0; dx < c.size.w; dx++) {
|
|
let xy = c.top.offset(new Point(dx, dy));
|
|
knife.map.get(xy).pickup = new BreakableBlockPickup(
|
|
new StatPickupCallbacks(vaultTemplate.stats.primary),
|
|
);
|
|
}
|
|
}
|
|
|
|
for (let dy = 0; dy < d.size.h; dy++) {
|
|
for (let dx = 0; dx < d.size.w; dx++) {
|
|
let xy = d.top.offset(new Point(dx, dy));
|
|
knife.map.get(xy).pickup = new BreakableBlockPickup(
|
|
new StatPickupCallbacks(vaultTemplate.stats.primary),
|
|
);
|
|
}
|
|
}
|
|
|
|
// now build connectors
|
|
let connectors = [
|
|
new Point(3, 1),
|
|
new Point(5, 3),
|
|
new Point(3, 5),
|
|
new Point(1, 3),
|
|
];
|
|
for (let offset of connectors.values()) {
|
|
let connector = room.top.offset(offset);
|
|
|
|
if (mergeRects(b, c).contains(connector)) {
|
|
// TODO: Put check 1 here
|
|
let check = vaultTemplate.checks[0];
|
|
if (check != null) {
|
|
knife.map.get(connector).pickup = new LockPickup(check);
|
|
}
|
|
knife.carve(
|
|
connector,
|
|
vaultTemplate.microtheme(),
|
|
vaultTemplate.roomLabels.backroom,
|
|
);
|
|
}
|
|
if (mergeRects(c, d).contains(connector)) {
|
|
// TODO: Put check 2 here
|
|
let check = vaultTemplate.checks[1];
|
|
if (check != null) {
|
|
knife.map.get(connector).pickup = new LockPickup(check);
|
|
}
|
|
knife.carve(
|
|
connector,
|
|
vaultTemplate.microtheme(),
|
|
vaultTemplate.roomLabels.closet,
|
|
);
|
|
}
|
|
}
|
|
|
|
// now place goodies
|
|
let goodies = [
|
|
new Point(1, 1),
|
|
new Point(5, 1),
|
|
new Point(1, 5),
|
|
new Point(5, 5),
|
|
];
|
|
for (let offset of goodies.values()) {
|
|
let goodie = room.top.offset(offset);
|
|
let cell = knife.map.get(goodie);
|
|
|
|
if (a.contains(goodie)) {
|
|
cell.pickup = new BreakableBlockPickup(new ExperiencePickupCallbacks());
|
|
let thrall = vaultTemplate.thrall();
|
|
if (!getPlayerProgress().isThrallUnlocked(thrall)) {
|
|
cell.pickup = new ThrallPickup(thrall);
|
|
}
|
|
}
|
|
|
|
if (b.contains(goodie)) {
|
|
cell.pickup = new BreakableBlockPickup(new ExperiencePickupCallbacks());
|
|
}
|
|
|
|
if (c.contains(goodie)) {
|
|
cell.pickup = new BreakableBlockPickup(new ExperiencePickupCallbacks());
|
|
// TODO: Fill this room with the common item for this room
|
|
}
|
|
|
|
if (d.contains(goodie)) {
|
|
cell.pickup = new BreakableBlockPickup(new ExperiencePickupCallbacks());
|
|
|
|
// replace with a fancy item if nothing is eligible
|
|
let thrallItem = vaultTemplate.thrallItem();
|
|
let stage = getPlayerProgress().getThrallItemStage(thrallItem);
|
|
if (stage == ItemStage.Untouched) {
|
|
cell.pickup = new ThrallItemPickup(thrallItem);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function carveStaircase(knife: Knife, room: Rect, ix: number) {
|
|
carveRoom(knife, room, null, "Stairwell");
|
|
|
|
let x = Math.floor(room.top.x + room.size.w / 2);
|
|
let y = Math.floor(room.top.y + room.size.h / 2);
|
|
let center = new Point(x, y);
|
|
|
|
if (ix == 0) {
|
|
// first staircase is the player entrance
|
|
knife.map.entrance = center;
|
|
knife.map.get(center).pickup = null;
|
|
} else {
|
|
knife.map.get(center).pickup = new LadderPickup();
|
|
}
|
|
}
|
|
|
|
function carveRoom(
|
|
knife: Knife,
|
|
room: Rect,
|
|
theme?: Microtheme | null,
|
|
label?: string,
|
|
) {
|
|
knife.startRegion();
|
|
knife.carveRoom(room, false, theme, label);
|
|
|
|
for (let dy = 0; dy < Math.ceil(room.size.h / 2); dy++) {
|
|
for (let dx = 0; dx < Math.ceil(room.size.w / 2); dx++) {
|
|
let xy0 = room.top.offset(new Point(dx, dy));
|
|
let xy1 = room.top.offset(new Point(room.size.w - dx - 1, dy));
|
|
let xy2 = room.top.offset(new Point(dx, room.size.h - dy - 1));
|
|
let xy3 = room.top.offset(
|
|
new Point(room.size.w - dx - 1, room.size.h - dy - 1),
|
|
);
|
|
let stat = choose(ALL_STATS);
|
|
let cb = choose([
|
|
() => new StatPickupCallbacks(stat),
|
|
() => new StatPickupCallbacks(stat),
|
|
() => new StatPickupCallbacks(stat),
|
|
() => new ExperiencePickupCallbacks(),
|
|
]);
|
|
knife.map.get(xy0).pickup = new BreakableBlockPickup(cb());
|
|
knife.map.get(xy1).pickup = new BreakableBlockPickup(cb());
|
|
knife.map.get(xy2).pickup = new BreakableBlockPickup(cb());
|
|
knife.map.get(xy3).pickup = new BreakableBlockPickup(cb());
|
|
}
|
|
}
|
|
}
|
|
|
|
let mergeRects = (a: Rect, b: Rect) => {
|
|
let abx0 = Math.min(a.top.x, b.top.x);
|
|
let aby0 = Math.min(a.top.y, b.top.y);
|
|
let abx1 = Math.max(a.top.x + a.size.w, b.top.x + b.size.w);
|
|
let aby1 = Math.max(a.top.y + a.size.h, b.top.y + b.size.h);
|
|
|
|
return new Rect(new Point(abx0, aby0), new Size(abx1 - abx0, aby1 - aby0));
|
|
};
|
|
|
|
export const CARDINAL_DIRECTIONS = [
|
|
new Point(-1, 0),
|
|
new Point(0, -1),
|
|
new Point(1, 0),
|
|
new Point(0, 1),
|
|
];
|
|
|
|
function connectRegions(knife: Knife) {
|
|
// this procedure is really complicated
|
|
// so please read here: https://github.com/munificent/hauberk/blob/db360d9efa714efb6d937c31953ef849c7394a39/lib/src/content/dungeon.dart#L173
|
|
let connectorRegions: Grid<number[]> = new Grid(knife.map.size, () => []);
|
|
let connectors: Point[] = [];
|
|
|
|
for (let y = 1; y < knife.map.size.h - 1; y++) {
|
|
for (let x = 1; x < knife.map.size.w - 1; x++) {
|
|
let pos = new Point(x, y);
|
|
if (knife.sealedWalls.get(pos)) {
|
|
continue;
|
|
}
|
|
if (knife.map.get(pos).architecture != Architecture.Wall) {
|
|
continue;
|
|
}
|
|
|
|
let regions = [];
|
|
for (let offset of CARDINAL_DIRECTIONS.values()) {
|
|
let region = knife.regions.get(pos.offset(offset));
|
|
if (region != null) {
|
|
regions.push(region);
|
|
}
|
|
}
|
|
regions = dedup(regions);
|
|
if (regions.length < 2) {
|
|
continue;
|
|
}
|
|
|
|
connectorRegions.set(pos, regions);
|
|
connectors.push(pos);
|
|
}
|
|
}
|
|
|
|
// map from original index to "region it has been merged to" index
|
|
let merged: Record<number, number> = {};
|
|
let openRegions = [];
|
|
for (let i = 0; i <= knife.region; i++) {
|
|
merged[i] = i;
|
|
openRegions.push(i);
|
|
}
|
|
|
|
let iter = 0;
|
|
|
|
while (openRegions.length > 1) {
|
|
if (iter > 100) {
|
|
throw new TryAgainException(
|
|
"algorithm was not quiescent for some reason",
|
|
);
|
|
}
|
|
iter++;
|
|
knife.showDebug(merged);
|
|
if (connectors.length == 0) {
|
|
throw new TryAgainException(
|
|
"couldn't figure out how to connect sections",
|
|
);
|
|
}
|
|
let connector = choose(connectors);
|
|
|
|
// create the connections
|
|
knife.map.get(connector).architecture = Architecture.Floor;
|
|
let basicRegions: number[] = connectorRegions.get(connector);
|
|
let sources: number[] = dedup(basicRegions.map((i) => merged[i]));
|
|
let dest: number | undefined = sources.pop();
|
|
if (dest == undefined) {
|
|
throw new BadMapError(
|
|
`each connector should touch more than one region but ${connector} does not`,
|
|
knife.map,
|
|
);
|
|
}
|
|
|
|
if (Math.random() > EXTRA_CONNECTOR_CHANCE) {
|
|
// at random, don't regard them as merged
|
|
for (let i = 0; i <= knife.region; i++) {
|
|
if (sources.indexOf(merged[i]) != -1) {
|
|
merged[i] = dest;
|
|
}
|
|
}
|
|
|
|
for (let src of sources.values()) {
|
|
let ix = openRegions.indexOf(src);
|
|
if (ix != -1) {
|
|
openRegions.splice(ix, 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
let connectors2 = [];
|
|
for (let other of connectors.values()) {
|
|
if (other.manhattan(connector) == 1) {
|
|
continue;
|
|
}
|
|
|
|
let connected = dedup(connectorRegions.get(other).map((m) => merged[m]));
|
|
if (connected.length <= 1) {
|
|
continue;
|
|
}
|
|
|
|
connectors2.push(other);
|
|
}
|
|
connectors = connectors2;
|
|
}
|
|
knife.showDebug(merged);
|
|
|
|
// The map should now be fully connected.
|
|
if (!knife.map.isConnected()) {
|
|
throw new BadMapError("unconnected", knife.map);
|
|
}
|
|
}
|
|
|
|
function growMaze(knife: Knife, start: Point) {
|
|
let cells: Point[] = [];
|
|
let lastDir: Point | null = null;
|
|
|
|
knife.startRegion();
|
|
knife.carve(start);
|
|
|
|
cells.push(start);
|
|
while (cells.length > 0) {
|
|
let cell = cells[cells.length - 1];
|
|
|
|
let unmadeCells: Point[] = [];
|
|
let lastDirOk = false;
|
|
for (let dir of CARDINAL_DIRECTIONS.values()) {
|
|
if (canCarve(knife, cell, dir)) {
|
|
unmadeCells.push(dir);
|
|
if (lastDir != null && dir.equals(lastDir)) {
|
|
lastDirOk = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (unmadeCells.length == 0) {
|
|
cells.pop();
|
|
lastDir = null;
|
|
continue;
|
|
}
|
|
|
|
let dir: Point;
|
|
if (lastDirOk && randrange(0, 100) > WINDING_PERCENT) {
|
|
dir = lastDir!;
|
|
} else {
|
|
dir = choose(unmadeCells); // TODO: Constrain windiness as Nystrom did
|
|
}
|
|
|
|
let c1 = cell.offset(dir);
|
|
let c2 = cell.offset(dir).offset(dir);
|
|
knife.carve(c1);
|
|
knife.carve(c2);
|
|
cells.push(c2);
|
|
lastDir = dir;
|
|
}
|
|
}
|
|
|
|
function canCarve(knife: Knife, pos: Point, direction: Point) {
|
|
let c2 = pos.offset(direction).offset(direction);
|
|
let c3 = c2.offset(direction);
|
|
let rect = new Rect(new Point(0, 0), knife.map.size);
|
|
if (!rect.contains(c3)) {
|
|
return false;
|
|
}
|
|
|
|
return knife.map.get(c2).architecture == Architecture.Wall;
|
|
}
|
|
|
|
function removeDeadEnds(knife: Knife) {
|
|
let done = false;
|
|
|
|
while (!done) {
|
|
done = true;
|
|
|
|
for (let y = 1; y < knife.map.size.h - 1; y++) {
|
|
for (let x = 1; x < knife.map.size.w - 1; x++) {
|
|
let xy = new Point(x, y);
|
|
if (knife.map.get(xy).architecture == Architecture.Wall) {
|
|
continue;
|
|
}
|
|
|
|
let exits = 0;
|
|
for (let dir of CARDINAL_DIRECTIONS.values()) {
|
|
if (knife.map.get(xy.offset(dir)).architecture != Architecture.Wall) {
|
|
exits++;
|
|
}
|
|
}
|
|
|
|
if (exits != 1) {
|
|
continue;
|
|
}
|
|
|
|
done = false;
|
|
knife.map.get(xy).architecture = Architecture.Wall;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function decorateRoom(_map: LoadedNewMap, _rect: Rect) {}
|
|
|
|
function randrange(lo: number, hi: number) {
|
|
if (lo >= hi) {
|
|
throw new Error(`randrange: hi must be >= lo, ${hi}, ${lo}`);
|
|
}
|
|
|
|
return lo + Math.floor(Math.random() * (hi - lo));
|
|
}
|
|
|
|
function dedup(items: number[]): number[] {
|
|
let deduped = [];
|
|
for (let i of items.values()) {
|
|
if (deduped.indexOf(i) != -1) {
|
|
continue;
|
|
}
|
|
deduped.push(i);
|
|
}
|
|
return deduped;
|
|
}
|
|
|
|
function showDebug(grid: LoadedNewMap) {
|
|
if (true) {
|
|
let out = "";
|
|
for (let y = 0; y < grid.size.h; y++) {
|
|
for (let x = 0; x < grid.size.w; x++) {
|
|
out +=
|
|
grid.get(new Point(x, y)).architecture == Architecture.Wall
|
|
? "#"
|
|
: ".";
|
|
}
|
|
out += "\n";
|
|
}
|
|
console.log(out);
|
|
}
|
|
}
|
|
|
|
class TryAgainException extends Error {}
|
|
class BadMapError extends Error {
|
|
badMap: LoadedNewMap;
|
|
|
|
constructor(msg: string, badMap: LoadedNewMap) {
|
|
super(msg);
|
|
this.badMap = badMap;
|
|
}
|
|
}
|