1
0
Fork 0

Optimize text display

This commit is contained in:
Pabloader 2026-05-05 14:41:40 +00:00
parent 7956d2bb51
commit 738272f2d4
6 changed files with 210 additions and 144 deletions

View File

@ -2,10 +2,11 @@ import '@common/assets/fonts/vga.font.css';
import { randInt } from "@common/utils"; import { randInt } from "@common/utils";
import { createCanvas } from './canvas'; import { createCanvas } from './canvas';
import type { Rect } from '@common/geometry'; import type { Rect } from '@common/geometry';
import { bresenhamCircleGen, bresenhamLineGen, type BresenhamCircleOptions, type BresenhamLineOptions } from '@common/navigation/bresenham';
export type IColorLike = string | number | Color; export type ColorLike = string | number | Color;
export type IChar = [string, IColorLike?, IColorLike?] | string; export type Char = [string, ColorLike?, ColorLike?] | string;
export type IDefinedChar = [string, IColorLike, IColorLike]; export type DefinedChar = [string, ColorLike, ColorLike];
export const randChar = (min = ' ', max = '~') => export const randChar = (min = ' ', max = '~') =>
String.fromCharCode(randInt( String.fromCharCode(randInt(
@ -68,16 +69,16 @@ export const isVertical = (char: string) => char === '│';
export const isHorizontal = (char: string) => char === '─'; export const isHorizontal = (char: string) => char === '─';
export const isCorner = (char: string) => '┌┐└┘'.includes(char); export const isCorner = (char: string) => '┌┐└┘'.includes(char);
interface IBoxOptions { interface BoxOptions {
vertical?: string; vertical?: string;
horizontal?: string; horizontal?: string;
topLeft?: string; topLeft?: string;
topRight?: string; topRight?: string;
bottomLeft?: string; bottomLeft?: string;
bottomRight?: string; bottomRight?: string;
fill?: IChar; fill?: Char;
fg?: IColorLike; fg?: ColorLike;
bg?: IColorLike; bg?: ColorLike;
title?: string; title?: string;
} }
@ -87,23 +88,23 @@ const NATIVE_FONT = `${CHAR_H}px "IBM VGA 8x16"`;
const FALLBACK_FONT = `${CHAR_H}px monospace`; const FALLBACK_FONT = `${CHAR_H}px monospace`;
const REGION_DATA = Symbol('TextRegion.data'); const REGION_DATA = Symbol('TextRegion.data');
const colorToCSS = (c: IColorLike): string => const colorToCSS = (c: ColorLike): string =>
typeof c === 'number' ? COLORS[c] : c as string; typeof c === 'number' ? COLORS[c] : c as string;
const parseChar = (char: IChar): IDefinedChar => ( const parseChar = (char: Char, fg: ColorLike = DEFAULT_FG, bg: ColorLike = DEFAULT_BG): DefinedChar => (
typeof char === 'string' typeof char === 'string'
? [char, DEFAULT_FG, DEFAULT_BG] ? [char, fg, bg]
: [ : [
char[0], char[0],
char[1] ?? DEFAULT_FG, char[1] ?? fg,
char[2] ?? DEFAULT_BG, char[2] ?? bg,
] ]
); );
export class TextDisplay { export class TextDisplay {
private chars: string[]; private chars: string[];
private fgs: IColorLike[]; private fgs: ColorLike[];
private bgs: IColorLike[]; private bgs: ColorLike[];
private canvas: HTMLCanvasElement; private canvas: HTMLCanvasElement;
private ctx: CanvasRenderingContext2D; private ctx: CanvasRenderingContext2D;
private font = FALLBACK_FONT; private font = FALLBACK_FONT;
@ -112,12 +113,13 @@ export class TextDisplay {
private clipTop: number = 0; private clipTop: number = 0;
private clipRight: number; private clipRight: number;
private clipBottom: number; private clipBottom: number;
private dirtySet = new Set<number>();
constructor( constructor(
public width = GAME_WIDTH, public width = GAME_WIDTH,
public height = GAME_HEIGHT, public height = GAME_HEIGHT,
parent?: HTMLCanvasElement, parent?: HTMLCanvasElement,
letterboxColor: IColorLike = DEFAULT_BG, letterboxColor: ColorLike = DEFAULT_BG,
) { ) {
this.letterboxColor = colorToCSS(letterboxColor); this.letterboxColor = colorToCSS(letterboxColor);
const canvas = parent ?? createCanvas(width * CHAR_W, height * CHAR_H); const canvas = parent ?? createCanvas(width * CHAR_W, height * CHAR_H);
@ -158,6 +160,14 @@ export class TextDisplay {
document.body.style.background = this.letterboxColor; document.body.style.background = this.letterboxColor;
} }
private redrawDirty() {
for (const i of this.dirtySet) {
const x = i % this.width;
const y = Math.floor(i / this.width);
this.drawCell(x, y);
}
}
private redraw() { private redraw() {
for (let y = 0; y < this.height; y++) { for (let y = 0; y < this.height; y++) {
for (let x = 0; x < this.width; x++) { for (let x = 0; x < this.width; x++) {
@ -166,6 +176,10 @@ export class TextDisplay {
} }
} }
private dirtyCell(x: number, y: number) {
this.dirtySet.add(y * this.width + x);
}
private drawCell(x: number, y: number) { private drawCell(x: number, y: number) {
const i = y * this.width + x; const i = y * this.width + x;
const px = x * CHAR_W; const px = x * CHAR_W;
@ -185,9 +199,10 @@ export class TextDisplay {
this.ctx.fillText(char, px, py); this.ctx.fillText(char, px, py);
this.ctx.restore(); this.ctx.restore();
} }
this.dirtySet.delete(i);
} }
private setCharRaw(x: number, y: number, char: string, fg: IColorLike, bg: IColorLike) { private setCharRaw(x: number, y: number, char: string, fg: ColorLike, bg: ColorLike) {
if (x < this.clipLeft || y < this.clipTop || x >= this.clipRight || y >= this.clipBottom) return; if (x < this.clipLeft || y < this.clipTop || x >= this.clipRight || y >= this.clipBottom) return;
if (!char || char === '\0') return; if (!char || char === '\0') return;
@ -206,15 +221,15 @@ export class TextDisplay {
dirty = true; dirty = true;
} }
if (dirty) { if (dirty) {
this.drawCell(x | 0, y | 0); this.dirtyCell(x | 0, y | 0);
} }
} }
setChar(x: number, y: number, char: IChar = '█') { setChar(x: number, y: number, char: Char = '█') {
this.setCharRaw(x | 0, y | 0, ...parseChar(char)); this.setCharRaw(x | 0, y | 0, ...parseChar(char));
} }
getChar(x: number, y: number): IDefinedChar { getChar(x: number, y: number): DefinedChar {
x = x | 0; y = y | 0; x = x | 0; y = y | 0;
if (x < this.clipLeft || y < this.clipTop || x >= this.clipRight || y >= this.clipBottom) { if (x < this.clipLeft || y < this.clipTop || x >= this.clipRight || y >= this.clipBottom) {
return [' ', DEFAULT_FG, DEFAULT_BG]; return [' ', DEFAULT_FG, DEFAULT_BG];
@ -239,53 +254,54 @@ export class TextDisplay {
const regRow = row - y; const regRow = row - y;
const regBase = regRow * rw + regColOffset; const regBase = regRow * rw + regColOffset;
const dispBase = row * this.width + x0; const dispBase = row * this.width + x0;
const regRowStr = chars[regRow];
for (let i = 0; i < copyW; i++) { for (let i = 0; i < copyW; i++) {
const ch = regRowStr[regColOffset + i]; const ch = chars[regBase + i];
if (ch === '\0') continue; if (ch === '\0') continue;
this.chars[dispBase + i] = ch; let dirty = false;
this.fgs[dispBase + i] = fgs[regBase + i]; if (ch !== this.chars[dispBase + i]) {
this.bgs[dispBase + i] = bgs[regBase + i]; dirty = true;
this.drawCell(x0 + i, row); this.chars[dispBase + i] = ch;
}
if (this.fgs[dispBase + i] !== fgs[regBase + i]) {
this.fgs[dispBase + i] = fgs[regBase + i];
dirty = true;
}
if (this.bgs[dispBase + i] !== bgs[regBase + i]) {
this.bgs[dispBase + i] = bgs[regBase + i];
dirty = true;
}
if (dirty) {
this.dirtyCell(x0 + i, row);
}
} }
} }
} }
getRegion(x: number, y: number, w: number, h: number): TextRegion { getRegion(x: number, y: number, w: number, h: number): TextRegion {
x = x | 0; y = y | 0; w = w | 0; h = h | 0; x = x | 0; y = y | 0; w = w | 0; h = h | 0;
const rows: string[] = []; const data: DefinedChar[][] = Array(h);
const fgs: IColorLike[][] = [];
const bgs: IColorLike[][] = [];
for (let row = 0; row < h; row++) { for (let row = 0; row < h; row++) {
const dispRow = y + row; const dispRow = y + row;
let rowStr = ''; const charsRow: DefinedChar[] = Array(w);
const fgRow: IColorLike[] = [];
const bgRow: IColorLike[] = [];
for (let col = 0; col < w; col++) { for (let col = 0; col < w; col++) {
const dispCol = x + col; const dispCol = x + col;
if (dispCol < this.clipLeft || dispRow < this.clipTop || dispCol >= this.clipRight || dispRow >= this.clipBottom) { if (dispCol < this.clipLeft || dispRow < this.clipTop || dispCol >= this.clipRight || dispRow >= this.clipBottom) {
rowStr += ' '; charsRow[col] = ['\0', DEFAULT_FG, DEFAULT_BG];
fgRow.push(DEFAULT_FG);
bgRow.push(DEFAULT_BG);
} else { } else {
const i = dispRow * this.width + dispCol; const i = dispRow * this.width + dispCol;
rowStr += this.chars[i]; charsRow[col] = [this.chars[i], this.fgs[i], this.bgs[i]]
fgRow.push(this.fgs[i]);
bgRow.push(this.bgs[i]);
} }
} }
rows.push(rowStr); data[row] = charsRow;
fgs.push(fgRow);
bgs.push(bgRow);
} }
return new TextRegion(rows, fgs, bgs); return new TextRegion(data);
} }
drawText(x: number, y: number, text: string, fg: IColorLike = DEFAULT_FG, bg: IColorLike = DEFAULT_BG) { drawString(text: unknown, x: number, y: number, fg: ColorLike = DEFAULT_FG, bg: ColorLike = DEFAULT_BG) {
x = x | 0; y = y | 0; x = x | 0; y = y | 0;
const lines = text.split('\n'); const lines = String(text).split('\n');
for (let row = 0; row < lines.length; row++) { for (let row = 0; row < lines.length; row++) {
const line = lines[row]; const line = lines[row];
const ry = y + row; const ry = y + row;
@ -296,7 +312,7 @@ export class TextDisplay {
} }
} }
drawVLine(x: number, y1: number, y2: number, char: IChar = '│') { drawVLine(x: number, y1: number, y2: number, char: Char = '│') {
x = x | 0; y1 = y1 | 0; y2 = y2 | 0; x = x | 0; y1 = y1 | 0; y2 = y2 | 0;
if (y2 < y1) { const t = y2; y2 = y1; y1 = t; } if (y2 < y1) { const t = y2; y2 = y1; y1 = t; }
@ -310,7 +326,7 @@ export class TextDisplay {
} }
} }
drawHLine(x1: number, x2: number, y: number, char: IChar = '─') { drawHLine(x1: number, x2: number, y: number, char: Char = '─') {
x1 = x1 | 0; x2 = x2 | 0; y = y | 0; x1 = x1 | 0; x2 = x2 | 0; y = y | 0;
if (x2 < x1) { const t = x2; x2 = x1; x1 = t; } if (x2 < x1) { const t = x2; x2 = x1; x1 = t; }
x1 = Math.max(this.clipLeft, x1); x1 = Math.max(this.clipLeft, x1);
@ -323,7 +339,7 @@ export class TextDisplay {
} }
} }
drawBox(x: number, y: number, width: number, height: number, options: IBoxOptions = {}) { drawBox(x: number, y: number, width: number, height: number, options: BoxOptions = {}) {
x = x | 0; y = y | 0; x = x | 0; y = y | 0;
const { const {
vertical = '│', vertical = '│',
@ -354,35 +370,67 @@ export class TextDisplay {
this.drawVLine(x + width + 1, y + 1, y + height, [vertical, fg, bg]); this.drawVLine(x + width + 1, y + 1, y + height, [vertical, fg, bg]);
if (title) { if (title) {
this.drawText(x + 1, y, title, fg, bg); this.drawString(title, x + 1, y, fg, bg);
} }
} }
drawTextInBox(x: number, y: number, text: string, options: IBoxOptions = {}) { drawStringInBox(text: unknown, x: number, y: number, options: BoxOptions = {}) {
x = x | 0; y = y | 0; x = x | 0; y = y | 0;
const { const {
fg = DEFAULT_FG, fg = DEFAULT_FG,
bg = DEFAULT_BG, bg = DEFAULT_BG,
} = options; } = options;
let width = 0; const lines = String(text).split('\n');
const lines = text.split('\n'); const width = lines.reduce((m, line) => Math.max(m, line.length), 0);
const height = lines.length; const height = lines.length;
for (const line of lines) { this.drawBox(x, y, width, height, { ...options, fill: ' ' });
if (line.length > width) width = line.length; this.drawString(text, x + 1, y + 1, fg, bg);
}
this.drawBox(x, y, width, height, { ...options, fill: [' '] });
this.drawText(x + 1, y + 1, text, fg, bg);
} }
fillBox(x: number, y: number, width: number, height: number, char: IChar = '█') { fillBox(x: number, y: number, width: number, height: number, char: Char = '█') {
x = x | 0; y = y | 0; x = x | 0; y = y | 0;
for (let i = y; i < y + height; i++) { for (let i = y; i < y + height; i++) {
this.drawHLine(x, x + width - 1, i, char); this.drawHLine(x, x + width - 1, i, char);
} }
} }
drawLine(fromX: number, fromY: number, toX: number, toY: number, char: Char = '█') {
const [ch, fg, bg] = parseChar(char);
const options: BresenhamLineOptions = {
minX: this.clipLeft,
minY: this.clipTop,
maxX: this.clipRight - 1,
maxY: this.clipBottom - 1,
directions: 8,
}
for (const { x, y } of bresenhamLineGen(fromX, fromY, toX, toY, options)) {
this.setCharRaw(x, y, ch, fg, bg);
}
}
#circle(cx: number, cy: number, radius: number, char: Char, fill: boolean) {
const [ch, fg, bg] = parseChar(char);
const options: BresenhamCircleOptions = {
minX: this.clipLeft,
minY: this.clipTop,
maxX: this.clipRight - 1,
maxY: this.clipBottom - 1,
fill,
}
for (const { x, y } of bresenhamCircleGen(cx, cy, radius, options)) {
this.setCharRaw(x, y, ch, fg, bg);
}
}
drawCircle(cx: number, cy: number, radius: number, char: Char = '█') {
this.#circle(cx, cy, radius, char, false);
}
fillCircle(cx: number, cy: number, radius: number, char: Char = '█') {
this.#circle(cx, cy, radius, char, true);
}
getClipRect(): Rect { getClipRect(): Rect {
return { return {
x: this.clipLeft, x: this.clipLeft,
@ -398,72 +446,86 @@ export class TextDisplay {
this.clipRight = Math.min(this.width, rect.x + rect.width); this.clipRight = Math.min(this.width, rect.x + rect.width);
this.clipBottom = Math.min(this.height, rect.y + rect.height); this.clipBottom = Math.min(this.height, rect.y + rect.height);
} }
update() {
this.redrawDirty();
}
}
function expandColors(chars: string[] | Char[][], colors: ColorLike | ColorLike[] | ColorLike[][]): ColorLike[][] {
if (!Array.isArray(colors)) {
return chars.map(row => Array.from(row, () => colors));
}
if (colors.length === 0) {
return [];
}
if (Array.isArray(colors[0])) {
return colors as ColorLike[][];
}
const oldColors = colors as ColorLike[];
const newColors: ColorLike[][] = [];
let i = 0;
for (const row of chars) {
newColors.push(Array.from(row, () => oldColors[i++]));
}
return newColors;
} }
export class TextRegion { export class TextRegion {
readonly #chars: string[]; // one string per row, all same length readonly #chars: string[];
readonly #fgs: IColorLike[]; readonly #fgs: ColorLike[];
readonly #bgs: IColorLike[]; readonly #bgs: ColorLike[];
readonly width: number;
readonly height: number;
get width(): number { return this.#chars[0]?.length ?? 0; } constructor(chars: Char[][], fg?: ColorLike, bg?: ColorLike);
get height(): number { return this.#chars.length; } constructor(chars: string, fg?: ColorLike | ColorLike[], bg?: ColorLike | ColorLike[]);
constructor(chars: string[], fg?: ColorLike | ColorLike[][], bg?: ColorLike | ColorLike[][]);
constructor(chars: IChar[][]);
constructor(chars: string, fg?: IColorLike | IColorLike[], bg?: IColorLike | IColorLike[]);
constructor(chars: string[], fg?: IColorLike | IColorLike[][], bg?: IColorLike | IColorLike[][]);
constructor( constructor(
chars: IChar[][] | string | string[], chars: Char[][] | string | string[],
fg?: IColorLike | IColorLike[] | IColorLike[][], fg: ColorLike | ColorLike[] | ColorLike[][] = DEFAULT_BG,
bg?: IColorLike | IColorLike[] | IColorLike[][], bg: ColorLike | ColorLike[] | ColorLike[][] = DEFAULT_FG,
) { ) {
if (typeof chars === 'string') { if (typeof chars === 'string') {
chars = chars.split('\n'); chars = chars.split('\n');
} }
if (chars.length === 0 || typeof chars[0] === 'string') { this.width = chars.reduce((m, r) => Math.max(m, r.length), 0);
const rows = chars as string[]; this.height = chars.length;
const w = rows.reduce((m, r) => Math.max(m, r.length), 0); const defaultFg = Array.isArray(fg) ? DEFAULT_FG : fg;
const h = rows.length; const defaultBg = Array.isArray(bg) ? DEFAULT_BG : bg;
this.#chars = rows.map(r => r.padEnd(w, ' '));
this.#fgs = Array(w * h).fill(DEFAULT_FG); fg = expandColors(chars, fg ?? defaultFg);
this.#bgs = Array(w * h).fill(DEFAULT_BG); bg = expandColors(chars, bg ?? defaultBg);
if (fg != null && !Array.isArray(fg)) this.#fgs.fill(fg);
if (bg != null && !Array.isArray(bg)) this.#bgs.fill(bg); const s = this.width * this.height;
if (Array.isArray(fg) || Array.isArray(bg)) { this.#chars = Array(s);
for (let y = 0; y < h; y++) { this.#fgs = Array(s);
for (let x = 0; x < w; x++) { this.#bgs = Array(s);
const i = y * w + x;
if (Array.isArray(fg)) this.#fgs[i] = (fg as IColorLike[][])[y]?.[x] ?? DEFAULT_FG; for (let y = 0; y < this.height; y++) {
if (Array.isArray(bg)) this.#bgs[i] = (bg as IColorLike[][])[y]?.[x] ?? DEFAULT_BG; for (let x = 0; x < this.width; x++) {
} const i = y * this.width + x;
} const char = chars[y]?.[x] ?? '\0';
} const charFG = fg[y]?.[x] ?? defaultFg;
} else { const charBG = bg[y]?.[x] ?? defaultBg;
const ichars = chars as IChar[][];
const h = ichars.length; const ch = parseChar(char, charFG, charBG);
const w = ichars.reduce((m, r) => Math.max(m, r.length), 0); this.#chars[i] = ch[0];
this.#chars = ichars.map(row => row.map(ch => ch[0]).join('').padEnd(w, ' ')); this.#fgs[i] = ch[1];
this.#fgs = Array(w * h).fill(DEFAULT_FG); this.#bgs[i] = ch[2];
this.#bgs = Array(w * h).fill(DEFAULT_BG);
for (let y = 0; y < h; y++) {
for (let x = 0; x < ichars[y].length; x++) {
const ch = ichars[y][x];
const i = y * w + x;
if (ch[1] != null) this.#fgs[i] = ch[1];
if (ch[2] != null) this.#bgs[i] = ch[2];
}
} }
} }
} }
get(x: number, y: number): IDefinedChar { get(x: number, y: number): DefinedChar {
const i = y * this.width + x; const i = y * this.width + x;
return [this.#chars[y][x], this.#fgs[i], this.#bgs[i]]; return [this.#chars[i], this.#fgs[i], this.#bgs[i]];
} }
set(x: number, y: number, char: IChar) { set(x: number, y: number, char: Char) {
const ch = parseChar(char); const ch = parseChar(char);
this.#chars[y] = this.#chars[y].slice(0, x) + ch[0] + this.#chars[y].slice(x + 1); this.#chars[y * this.width + x] = ch[0];
this.#fgs[y * this.width + x] = ch[1]; this.#fgs[y * this.width + x] = ch[1];
this.#bgs[y * this.width + x] = ch[2]; this.#bgs[y * this.width + x] = ch[2];
} }

View File

@ -2,13 +2,17 @@ import { formatError, formatErrorMessage } from "./errors";
import Input from "./input"; import Input from "./input";
import { nextFrame } from "./utils"; import { nextFrame } from "./utils";
type Setup<T> = () => Promise<T> | T; interface FrameMeta {
type Frame<T> = (dt: number, state: T) => Promise<T | void> | T | void; fps: number;
type GameMain = () => Promise<void>; }
type Awaitable<T> = PromiseLike<T> | T;
export function gameLoop<T>(frame: Frame<T>): GameMain; type Setup<T> = () => Awaitable<T>;
export function gameLoop<T>(setup: Setup<T>, frame: Frame<T>): GameMain; type Frame<T> = (dt: number, state: T, meta: FrameMeta) => Awaitable<T | void>;
export function gameLoop<T>(setupOrFrame: Setup<T> | Frame<T>, frame?: Frame<T>): GameMain {
export function gameLoop<T>(frame: Frame<T>): RunGame;
export function gameLoop<T>(setup: Setup<T>, frame: Frame<T>): RunGame;
export function gameLoop<T>(setupOrFrame: Setup<T> | Frame<T>, frame?: Frame<T>): RunGame {
return async () => { return async () => {
let state: T; let state: T;
try { try {
@ -26,6 +30,9 @@ export function gameLoop<T>(setupOrFrame: Setup<T> | Frame<T>, frame?: Frame<T>)
try { try {
let prevFrame = performance.now(); let prevFrame = performance.now();
let fpsCounter = 0;
let fpsTimer = 0;
const meta: FrameMeta = { fps: 0 };
while (true) { while (true) {
await nextFrame(); await nextFrame();
Input.updateKeys(); Input.updateKeys();
@ -33,13 +40,20 @@ export function gameLoop<T>(setupOrFrame: Setup<T> | Frame<T>, frame?: Frame<T>)
const now = performance.now(); const now = performance.now();
const dt = (now - prevFrame) / 1000; const dt = (now - prevFrame) / 1000;
if (dt < 1) { if (dt < 1) { // skip long pause to avoid blowing up values
const newState = await frame(dt, state); const newState = await frame(dt, state, meta);
if (newState) { if (newState) {
state = newState; state = newState;
} }
} }
fpsCounter += 1;
if (now - fpsTimer > 500) {
meta.fps = fpsCounter * 2;
fpsCounter = 0;
fpsTimer = now;
}
prevFrame = performance.now(); prevFrame = performance.now();
} }
} catch (e) { } catch (e) {

View File

@ -41,7 +41,7 @@ interface BresenhamCircleFovOptions extends BresenhamCircleBaseOptions<'fov' | '
breaker?: (x: number, y: number) => boolean; breaker?: (x: number, y: number) => boolean;
} }
type BresenhamCircleOptions = BresenhamCircleFillOptions | BresenhamCircleFovOptions; export type BresenhamCircleOptions = BresenhamCircleFillOptions | BresenhamCircleFovOptions;
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Cohen-Sutherland outcodes // Cohen-Sutherland outcodes

View File

@ -48,5 +48,7 @@ export class TextDisplaySystem extends System {
this.display.setClipRect(clipRect); this.display.setClipRect(clipRect);
} }
} }
this.display.update();
} }
} }

View File

@ -21,8 +21,8 @@ function createMap(world: World, random: SeededRandom) {
BSP.generateLevel(MAP_SIZE, MAP_SIZE, (x, y) => { mapData[x + y * MAP_SIZE] = '.'; }, { BSP.generateLevel(MAP_SIZE, MAP_SIZE, (x, y) => { mapData[x + y * MAP_SIZE] = '.'; }, {
minWidth: 8, minWidth: 8,
minHeight: 8, minHeight: 4,
depth: 8, depth: 10,
random, random,
}); });
const mapDataString: string[] = []; const mapDataString: string[] = [];
@ -49,17 +49,9 @@ function createPlayer(world: World, x = 0, y = 0) {
const player = world.createEntity('player'); const player = world.createEntity('player');
player.add(new Position(x, y, 10)); player.add(new Position(x, y, 10));
player.add(new Sprite(Resources.add('player', new TextRegion(PLAYER, Color.YELLOW)))); player.add(new Sprite(Resources.add('player', new TextRegion(PLAYER, Color.YELLOW))));
;
return player; 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;
@ -89,14 +81,12 @@ export default gameLoop(() => {
screenY: 0, screenY: 0,
}); });
const player = createPlayer(world, startCell.x, startCell.y); const player = createPlayer(world, startCell.x, startCell.y);
createFPS(world, 0, 0);
return { return {
display,
world, world,
map, mapData, map, mapData,
mask, maskData, maskDirty: true, mask, maskData, maskDirty: true,
fps: 0,
fpsTimer: 0,
random, random,
viewport, viewport,
player, player,
@ -107,13 +97,20 @@ export default gameLoop(() => {
}, },
}; };
}, (dt, state) => { }, (dt, state) => {
const { world, viewport, player, lastMove, now, mapData, maskData, isWall } = state; const {
world,
viewport,
player,
lastMove, now,
mapData, maskData,
isWall,
} = state;
let dx = -Math.sign(Input.getHorizontal()); let dx = -Math.sign(Input.getHorizontal());
let dy = -Math.sign(Input.getVertical()); let dy = -Math.sign(Input.getVertical());
const playerPos = getPosition(player)!; const playerPos = getPosition(player)!;
if (now - lastMove > 0.03) { if (now - lastMove > 0.05) {
if (isWall(playerPos.x + dx, playerPos.y)) { if (isWall(playerPos.x + dx, playerPos.y)) {
dx = 0; dx = 0;
} }
@ -157,14 +154,5 @@ export default gameLoop(() => {
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; state.now += dt;
}); });

View File

@ -19,7 +19,7 @@ export default async function main() {
const display = new TextDisplay(); const display = new TextDisplay();
world.addSystem(new TextDisplaySystem(display)); world.addSystem(new TextDisplaySystem(display));
display.drawTextInBox(8, 11, 'Use arrow keys to move\nSHIFT to run\nSPACE to activate\nAWOO!', { fg: Color.CYAN }); display.drawStringInBox('Use arrow keys to move\nSHIFT to run\nSPACE to activate\nAWOO!', 8, 11, { fg: Color.CYAN });
let roomEntity = getRoom(world, 0, 0, 0); let roomEntity = getRoom(world, 0, 0, 0);
let room = roomEntity?.get(Room); let room = roomEntity?.get(Room);
@ -72,7 +72,7 @@ export default async function main() {
const foundItems = inv ? Object.values(inv.state.slots).length : 0; const foundItems = inv ? Object.values(inv.state.slots).length : 0;
const totalItems = Array.from(world.query(Item)).length; const totalItems = Array.from(world.query(Item)).length;
const items = `${foundItems}/${totalItems}`.padStart(coords.length - 2); const items = `${foundItems}/${totalItems}`.padStart(coords.length - 2);
display.drawTextInBox(0, 0, `Pos: ${coords}\nRooms: ${rooms}\nItems: ${items}`, { fg: Color.YELLOW, title: 'Info' }); display.drawStringInBox(`Pos: ${coords}\nRooms: ${rooms}\nItems: ${items}`, 0, 0, { fg: Color.YELLOW, title: 'Info' });
} }
let lastMove = Date.now(); let lastMove = Date.now();