1
0
Fork 0

Dungeon crawl

This commit is contained in:
Pabloader 2026-05-05 11:21:06 +00:00
parent 17e34d5ba2
commit 0c10347c78
5 changed files with 184 additions and 49 deletions

View File

@ -188,7 +188,9 @@ export class TextDisplay {
} }
private setCharRaw(x: number, y: number, char: string, fg: IColorLike, bg: IColorLike) { private setCharRaw(x: number, y: number, char: string, fg: IColorLike, bg: IColorLike) {
if (x < this.clipLeft || y < this.clipTop || x >= this.clipRight || y >= this.clipBottom || !char) return; if (x < this.clipLeft || y < this.clipTop || x >= this.clipRight || y >= this.clipBottom) return;
if (!char || char === '\0') return;
const i = (y | 0) * this.width + (x | 0); const i = (y | 0) * this.width + (x | 0);
let dirty = false; let dirty = false;
if (this.chars[i] !== char) { if (this.chars[i] !== char) {
@ -239,7 +241,9 @@ export class TextDisplay {
const dispBase = row * this.width + x0; const dispBase = row * this.width + x0;
const regRowStr = chars[regRow]; const regRowStr = chars[regRow];
for (let i = 0; i < copyW; i++) { for (let i = 0; i < copyW; i++) {
this.chars[dispBase + i] = regRowStr[regColOffset + i]; const ch = regRowStr[regColOffset + i];
if (ch === '\0') continue;
this.chars[dispBase + i] = ch;
this.fgs[dispBase + i] = fgs[regBase + i]; this.fgs[dispBase + i] = fgs[regBase + i];
this.bgs[dispBase + i] = bgs[regBase + i]; this.bgs[dispBase + i] = bgs[regBase + i];
this.drawCell(x0 + i, row); this.drawCell(x0 + i, row);
@ -452,6 +456,18 @@ export class TextRegion {
} }
} }
get(x: number, y: number): IDefinedChar {
const i = y * this.width + x;
return [this.#chars[y][x], this.#fgs[i], this.#bgs[i]];
}
set(x: number, y: number, char: IChar) {
const ch = parseChar(char);
this.#chars[y] = this.#chars[y].slice(0, x) + ch[0] + this.#chars[y].slice(x + 1);
this.#fgs[y * this.width + x] = ch[1];
this.#bgs[y * this.width + x] = ch[2];
}
get [REGION_DATA]() { get [REGION_DATA]() {
return { return {
chars: this.#chars, chars: this.#chars,

View File

@ -2,9 +2,9 @@ import { Component, Entity } from "../core/world";
import { component } from "../utils/decorators"; import { component } from "../utils/decorators";
@component({ variables: ['x', 'y', 'z'] }) @component({ variables: ['x', 'y', 'z'] })
export class Position extends Component<{ x: number, y: number, z: number }> { export class Position extends Component<{ x: number, y: number, z: number, absolute: boolean }> {
constructor(x = 0, y = 0, z = 0) { constructor(x = 0, y = 0, z = 0, absolute = false) {
super({ x, y, z }); super({ x, y, z, absolute });
} }
get x() { get x() {
@ -25,6 +25,19 @@ export class Position extends Component<{ x: number, y: number, z: number }> {
set z(z: number) { set z(z: number) {
this.state.z = z; this.state.z = z;
} }
get absolute() {
return this.state.absolute;
}
set absolute(absolute: boolean) {
this.state.absolute = absolute;
}
} }
export const getPosition = (entity?: Entity, key?: string) => entity?.get(Position, key)?.state; export const getPosition = (entity?: Entity, key?: string) => entity?.get(Position, key)?.state;
export const move = (entity: Entity, dx: number, dy: number, dz = 0) => {
const pos = getPosition(entity);
if (!pos) return;
pos.x += dx;
pos.y += dy;
pos.z += dz;
};

View File

@ -37,14 +37,6 @@ export class Viewport extends Component<void> {
get worldY(): number { get worldY(): number {
return Math.round(getPosition(this.entity)?.y ?? 0); return Math.round(getPosition(this.entity)?.y ?? 0);
} }
move(dx: number, dy: number) {
const pos = getPosition(this.entity);
if (!pos) return;
pos.x += dx;
pos.y += dy;
}
} }
export const createViewport = (world: World, viewportData: ViewportData) => { export const createViewport = (world: World, viewportData: ViewportData) => {

View File

@ -21,16 +21,12 @@ export class TextDisplaySystem extends System {
x: viewport.worldX - viewport.screenX, x: viewport.worldX - viewport.screenX,
y: viewport.worldY - viewport.screenY, y: viewport.worldY - viewport.screenY,
} : { x: 0, y: 0 }; } : { x: 0, y: 0 };
const clipRect = this.display.getClipRect();
if (viewport) {
this.display.setClipRect({ x: viewport.screenX, y: viewport.screenY, width: viewport.width, height: viewport.height });
}
const sprites = Array.from(world.query(Sprite, Position)).sort((a, b) => a[2].state.z - b[2].state.z); const sprites = Array.from(world.query(Sprite, Position)).sort((a, b) => a[2].state.z - b[2].state.z);
for (const [e, sprite, pos] of sprites) { for (const [e, sprite, pos] of sprites) {
if (e.has(Hidden)) continue; if (e.has(Hidden)) continue;
const image = sprite.image; const image = sprite.image;
const { x, y } = pos.state; const { x, y, absolute } = pos;
const data = const data =
Resources.get(TextRegion, image) Resources.get(TextRegion, image)
@ -38,9 +34,17 @@ export class TextDisplaySystem extends System {
?? image; ?? image;
const region = data instanceof TextRegion ? data : new TextRegion(data); const region = data instanceof TextRegion ? data : new TextRegion(data);
this.display.setRegion(x - offset.x, y - offset.y, region);
}
if (absolute) {
this.display.setRegion(x, y, region);
} else {
const clipRect = this.display.getClipRect();
if (viewport) {
this.display.setClipRect({ x: viewport.screenX, y: viewport.screenY, width: viewport.width, height: viewport.height });
}
this.display.setRegion(x - offset.x, y - offset.y, region);
this.display.setClipRect(clipRect); this.display.setClipRect(clipRect);
} }
} }
}
}

View File

@ -1,60 +1,170 @@
import { TextRegion } from "@common/display/text"; import { Color, TextRegion } from "@common/display/text";
import { gameLoop } from "@common/game"; import { gameLoop } from "@common/game";
import type { Point } from "@common/geometry";
import Input from "@common/input"; import Input from "@common/input";
import { BSP } from "@common/level/bsp"; import { BSP } from "@common/level/bsp";
import { bresenhamCircleGen } from "@common/navigation/bresenham";
import { SeededRandom } from "@common/random"; import { SeededRandom } from "@common/random";
import { Position } from "@common/rpg/components/position"; import { getPosition, move, Position } from "@common/rpg/components/position";
import { createViewport, Viewport } from "@common/rpg/components/render/viewport"; import { createViewport } from "@common/rpg/components/render/viewport";
import { Sprite } from "@common/rpg/components/sprite"; import { Sprite } from "@common/rpg/components/sprite";
import { World } from "@common/rpg/core/world"; import { World } from "@common/rpg/core/world";
import { TextDisplaySystem } from "@common/rpg/systems/render/text"; import { TextDisplaySystem } from "@common/rpg/systems/render/text";
import { Resources } from "@common/rpg/utils/resources"; import { Resources } from "@common/rpg/utils/resources";
const WALL = '#';
const PLAYER = '@';
function createMap(world: World, random: SeededRandom) { function createMap(world: World, random: SeededRandom) {
const mapSize = 100; const MAP_SIZE = 100;
const mapData = new Array(mapSize * mapSize).fill('#'); const mapData = new Array(MAP_SIZE * MAP_SIZE).fill(WALL);
BSP.generateLevel(mapSize, mapSize, (x, y) => { mapData[x + y * mapSize] = '.'; }, { BSP.generateLevel(MAP_SIZE, MAP_SIZE, (x, y) => { mapData[x + y * MAP_SIZE] = '.'; }, {
minWidth: 8, minWidth: 8,
minHeight: 8, minHeight: 8,
depth: 8, depth: 8,
random, random,
}); });
const mapDataString: string[] = [];
const maskDataString: string[] = [];
for (let i = 0; i < MAP_SIZE; i++) {
mapDataString.push(mapData.slice(i * MAP_SIZE, (i + 1) * MAP_SIZE).join(''));
}
for (let i = 0; i < MAP_SIZE * 3; i++) {
maskDataString.push(Array.from({ length: MAP_SIZE * 3 }, () => ' ').join(''));
}
const map = world.createEntity('map'); const map = world.createEntity('map');
const mapDataString: string[] = []; map.add(new Sprite(Resources.add('map', new TextRegion(mapDataString))));
for (let i = 0; i < mapSize; i++) { map.add(new Position(0, 0, -2));
mapDataString.push(mapData.slice(i * mapSize, (i + 1) * mapSize).join(''));
}
Resources.add('map', new TextRegion(mapDataString));
map.add(new Sprite('map'));
map.add(new Position(0, 0));
return map; const mask = world.createEntity('mask');
mask.add(new Sprite(Resources.add('mask', new TextRegion(maskDataString))));
mask.add(new Position(-MAP_SIZE, -MAP_SIZE, 9));
return { map, mask };
}
function createPlayer(world: World, x = 0, y = 0) {
const player = world.createEntity('player');
player.add(new Position(x, y, 10));
player.add(new Sprite(Resources.add('player', new TextRegion(PLAYER, Color.YELLOW))));
;
return player;
}
function createFPS(world: World, x = 0, y = 0) {
const fps = world.createEntity('fps');
fps.add(new Position(x, y, 100, true));
fps.add(new Sprite(Resources.add('fps', '60')));
return fps;
} }
export default gameLoop(() => { export default gameLoop(() => {
const world = new World(); const world = new World();
const display = world.addSystem(new TextDisplaySystem(100, 25)).display; const display = world.addSystem(new TextDisplaySystem(100, 25)).display;
const random = new SeededRandom('awoorwa'); const random = new SeededRandom();
const map = createMap(world, random); const { map, mask } = createMap(world, random);
const viewportEntity = createViewport(world, {
width: display.width >> 1, const mapData = Resources.get(TextRegion, map.get(Sprite)!.image)!;
const emptyCells: Point[] = [];
for (let x = 0; x < mapData.width; x++) {
for (let y = 0; y < mapData.height; y++) {
if (mapData.get(x, y)[0] === '.') {
emptyCells.push({ x, y });
}
}
}
const maskData = Resources.get(TextRegion, mask.get(Sprite)!.image)!;
const startCell = random.choice(emptyCells);
const viewport = createViewport(world, {
width: display.width,
height: display.height, height: display.height,
worldX: 0, worldX: startCell.x - (display.width >> 1),
worldY: 0, worldY: startCell.y - (display.height >> 1),
screenX: display.width >> 1, screenX: 0,
screenY: 0, screenY: 0,
}); });
const viewport = viewportEntity.get(Viewport)!; const player = createPlayer(world, startCell.x, startCell.y);
return { world, map, random, viewport }; createFPS(world, 0, 0);
}, (dt, { world, viewport }) => {
const dx = Input.getHorizontal();
const dy = Input.getVertical();
viewport.move(dx * dt * 32, dy * dt * 16); return {
world,
map, mapData,
mask, maskData, maskDirty: true,
fps: 0,
fpsTimer: 0,
random,
viewport,
player,
lastMove: 0, now: 0,
isWall: (x: number, y: number): boolean => {
const [ch] = mapData.get(x, y);
return ch === WALL;
},
};
}, (dt, state) => {
const { world, viewport, player, lastMove, now, mapData, maskData, isWall } = state;
let dx = -Math.sign(Input.getHorizontal());
let dy = -Math.sign(Input.getVertical());
const playerPos = getPosition(player)!;
if (now - lastMove > 0.03) {
if (isWall(playerPos.x + dx, playerPos.y)) {
dx = 0;
}
if (isWall(playerPos.x, playerPos.y + dy)) {
dy = 0;
}
if (isWall(playerPos.x + dx, playerPos.y + dy)) {
dx = 0
dy = 0;
}
} else {
dx = 0;
dy = 0;
}
if (dx || dy) {
move(player, dx, dy);
move(viewport, dx, dy);
playerPos.x + dx;
playerPos.y + dy;
state.maskDirty = true;
state.lastMove = now;
}
if (state.maskDirty) {
for (let x = 0; x < mapData.width; x++) {
for (let y = 0; y < mapData.height; y++) {
const [ch, fg] = maskData.get(x + mapData.width, y + mapData.height);
if (ch !== ' ' && fg !== Color.GRAY) {
maskData.set(x + mapData.width, y + mapData.height, [mapData.get(x, y)[0], Color.GRAY]);
}
}
}
for (const { x, y } of bresenhamCircleGen(playerPos.x, playerPos.y, 10, { fill: 'shadow', breaker: isWall })) {
maskData.set(x + mapData.width, y + mapData.height, '\0');
}
state.maskDirty = false;
}
world.update(dt); world.update(dt);
state.fpsTimer += dt;
if (state.fpsTimer >= 0.5) {
Resources.update('fps', (state.fps * 2).toString());
state.fps = 0;
state.fpsTimer = 0;
} else {
state.fps++;
};
state.now += dt;
}); });