Optimize text display
This commit is contained in:
parent
7956d2bb51
commit
738272f2d4
|
|
@ -2,10 +2,11 @@ import '@common/assets/fonts/vga.font.css';
|
|||
import { randInt } from "@common/utils";
|
||||
import { createCanvas } from './canvas';
|
||||
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 IChar = [string, IColorLike?, IColorLike?] | string;
|
||||
export type IDefinedChar = [string, IColorLike, IColorLike];
|
||||
export type ColorLike = string | number | Color;
|
||||
export type Char = [string, ColorLike?, ColorLike?] | string;
|
||||
export type DefinedChar = [string, ColorLike, ColorLike];
|
||||
|
||||
export const randChar = (min = ' ', max = '~') =>
|
||||
String.fromCharCode(randInt(
|
||||
|
|
@ -68,16 +69,16 @@ export const isVertical = (char: string) => char === '│';
|
|||
export const isHorizontal = (char: string) => char === '─';
|
||||
export const isCorner = (char: string) => '┌┐└┘'.includes(char);
|
||||
|
||||
interface IBoxOptions {
|
||||
interface BoxOptions {
|
||||
vertical?: string;
|
||||
horizontal?: string;
|
||||
topLeft?: string;
|
||||
topRight?: string;
|
||||
bottomLeft?: string;
|
||||
bottomRight?: string;
|
||||
fill?: IChar;
|
||||
fg?: IColorLike;
|
||||
bg?: IColorLike;
|
||||
fill?: Char;
|
||||
fg?: ColorLike;
|
||||
bg?: ColorLike;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
|
|
@ -87,23 +88,23 @@ const NATIVE_FONT = `${CHAR_H}px "IBM VGA 8x16"`;
|
|||
const FALLBACK_FONT = `${CHAR_H}px monospace`;
|
||||
const REGION_DATA = Symbol('TextRegion.data');
|
||||
|
||||
const colorToCSS = (c: IColorLike): string =>
|
||||
const colorToCSS = (c: ColorLike): 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'
|
||||
? [char, DEFAULT_FG, DEFAULT_BG]
|
||||
? [char, fg, bg]
|
||||
: [
|
||||
char[0],
|
||||
char[1] ?? DEFAULT_FG,
|
||||
char[2] ?? DEFAULT_BG,
|
||||
char[1] ?? fg,
|
||||
char[2] ?? bg,
|
||||
]
|
||||
);
|
||||
|
||||
export class TextDisplay {
|
||||
private chars: string[];
|
||||
private fgs: IColorLike[];
|
||||
private bgs: IColorLike[];
|
||||
private fgs: ColorLike[];
|
||||
private bgs: ColorLike[];
|
||||
private canvas: HTMLCanvasElement;
|
||||
private ctx: CanvasRenderingContext2D;
|
||||
private font = FALLBACK_FONT;
|
||||
|
|
@ -112,12 +113,13 @@ export class TextDisplay {
|
|||
private clipTop: number = 0;
|
||||
private clipRight: number;
|
||||
private clipBottom: number;
|
||||
private dirtySet = new Set<number>();
|
||||
|
||||
constructor(
|
||||
public width = GAME_WIDTH,
|
||||
public height = GAME_HEIGHT,
|
||||
parent?: HTMLCanvasElement,
|
||||
letterboxColor: IColorLike = DEFAULT_BG,
|
||||
letterboxColor: ColorLike = DEFAULT_BG,
|
||||
) {
|
||||
this.letterboxColor = colorToCSS(letterboxColor);
|
||||
const canvas = parent ?? createCanvas(width * CHAR_W, height * CHAR_H);
|
||||
|
|
@ -158,6 +160,14 @@ export class TextDisplay {
|
|||
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() {
|
||||
for (let y = 0; y < this.height; y++) {
|
||||
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) {
|
||||
const i = y * this.width + x;
|
||||
const px = x * CHAR_W;
|
||||
|
|
@ -185,9 +199,10 @@ export class TextDisplay {
|
|||
this.ctx.fillText(char, px, py);
|
||||
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 (!char || char === '\0') return;
|
||||
|
||||
|
|
@ -206,15 +221,15 @@ export class TextDisplay {
|
|||
dirty = true;
|
||||
}
|
||||
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));
|
||||
}
|
||||
|
||||
getChar(x: number, y: number): IDefinedChar {
|
||||
getChar(x: number, y: number): DefinedChar {
|
||||
x = x | 0; y = y | 0;
|
||||
if (x < this.clipLeft || y < this.clipTop || x >= this.clipRight || y >= this.clipBottom) {
|
||||
return [' ', DEFAULT_FG, DEFAULT_BG];
|
||||
|
|
@ -239,53 +254,54 @@ export class TextDisplay {
|
|||
const regRow = row - y;
|
||||
const regBase = regRow * rw + regColOffset;
|
||||
const dispBase = row * this.width + x0;
|
||||
const regRowStr = chars[regRow];
|
||||
for (let i = 0; i < copyW; i++) {
|
||||
const ch = regRowStr[regColOffset + i];
|
||||
const ch = chars[regBase + i];
|
||||
if (ch === '\0') continue;
|
||||
let dirty = false;
|
||||
if (ch !== this.chars[dispBase + i]) {
|
||||
dirty = true;
|
||||
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];
|
||||
this.drawCell(x0 + i, row);
|
||||
dirty = true;
|
||||
}
|
||||
if (dirty) {
|
||||
this.dirtyCell(x0 + i, row);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getRegion(x: number, y: number, w: number, h: number): TextRegion {
|
||||
x = x | 0; y = y | 0; w = w | 0; h = h | 0;
|
||||
const rows: string[] = [];
|
||||
const fgs: IColorLike[][] = [];
|
||||
const bgs: IColorLike[][] = [];
|
||||
const data: DefinedChar[][] = Array(h);
|
||||
|
||||
for (let row = 0; row < h; row++) {
|
||||
const dispRow = y + row;
|
||||
let rowStr = '';
|
||||
const fgRow: IColorLike[] = [];
|
||||
const bgRow: IColorLike[] = [];
|
||||
const charsRow: DefinedChar[] = Array(w);
|
||||
for (let col = 0; col < w; col++) {
|
||||
const dispCol = x + col;
|
||||
if (dispCol < this.clipLeft || dispRow < this.clipTop || dispCol >= this.clipRight || dispRow >= this.clipBottom) {
|
||||
rowStr += ' ';
|
||||
fgRow.push(DEFAULT_FG);
|
||||
bgRow.push(DEFAULT_BG);
|
||||
charsRow[col] = ['\0', DEFAULT_FG, DEFAULT_BG];
|
||||
} else {
|
||||
const i = dispRow * this.width + dispCol;
|
||||
rowStr += this.chars[i];
|
||||
fgRow.push(this.fgs[i]);
|
||||
bgRow.push(this.bgs[i]);
|
||||
charsRow[col] = [this.chars[i], this.fgs[i], this.bgs[i]]
|
||||
}
|
||||
}
|
||||
rows.push(rowStr);
|
||||
fgs.push(fgRow);
|
||||
bgs.push(bgRow);
|
||||
data[row] = charsRow;
|
||||
}
|
||||
|
||||
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;
|
||||
const lines = text.split('\n');
|
||||
const lines = String(text).split('\n');
|
||||
for (let row = 0; row < lines.length; row++) {
|
||||
const line = lines[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;
|
||||
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;
|
||||
if (x2 < x1) { const t = x2; x2 = x1; x1 = t; }
|
||||
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;
|
||||
const {
|
||||
vertical = '│',
|
||||
|
|
@ -354,35 +370,67 @@ export class TextDisplay {
|
|||
this.drawVLine(x + width + 1, y + 1, y + height, [vertical, fg, bg]);
|
||||
|
||||
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;
|
||||
const {
|
||||
fg = DEFAULT_FG,
|
||||
bg = DEFAULT_BG,
|
||||
} = options;
|
||||
let width = 0;
|
||||
const lines = text.split('\n');
|
||||
const lines = String(text).split('\n');
|
||||
const width = lines.reduce((m, line) => Math.max(m, line.length), 0);
|
||||
const height = lines.length;
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.length > width) width = line.length;
|
||||
this.drawBox(x, y, width, height, { ...options, fill: ' ' });
|
||||
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;
|
||||
for (let i = y; i < y + height; i++) {
|
||||
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 {
|
||||
return {
|
||||
x: this.clipLeft,
|
||||
|
|
@ -398,72 +446,86 @@ export class TextDisplay {
|
|||
this.clipRight = Math.min(this.width, rect.x + rect.width);
|
||||
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 {
|
||||
readonly #chars: string[]; // one string per row, all same length
|
||||
readonly #fgs: IColorLike[];
|
||||
readonly #bgs: IColorLike[];
|
||||
readonly #chars: string[];
|
||||
readonly #fgs: ColorLike[];
|
||||
readonly #bgs: ColorLike[];
|
||||
readonly width: number;
|
||||
readonly height: number;
|
||||
|
||||
get width(): number { return this.#chars[0]?.length ?? 0; }
|
||||
get height(): number { return this.#chars.length; }
|
||||
|
||||
constructor(chars: IChar[][]);
|
||||
constructor(chars: string, fg?: IColorLike | IColorLike[], bg?: IColorLike | IColorLike[]);
|
||||
constructor(chars: string[], fg?: IColorLike | IColorLike[][], bg?: IColorLike | IColorLike[][]);
|
||||
constructor(chars: Char[][], fg?: ColorLike, bg?: ColorLike);
|
||||
constructor(chars: string, fg?: ColorLike | ColorLike[], bg?: ColorLike | ColorLike[]);
|
||||
constructor(chars: string[], fg?: ColorLike | ColorLike[][], bg?: ColorLike | ColorLike[][]);
|
||||
|
||||
constructor(
|
||||
chars: IChar[][] | string | string[],
|
||||
fg?: IColorLike | IColorLike[] | IColorLike[][],
|
||||
bg?: IColorLike | IColorLike[] | IColorLike[][],
|
||||
chars: Char[][] | string | string[],
|
||||
fg: ColorLike | ColorLike[] | ColorLike[][] = DEFAULT_BG,
|
||||
bg: ColorLike | ColorLike[] | ColorLike[][] = DEFAULT_FG,
|
||||
) {
|
||||
if (typeof chars === 'string') {
|
||||
chars = chars.split('\n');
|
||||
}
|
||||
if (chars.length === 0 || typeof chars[0] === 'string') {
|
||||
const rows = chars as string[];
|
||||
const w = rows.reduce((m, r) => Math.max(m, r.length), 0);
|
||||
const h = rows.length;
|
||||
this.#chars = rows.map(r => r.padEnd(w, ' '));
|
||||
this.#fgs = Array(w * h).fill(DEFAULT_FG);
|
||||
this.#bgs = Array(w * h).fill(DEFAULT_BG);
|
||||
if (fg != null && !Array.isArray(fg)) this.#fgs.fill(fg);
|
||||
if (bg != null && !Array.isArray(bg)) this.#bgs.fill(bg);
|
||||
if (Array.isArray(fg) || Array.isArray(bg)) {
|
||||
for (let y = 0; y < h; y++) {
|
||||
for (let x = 0; x < w; x++) {
|
||||
const i = y * w + x;
|
||||
if (Array.isArray(fg)) this.#fgs[i] = (fg as IColorLike[][])[y]?.[x] ?? DEFAULT_FG;
|
||||
if (Array.isArray(bg)) this.#bgs[i] = (bg as IColorLike[][])[y]?.[x] ?? DEFAULT_BG;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const ichars = chars as IChar[][];
|
||||
const h = ichars.length;
|
||||
const w = ichars.reduce((m, r) => Math.max(m, r.length), 0);
|
||||
this.#chars = ichars.map(row => row.map(ch => ch[0]).join('').padEnd(w, ' '));
|
||||
this.#fgs = Array(w * h).fill(DEFAULT_FG);
|
||||
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];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
this.width = chars.reduce((m, r) => Math.max(m, r.length), 0);
|
||||
this.height = chars.length;
|
||||
const defaultFg = Array.isArray(fg) ? DEFAULT_FG : fg;
|
||||
const defaultBg = Array.isArray(bg) ? DEFAULT_BG : bg;
|
||||
|
||||
get(x: number, y: number): IDefinedChar {
|
||||
fg = expandColors(chars, fg ?? defaultFg);
|
||||
bg = expandColors(chars, bg ?? defaultBg);
|
||||
|
||||
const s = this.width * this.height;
|
||||
this.#chars = Array(s);
|
||||
this.#fgs = Array(s);
|
||||
this.#bgs = Array(s);
|
||||
|
||||
for (let y = 0; y < this.height; y++) {
|
||||
for (let x = 0; x < this.width; x++) {
|
||||
const i = y * this.width + x;
|
||||
return [this.#chars[y][x], this.#fgs[i], this.#bgs[i]];
|
||||
const char = chars[y]?.[x] ?? '\0';
|
||||
const charFG = fg[y]?.[x] ?? defaultFg;
|
||||
const charBG = bg[y]?.[x] ?? defaultBg;
|
||||
|
||||
const ch = parseChar(char, charFG, charBG);
|
||||
this.#chars[i] = ch[0];
|
||||
this.#fgs[i] = ch[1];
|
||||
this.#bgs[i] = ch[2];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
set(x: number, y: number, char: IChar) {
|
||||
get(x: number, y: number): DefinedChar {
|
||||
const i = y * this.width + x;
|
||||
return [this.#chars[i], this.#fgs[i], this.#bgs[i]];
|
||||
}
|
||||
|
||||
set(x: number, y: number, char: 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.#bgs[y * this.width + x] = ch[2];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,13 +2,17 @@ import { formatError, formatErrorMessage } from "./errors";
|
|||
import Input from "./input";
|
||||
import { nextFrame } from "./utils";
|
||||
|
||||
type Setup<T> = () => Promise<T> | T;
|
||||
type Frame<T> = (dt: number, state: T) => Promise<T | void> | T | void;
|
||||
type GameMain = () => Promise<void>;
|
||||
interface FrameMeta {
|
||||
fps: number;
|
||||
}
|
||||
type Awaitable<T> = PromiseLike<T> | T;
|
||||
|
||||
export function gameLoop<T>(frame: Frame<T>): GameMain;
|
||||
export function gameLoop<T>(setup: Setup<T>, frame: Frame<T>): GameMain;
|
||||
export function gameLoop<T>(setupOrFrame: Setup<T> | Frame<T>, frame?: Frame<T>): GameMain {
|
||||
type Setup<T> = () => Awaitable<T>;
|
||||
type Frame<T> = (dt: number, state: T, meta: FrameMeta) => Awaitable<T | void>;
|
||||
|
||||
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 () => {
|
||||
let state: T;
|
||||
try {
|
||||
|
|
@ -26,6 +30,9 @@ export function gameLoop<T>(setupOrFrame: Setup<T> | Frame<T>, frame?: Frame<T>)
|
|||
|
||||
try {
|
||||
let prevFrame = performance.now();
|
||||
let fpsCounter = 0;
|
||||
let fpsTimer = 0;
|
||||
const meta: FrameMeta = { fps: 0 };
|
||||
while (true) {
|
||||
await nextFrame();
|
||||
Input.updateKeys();
|
||||
|
|
@ -33,13 +40,20 @@ export function gameLoop<T>(setupOrFrame: Setup<T> | Frame<T>, frame?: Frame<T>)
|
|||
const now = performance.now();
|
||||
const dt = (now - prevFrame) / 1000;
|
||||
|
||||
if (dt < 1) {
|
||||
const newState = await frame(dt, state);
|
||||
if (dt < 1) { // skip long pause to avoid blowing up values
|
||||
const newState = await frame(dt, state, meta);
|
||||
if (newState) {
|
||||
state = newState;
|
||||
}
|
||||
}
|
||||
|
||||
fpsCounter += 1;
|
||||
if (now - fpsTimer > 500) {
|
||||
meta.fps = fpsCounter * 2;
|
||||
fpsCounter = 0;
|
||||
fpsTimer = now;
|
||||
}
|
||||
|
||||
prevFrame = performance.now();
|
||||
}
|
||||
} catch (e) {
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ interface BresenhamCircleFovOptions extends BresenhamCircleBaseOptions<'fov' | '
|
|||
breaker?: (x: number, y: number) => boolean;
|
||||
}
|
||||
|
||||
type BresenhamCircleOptions = BresenhamCircleFillOptions | BresenhamCircleFovOptions;
|
||||
export type BresenhamCircleOptions = BresenhamCircleFillOptions | BresenhamCircleFovOptions;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Cohen-Sutherland outcodes
|
||||
|
|
|
|||
|
|
@ -48,5 +48,7 @@ export class TextDisplaySystem extends System {
|
|||
this.display.setClipRect(clipRect);
|
||||
}
|
||||
}
|
||||
|
||||
this.display.update();
|
||||
}
|
||||
}
|
||||
|
|
@ -21,8 +21,8 @@ function createMap(world: World, random: SeededRandom) {
|
|||
|
||||
BSP.generateLevel(MAP_SIZE, MAP_SIZE, (x, y) => { mapData[x + y * MAP_SIZE] = '.'; }, {
|
||||
minWidth: 8,
|
||||
minHeight: 8,
|
||||
depth: 8,
|
||||
minHeight: 4,
|
||||
depth: 10,
|
||||
random,
|
||||
});
|
||||
const mapDataString: string[] = [];
|
||||
|
|
@ -49,17 +49,9 @@ 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(() => {
|
||||
const world = new World();
|
||||
const display = world.addSystem(new TextDisplaySystem(100, 25)).display;
|
||||
|
|
@ -89,14 +81,12 @@ export default gameLoop(() => {
|
|||
screenY: 0,
|
||||
});
|
||||
const player = createPlayer(world, startCell.x, startCell.y);
|
||||
createFPS(world, 0, 0);
|
||||
|
||||
return {
|
||||
display,
|
||||
world,
|
||||
map, mapData,
|
||||
mask, maskData, maskDirty: true,
|
||||
fps: 0,
|
||||
fpsTimer: 0,
|
||||
random,
|
||||
viewport,
|
||||
player,
|
||||
|
|
@ -107,13 +97,20 @@ export default gameLoop(() => {
|
|||
},
|
||||
};
|
||||
}, (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 dy = -Math.sign(Input.getVertical());
|
||||
const playerPos = getPosition(player)!;
|
||||
|
||||
if (now - lastMove > 0.03) {
|
||||
if (now - lastMove > 0.05) {
|
||||
if (isWall(playerPos.x + dx, playerPos.y)) {
|
||||
dx = 0;
|
||||
}
|
||||
|
|
@ -157,14 +154,5 @@ export default gameLoop(() => {
|
|||
|
||||
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;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ export default async function main() {
|
|||
const display = new TextDisplay();
|
||||
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 room = roomEntity?.get(Room);
|
||||
|
|
@ -72,7 +72,7 @@ export default async function main() {
|
|||
const foundItems = inv ? Object.values(inv.state.slots).length : 0;
|
||||
const totalItems = Array.from(world.query(Item)).length;
|
||||
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();
|
||||
|
|
|
|||
Loading…
Reference in New Issue