221 lines
7.5 KiB
TypeScript
221 lines
7.5 KiB
TypeScript
import { randInt } from "@common/utils";
|
|
import { Color, Direction, DIRECTION_OFFSETS, getOppositeDirection, ROOM_AREA_HEIGHT, ROOM_AREA_WIDTH, ROOM_AREA_X, ROOM_AREA_Y } from "./const";
|
|
import { drawBox } from "./display";
|
|
import { Drawable } from "./drawable";
|
|
import { Figure } from "./figure";
|
|
import { DOOR_SPRITE } from "./images";
|
|
import { getItemsCount, Item } from "./item";
|
|
|
|
type IDoors = [Door | null, Door | null, Door | null, Door | null, Door | null, Door | null];
|
|
|
|
export class Door extends Figure {
|
|
constructor(
|
|
x: number,
|
|
y: number,
|
|
direction: Direction,
|
|
public worldX: number,
|
|
public worldY: number,
|
|
public worldZ: number,
|
|
) {
|
|
super(x, y, DOOR_SPRITE);
|
|
this.frame = direction;
|
|
}
|
|
|
|
isActivated(x: number, y: number): boolean {
|
|
const cx = Math.floor(this.x + this.width / 2);
|
|
const cy = Math.floor(this.y + this.height / 2);
|
|
|
|
return (cx === x && cy === y);
|
|
}
|
|
|
|
get direction(): Direction {
|
|
return this.frame as Direction;
|
|
}
|
|
}
|
|
|
|
export class Room extends Drawable {
|
|
constructor(
|
|
public readonly x: number,
|
|
public readonly y: number,
|
|
public readonly width: number,
|
|
public readonly height: number,
|
|
public readonly worldX: number,
|
|
public readonly worldY: number,
|
|
public readonly worldZ: number,
|
|
public readonly doors: IDoors = [null, null, null, null, null, null],
|
|
public readonly items: Item[] = [],
|
|
) {
|
|
super();
|
|
}
|
|
|
|
override doDraw() {
|
|
drawBox(ROOM_AREA_X, ROOM_AREA_Y, ROOM_AREA_WIDTH - 2, ROOM_AREA_HEIGHT - 2, { fill: [' '] });
|
|
drawBox(this.x, this.y, this.width, this.height, { fill: ['.', Color.GRAY] });
|
|
|
|
this.doors.forEach((door) => {
|
|
if (door) {
|
|
door.doDraw();
|
|
}
|
|
});
|
|
|
|
this.items.forEach((item) => item.doDraw());
|
|
}
|
|
|
|
getActivatedDoor(x: number, y: number): Door | null {
|
|
return this.doors.find((door) => door?.isActivated(x, y)) ?? null;
|
|
}
|
|
|
|
pickItem(x: number, y: number): Item | null {
|
|
const itemIndex = this.items.findIndex((item) => item.x === x && item.y === y);
|
|
if (itemIndex >= 0) {
|
|
const [item] = this.items.splice(itemIndex, 1);
|
|
this.invalidate();
|
|
return item;
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
|
|
type IRoomBlank = {
|
|
-readonly [P in keyof Room]?: Room[P]
|
|
};
|
|
|
|
const rooms = new Map<string, Room>();
|
|
|
|
const generateRoomId = (worldX: number, worldY: number, worldZ: number) => `${worldX},${worldY},${worldZ}`;
|
|
|
|
const generateDoors = ({ x, y, width, height, worldX, worldY, worldZ }: IRoomBlank): IDoors => {
|
|
const doors: IDoors = [null, null, null, null, null, null];
|
|
if (typeof worldX === 'undefined' || typeof worldY === 'undefined' || typeof worldZ === 'undefined') {
|
|
console.error('World coordinates not defined for doors generation');
|
|
return doors;
|
|
}
|
|
if (typeof x === 'undefined' || typeof y === 'undefined' || typeof width === 'undefined' || typeof height === 'undefined') {
|
|
console.error('Screen coordinates not defined for doors generation');
|
|
return doors;
|
|
}
|
|
|
|
for (let i = 0; i < 4; i++) {
|
|
const [xoff, yoff] = DIRECTION_OFFSETS[i];
|
|
const doorWorldX = worldX + xoff;
|
|
const doorWorldY = worldY + yoff;
|
|
const doorId = generateRoomId(doorWorldX, doorWorldY, worldZ);
|
|
const doorRoom = rooms.get(doorId);
|
|
if (doorRoom && !doorRoom.doors[getOppositeDirection(i)]) {
|
|
continue;
|
|
}
|
|
|
|
if ((worldX === 0 && worldY === 0) || doorRoom || Math.random() < 0.3) {
|
|
const doorX = randInt(x + 1, x + width - 2);
|
|
const doorY = randInt(y + 1, y + height - 2);
|
|
let dx = 0;
|
|
let dy = 0;
|
|
switch (i) {
|
|
case Direction.NORTH: dx = doorX; dy = y; break;
|
|
case Direction.SOUTH: dx = doorX; dy = y + height + 1; break;
|
|
case Direction.EAST: dx = x + width + 1; dy = doorY; break;
|
|
case Direction.WEST: dx = x; dy = doorY; break;
|
|
}
|
|
doors[i] = new Door(dx, dy, i as Direction, doorWorldX, doorWorldY, worldZ);
|
|
}
|
|
}
|
|
|
|
if (worldX === 0 && worldY === 0) {
|
|
const doorX = randInt(x + 1, x + width - 2);
|
|
const doorY = randInt(y + 1, y + height - 2);
|
|
const [, , zoff] = DIRECTION_OFFSETS[Direction.DOWN];
|
|
doors[Direction.DOWN] = new Door(doorX, doorY, Direction.DOWN, 0, 0, worldZ + zoff);
|
|
|
|
if (worldZ !== 0) {
|
|
let upDoorX: number;
|
|
let upDoorY: number;
|
|
do {
|
|
upDoorX = randInt(x + 1, x + width - 2);
|
|
upDoorY = randInt(y + 1, y + height - 2);
|
|
} while (doorX === upDoorX && doorY === upDoorY);
|
|
const [, , upZoff] = DIRECTION_OFFSETS[Direction.UP];
|
|
doors[Direction.UP] = new Door(upDoorX, upDoorY, Direction.UP, 0, 0, worldZ + upZoff);
|
|
}
|
|
}
|
|
|
|
return doors;
|
|
};
|
|
|
|
const generatedItems = new Set<number>();
|
|
|
|
const generateItems = ({ x, y, width, height, doors }: IRoomBlank): Item[] => {
|
|
const items: Item[] = [];
|
|
if (typeof x === 'undefined' || typeof y === 'undefined' || typeof width === 'undefined' || typeof height === 'undefined') {
|
|
console.error('Screen coordinates not defined for items generation');
|
|
return items;
|
|
}
|
|
if (typeof doors === 'undefined') {
|
|
console.error('Doors not defined for items generation');
|
|
return items;
|
|
}
|
|
|
|
const hasObjectAt = (x: number, y: number) =>
|
|
doors.some((d) => d?.x === x && d?.y === y) ||
|
|
items.some((i) => i.x === x && i.y === y);
|
|
|
|
const oneDoor = doors.filter((d) => Boolean(d)).length === 1;
|
|
|
|
const id = randInt(0, getItemsCount());
|
|
|
|
if (Math.random() < 0.4 && oneDoor && !generatedItems.has(id)) {
|
|
let newItemX: number;
|
|
let newItemY: number;
|
|
|
|
do {
|
|
newItemX = randInt(x + 1, x + width - 2);
|
|
newItemY = randInt(y + 1, y + height - 2);
|
|
} while (hasObjectAt(newItemX, newItemY));
|
|
const item = new Item(id, 1, newItemX, newItemY);
|
|
|
|
items.push(item);
|
|
generatedItems.add(id);
|
|
}
|
|
|
|
return items;
|
|
};
|
|
|
|
export function getRoom(worldX: number, worldY: number, worldZ: number) {
|
|
const id = generateRoomId(worldX, worldY, worldZ);
|
|
const existingRoom = rooms.get(id);
|
|
if (existingRoom) {
|
|
return existingRoom;
|
|
}
|
|
const width = randInt(5, ROOM_AREA_WIDTH - 5);
|
|
const height = randInt(5, ROOM_AREA_HEIGHT - 5);
|
|
|
|
const x = Math.max(ROOM_AREA_X + randInt(2, ROOM_AREA_WIDTH - width - 2), ROOM_AREA_X + 2);
|
|
const y = Math.max(ROOM_AREA_Y + randInt(2, ROOM_AREA_HEIGHT - height - 2), ROOM_AREA_Y + 2);
|
|
|
|
const roomBlank: IRoomBlank = { x, y, width, height, worldX, worldY, worldZ };
|
|
|
|
roomBlank.doors = generateDoors(roomBlank);
|
|
roomBlank.items = generateItems(roomBlank);
|
|
|
|
const room = new Room(x, y, width, height, worldX, worldY, worldZ, roomBlank.doors, roomBlank.items);
|
|
|
|
rooms.set(id, room);
|
|
return room;
|
|
}
|
|
|
|
export const getRoomsCount = () => rooms.size;
|
|
export const getPossibleRoomsCount = () => {
|
|
const roomsSet = new Set(rooms.keys());
|
|
for (const room of rooms.values()) {
|
|
room.doors.forEach((d) => {
|
|
if (d) {
|
|
const id = generateRoomId(d.worldX, d.worldY, d.worldZ);
|
|
roomsSet.add(id);
|
|
}
|
|
});
|
|
}
|
|
|
|
return roomsSet.size;
|
|
};
|
|
|
|
export const getRoomsForLayer = (z: number): Room[] => Array.from(rooms.values()).filter((r) => r.worldZ === z);
|