1
0
Fork 0
tsgames/src/games/text-dungeon/room.ts

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);