initial: dungeon crawler with Bun + TypeScript modular architecture

- 19 TypeScript modules under src/ (constants, entities, room, game, render)
- Canvas 2D rendering with dark fantasy palette
- Room-based navigation, random 7x7 map generation
- Two combat modes: ranged (pistol) and melee (knife)
- Wall collision with door opening support
- Minimap, HP bar, enemy AI
- Bun build pipeline: src/main.ts -> dist/main.js + dist/index.html
This commit is contained in:
Volodia
2026-06-18 12:55:17 +03:00
commit b947fbb7fb
30 changed files with 2376 additions and 0 deletions
+89
View File
@@ -0,0 +1,89 @@
import { MAP_RADIUS, MIN_ROOMS, EXTRA_ROOMS } from '../constants';
import { Room } from './Room';
import type { RoomType } from '../types';
import { shuffle, ri } from '../math';
import { OPP } from '../doors';
export class RoomMap {
rooms: Map<string, Room> = new Map();
key(c: number, r: number): string {
return c + ',' + r;
}
get(c: number, r: number): Room | undefined {
return this.rooms.get(this.key(c, r));
}
has(c: number, r: number): boolean {
return this.rooms.has(this.key(c, r));
}
private add(c: number, r: number, type: RoomType): Room {
const room = new Room(c, r, type);
this.rooms.set(this.key(c, r), room);
return room;
}
hasBoss(): boolean {
for (const room of this.rooms.values()) {
if (room.type === 'boss') return true;
}
return false;
}
/** Generate a connected 7×7 grid of rooms using a random walk */
generate(): void {
this.add(0, 0, 'spawn');
const frontier: [number, number][] = [[0, 0]];
let count = 1;
const target = MIN_ROOMS + ri(0, EXTRA_ROOMS);
const dirs: [string, number, number][] = [
['up', 0, -1],
['down', 0, 1],
['left', -1, 0],
['right', 1, 0],
];
while (frontier.length > 0 && count < target) {
const idx = ri(0, frontier.length - 1);
const [cr, cc] = frontier[idx];
shuffle(dirs);
let added = false;
for (const [_d, dc, dr] of dirs) {
if (count >= target) break;
const nc = cr + dc;
const nr = cc + dr;
if (Math.abs(nc) > MAP_RADIUS || Math.abs(nr) > MAP_RADIUS) continue;
if (this.has(nc, nr)) continue;
let type: RoomType = 'normal';
if (!this.hasBoss() && (count === target - 1 || (Math.random() < 0.2 && count >= 3))) {
type = 'boss';
} else if (Math.random() < 0.12 && count >= 2) {
type = 'treasure';
}
this.add(nc, nr, type);
const dir = _d as keyof typeof OPP;
this.get(cr, cc)!.doors[dir] = true;
this.get(nc, nr)!.doors[OPP[dir] as keyof typeof OPP] = true;
frontier.push([nc, nr]);
count++;
added = true;
}
if (!added) frontier.splice(idx, 1);
}
// Ensure at least one boss room exists
if (!this.hasBoss()) {
const normals = [...this.rooms.values()].filter(r => r.type === 'normal');
if (normals.length > 0) {
normals[ri(0, normals.length - 1)].type = 'boss';
}
}
}
}