Replace map generator again
This commit is contained in:
parent
b20f2760d4
commit
b1ac26fa78
Binary file not shown.
Before Width: | Height: | Size: 179 B After Width: | Height: | Size: 171 B |
@ -55,6 +55,10 @@ export class Point {
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return `${this.x},${this.y}`
|
||||
}
|
||||
|
||||
offset(other: Point | Size): Point {
|
||||
if (other instanceof Point) {
|
||||
return new Point(this.x + other.x, this.y + other.y);
|
||||
@ -81,6 +85,10 @@ export class Point {
|
||||
subtract(top: Point): Size {
|
||||
return new Size(this.x - top.x, this.y - top.y);
|
||||
}
|
||||
|
||||
manhattan(other: Point) {
|
||||
return Math.abs(this.x - other.x) + Math.abs(this.y - other.y);
|
||||
}
|
||||
}
|
||||
|
||||
export class Size {
|
||||
@ -121,6 +129,20 @@ export class Rect {
|
||||
contains(other: Point) {
|
||||
return (other.x >= this.top.x && other.y >= this.top.y && other.x < this.top.x + this.size.w && other.y < this.top.y + this.size.h);
|
||||
}
|
||||
|
||||
overlaps(other: Rect) {
|
||||
let ax0 = this.top.x;
|
||||
let ay0 = this.top.y;
|
||||
let ax1 = ax0 + this.size.w;
|
||||
let ay1 = ay0 + this.size.h;
|
||||
let bx0 = other.top.x;
|
||||
let by0 = other.top.y;
|
||||
let bx1 = bx0 + other.size.w;
|
||||
let by1 = by0 + other.size.h;
|
||||
|
||||
let noOverlap = ax0 > bx1 || bx0 > ax1 || ay0 > by1 || by0 > ay1;
|
||||
return !noOverlap;
|
||||
}
|
||||
}
|
||||
|
||||
export class Grid<T> {
|
||||
@ -201,7 +223,7 @@ export class Grid<T> {
|
||||
(position.x < 0 || position.x >= this.size.w || Math.floor(position.x) != position.x) ||
|
||||
(position.y < 0 || position.y >= this.size.h || Math.floor(position.y) != position.y)
|
||||
) {
|
||||
throw `invalid position for ${this.size}: ${position}`
|
||||
throw new Error(`invalid position for ${this.size}: ${position}`)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,8 +1,8 @@
|
||||
import {Point, Rect, Size} from "./engine/datatypes.ts";
|
||||
import {Point} from "./engine/datatypes.ts";
|
||||
import {ALL_STATS, Stat} from "./datatypes.ts";
|
||||
import {DrawPile} from "./drawpile.ts";
|
||||
import {D} from "./engine/public.ts";
|
||||
import {sprRaccoonWalking, sprResourcePickup, sprStatPickup} from "./sprites.ts";
|
||||
import {sprLadder, sprRaccoonWalking, sprResourcePickup, sprStatPickup} from "./sprites.ts";
|
||||
import {
|
||||
BG_INSET,
|
||||
BG_WALL_OR_UNREVEALED,
|
||||
@ -14,6 +14,7 @@ import {getPlayerProgress} from "./playerprogress.ts";
|
||||
import {Architecture, LoadedNewMap} from "./newmap.ts";
|
||||
import {FLOOR_CELL_SIZE, GridArt} from "./gridart.ts";
|
||||
import {shadowcast} from "./shadowcast.ts";
|
||||
import {generateMap} from "./mapgen.ts";
|
||||
|
||||
|
||||
export class HuntMode {
|
||||
@ -41,13 +42,6 @@ export class HuntMode {
|
||||
#collectResources() {
|
||||
let cell = this.map.get(this.player);
|
||||
|
||||
/*
|
||||
if (present.content.type == "stairs") {
|
||||
getPlayerProgress().addBlood(1000);
|
||||
initHuntMode(new HuntMode(this.depth + 1));
|
||||
}
|
||||
*/
|
||||
|
||||
let pickup = cell.pickup;
|
||||
if (pickup != null) {
|
||||
switch (pickup) {
|
||||
@ -59,9 +53,13 @@ export class HuntMode {
|
||||
getPlayerProgress().purloinItem();
|
||||
break;
|
||||
case "EXP":
|
||||
getPlayerProgress().addExperience(25);
|
||||
getPlayerProgress().addExperience(250);
|
||||
getPlayerProgress().purloinItem();
|
||||
break;
|
||||
case "Ladder":
|
||||
getPlayerProgress().addBlood(1000);
|
||||
initHuntMode(new HuntMode(this.depth + 1, generateMap()));
|
||||
break;
|
||||
default:
|
||||
throw `not sure how to handle ${pickup}`
|
||||
}
|
||||
@ -84,6 +82,7 @@ export class HuntMode {
|
||||
if (dist != 1) { return null; }
|
||||
|
||||
let pickup = present.pickup;
|
||||
if (pickup == "Ladder") { return 0; }
|
||||
if (pickup == null) { return 10; }
|
||||
return 100; // any other pickup (EXP, stats, etc)
|
||||
}
|
||||
@ -127,7 +126,8 @@ export class HuntMode {
|
||||
shadowcast(
|
||||
[this.player.x, this.player.y],
|
||||
([x, y]: [number, number]): boolean => {
|
||||
return this.map.get(new Point(x, y)).architecture == Architecture.Wall;
|
||||
let cell = this.map.get(new Point(x, y));
|
||||
return cell.architecture == Architecture.Wall || ALL_STATS.indexOf(cell.pickup as Stat) != -1;
|
||||
},
|
||||
([x, y]: [number, number]) => {
|
||||
let dx = x - this.player.x;
|
||||
@ -183,6 +183,13 @@ export class HuntMode {
|
||||
(hover: boolean) => {
|
||||
gridArt.drawFloor(hover ? FG_TEXT : BG_INSET)
|
||||
|
||||
if (cellData.pickup == "Ladder") {
|
||||
D.drawSprite(sprLadder, gridArt.project(0.0), 0, {
|
||||
xScale: 2.0,
|
||||
yScale: 2.0,
|
||||
})
|
||||
}
|
||||
|
||||
/*
|
||||
// TODO: Stairs
|
||||
if (cellData.content.type == "stairs") {
|
||||
|
579
src/mapgen.ts
Normal file
579
src/mapgen.ts
Normal file
@ -0,0 +1,579 @@
|
||||
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";
|
||||
|
||||
const WIDTH = 19;
|
||||
const HEIGHT = 19;
|
||||
|
||||
const MIN_VAULTS = 1;
|
||||
const MAX_VAULTS = 1;
|
||||
const NUM_VAULT_TRIES = 90;
|
||||
const NUM_ROOM_TRIES = 90;
|
||||
const NUM_STAIRCASE_TRIES = 90;
|
||||
const NUM_STAIRCASES_DESIRED = 3
|
||||
const NUM_ROOMS_DESIRED = 4;
|
||||
|
||||
const EXTRA_CONNECTOR_CHANCE = 0.15;
|
||||
const WINDING_PERCENT = 0;
|
||||
|
||||
// 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) {
|
||||
this.#regions.set(point, this.#region)
|
||||
this.map.get(point).architecture = Architecture.Floor;
|
||||
}
|
||||
|
||||
carveRoom(room: Rect, protect?: boolean) {
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function generateMap(): LoadedNewMap {
|
||||
for (let i= 0; i < 1000; i++) {
|
||||
try {
|
||||
return tryGenerateMap(standardVaultTemplates)
|
||||
} catch (e) {
|
||||
if (e instanceof TryAgainException) {
|
||||
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 "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);
|
||||
knife.carveRoom(c, true);
|
||||
knife.carveRoom(d, true);
|
||||
|
||||
// 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 = 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 = 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 = 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
|
||||
knife.carve(connector)
|
||||
}
|
||||
if (mergeRects(c, d).contains(connector)) {
|
||||
// TODO: Put check 2 here
|
||||
knife.carve(connector)
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
|
||||
if (a.contains(goodie)) {
|
||||
// TODO: Place the zone's NPC here
|
||||
}
|
||||
|
||||
if (b.contains(goodie)) {
|
||||
knife.map.setPickup(goodie, "EXP");
|
||||
}
|
||||
|
||||
if (c.contains(goodie)) {
|
||||
knife.map.setPickup(goodie, "EXP");
|
||||
// TODO: Fill this room with the common item for this room
|
||||
}
|
||||
|
||||
if (d.contains(goodie)) {
|
||||
// TOOD: Put a fancy item here
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function carveStaircase(knife: Knife, room: Rect, ix: number) {
|
||||
carveRoom(knife, room);
|
||||
|
||||
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 = "Ladder";
|
||||
}
|
||||
}
|
||||
|
||||
function carveRoom(knife: Knife, room: Rect) {
|
||||
knife.startRegion();
|
||||
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++) {
|
||||
knife.carve(new Point(x, y));
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
knife.map.get(xy0).pickup = stat;
|
||||
knife.map.get(xy1).pickup = stat;
|
||||
knife.map.get(xy2).pickup = stat;
|
||||
knife.map.get(xy3).pickup = stat;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
);
|
||||
}
|
||||
|
||||
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++;
|
||||
showDebug(knife.map);
|
||||
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 "each connector should touch more than one region"
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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 `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 {
|
||||
|
||||
}
|
@ -6,7 +6,7 @@ import {VNScene} from "./vnscene.ts";
|
||||
export type Province = "a" | "b" | "c";
|
||||
export type Check = "1" | "2";
|
||||
export type Progress = Stat | Resource
|
||||
export type Pickup = Progress; // TODO: Items
|
||||
export type Pickup = Progress | "Ladder"; // TODO: Items
|
||||
export type NewMapInput = {
|
||||
id: string,
|
||||
data: {
|
||||
|
@ -46,6 +46,6 @@ export let sprDrips = new Sprite(
|
||||
);
|
||||
|
||||
export let sprLadder = new Sprite(
|
||||
imgLadder, new Size(32, 24), new Point(0, 0),
|
||||
imgLadder, new Size(16, 16), new Point(8, 8),
|
||||
new Size(1, 1), 1
|
||||
);
|
||||
|
@ -5,7 +5,7 @@ import {getVNModal} from "./vnmodal.ts";
|
||||
import {getScorer} from "./scorer.ts";
|
||||
import {getEndgameModal} from "./endgamemodal.ts";
|
||||
import {SuccessorOption, Wish} from "./datatypes.ts";
|
||||
import mapHub from "./newmaps/hub/map.ts";
|
||||
import {generateMap} from "./mapgen.ts";
|
||||
|
||||
const N_TURNS: number = 9;
|
||||
|
||||
@ -22,7 +22,7 @@ export class StateManager {
|
||||
|
||||
startGame(asSuccessor: SuccessorOption, withWish: Wish | null) {
|
||||
this.#turn = 1;
|
||||
initHuntMode(new HuntMode(1, mapHub()));
|
||||
initHuntMode(new HuntMode(1, generateMap()));
|
||||
initPlayerProgress(asSuccessor, withWish);
|
||||
}
|
||||
|
||||
@ -33,7 +33,7 @@ export class StateManager {
|
||||
this.#turn += 1;
|
||||
getPlayerProgress().applyEndOfTurn();
|
||||
getPlayerProgress().refill();
|
||||
initHuntMode(new HuntMode(getHuntMode().depth, mapHub()));
|
||||
initHuntMode(new HuntMode(getHuntMode().depth, generateMap()));
|
||||
} else {
|
||||
// TODO: Play a specific scene
|
||||
let ending = getScorer().pickEnding();
|
||||
|
@ -1,6 +1,6 @@
|
||||
export function choose<T>(array: Array<T>): T {
|
||||
if (array.length == 0) {
|
||||
throw `array cannot have length 0 for choose`
|
||||
throw new Error(`array cannot have length 0 for choose`);
|
||||
}
|
||||
return array[Math.floor(Math.random() * array.length)]
|
||||
}
|
||||
|
32
src/vaulttemplate.ts
Normal file
32
src/vaulttemplate.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import {Stat} from "./datatypes.ts";
|
||||
|
||||
export type VaultTemplate = {
|
||||
stats: {primary: Stat, secondary: Stat},
|
||||
}
|
||||
|
||||
export const standardVaultTemplates: VaultTemplate[] = [
|
||||
{
|
||||
// blood bank
|
||||
stats: {primary: "AGI", secondary: "INT"},
|
||||
},
|
||||
{
|
||||
// club,
|
||||
stats: {primary: "CHA", secondary: "PSI"},
|
||||
},
|
||||
{
|
||||
// coffee shop
|
||||
stats: {primary: "PSI", secondary: "CHA"},
|
||||
},
|
||||
{
|
||||
// library
|
||||
stats: {primary: "INT", secondary: "CHA"},
|
||||
},
|
||||
{
|
||||
// optometrist
|
||||
stats: {primary: "PSI", secondary: "PSI"},
|
||||
},
|
||||
{
|
||||
// zoo
|
||||
stats: {primary: "AGI", secondary: "PSI"}
|
||||
}
|
||||
]
|
Loading…
x
Reference in New Issue
Block a user