shellquest
Version:
Terminal-based procedurally generated dungeon crawler
1,035 lines (928 loc) • 35.2 kB
text/typescript
import type {PRNG} from 'seedrandom';
import type {LevelTile} from '../types.ts';
export class FenceGenerator {
constructor(
private tiles: LevelTile[][],
private width: number,
private height: number,
) {}
generateHouseFence(
houseX: number,
houseY: number,
houseWidth: number,
houseHeight: number,
rng: PRNG,
): void {
const pattern = Math.floor(rng() * 4);
switch (pattern) {
case 0:
this.generateYardFence(houseX, houseY, houseWidth, houseHeight, rng);
break;
case 1:
this.generateGardenFence(houseX, houseY, houseWidth, houseHeight, rng);
break;
case 2:
this.generatePropertyLineFence(houseX, houseY, houseWidth, houseHeight, rng);
break;
case 3:
this.generateCornerFence(houseX, houseY, houseWidth, houseHeight, rng);
break;
}
}
private generateYardFence(
houseX: number,
houseY: number,
houseWidth: number,
houseHeight: number,
rng: PRNG,
): void {
const side = rng() < 0.5 ? 'front' : 'side';
if (side === 'front' && houseY + houseHeight + 8 < this.height) {
const yardDepth = Math.floor(rng() * 3) + 4;
const fenceY = houseY + houseHeight + yardDepth;
for (let dy = 0; dy < yardDepth; dy++) {
const y = houseY + houseHeight + dy;
const x = houseX - 1;
if (x >= 0 && !this.tiles[y][x].topTile) {
if (dy === 0) {
this.tiles[y][x].topTile = 'fence-corner-top-right';
} else if (dy === yardDepth - 1) {
this.tiles[y][x].topTile = 'fence-corner-bottom-left';
} else {
this.tiles[y][x].topTile = 'fence-vertical-middle-withbar';
}
this.tiles[y][x].solid = true;
}
}
const gatePos = Math.floor(houseWidth / 2);
for (let dx = 0; dx < houseWidth + 2; dx++) {
const x = houseX - 1 + dx;
if (x >= 0 && x < this.width && !this.tiles[fenceY][x].topTile) {
if (dx === gatePos || dx === gatePos + 1) continue;
if (dx === 0) {
} else if (dx === houseWidth + 1) {
this.tiles[fenceY][x].topTile = 'fence-corner-bottom-right';
} else if (dx === gatePos - 1) {
this.tiles[fenceY][x].topTile = 'fence-horizontal-right';
} else if (dx === gatePos + 2) {
this.tiles[fenceY][x].topTile = 'fence-horizontal-left';
} else {
this.tiles[fenceY][x].topTile =
dx % 2 === 0 ? 'fence-horizontal-withbar' : 'fence-horizontal-middle-nobar';
}
if (this.tiles[fenceY][x].topTile) {
this.tiles[fenceY][x].solid = true;
}
}
}
for (let dy = 0; dy < yardDepth; dy++) {
const y = houseY + houseHeight + dy;
const x = houseX + houseWidth;
if (x < this.width && !this.tiles[y][x].topTile) {
if (dy === 0) {
this.tiles[y][x].topTile = 'fence-corner-top-left';
} else if (dy === yardDepth - 1) {
} else {
this.tiles[y][x].topTile = 'fence-vertical-middle-withbar';
}
if (this.tiles[y][x].topTile) {
this.tiles[y][x].solid = true;
}
}
}
} else if (side === 'side') {
const sideChoice = rng() < 0.5 ? 'left' : 'right';
const fenceLength = Math.floor(rng() * 4) + 6;
if (sideChoice === 'left' && houseX > fenceLength) {
const startX = houseX - fenceLength;
const attachY = houseY + Math.floor(houseHeight / 2);
for (let i = 0; i < fenceLength; i++) {
if (!this.tiles[attachY][startX + i].topTile) {
if (i === 0) {
this.tiles[attachY][startX + i].topTile = 'fence-horizontal-left';
} else if (i === fenceLength - 1) {
this.tiles[attachY][startX + i].topTile = 'fence-corner-top-right';
} else {
this.tiles[attachY][startX + i].topTile =
i % 2 === 0 ? 'fence-horizontal-withbar' : 'fence-horizontal-middle-nobar';
}
this.tiles[attachY][startX + i].solid = true;
}
}
const returnLength = Math.floor(rng() * 3) + 3;
for (let j = 1; j < returnLength; j++) {
const y = attachY + j;
if (y < this.height && !this.tiles[y][houseX - 1].topTile) {
if (j === returnLength - 1) {
this.tiles[y][houseX - 1].topTile = 'fence-vertical-bottom';
} else {
this.tiles[y][houseX - 1].topTile = 'fence-vertical-middle-withbar';
}
this.tiles[y][houseX - 1].solid = true;
}
}
}
}
}
private generateGardenFence(
houseX: number,
houseY: number,
houseWidth: number,
houseHeight: number,
rng: PRNG,
): void {
const gardenWidth = Math.floor(rng() * 2) + 4;
const gardenHeight = Math.floor(rng() * 2) + 3;
const side = rng() < 0.5 ? 'left' : 'right';
let gardenX: number, gardenY: number;
if (side === 'left') {
gardenX = houseX - gardenWidth - 2;
gardenY = houseY + houseHeight - gardenHeight;
} else {
gardenX = houseX + houseWidth + 2;
gardenY = houseY + houseHeight - gardenHeight;
}
if (
gardenX < 0 ||
gardenX + gardenWidth >= this.width ||
gardenY < 0 ||
gardenY + gardenHeight >= this.height
)
return;
for (let dy = 0; dy < gardenHeight; dy++) {
for (let dx = 0; dx < gardenWidth; dx++) {
if (this.tiles[gardenY + dy][gardenX + dx].topTile) return;
}
}
this.placeRectangularFence(gardenX, gardenY, gardenWidth, gardenHeight, true, rng);
}
private generatePropertyLineFence(
houseX: number,
houseY: number,
houseWidth: number,
houseHeight: number,
rng: PRNG,
): void {
const side = Math.floor(rng() * 4);
switch (side) {
case 0:
if (houseX > 8) {
const fenceX = houseX - 6;
const startY = Math.max(0, houseY - 2);
const endY = Math.min(this.height - 1, houseY + houseHeight + 4);
for (let y = startY; y < endY; y++) {
if (!this.tiles[y][fenceX].topTile) {
if (y === startY) {
this.tiles[y][fenceX].topTile = 'fence-vertical-top';
} else if (y === endY - 1) {
this.tiles[y][fenceX].topTile = 'fence-vertical-bottom';
} else {
this.tiles[y][fenceX].topTile = 'fence-vertical-middle-withbar';
}
this.tiles[y][fenceX].solid = true;
}
}
}
break;
case 1:
if (houseX + houseWidth + 8 < this.width) {
const fenceX = houseX + houseWidth + 6;
const startY = Math.max(0, houseY - 2);
const endY = Math.min(this.height - 1, houseY + houseHeight + 4);
for (let y = startY; y < endY; y++) {
if (!this.tiles[y][fenceX].topTile) {
if (y === startY) {
this.tiles[y][fenceX].topTile = 'fence-vertical-top';
} else if (y === endY - 1) {
this.tiles[y][fenceX].topTile = 'fence-vertical-bottom';
} else {
this.tiles[y][fenceX].topTile = 'fence-vertical-middle-withbar';
}
this.tiles[y][fenceX].solid = true;
}
}
}
break;
case 2:
if (houseY > 8) {
const fenceY = houseY - 4;
const startX = Math.max(0, houseX - 3);
const endX = Math.min(this.width - 1, houseX + houseWidth + 3);
for (let x = startX; x < endX; x++) {
if (!this.tiles[fenceY][x].topTile) {
if (x === startX) {
this.tiles[fenceY][x].topTile = 'fence-horizontal-left';
} else if (x === endX - 1) {
this.tiles[fenceY][x].topTile = 'fence-horizontal-right';
} else {
this.tiles[fenceY][x].topTile =
x % 2 === 0 ? 'fence-horizontal-withbar' : 'fence-horizontal-middle-nobar';
}
this.tiles[fenceY][x].solid = true;
}
}
}
break;
}
}
private generateCornerFence(
houseX: number,
houseY: number,
houseWidth: number,
houseHeight: number,
rng: PRNG,
): void {
const corner = Math.floor(rng() * 4);
const length1 = Math.floor(rng() * 3) + 4;
const length2 = Math.floor(rng() * 3) + 4;
let startX: number, startY: number;
let dir1X: number, dir1Y: number;
let dir2X: number, dir2Y: number;
switch (corner) {
case 0:
startX = houseX - 2;
startY = houseY - 2;
dir1X = -1;
dir1Y = 0;
dir2X = 0;
dir2Y = 1;
break;
case 1:
startX = houseX + houseWidth + 1;
startY = houseY - 2;
dir1X = 1;
dir1Y = 0;
dir2X = 0;
dir2Y = 1;
break;
case 2:
startX = houseX - 2;
startY = houseY + houseHeight + 1;
dir1X = -1;
dir1Y = 0;
dir2X = 0;
dir2Y = -1;
break;
default:
startX = houseX + houseWidth + 1;
startY = houseY + houseHeight + 1;
dir1X = 1;
dir1Y = 0;
dir2X = 0;
dir2Y = -1;
break;
}
let canPlace = true;
for (let i = 0; i < length1; i++) {
const x = startX + dir1X * i;
const y = startY + dir1Y * i;
if (x < 0 || x >= this.width || y < 0 || y >= this.height || this.tiles[y][x].topTile) {
canPlace = false;
break;
}
}
if (!canPlace) return;
for (let i = 0; i < length1; i++) {
const x = startX + dir1X * i;
const y = startY + dir1Y * i;
if (dir1Y === 0) {
if (i === 0 && dir1X < 0) {
this.tiles[y][x].topTile = 'fence-horizontal-right';
} else if (i === 0 && dir1X > 0) {
this.tiles[y][x].topTile = 'fence-horizontal-left';
} else if (i === length1 - 1) {
if (dir1X < 0 && dir2Y > 0) this.tiles[y][x].topTile = 'fence-corner-bottom-left';
else if (dir1X < 0 && dir2Y < 0) this.tiles[y][x].topTile = 'fence-corner-top-left';
else if (dir1X > 0 && dir2Y > 0) this.tiles[y][x].topTile = 'fence-corner-bottom-right';
else if (dir1X > 0 && dir2Y < 0) this.tiles[y][x].topTile = 'fence-corner-top-right';
} else {
this.tiles[y][x].topTile =
i % 2 === 0 ? 'fence-horizontal-withbar' : 'fence-horizontal-middle-nobar';
}
} else {
if (i === 0 && dir1Y < 0) {
this.tiles[y][x].topTile = 'fence-vertical-bottom';
} else if (i === 0 && dir1Y > 0) {
this.tiles[y][x].topTile = 'fence-vertical-top';
} else if (i === length1 - 1) {
} else {
this.tiles[y][x].topTile = 'fence-vertical-middle-withbar';
}
}
this.tiles[y][x].solid = true;
}
const cornerX = startX + dir1X * (length1 - 1);
const cornerY = startY + dir1Y * (length1 - 1);
for (let i = 1; i < length2; i++) {
const x = cornerX + dir2X * i;
const y = cornerY + dir2Y * i;
if (x >= 0 && x < this.width && y >= 0 && y < this.height && !this.tiles[y][x].topTile) {
if (dir2Y === 0) {
if (i === length2 - 1 && dir2X < 0) {
this.tiles[y][x].topTile = 'fence-horizontal-left';
} else if (i === length2 - 1 && dir2X > 0) {
this.tiles[y][x].topTile = 'fence-horizontal-right';
} else {
this.tiles[y][x].topTile =
i % 2 === 0 ? 'fence-horizontal-withbar' : 'fence-horizontal-middle-nobar';
}
} else {
if (i === length2 - 1 && dir2Y < 0) {
this.tiles[y][x].topTile = 'fence-vertical-top';
} else if (i === length2 - 1 && dir2Y > 0) {
this.tiles[y][x].topTile = 'fence-vertical-bottom';
} else {
this.tiles[y][x].topTile = 'fence-vertical-middle-withbar';
}
}
this.tiles[y][x].solid = true;
}
}
}
placeRectangularFence(
x: number,
y: number,
width: number,
height: number,
hasGate: boolean,
rng: PRNG,
): void {
for (let dx = 0; dx < width; dx++) {
if (!this.tiles[y][x + dx].topTile) {
if (dx === 0) {
this.tiles[y][x + dx].topTile = 'fence-corner-top-left';
} else if (dx === width - 1) {
this.tiles[y][x + dx].topTile = 'fence-corner-top-right';
} else {
this.tiles[y][x + dx].topTile =
dx % 2 === 0 ? 'fence-horizontal-withbar' : 'fence-horizontal-middle-nobar';
}
this.tiles[y][x + dx].solid = true;
}
}
for (let dy = 1; dy < height - 1; dy++) {
if (!this.tiles[y + dy][x].topTile) {
this.tiles[y + dy][x].topTile = 'fence-vertical-left-withbar';
this.tiles[y + dy][x].solid = true;
}
if (!this.tiles[y + dy][x + width - 1].topTile) {
this.tiles[y + dy][x + width - 1].topTile = 'fence-vertical-right-withbar';
this.tiles[y + dy][x + width - 1].solid = true;
}
}
const gatePos = hasGate ? Math.floor(width / 2) : -1;
for (let dx = 0; dx < width; dx++) {
if (hasGate && (dx === gatePos || (width > 3 && dx === gatePos - 1))) continue;
if (!this.tiles[y + height - 1][x + dx].topTile) {
if (dx === 0) {
this.tiles[y + height - 1][x + dx].topTile = 'fence-corner-bottom-left';
} else if (dx === width - 1) {
this.tiles[y + height - 1][x + dx].topTile = 'fence-corner-bottom-right';
} else if (hasGate && dx === gatePos - 2) {
this.tiles[y + height - 1][x + dx].topTile = 'fence-horizontal-right';
} else if (hasGate && dx === gatePos + 1) {
this.tiles[y + height - 1][x + dx].topTile = 'fence-horizontal-left';
} else {
this.tiles[y + height - 1][x + dx].topTile =
dx % 2 === 0 ? 'fence-horizontal-withbar' : 'fence-horizontal-middle-nobar';
}
this.tiles[y + height - 1][x + dx].solid = true;
}
}
}
generateTownFences(rng: PRNG): void {
const fenceGroups = 25;
const placedFences: Array<{x: number; y: number; w: number; h: number}> = [];
for (let i = 0; i < 5; i++) {
const isHorizontal = rng() < 0.5;
if (isHorizontal) {
const y = Math.floor(rng() * (this.height - 20)) + 10;
const startX = Math.floor(rng() * 20) + 5;
const length = Math.floor(rng() * 15) + 10;
this.placeLongFenceLine(startX, y, length, true, rng);
placedFences.push({x: startX, y: y, w: length, h: 1});
} else {
const x = Math.floor(rng() * (this.width - 20)) + 10;
const startY = Math.floor(rng() * 20) + 5;
const length = Math.floor(rng() * 15) + 10;
this.placeLongFenceLine(x, startY, length, false, rng);
placedFences.push({x: x, y: startY, w: 1, h: length});
}
}
for (let i = 0; i < fenceGroups; i++) {
let x: number, y: number;
if (rng() < 0.3 && placedFences.length > 0) {
const nearFence = placedFences[Math.floor(rng() * placedFences.length)];
x = nearFence.x + Math.floor(rng() * 20) - 10;
y = nearFence.y + Math.floor(rng() * 20) - 10;
} else {
x = Math.floor(rng() * (this.width - 10)) + 5;
y = Math.floor(rng() * (this.height - 10)) + 5;
}
x = Math.max(2, Math.min(this.width - 15, x));
y = Math.max(2, Math.min(this.height - 15, y));
const rand = rng();
if (rand < 0.15) {
this.placeLShapedFence(x, y, rng);
} else if (rand < 0.3) {
this.placeSmallEnclosure(x, y, rng);
} else if (rand < 0.45) {
this.placeZigzagFence(x, y, rng);
} else if (rand < 0.6) {
this.placeUShapedFence(x, y, rng);
} else if (rand < 0.75) {
this.placeDiagonalFence(x, y, rng);
} else if (rand < 0.85) {
this.placeConnectedFences(x, y, rng);
} else {
this.placeStraightFence(x, y, rng);
}
}
for (let i = 0; i < 10; i++) {
const x = Math.floor(rng() * (this.width - 5)) + 2;
const y = Math.floor(rng() * (this.height - 5)) + 2;
this.placeMiniFence(x, y, rng);
}
}
private placeLongFenceLine(
startX: number,
startY: number,
length: number,
isHorizontal: boolean,
rng: PRNG,
): void {
if (isHorizontal) {
for (let i = 0; i < length; i++) {
const x = startX + i;
if (x >= this.width) break;
if (this.tiles[startY][x].topTile) {
if (rng() < 0.3) {
const dir = rng() < 0.5 ? -1 : 1;
for (let j = 1; j <= 2; j++) {
const y = startY + j * dir;
if (y >= 0 && y < this.height && !this.tiles[y][x].topTile) {
this.tiles[y][x].topTile = 'fence-vertical-middle-withbar';
this.tiles[y][x].solid = true;
}
}
}
continue;
}
if (i === 0) {
this.tiles[startY][x].topTile = 'fence-horizontal-left';
} else if (i === length - 1) {
this.tiles[startY][x].topTile = 'fence-horizontal-right';
} else {
if (rng() < 0.1 && i > 2 && i < length - 2) {
continue;
}
this.tiles[startY][x].topTile =
i % 2 === 0 ? 'fence-horizontal-withbar' : 'fence-horizontal-middle-nobar';
}
this.tiles[startY][x].solid = true;
}
} else {
for (let i = 0; i < length; i++) {
const y = startY + i;
if (y >= this.height) break;
if (this.tiles[y][startX].topTile) {
if (rng() < 0.3) {
const dir = rng() < 0.5 ? -1 : 1;
for (let j = 1; j <= 2; j++) {
const x = startX + j * dir;
if (x >= 0 && x < this.width && !this.tiles[y][x].topTile) {
this.tiles[y][x].topTile = 'fence-horizontal-withbar';
this.tiles[y][x].solid = true;
}
}
}
continue;
}
if (i === 0) {
this.tiles[y][startX].topTile = 'fence-vertical-top';
} else if (i === length - 1) {
this.tiles[y][startX].topTile = 'fence-vertical-bottom';
} else {
if (rng() < 0.1 && i > 2 && i < length - 2) {
continue;
}
this.tiles[y][startX].topTile = 'fence-vertical-middle-withbar';
}
this.tiles[y][startX].solid = true;
}
}
}
private placeUShapedFence(x: number, y: number, rng: PRNG): void {
const width = Math.floor(rng() * 3) + 4;
const height = Math.floor(rng() * 3) + 3;
const openSide = Math.floor(rng() * 4);
if (x + width >= this.width || y + height >= this.height) return;
for (let dy = 0; dy < height; dy++) {
for (let dx = 0; dx < width; dx++) {
if (this.tiles[y + dy][x + dx].topTile) return;
}
}
if (openSide !== 0) {
for (let dx = 0; dx < width; dx++) {
if (!this.tiles[y][x + dx].topTile) {
if (dx === 0) {
this.tiles[y][x + dx].topTile = 'fence-corner-top-left';
} else if (dx === width - 1) {
this.tiles[y][x + dx].topTile = 'fence-corner-top-right';
} else {
this.tiles[y][x + dx].topTile =
dx % 2 === 0 ? 'fence-horizontal-withbar' : 'fence-horizontal-middle-nobar';
}
this.tiles[y][x + dx].solid = true;
}
}
}
if (openSide !== 2) {
for (let dx = 0; dx < width; dx++) {
if (!this.tiles[y + height - 1][x + dx].topTile) {
if (dx === 0 && openSide !== 3) {
this.tiles[y + height - 1][x + dx].topTile = 'fence-corner-bottom-left';
} else if (dx === width - 1 && openSide !== 1) {
this.tiles[y + height - 1][x + dx].topTile = 'fence-corner-bottom-right';
} else if (openSide === 3 && dx === 0) {
this.tiles[y + height - 1][x + dx].topTile = 'fence-horizontal-left';
} else if (openSide === 1 && dx === width - 1) {
this.tiles[y + height - 1][x + dx].topTile = 'fence-horizontal-right';
} else {
this.tiles[y + height - 1][x + dx].topTile =
dx % 2 === 0 ? 'fence-horizontal-withbar' : 'fence-horizontal-middle-nobar';
}
this.tiles[y + height - 1][x + dx].solid = true;
}
}
}
if (openSide !== 3) {
for (let dy = 1; dy < height - 1; dy++) {
if (!this.tiles[y + dy][x].topTile) {
this.tiles[y + dy][x].topTile = 'fence-vertical-left-withbar';
this.tiles[y + dy][x].solid = true;
}
}
}
if (openSide !== 1) {
for (let dy = 1; dy < height - 1; dy++) {
if (!this.tiles[y + dy][x + width - 1].topTile) {
this.tiles[y + dy][x + width - 1].topTile = 'fence-vertical-right-withbar';
this.tiles[y + dy][x + width - 1].solid = true;
}
}
}
}
private placeDiagonalFence(x: number, y: number, rng: PRNG): void {
const length = Math.floor(rng() * 4) + 3;
const goingRight = rng() < 0.5;
const goingDown = rng() < 0.5;
let currentX = x;
let currentY = y;
for (let i = 0; i < length; i++) {
if (currentX < this.width && currentY < this.height && currentX >= 0 && currentY >= 0) {
if (!this.tiles[currentY][currentX].topTile) {
this.tiles[currentY][currentX].topTile = 'fence-horizontal-withbar';
this.tiles[currentY][currentX].solid = true;
}
if (i < length - 1) {
const nextY = currentY + (goingDown ? 1 : -1);
if (nextY >= 0 && nextY < this.height && !this.tiles[nextY][currentX].topTile) {
this.tiles[nextY][currentX].topTile = 'fence-vertical-middle-withbar';
this.tiles[nextY][currentX].solid = true;
}
}
}
currentX += goingRight ? 1 : -1;
currentY += goingDown ? 1 : -1;
}
}
private placeConnectedFences(x: number, y: number, rng: PRNG): void {
const segments = Math.floor(rng() * 2) + 2;
let currentX = x;
let currentY = y;
for (let seg = 0; seg < segments; seg++) {
const length = Math.floor(rng() * 4) + 3;
const direction = Math.floor(rng() * 4);
for (let i = 0; i < length; i++) {
let nextX = currentX;
let nextY = currentY;
switch (direction) {
case 0:
nextY--;
break;
case 1:
nextX++;
break;
case 2:
nextY++;
break;
case 3:
nextX--;
break;
}
if (nextX < 0 || nextX >= this.width || nextY < 0 || nextY >= this.height) break;
if (this.tiles[nextY][nextX].topTile) break;
if (direction === 0 || direction === 2) {
if (i === 0 && seg === 0) {
this.tiles[nextY][nextX].topTile =
direction === 0 ? 'fence-vertical-bottom' : 'fence-vertical-top';
} else if (i === length - 1 && seg === segments - 1) {
this.tiles[nextY][nextX].topTile =
direction === 0 ? 'fence-vertical-top' : 'fence-vertical-bottom';
} else {
this.tiles[nextY][nextX].topTile = 'fence-vertical-middle-withbar';
}
} else {
if (i === 0 && seg === 0) {
this.tiles[nextY][nextX].topTile =
direction === 3 ? 'fence-horizontal-right' : 'fence-horizontal-left';
} else if (i === length - 1 && seg === segments - 1) {
this.tiles[nextY][nextX].topTile =
direction === 3 ? 'fence-horizontal-left' : 'fence-horizontal-right';
} else {
this.tiles[nextY][nextX].topTile =
i % 2 === 0 ? 'fence-horizontal-withbar' : 'fence-horizontal-middle-nobar';
}
}
this.tiles[nextY][nextX].solid = true;
currentX = nextX;
currentY = nextY;
}
}
}
private placeMiniFence(x: number, y: number, rng: PRNG): void {
const length = Math.floor(rng() * 2) + 2;
const isHorizontal = rng() < 0.5;
if (isHorizontal) {
for (let i = 0; i < length; i++) {
if (x + i >= this.width || this.tiles[y][x + i].topTile) return;
}
for (let i = 0; i < length; i++) {
if (i === 0) {
this.tiles[y][x + i].topTile = 'fence-horizontal-left';
} else if (i === length - 1) {
this.tiles[y][x + i].topTile = 'fence-horizontal-right';
} else {
this.tiles[y][x + i].topTile = 'fence-horizontal-withbar';
}
this.tiles[y][x + i].solid = true;
}
} else {
for (let i = 0; i < length; i++) {
if (y + i >= this.height || this.tiles[y + i][x].topTile) return;
}
for (let i = 0; i < length; i++) {
if (i === 0) {
this.tiles[y + i][x].topTile = 'fence-vertical-top';
} else if (i === length - 1) {
this.tiles[y + i][x].topTile = 'fence-vertical-bottom';
} else {
this.tiles[y + i][x].topTile = 'fence-vertical-middle-withbar';
}
this.tiles[y + i][x].solid = true;
}
}
}
private placeLShapedFence(x: number, y: number, rng: PRNG): void {
const length1 = Math.floor(rng() * 4) + 4;
const length2 = Math.floor(rng() * 4) + 4;
let canPlace = true;
for (let i = 0; i < length1; i++) {
if (x + i >= this.width || this.tiles[y][x + i].topTile) {
canPlace = false;
break;
}
}
if (canPlace) {
for (let i = 0; i < length1; i++) {
if (i === 0) {
this.tiles[y][x + i].topTile = 'fence-horizontal-left';
} else if (i === length1 - 1) {
this.tiles[y][x + i].topTile = 'fence-corner-top-right';
} else {
this.tiles[y][x + i].topTile =
i % 2 === 0 ? 'fence-horizontal-withbar' : 'fence-horizontal-middle-nobar';
}
this.tiles[y][x + i].solid = true;
}
for (let j = 1; j < length2; j++) {
if (y + j < this.height && !this.tiles[y + j][x + length1 - 1].topTile) {
if (j === length2 - 1) {
this.tiles[y + j][x + length1 - 1].topTile = 'fence-vertical-bottom';
} else {
this.tiles[y + j][x + length1 - 1].topTile = 'fence-vertical-right-withbar';
}
this.tiles[y + j][x + length1 - 1].solid = true;
}
}
}
}
private placeStraightFence(x: number, y: number, rng: PRNG): void {
const isHorizontal = rng() < 0.5;
const length = Math.floor(rng() * 6) + 5;
if (isHorizontal) {
let canPlace = true;
for (let i = 0; i < length; i++) {
if (x + i >= this.width || this.tiles[y][x + i].topTile) {
canPlace = false;
break;
}
}
if (canPlace) {
for (let i = 0; i < length; i++) {
if (i === 0) {
this.tiles[y][x + i].topTile = 'fence-horizontal-left';
} else if (i === length - 1) {
this.tiles[y][x + i].topTile = 'fence-horizontal-right';
} else {
this.tiles[y][x + i].topTile =
i % 2 === 0 ? 'fence-horizontal-withbar' : 'fence-horizontal-middle-nobar';
}
this.tiles[y][x + i].solid = true;
}
}
} else {
let canPlace = true;
for (let i = 0; i < length; i++) {
if (y + i >= this.height || x < 0 || x >= this.width || this.tiles[y + i]?.[x].topTile) {
canPlace = false;
break;
}
}
if (canPlace) {
for (let i = 0; i < length; i++) {
if (i === 0) {
this.tiles[y + i][x].topTile = 'fence-vertical-top';
} else if (i === length - 1) {
this.tiles[y + i][x].topTile = 'fence-vertical-bottom';
} else {
this.tiles[y + i][x].topTile = 'fence-vertical-middle-withbar';
}
this.tiles[y + i][x].solid = true;
}
}
}
}
private placeSmallEnclosure(x: number, y: number, rng: PRNG): void {
const width = Math.floor(rng() * 2) + 3;
const height = Math.floor(rng() * 2) + 3;
let canPlace = true;
for (let dy = 0; dy < height; dy++) {
for (let dx = 0; dx < width; dx++) {
if (x + dx >= this.width || y + dy >= this.height || this.tiles[y + dy][x + dx].topTile) {
canPlace = false;
break;
}
}
if (!canPlace) break;
}
if (!canPlace) return;
for (let dx = 0; dx < width; dx++) {
if (dx === 0) {
this.tiles[y][x + dx].topTile = 'fence-corner-top-left';
} else if (dx === width - 1) {
this.tiles[y][x + dx].topTile = 'fence-corner-top-right';
} else {
this.tiles[y][x + dx].topTile = 'fence-horizontal-withbar';
}
this.tiles[y][x + dx].solid = true;
}
for (let dy = 1; dy < height - 1; dy++) {
this.tiles[y + dy][x].topTile = 'fence-vertical-left-withbar';
this.tiles[y + dy][x].solid = true;
this.tiles[y + dy][x + width - 1].topTile = 'fence-vertical-right-withbar';
this.tiles[y + dy][x + width - 1].solid = true;
}
const gapPos = Math.floor(width / 2);
for (let dx = 0; dx < width; dx++) {
if (dx === gapPos) continue;
if (dx === 0) {
this.tiles[y + height - 1][x + dx].topTile = 'fence-corner-bottom-left';
} else if (dx === width - 1) {
this.tiles[y + height - 1][x + dx].topTile = 'fence-corner-bottom-right';
} else if (dx === gapPos - 1) {
this.tiles[y + height - 1][x + dx].topTile = 'fence-horizontal-right';
} else if (dx === gapPos + 1) {
this.tiles[y + height - 1][x + dx].topTile = 'fence-horizontal-left';
} else {
this.tiles[y + height - 1][x + dx].topTile = 'fence-horizontal-withbar';
}
if (this.tiles[y + height - 1][x + dx].topTile) {
this.tiles[y + height - 1][x + dx].solid = true;
}
}
}
private placeZigzagFence(x: number, y: number, rng: PRNG): void {
const segments = Math.floor(rng() * 3) + 3;
let currentX = x;
let currentY = y;
let lastDirection = -1;
for (let seg = 0; seg < segments; seg++) {
const length = Math.floor(rng() * 4) + 3;
let direction: number;
if (lastDirection === -1) {
direction = Math.floor(rng() * 4);
} else {
if (lastDirection === 0 || lastDirection === 2) {
direction = rng() < 0.5 ? 1 : 3;
} else {
direction = rng() < 0.5 ? 0 : 2;
}
}
let canPlace = true;
for (let i = 0; i < length; i++) {
let checkX = currentX;
let checkY = currentY;
switch (direction) {
case 0:
checkY -= i;
break;
case 1:
checkX += i;
break;
case 2:
checkY += i;
break;
case 3:
checkX -= i;
break;
}
if (
checkX < 0 ||
checkX >= this.width ||
checkY < 0 ||
checkY >= this.height ||
this.tiles[checkY][checkX].topTile
) {
canPlace = false;
break;
}
}
if (!canPlace) break;
for (let i = 0; i < length; i++) {
let tileX = currentX;
let tileY = currentY;
switch (direction) {
case 0:
tileY -= i;
break;
case 1:
tileX += i;
break;
case 2:
tileY += i;
break;
case 3:
tileX -= i;
break;
}
let tileName: string;
if (i === 0 && seg > 0) {
if (lastDirection === 0 && direction === 1) tileName = 'fence-corner-bottom-right';
else if (lastDirection === 0 && direction === 3) tileName = 'fence-corner-bottom-left';
else if (lastDirection === 2 && direction === 1) tileName = 'fence-corner-top-right';
else if (lastDirection === 2 && direction === 3) tileName = 'fence-corner-top-left';
else if (lastDirection === 1 && direction === 0) tileName = 'fence-corner-top-left';
else if (lastDirection === 1 && direction === 2) tileName = 'fence-corner-bottom-left';
else if (lastDirection === 3 && direction === 0) tileName = 'fence-corner-top-right';
else if (lastDirection === 3 && direction === 2) tileName = 'fence-corner-bottom-right';
else tileName = 'fence-horizontal-withbar';
} else if (direction === 0 || direction === 2) {
if (i === 0 && seg === 0) {
tileName = direction === 0 ? 'fence-vertical-bottom' : 'fence-vertical-top';
} else if (i === length - 1 && seg === segments - 1) {
tileName = direction === 0 ? 'fence-vertical-top' : 'fence-vertical-bottom';
} else {
tileName = 'fence-vertical-middle-withbar';
}
} else {
if (i === 0 && seg === 0) {
tileName = direction === 3 ? 'fence-horizontal-right' : 'fence-horizontal-left';
} else if (i === length - 1 && seg === segments - 1) {
tileName = direction === 3 ? 'fence-horizontal-left' : 'fence-horizontal-right';
} else {
tileName = i % 2 === 0 ? 'fence-horizontal-withbar' : 'fence-horizontal-middle-nobar';
}
}
this.tiles[tileY][tileX].topTile = tileName;
this.tiles[tileY][tileX].solid = true;
}
switch (direction) {
case 0:
currentY -= length - 1;
break;
case 1:
currentX += length - 1;
break;
case 2:
currentY += length - 1;
break;
case 3:
currentX -= length - 1;
break;
}
lastDirection = direction;
}
}
}