Add TextDisplay with canvas
This commit is contained in:
parent
6b3c0c77a1
commit
b549193a3b
|
|
@ -0,0 +1,305 @@
|
||||||
|
import '@common/assets/vga.font.css';
|
||||||
|
import { randInt } from "@common/utils";
|
||||||
|
import { createCanvas } from './canvas';
|
||||||
|
|
||||||
|
export type IColorLike = string | number | Color;
|
||||||
|
export type IChar = [string, IColorLike?, IColorLike?];
|
||||||
|
export type IRegion = IChar[][];
|
||||||
|
|
||||||
|
export interface ISpriteDefinition {
|
||||||
|
frames: IRegion[];
|
||||||
|
animationPeriod?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const randChar = (min = ' ', max = '~') =>
|
||||||
|
String.fromCharCode(randInt(
|
||||||
|
min.charCodeAt(0),
|
||||||
|
max.charCodeAt(0) + 1,
|
||||||
|
));
|
||||||
|
|
||||||
|
const generateColors = () => {
|
||||||
|
const colors: string[] = [];
|
||||||
|
for (let i = 0; i < 16; i++) {
|
||||||
|
const high = ((i & 0b1000) > 0) ? 'ff' : '7f';
|
||||||
|
|
||||||
|
const b = ((i & 0b0001) > 0) ? high : '00';
|
||||||
|
const g = ((i & 0b0010) > 0) ? high : '00';
|
||||||
|
const r = ((i & 0b0100) > 0) ? high : '00';
|
||||||
|
|
||||||
|
const color = `#${r}${g}${b}`;
|
||||||
|
colors.push(color);
|
||||||
|
}
|
||||||
|
|
||||||
|
return colors;
|
||||||
|
};
|
||||||
|
|
||||||
|
const COLORS = generateColors();
|
||||||
|
|
||||||
|
export const GAME_WIDTH = 80;
|
||||||
|
export const GAME_HEIGHT = 25;
|
||||||
|
|
||||||
|
export enum Color {
|
||||||
|
BLACK = 0b0000,
|
||||||
|
DARK_BLUE = 0b0001,
|
||||||
|
DARK_CYAN = 0b0011,
|
||||||
|
DARK_GREEN = 0b0010,
|
||||||
|
DARK_YELLOW = 0b0110,
|
||||||
|
DARK_RED = 0b0100,
|
||||||
|
DARK_MAGENTA = 0b0101,
|
||||||
|
GRAY = 0b0111,
|
||||||
|
BLUE = 0b1001,
|
||||||
|
CYAN = 0b1011,
|
||||||
|
GREEN = 0b1010,
|
||||||
|
YELLOW = 0b1110,
|
||||||
|
RED = 0b1100,
|
||||||
|
MAGENTA = 0b1101,
|
||||||
|
WHITE = 0b1111,
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum Direction {
|
||||||
|
NORTH = 0,
|
||||||
|
EAST = 1,
|
||||||
|
SOUTH = 2,
|
||||||
|
WEST = 3,
|
||||||
|
UP = 4,
|
||||||
|
DOWN = 5,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isVertical = (char: string) => char === '│';
|
||||||
|
export const isHorizontal = (char: string) => char === '─';
|
||||||
|
export const isCorner = (char: string) => '┌┐└┘'.includes(char);
|
||||||
|
|
||||||
|
interface IBoxOptions {
|
||||||
|
vertical?: string;
|
||||||
|
horizontal?: string;
|
||||||
|
topLeft?: string;
|
||||||
|
topRight?: string;
|
||||||
|
bottomLeft?: string;
|
||||||
|
bottomRight?: string;
|
||||||
|
fill?: IChar;
|
||||||
|
fg?: IColorLike;
|
||||||
|
bg?: IColorLike;
|
||||||
|
title?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CHAR_W = 8;
|
||||||
|
const CHAR_H = 16;
|
||||||
|
const NATIVE_FONT = `${CHAR_H}px "IBM VGA 8x16"`;
|
||||||
|
const FALLBACK_FONT = `${CHAR_H}px monospace`;
|
||||||
|
|
||||||
|
const colorToCSS = (c: IColorLike): string =>
|
||||||
|
typeof c === 'number' ? COLORS[c] : c as string;
|
||||||
|
|
||||||
|
export class TextDisplay {
|
||||||
|
private chars: string[];
|
||||||
|
private fgs: IColorLike[];
|
||||||
|
private bgs: IColorLike[];
|
||||||
|
private canvas: HTMLCanvasElement;
|
||||||
|
private ctx: CanvasRenderingContext2D;
|
||||||
|
private font = FALLBACK_FONT;
|
||||||
|
private letterboxColor: string;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public width = GAME_WIDTH,
|
||||||
|
public height = GAME_HEIGHT,
|
||||||
|
parent?: HTMLCanvasElement,
|
||||||
|
letterboxColor: IColorLike = Color.BLACK,
|
||||||
|
) {
|
||||||
|
this.letterboxColor = colorToCSS(letterboxColor);
|
||||||
|
const canvas = parent ?? createCanvas(width * CHAR_W, height * CHAR_H);
|
||||||
|
canvas.width = width * CHAR_W;
|
||||||
|
canvas.height = height * CHAR_H;
|
||||||
|
canvas.style.imageRendering = 'pixelated';
|
||||||
|
this.canvas = canvas;
|
||||||
|
this.ctx = canvas.getContext('2d')!;
|
||||||
|
this.ctx.textBaseline = 'top';
|
||||||
|
|
||||||
|
const size = width * height;
|
||||||
|
this.chars = Array.from({ length: size }, () => randChar('!'));
|
||||||
|
this.fgs = new Array(size).fill(COLORS[7]);
|
||||||
|
this.bgs = new Array(size).fill(COLORS[0]);
|
||||||
|
|
||||||
|
window.addEventListener('resize', () => this.updateScale());
|
||||||
|
this.updateScale();
|
||||||
|
|
||||||
|
Promise.race([
|
||||||
|
document.fonts.load(NATIVE_FONT).then(() => true).catch(() => false),
|
||||||
|
new Promise<boolean>(resolve => setTimeout(() => resolve(false), 1000)),
|
||||||
|
]).then(loaded => {
|
||||||
|
this.font = loaded ? NATIVE_FONT : FALLBACK_FONT;
|
||||||
|
this.redraw();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateScale() {
|
||||||
|
const scale = Math.max(1, Math.min(
|
||||||
|
Math.floor(window.innerWidth / (this.width * CHAR_W)),
|
||||||
|
Math.floor(window.innerHeight / (this.height * CHAR_H)),
|
||||||
|
));
|
||||||
|
this.canvas.style.width = `${this.width * CHAR_W * scale}px`;
|
||||||
|
this.canvas.style.height = `${this.height * CHAR_H * scale}px`;
|
||||||
|
document.body.style.background = this.letterboxColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
private redraw() {
|
||||||
|
for (let y = 0; y < this.height; y++) {
|
||||||
|
for (let x = 0; x < this.width; x++) {
|
||||||
|
this.drawCell(x, y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private drawCell(x: number, y: number) {
|
||||||
|
const i = y * this.width + x;
|
||||||
|
const px = x * CHAR_W;
|
||||||
|
const py = y * CHAR_H;
|
||||||
|
|
||||||
|
this.ctx.fillStyle = colorToCSS(this.bgs[i]);
|
||||||
|
this.ctx.fillRect(px, py, CHAR_W, CHAR_H);
|
||||||
|
|
||||||
|
const char = this.chars[i];
|
||||||
|
if (char !== ' ') {
|
||||||
|
this.ctx.save();
|
||||||
|
this.ctx.beginPath();
|
||||||
|
this.ctx.rect(px, py, CHAR_W, CHAR_H);
|
||||||
|
this.ctx.clip();
|
||||||
|
this.ctx.font = this.font;
|
||||||
|
this.ctx.fillStyle = colorToCSS(this.fgs[i]);
|
||||||
|
this.ctx.fillText(char, px, py);
|
||||||
|
this.ctx.restore();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private setCharRaw(x: number, y: number, char: string, fg: IColorLike, bg: IColorLike) {
|
||||||
|
if (x < 0 || y < 0 || y >= this.height || x >= this.width || !char) return;
|
||||||
|
const i = (y | 0) * this.width + (x | 0);
|
||||||
|
if (this.chars[i] === char && this.fgs[i] === fg && this.bgs[i] === bg) return;
|
||||||
|
this.chars[i] = char;
|
||||||
|
this.fgs[i] = fg;
|
||||||
|
this.bgs[i] = bg;
|
||||||
|
this.drawCell(x | 0, y | 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
setChar(x: number, y: number, [char, fg = 'white', bg = 'black']: IChar = ['█']) {
|
||||||
|
this.setCharRaw(x, y, char, fg, bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
getChar(x: number, y: number): IChar {
|
||||||
|
if (x < 0 || y < 0 || y >= this.height || x >= this.width) {
|
||||||
|
return [' ', COLORS[0], COLORS[1]];
|
||||||
|
}
|
||||||
|
const i = (y | 0) * this.width + (x | 0);
|
||||||
|
return [this.chars[i], this.fgs[i], this.bgs[i]];
|
||||||
|
}
|
||||||
|
|
||||||
|
setRegion(x: number, y: number, w: number, h: number, region: IRegion) {
|
||||||
|
for (let row = 0; row < h; row++) {
|
||||||
|
for (let col = 0; col < w; col++) {
|
||||||
|
const [char, fg = 'white', bg = 'black'] = region[row][col];
|
||||||
|
this.setCharRaw(x + col, y + row, char, fg, bg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getRegion(x: number, y: number, w: number, h: number) {
|
||||||
|
const region: IRegion = [];
|
||||||
|
for (let row = 0; row < h; row++) {
|
||||||
|
const line: IChar[] = [];
|
||||||
|
for (let col = 0; col < w; col++) {
|
||||||
|
line.push(this.getChar(x + col, y + row));
|
||||||
|
}
|
||||||
|
region.push(line);
|
||||||
|
}
|
||||||
|
return region;
|
||||||
|
}
|
||||||
|
|
||||||
|
drawText(x: number, y: number, text: string, fg: IColorLike = Color.WHITE, bg: IColorLike = Color.BLACK) {
|
||||||
|
const lines = text.split('\n');
|
||||||
|
for (let row = 0; row < lines.length; row++) {
|
||||||
|
const line = lines[row];
|
||||||
|
const ry = y + row;
|
||||||
|
if (ry < 0 || ry >= this.height) continue;
|
||||||
|
for (let col = 0; col < line.length; col++) {
|
||||||
|
this.setCharRaw(x + col, ry, line[col], fg, bg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
drawVLine(x: number, y1: number, y2: number, [char, fg = 'white', bg = 'black']: IChar = ['│']) {
|
||||||
|
if (y2 < y1) { const t = y2; y2 = y1; y1 = t; }
|
||||||
|
y1 = Math.max(0, y1);
|
||||||
|
y2 = Math.min(this.height - 1, y2);
|
||||||
|
if (x < 0 || x >= this.width) return;
|
||||||
|
for (let y = y1; y <= y2; y++) {
|
||||||
|
this.setCharRaw(x, y, char, fg, bg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
drawHLine(x1: number, x2: number, y: number, [char, fg = 'white', bg = 'black']: IChar = ['─']) {
|
||||||
|
if (x2 < x1) { const t = x2; x2 = x1; x1 = t; }
|
||||||
|
x1 = Math.max(0, x1);
|
||||||
|
x2 = Math.min(this.width - 1, x2);
|
||||||
|
if (y < 0 || y >= this.height) return;
|
||||||
|
for (let x = x1; x <= x2; x++) {
|
||||||
|
this.setCharRaw(x, y, char, fg, bg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
drawBox(x: number, y: number, width: number, height: number, options: IBoxOptions = {}) {
|
||||||
|
const {
|
||||||
|
vertical = '│',
|
||||||
|
horizontal = '─',
|
||||||
|
topLeft = '┌',
|
||||||
|
topRight = '┐',
|
||||||
|
bottomLeft = '└',
|
||||||
|
bottomRight = '┘',
|
||||||
|
fg = Color.WHITE,
|
||||||
|
bg = Color.BLACK,
|
||||||
|
fill,
|
||||||
|
title,
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
this.setCharRaw(x, y, topLeft, fg, bg);
|
||||||
|
this.setCharRaw(x + width + 1, y, topRight, fg, bg);
|
||||||
|
this.setCharRaw(x, y + height + 1, bottomLeft, fg, bg);
|
||||||
|
this.setCharRaw(x + width + 1, y + height + 1, bottomRight, fg, bg);
|
||||||
|
|
||||||
|
if (fill) {
|
||||||
|
this.fillBox(x + 1, y + 1, width, height, fill);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.drawHLine(x + 1, x + width, y, [horizontal, fg, bg]);
|
||||||
|
this.drawHLine(x + 1, x + width, y + height + 1, [horizontal, fg, bg]);
|
||||||
|
|
||||||
|
this.drawVLine(x, y + 1, y + height, [vertical, fg, bg]);
|
||||||
|
this.drawVLine(x + width + 1, y + 1, y + height, [vertical, fg, bg]);
|
||||||
|
|
||||||
|
if (title) {
|
||||||
|
this.drawText(x + 1, y, title, fg, bg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
drawTextInBox(x: number, y: number, text: string, options: IBoxOptions = {}) {
|
||||||
|
const {
|
||||||
|
fg = Color.WHITE,
|
||||||
|
bg = Color.BLACK,
|
||||||
|
} = options;
|
||||||
|
let width = 0;
|
||||||
|
const lines = text.split('\n');
|
||||||
|
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.drawText(x + 1, y + 1, text, fg, bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
fillBox(x: number, y: number, width: number, height: number, char: IChar = ['█']) {
|
||||||
|
for (let i = y; i < y + height; i++) {
|
||||||
|
this.drawHLine(x, x + width - 1, i, char);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -49,7 +49,7 @@ namespace Input {
|
||||||
const KEYS: Partial<Record<KeyCode, IKeyState>> = {};
|
const KEYS: Partial<Record<KeyCode, IKeyState>> = {};
|
||||||
|
|
||||||
const onStateChange = (keyId: KeyCode, state: boolean) => {
|
const onStateChange = (keyId: KeyCode, state: boolean) => {
|
||||||
console.debug(`[Input] Pressed ${keyId}`);
|
console.debug(`[Input] ${state ? 'Pressed' : 'Released'} ${keyId}`);
|
||||||
if (KEYS[keyId]) {
|
if (KEYS[keyId]) {
|
||||||
KEYS[keyId].state = state;
|
KEYS[keyId].state = state;
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -37,10 +37,12 @@ interface EquipmentState {
|
||||||
slots: Record<string, SlotRecord>;
|
slots: Record<string, SlotRecord>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SlotInput =
|
type SlotDefinition =
|
||||||
| string // generic slot
|
| string // generic slot
|
||||||
| { slotName: string; type?: string }; // typed slot
|
| { slotName: string; type?: string }; // typed slot
|
||||||
|
|
||||||
|
export type SlotInput = SlotDefinition | SlotDefinition[];
|
||||||
|
|
||||||
@component
|
@component
|
||||||
export class Equipment extends Component<EquipmentState> {
|
export class Equipment extends Component<EquipmentState> {
|
||||||
#cachedVars: RPGVariables | null = null;
|
#cachedVars: RPGVariables | null = null;
|
||||||
|
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
import { createCanvas } from "@common/display/canvas";
|
|
||||||
import { gameLoop } from "@common/game";
|
|
||||||
|
|
||||||
type State = ReturnType<typeof setup>;
|
|
||||||
|
|
||||||
const setup = () => {
|
|
||||||
const canvas = createCanvas(800, 600);
|
|
||||||
const ctx = canvas.getContext("2d");
|
|
||||||
|
|
||||||
if (!ctx) {
|
|
||||||
throw new Error("Failed to get canvas context");
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
canvas,
|
|
||||||
ctx,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const frame = (dt: number, state: State) => {
|
|
||||||
const { ctx } = state;
|
|
||||||
ctx.fillStyle = "blue";
|
|
||||||
ctx.fillRect(0, 0, 800, 600);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default gameLoop(setup, frame);
|
|
||||||
|
|
@ -65,10 +65,10 @@ export const getOppositeDirection = (d: Direction): Direction => {
|
||||||
|
|
||||||
export const MAP_ROOM_CHARS: Record<number, string> = {
|
export const MAP_ROOM_CHARS: Record<number, string> = {
|
||||||
[0b0000]: ' ',
|
[0b0000]: ' ',
|
||||||
[0b0001]: '█',
|
[0b0001]: '╨',
|
||||||
[0b0010]: '█',
|
[0b0010]: '╞',
|
||||||
[0b0100]: '█',
|
[0b0100]: '╥',
|
||||||
[0b1000]: '█',
|
[0b1000]: '╡',
|
||||||
[0b0011]: '╚',
|
[0b0011]: '╚',
|
||||||
[0b0110]: '╔',
|
[0b0110]: '╔',
|
||||||
[0b1100]: '╗',
|
[0b1100]: '╗',
|
||||||
|
|
|
||||||
|
|
@ -1,230 +0,0 @@
|
||||||
import { nextFrame } from "@common/utils";
|
|
||||||
import { Color, GAME_HEIGHT, GAME_WIDTH } from "./const";
|
|
||||||
import type { IChar, IColorLike, IRegion } from "./types";
|
|
||||||
import { generateColors, randChar } from "./utils";
|
|
||||||
|
|
||||||
const ROOT_NODE = document.getElementById('root');
|
|
||||||
const FPS_NODE = document.getElementById('fps');
|
|
||||||
|
|
||||||
const COLORS = generateColors();
|
|
||||||
const GAME_FIELD = generateField();
|
|
||||||
|
|
||||||
function handleResize() {
|
|
||||||
const windowWidth = window.innerWidth;
|
|
||||||
const windowHeight = window.innerHeight;
|
|
||||||
|
|
||||||
const charWidth = windowWidth / GAME_WIDTH;
|
|
||||||
const charHeight = windowHeight / GAME_HEIGHT;
|
|
||||||
|
|
||||||
// TODO
|
|
||||||
}
|
|
||||||
|
|
||||||
function generateField(width = GAME_WIDTH, height = GAME_HEIGHT, parent = ROOT_NODE): HTMLSpanElement[][] {
|
|
||||||
if (!parent) return [];
|
|
||||||
|
|
||||||
let field: HTMLSpanElement[][] = [];
|
|
||||||
parent.textContent = '';
|
|
||||||
|
|
||||||
for (let row = 0; row < height; row++) {
|
|
||||||
const line = document.createElement('div');
|
|
||||||
field[row] = [];
|
|
||||||
|
|
||||||
for (let column = 0; column < width; column++) {
|
|
||||||
const span = document.createElement('span');
|
|
||||||
span.innerHTML = randChar('!');
|
|
||||||
span.style.color = COLORS[7];
|
|
||||||
line.append(span);
|
|
||||||
|
|
||||||
field[row][column] = span;
|
|
||||||
}
|
|
||||||
|
|
||||||
parent.append(line);
|
|
||||||
}
|
|
||||||
|
|
||||||
return field;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setChar(x: number, y: number, [char, fg = 'white', bg = 'black']: IChar = ['█']) {
|
|
||||||
if (x < 0 || y < 0 || y >= GAME_HEIGHT || x >= GAME_WIDTH || !char) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const span = GAME_FIELD[y | 0][x | 0];
|
|
||||||
|
|
||||||
if (typeof fg === 'number') fg = COLORS[fg];
|
|
||||||
if (typeof bg === 'number') bg = COLORS[bg];
|
|
||||||
|
|
||||||
if (char === ' ') {
|
|
||||||
span.innerHTML = ' ';
|
|
||||||
} else {
|
|
||||||
span.textContent = char.toString();
|
|
||||||
span.style.color = fg;
|
|
||||||
span.style.backgroundColor = bg;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getChar(x: number, y: number): IChar {
|
|
||||||
if (x < 0 || y < 0 || y >= GAME_HEIGHT || x >= GAME_WIDTH) {
|
|
||||||
return [' ', COLORS[0], COLORS[1]];
|
|
||||||
}
|
|
||||||
|
|
||||||
const span = GAME_FIELD[y | 0][x | 0];
|
|
||||||
|
|
||||||
return [
|
|
||||||
span.textContent?.[0] ?? ' ',
|
|
||||||
span.style.color,
|
|
||||||
span.style.backgroundColor,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setRegion(x: number, y: number, w: number, h: number, region: IRegion) {
|
|
||||||
for (let screenY = y; screenY < y + h; screenY++) {
|
|
||||||
for (let screenX = x; screenX < x + w; screenX++) {
|
|
||||||
const char = region[screenY - y][screenX - x]
|
|
||||||
setChar(screenX, screenY, char);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getRegion(x: number, y: number, w: number, h: number) {
|
|
||||||
const region: IRegion = [];
|
|
||||||
for (let screenY = y; screenY < y + h; screenY++) {
|
|
||||||
const line: IChar[] = []
|
|
||||||
for (let screenX = x; screenX < x + w; screenX++) {
|
|
||||||
line.push(getChar(screenX, screenY));
|
|
||||||
}
|
|
||||||
region.push(line);
|
|
||||||
}
|
|
||||||
|
|
||||||
return region;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function drawText(x: number, y: number, text: string, fg: IColorLike = Color.WHITE, bg: IColorLike = Color.BLACK) {
|
|
||||||
if (text.includes('\n')) {
|
|
||||||
const lines = text.split('\n');
|
|
||||||
for (let line = 0; line < lines.length; line++) {
|
|
||||||
drawText(x, y + line, lines[line], fg, bg);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for (let i = 0; i < text.length; i++) {
|
|
||||||
setChar(x + i, y, [text[i], fg, bg]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function drawVLine(x: number, y1: number, y2: number, char: IChar = ['│']) {
|
|
||||||
if (y2 < y1) {
|
|
||||||
const t = y2;
|
|
||||||
y2 = y1;
|
|
||||||
y1 = t;
|
|
||||||
}
|
|
||||||
for (let y = y1; y <= y2; y++) {
|
|
||||||
setChar(x, y, char);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function drawHLine(x1: number, x2: number, y: number, char: IChar = ['─']) {
|
|
||||||
if (x2 < x1) {
|
|
||||||
const t = x2;
|
|
||||||
x2 = x1;
|
|
||||||
x1 = t;
|
|
||||||
}
|
|
||||||
for (let x = x1; x <= x2; x++) {
|
|
||||||
setChar(x, y, char);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const isVertical = (char: string) => char === '│';
|
|
||||||
export const isHorizontal = (char: string) => char === '─';
|
|
||||||
export const isCorner = (char: string) => '┌┐└┘'.includes(char);
|
|
||||||
|
|
||||||
interface IBoxOptions {
|
|
||||||
vertical?: string;
|
|
||||||
horizontal?: string;
|
|
||||||
topLeft?: string;
|
|
||||||
topRight?: string;
|
|
||||||
bottomLeft?: string;
|
|
||||||
bottomRight?: string;
|
|
||||||
fill?: IChar;
|
|
||||||
fg?: IColorLike;
|
|
||||||
bg?: IColorLike;
|
|
||||||
title?: string;
|
|
||||||
}
|
|
||||||
export function drawBox(x: number, y: number, width: number, height: number, options: IBoxOptions = {}) {
|
|
||||||
const {
|
|
||||||
vertical = '│',
|
|
||||||
horizontal = '─',
|
|
||||||
topLeft = '┌',
|
|
||||||
topRight = '┐',
|
|
||||||
bottomLeft = '└',
|
|
||||||
bottomRight = '┘',
|
|
||||||
fg = Color.WHITE,
|
|
||||||
bg = Color.BLACK,
|
|
||||||
fill,
|
|
||||||
title,
|
|
||||||
} = options;
|
|
||||||
|
|
||||||
setChar(x, y, [topLeft, fg, bg]);
|
|
||||||
setChar(x + width + 1, y, [topRight, fg, bg]);
|
|
||||||
setChar(x, y + height + 1, [bottomLeft, fg, bg]);
|
|
||||||
setChar(x + width + 1, y + height + 1, [bottomRight, fg, bg]);
|
|
||||||
|
|
||||||
if (fill) {
|
|
||||||
fillBox(x + 1, y + 1, width, height, fill);
|
|
||||||
}
|
|
||||||
|
|
||||||
drawHLine(x + 1, x + width, y, [horizontal, fg, bg]);
|
|
||||||
drawHLine(x + 1, x + width, y + height + 1, [horizontal, fg, bg]);
|
|
||||||
|
|
||||||
drawVLine(x, y + 1, y + height, [vertical, fg, bg]);
|
|
||||||
drawVLine(x + width + 1, y + 1, y + height, [vertical, fg, bg]);
|
|
||||||
|
|
||||||
if (title) {
|
|
||||||
drawText(x + 1, y, title, fg, bg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function drawTextInBox(x: number, y: number, text: string, options: IBoxOptions = {}) {
|
|
||||||
const {
|
|
||||||
fg = Color.WHITE,
|
|
||||||
bg = Color.BLACK,
|
|
||||||
} = options;
|
|
||||||
let width = 0;
|
|
||||||
const lines = text.split('\n');
|
|
||||||
const height = lines.length;
|
|
||||||
|
|
||||||
for (const line of lines) {
|
|
||||||
if (line.length > width) {
|
|
||||||
width = line.length;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
drawBox(x, y, width, height, { ...options, fill: [' '] });
|
|
||||||
drawText(x + 1, y + 1, text, fg, bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function fillBox(x: number, y: number, width: number, height: number, char: IChar = ['█']) {
|
|
||||||
for (let i = y; i < y + height; i++) {
|
|
||||||
drawHLine(x, x + width - 1, i, char);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let prevTime = 0;
|
|
||||||
let dt = 0;
|
|
||||||
|
|
||||||
export async function tick(desiredFPS = 60) {
|
|
||||||
const time = await nextFrame();
|
|
||||||
dt = time - prevTime;
|
|
||||||
prevTime = time;
|
|
||||||
if (FPS_NODE) {
|
|
||||||
FPS_NODE.textContent = `${Math.round(1000 / dt)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const totalDelay = 1000 / desiredFPS;
|
|
||||||
const remainingDelay = totalDelay - dt;
|
|
||||||
if (remainingDelay > 4) {
|
|
||||||
await Bun.sleep(remainingDelay);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener('resize', handleResize);
|
|
||||||
handleResize();
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
|
import type { TextDisplay } from "@common/display/text";
|
||||||
|
|
||||||
export abstract class Drawable {
|
export abstract class Drawable {
|
||||||
protected dirty: boolean = true;
|
protected dirty: boolean = true;
|
||||||
abstract doDraw(): void;
|
abstract doDraw(): void;
|
||||||
|
constructor(protected display: TextDisplay) { }
|
||||||
|
|
||||||
draw() {
|
draw() {
|
||||||
if (this.dirty) {
|
if (this.dirty) {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { setRegion } from "./display";
|
import type { TextDisplay } from "@common/display/text";
|
||||||
import { Drawable } from "./drawable";
|
import { Drawable } from "./drawable";
|
||||||
import type { IRegion, ISpriteDefinition } from "./types";
|
import type { IRegion, ISpriteDefinition } from "./types";
|
||||||
|
|
||||||
|
|
@ -8,12 +8,13 @@ export class Figure extends Drawable {
|
||||||
private animationPeriod;
|
private animationPeriod;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
display: TextDisplay,
|
||||||
public x: number,
|
public x: number,
|
||||||
public y: number,
|
public y: number,
|
||||||
definition: ISpriteDefinition,
|
definition: ISpriteDefinition,
|
||||||
public frame: number = 0,
|
public frame: number = 0,
|
||||||
) {
|
) {
|
||||||
super();
|
super(display);
|
||||||
this.x = x | 0;
|
this.x = x | 0;
|
||||||
this.y = y | 0;
|
this.y = y | 0;
|
||||||
this.frames = definition.frames;
|
this.frames = definition.frames;
|
||||||
|
|
@ -28,7 +29,7 @@ export class Figure extends Drawable {
|
||||||
this.frame = (this.frame + 1) % this.numFrames;
|
this.frame = (this.frame + 1) % this.numFrames;
|
||||||
}
|
}
|
||||||
|
|
||||||
setRegion(this.x, this.y, this.width, this.height, this.image);
|
this.display.setRegion(this.x, this.y, this.width, this.height, this.image);
|
||||||
}
|
}
|
||||||
|
|
||||||
get width() {
|
get width() {
|
||||||
|
|
|
||||||
|
|
@ -1,141 +0,0 @@
|
||||||
import { Color, Direction, getOppositeDirection } from './const';
|
|
||||||
import { drawTextInBox, tick } from './display';
|
|
||||||
import Input from '@common/input';
|
|
||||||
import { createItems, getItemsCount } from './item';
|
|
||||||
import { GameMap } from './map';
|
|
||||||
import { Player } from './player';
|
|
||||||
import { getPossibleRoomsCount, getRoom, getRoomsCount } from './room';
|
|
||||||
|
|
||||||
let currentRoom = getRoom(0, 0, 0);
|
|
||||||
const map = new GameMap(currentRoom);
|
|
||||||
currentRoom.draw();
|
|
||||||
const player = new Player(currentRoom.x + currentRoom.width / 2, currentRoom.y + currentRoom.height / 2);
|
|
||||||
|
|
||||||
let lastMove = Date.now();
|
|
||||||
function handleInput() {
|
|
||||||
const isSpacePressed = Input.isPressed(Input.KeyCode.SPACE);
|
|
||||||
|
|
||||||
if (Date.now() - lastMove < 75 && !Input.isHeld(Input.KeyCode.SHIFT) && !isSpacePressed) return;
|
|
||||||
lastMove = Date.now();
|
|
||||||
let newX = player.x;
|
|
||||||
let newY = player.y;
|
|
||||||
let moved = isSpacePressed;
|
|
||||||
|
|
||||||
if (Input.isHeld(Input.KeyCode.UP)) {
|
|
||||||
newY--;
|
|
||||||
moved = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Input.isHeld(Input.KeyCode.DOWN)) {
|
|
||||||
newY++;
|
|
||||||
moved = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Input.isHeld(Input.KeyCode.LEFT)) {
|
|
||||||
newX--;
|
|
||||||
moved = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Input.isHeld(Input.KeyCode.RIGHT)) {
|
|
||||||
newX++;
|
|
||||||
moved = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (moved) {
|
|
||||||
const activatedDoor = currentRoom.getActivatedDoor(newX, newY);
|
|
||||||
if (activatedDoor) {
|
|
||||||
const shouldTravel = ![Direction.UP, Direction.DOWN].includes(activatedDoor.direction) || isSpacePressed;
|
|
||||||
|
|
||||||
if (shouldTravel) {
|
|
||||||
currentRoom = getRoom(activatedDoor.worldX, activatedDoor.worldY, activatedDoor.worldZ);
|
|
||||||
currentRoom.invalidate();
|
|
||||||
map.currentRoom = currentRoom;
|
|
||||||
map.invalidate();
|
|
||||||
player.skipNextBackgroundRestore = true;
|
|
||||||
const oppositeDoor = currentRoom.doors[getOppositeDirection(activatedDoor.direction)];
|
|
||||||
|
|
||||||
if (oppositeDoor) {
|
|
||||||
switch (activatedDoor.direction) {
|
|
||||||
case Direction.NORTH:
|
|
||||||
newX = oppositeDoor.x + 1;
|
|
||||||
newY = oppositeDoor.y - 1;
|
|
||||||
break;
|
|
||||||
case Direction.SOUTH:
|
|
||||||
newX = oppositeDoor.x + 1;
|
|
||||||
newY = oppositeDoor.y + 1;
|
|
||||||
break;
|
|
||||||
case Direction.EAST:
|
|
||||||
newX = oppositeDoor.x + 1;
|
|
||||||
newY = oppositeDoor.y + 1;
|
|
||||||
break;
|
|
||||||
case Direction.WEST:
|
|
||||||
newX = oppositeDoor.x - 1;
|
|
||||||
newY = oppositeDoor.y + 1;
|
|
||||||
break;
|
|
||||||
case Direction.UP:
|
|
||||||
case Direction.DOWN:
|
|
||||||
newX = oppositeDoor.x;
|
|
||||||
newY = oppositeDoor.y;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
newX = currentRoom.x + currentRoom.width / 2;
|
|
||||||
newY = currentRoom.y + currentRoom.height / 2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (newX < currentRoom.x + 1 || newX >= currentRoom.x + currentRoom.width + 1) {
|
|
||||||
newX = player.x;
|
|
||||||
}
|
|
||||||
if (newY < currentRoom.y + 1 || newY >= currentRoom.y + currentRoom.height + 1) {
|
|
||||||
newY = player.y;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const pickedItem = currentRoom.pickItem(newX, newY);
|
|
||||||
if (pickedItem) {
|
|
||||||
player.addItem(pickedItem);
|
|
||||||
}
|
|
||||||
|
|
||||||
player.x = newX;
|
|
||||||
player.y = newY;
|
|
||||||
player.invalidate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleLogic() {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
function drawInfo() {
|
|
||||||
const coords = `${currentRoom.worldX},${currentRoom.worldY},${currentRoom.worldZ}`.padStart(9);
|
|
||||||
const foundRooms = getRoomsCount();
|
|
||||||
const totalRooms = getPossibleRoomsCount();
|
|
||||||
const rooms = `${foundRooms}/${totalRooms}${foundRooms === totalRooms ? '' : '+'}`.padStart(coords.length - 2);
|
|
||||||
const items = `${player.foundItems}/${getItemsCount()}`.padStart(coords.length - 2);
|
|
||||||
drawTextInBox(0, 0, `Pos: ${coords}\nRooms: ${rooms}\nItems: ${items}`, { fg: Color.YELLOW, title: 'Info' });
|
|
||||||
}
|
|
||||||
|
|
||||||
function draw() {
|
|
||||||
currentRoom.draw();
|
|
||||||
map.draw();
|
|
||||||
player.draw();
|
|
||||||
|
|
||||||
drawInfo();
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function main() {
|
|
||||||
createItems();
|
|
||||||
currentRoom.invalidate();
|
|
||||||
player.invalidate();
|
|
||||||
drawTextInBox(8, 11, 'Use arrow keys to move\nSHIFT to run\nSPACE to activate\nAWOO!', { fg: Color.CYAN });
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
Input.updateKeys();
|
|
||||||
handleInput();
|
|
||||||
handleLogic();
|
|
||||||
draw();
|
|
||||||
|
|
||||||
await tick(60);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,15 +1,144 @@
|
||||||
import '@common/assets/vga.font.css';
|
import { TextDisplay } from '@common/display/text';
|
||||||
|
import Input from '@common/input';
|
||||||
|
import { nextFrame } from '@common/utils';
|
||||||
import './assets/style.css';
|
import './assets/style.css';
|
||||||
|
import { Color, Direction, getOppositeDirection } from './const';
|
||||||
|
import { createItems, getItemsCount } from './item';
|
||||||
|
import { GameMap } from './map';
|
||||||
|
import { Player } from './player';
|
||||||
|
import { getPossibleRoomsCount, getRoom, getRoomsCount } from './room';
|
||||||
|
|
||||||
export default async function run() {
|
const display = new TextDisplay();
|
||||||
const root = document.createElement('div');
|
let currentRoom = getRoom(display, 0, 0, 0);
|
||||||
root.id = 'root';
|
const map = new GameMap(display, currentRoom);
|
||||||
document.body.append(root);
|
currentRoom.draw();
|
||||||
|
const player = new Player(display, currentRoom.x + currentRoom.width / 2, currentRoom.y + currentRoom.height / 2);
|
||||||
|
|
||||||
const fps = document.createElement('div');
|
let lastMove = Date.now();
|
||||||
fps.id = 'fps';
|
function handleInput() {
|
||||||
document.body.append(fps);
|
const isSpacePressed = Input.isPressed(Input.KeyCode.SPACE);
|
||||||
|
|
||||||
const { main } = await import('./game');
|
if (Date.now() - lastMove < 75 && !Input.isHeld(Input.KeyCode.SHIFT) && !isSpacePressed) return;
|
||||||
await main();
|
lastMove = Date.now();
|
||||||
|
let newX = player.x;
|
||||||
|
let newY = player.y;
|
||||||
|
let moved = isSpacePressed;
|
||||||
|
|
||||||
|
if (Input.isHeld(Input.KeyCode.UP)) {
|
||||||
|
newY--;
|
||||||
|
moved = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Input.isHeld(Input.KeyCode.DOWN)) {
|
||||||
|
newY++;
|
||||||
|
moved = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Input.isHeld(Input.KeyCode.LEFT)) {
|
||||||
|
newX--;
|
||||||
|
moved = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Input.isHeld(Input.KeyCode.RIGHT)) {
|
||||||
|
newX++;
|
||||||
|
moved = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (moved) {
|
||||||
|
const activatedDoor = currentRoom.getActivatedDoor(newX, newY);
|
||||||
|
if (activatedDoor) {
|
||||||
|
const shouldTravel = ![Direction.UP, Direction.DOWN].includes(activatedDoor.direction) || isSpacePressed;
|
||||||
|
|
||||||
|
if (shouldTravel) {
|
||||||
|
currentRoom = getRoom(display, activatedDoor.worldX, activatedDoor.worldY, activatedDoor.worldZ);
|
||||||
|
currentRoom.invalidate();
|
||||||
|
map.currentRoom = currentRoom;
|
||||||
|
map.invalidate();
|
||||||
|
player.skipNextBackgroundRestore = true;
|
||||||
|
const oppositeDoor = currentRoom.doors[getOppositeDirection(activatedDoor.direction)];
|
||||||
|
|
||||||
|
if (oppositeDoor) {
|
||||||
|
switch (activatedDoor.direction) {
|
||||||
|
case Direction.NORTH:
|
||||||
|
newX = oppositeDoor.x + 1;
|
||||||
|
newY = oppositeDoor.y - 1;
|
||||||
|
break;
|
||||||
|
case Direction.SOUTH:
|
||||||
|
newX = oppositeDoor.x + 1;
|
||||||
|
newY = oppositeDoor.y + 1;
|
||||||
|
break;
|
||||||
|
case Direction.EAST:
|
||||||
|
newX = oppositeDoor.x + 1;
|
||||||
|
newY = oppositeDoor.y + 1;
|
||||||
|
break;
|
||||||
|
case Direction.WEST:
|
||||||
|
newX = oppositeDoor.x - 1;
|
||||||
|
newY = oppositeDoor.y + 1;
|
||||||
|
break;
|
||||||
|
case Direction.UP:
|
||||||
|
case Direction.DOWN:
|
||||||
|
newX = oppositeDoor.x;
|
||||||
|
newY = oppositeDoor.y;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
newX = currentRoom.x + currentRoom.width / 2;
|
||||||
|
newY = currentRoom.y + currentRoom.height / 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (newX < currentRoom.x + 1 || newX >= currentRoom.x + currentRoom.width + 1) {
|
||||||
|
newX = player.x;
|
||||||
|
}
|
||||||
|
if (newY < currentRoom.y + 1 || newY >= currentRoom.y + currentRoom.height + 1) {
|
||||||
|
newY = player.y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const pickedItem = currentRoom.pickItem(newX, newY);
|
||||||
|
if (pickedItem) {
|
||||||
|
player.addItem(pickedItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
player.x = newX;
|
||||||
|
player.y = newY;
|
||||||
|
player.invalidate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleLogic() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawInfo() {
|
||||||
|
const coords = `${currentRoom.worldX},${currentRoom.worldY},${currentRoom.worldZ}`.padStart(9);
|
||||||
|
const foundRooms = getRoomsCount();
|
||||||
|
const totalRooms = getPossibleRoomsCount();
|
||||||
|
const rooms = `${foundRooms}/${totalRooms}${foundRooms === totalRooms ? '' : '+'}`.padStart(coords.length - 2);
|
||||||
|
const items = `${player.foundItems}/${getItemsCount()}`.padStart(coords.length - 2);
|
||||||
|
display.drawTextInBox(0, 0, `Pos: ${coords}\nRooms: ${rooms}\nItems: ${items}`, { fg: Color.YELLOW, title: 'Info' });
|
||||||
|
}
|
||||||
|
|
||||||
|
function draw() {
|
||||||
|
currentRoom.draw();
|
||||||
|
map.draw();
|
||||||
|
player.draw();
|
||||||
|
|
||||||
|
drawInfo();
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function main() {
|
||||||
|
createItems(display);
|
||||||
|
currentRoom.invalidate();
|
||||||
|
player.invalidate();
|
||||||
|
display.drawTextInBox(8, 11, 'Use arrow keys to move\nSHIFT to run\nSPACE to activate\nAWOO!', { fg: Color.CYAN });
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
Input.updateKeys();
|
||||||
|
handleInput();
|
||||||
|
handleLogic();
|
||||||
|
draw();
|
||||||
|
|
||||||
|
await nextFrame();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import type { TextDisplay } from "@common/display/text";
|
||||||
import { Drawable } from "./drawable";
|
import { Drawable } from "./drawable";
|
||||||
import { Figure } from "./figure";
|
import { Figure } from "./figure";
|
||||||
import { ITEM_KEY_SPRITE } from "./images";
|
import { ITEM_KEY_SPRITE } from "./images";
|
||||||
|
|
@ -7,23 +8,24 @@ const idMap = new Map<number, Figure>();
|
||||||
|
|
||||||
export const getItemsCount = () => idMap.size;
|
export const getItemsCount = () => idMap.size;
|
||||||
|
|
||||||
export function createItems() {
|
export function createItems(display: TextDisplay) {
|
||||||
for (let frame = 0; frame < ITEM_KEY_SPRITE.frames.length; frame++) {
|
for (let frame = 0; frame < ITEM_KEY_SPRITE.frames.length; frame++) {
|
||||||
idMap.set(
|
idMap.set(
|
||||||
globalItemId++,
|
globalItemId++,
|
||||||
new Figure(0, 0, ITEM_KEY_SPRITE, frame),
|
new Figure(display, 0, 0, ITEM_KEY_SPRITE, frame),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Item extends Drawable {
|
export class Item extends Drawable {
|
||||||
constructor(
|
constructor(
|
||||||
|
display: TextDisplay,
|
||||||
public id: number,
|
public id: number,
|
||||||
public count: number = 1,
|
public count: number = 1,
|
||||||
public x: number = -1,
|
public x: number = -1,
|
||||||
public y: number = -1,
|
public y: number = -1,
|
||||||
) {
|
) {
|
||||||
super();
|
super(display);
|
||||||
}
|
}
|
||||||
|
|
||||||
override doDraw() {
|
override doDraw() {
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,15 @@
|
||||||
|
import type { TextDisplay } from "@common/display/text";
|
||||||
import { Color, MAP_HEIGHT, MAP_ROOM_CHARS, MAP_WIDTH, MAP_X, MAP_Y } from "./const";
|
import { Color, MAP_HEIGHT, MAP_ROOM_CHARS, MAP_WIDTH, MAP_X, MAP_Y } from "./const";
|
||||||
import { drawBox, setChar } from "./display";
|
|
||||||
import { Drawable } from "./drawable";
|
import { Drawable } from "./drawable";
|
||||||
import { getRoomsForLayer, Room } from "./room";
|
import { getRoomsForLayer, Room } from "./room";
|
||||||
|
|
||||||
export class GameMap extends Drawable {
|
export class GameMap extends Drawable {
|
||||||
constructor(public currentRoom: Room) {
|
constructor(display: TextDisplay, public currentRoom: Room) {
|
||||||
super();
|
super(display);
|
||||||
}
|
}
|
||||||
|
|
||||||
override doDraw() {
|
override doDraw() {
|
||||||
drawBox(MAP_X, MAP_Y, MAP_WIDTH - 2, MAP_HEIGHT - 2, { fill: [' '], title: 'Map' });
|
this.display.drawBox(MAP_X, MAP_Y, MAP_WIDTH - 2, MAP_HEIGHT - 2, { fill: [' '], title: 'Map' });
|
||||||
const centerX = this.currentRoom.worldX - MAP_X - MAP_WIDTH / 2;
|
const centerX = this.currentRoom.worldX - MAP_X - MAP_WIDTH / 2;
|
||||||
const centerY = this.currentRoom.worldY - MAP_Y - MAP_HEIGHT / 2;
|
const centerY = this.currentRoom.worldY - MAP_Y - MAP_HEIGHT / 2;
|
||||||
|
|
||||||
|
|
@ -19,7 +19,7 @@ export class GameMap extends Drawable {
|
||||||
|
|
||||||
if (x > MAP_X && x < MAP_X + MAP_WIDTH - 1 && y > MAP_Y && y < MAP_Y + MAP_HEIGHT - 1) {
|
if (x > MAP_X && x < MAP_X + MAP_WIDTH - 1 && y > MAP_Y && y < MAP_Y + MAP_HEIGHT - 1) {
|
||||||
const char = getMapRoomChar(room);
|
const char = getMapRoomChar(room);
|
||||||
setChar(x, y, [char, room === this.currentRoom ? Color.YELLOW : Color.WHITE]);
|
this.display.setChar(x, y, [char, room === this.currentRoom ? Color.YELLOW : Color.WHITE]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import { INVENTORY_X, INVENTORY_Y } from "./const";
|
import { INVENTORY_X, INVENTORY_Y } from "./const";
|
||||||
import { drawBox } from "./display";
|
|
||||||
import { getItemsCount, Item } from "./item";
|
import { getItemsCount, Item } from "./item";
|
||||||
import { Sprite } from "./sprite";
|
import { Sprite } from "./sprite";
|
||||||
|
|
||||||
|
|
@ -14,6 +13,7 @@ export class Player extends Sprite {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.inventory.push(new Item(
|
this.inventory.push(new Item(
|
||||||
|
this.display,
|
||||||
item.id,
|
item.id,
|
||||||
item.count,
|
item.count,
|
||||||
INVENTORY_X + 1 + this.inventory.length,
|
INVENTORY_X + 1 + this.inventory.length,
|
||||||
|
|
@ -37,7 +37,7 @@ export class Player extends Sprite {
|
||||||
override doDraw() {
|
override doDraw() {
|
||||||
super.doDraw();
|
super.doDraw();
|
||||||
|
|
||||||
drawBox(INVENTORY_X, INVENTORY_Y, getItemsCount(), 1, { fill: [' '], title: 'Inv' });
|
this.display.drawBox(INVENTORY_X, INVENTORY_Y, getItemsCount(), 1, { fill: [' '], title: 'Inv' });
|
||||||
this.inventory.forEach((item) => {
|
this.inventory.forEach((item) => {
|
||||||
if (item.count > 0) {
|
if (item.count > 0) {
|
||||||
item.doDraw();
|
item.doDraw();
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,16 @@
|
||||||
import { randInt } from "@common/utils";
|
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 { 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 { Drawable } from "./drawable";
|
||||||
import { Figure } from "./figure";
|
import { Figure } from "./figure";
|
||||||
import { DOOR_SPRITE } from "./images";
|
import { DOOR_SPRITE } from "./images";
|
||||||
import { getItemsCount, Item } from "./item";
|
import { getItemsCount, Item } from "./item";
|
||||||
|
import type { TextDisplay } from "@common/display/text";
|
||||||
|
|
||||||
type IDoors = [Door | null, Door | null, Door | null, Door | null, Door | null, Door | null];
|
type IDoors = [Door | null, Door | null, Door | null, Door | null, Door | null, Door | null];
|
||||||
|
|
||||||
export class Door extends Figure {
|
export class Door extends Figure {
|
||||||
constructor(
|
constructor(
|
||||||
|
display: TextDisplay,
|
||||||
x: number,
|
x: number,
|
||||||
y: number,
|
y: number,
|
||||||
direction: Direction,
|
direction: Direction,
|
||||||
|
|
@ -17,7 +18,7 @@ export class Door extends Figure {
|
||||||
public worldY: number,
|
public worldY: number,
|
||||||
public worldZ: number,
|
public worldZ: number,
|
||||||
) {
|
) {
|
||||||
super(x, y, DOOR_SPRITE);
|
super(display, x, y, DOOR_SPRITE);
|
||||||
this.frame = direction;
|
this.frame = direction;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -35,6 +36,7 @@ export class Door extends Figure {
|
||||||
|
|
||||||
export class Room extends Drawable {
|
export class Room extends Drawable {
|
||||||
constructor(
|
constructor(
|
||||||
|
display: TextDisplay,
|
||||||
public readonly x: number,
|
public readonly x: number,
|
||||||
public readonly y: number,
|
public readonly y: number,
|
||||||
public readonly width: number,
|
public readonly width: number,
|
||||||
|
|
@ -45,12 +47,12 @@ export class Room extends Drawable {
|
||||||
public readonly doors: IDoors = [null, null, null, null, null, null],
|
public readonly doors: IDoors = [null, null, null, null, null, null],
|
||||||
public readonly items: Item[] = [],
|
public readonly items: Item[] = [],
|
||||||
) {
|
) {
|
||||||
super();
|
super(display);
|
||||||
}
|
}
|
||||||
|
|
||||||
override doDraw() {
|
override doDraw() {
|
||||||
drawBox(ROOM_AREA_X, ROOM_AREA_Y, ROOM_AREA_WIDTH - 2, ROOM_AREA_HEIGHT - 2, { fill: [' '] });
|
this.display.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.display.drawBox(this.x, this.y, this.width, this.height, { fill: ['.', Color.GRAY] });
|
||||||
|
|
||||||
this.doors.forEach((door) => {
|
this.doors.forEach((door) => {
|
||||||
if (door) {
|
if (door) {
|
||||||
|
|
@ -84,7 +86,7 @@ const rooms = new Map<string, Room>();
|
||||||
|
|
||||||
const generateRoomId = (worldX: number, worldY: number, worldZ: number) => `${worldX},${worldY},${worldZ}`;
|
const generateRoomId = (worldX: number, worldY: number, worldZ: number) => `${worldX},${worldY},${worldZ}`;
|
||||||
|
|
||||||
const generateDoors = ({ x, y, width, height, worldX, worldY, worldZ }: IRoomBlank): IDoors => {
|
const generateDoors = (display: TextDisplay, { x, y, width, height, worldX, worldY, worldZ }: IRoomBlank): IDoors => {
|
||||||
const doors: IDoors = [null, null, null, null, null, null];
|
const doors: IDoors = [null, null, null, null, null, null];
|
||||||
if (typeof worldX === 'undefined' || typeof worldY === 'undefined' || typeof worldZ === 'undefined') {
|
if (typeof worldX === 'undefined' || typeof worldY === 'undefined' || typeof worldZ === 'undefined') {
|
||||||
console.error('World coordinates not defined for doors generation');
|
console.error('World coordinates not defined for doors generation');
|
||||||
|
|
@ -116,7 +118,7 @@ const generateDoors = ({ x, y, width, height, worldX, worldY, worldZ }: IRoomBla
|
||||||
case Direction.EAST: dx = x + width + 1; dy = doorY; break;
|
case Direction.EAST: dx = x + width + 1; dy = doorY; break;
|
||||||
case Direction.WEST: dx = x; dy = doorY; break;
|
case Direction.WEST: dx = x; dy = doorY; break;
|
||||||
}
|
}
|
||||||
doors[i] = new Door(dx, dy, i as Direction, doorWorldX, doorWorldY, worldZ);
|
doors[i] = new Door(display, dx, dy, i as Direction, doorWorldX, doorWorldY, worldZ);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -124,7 +126,7 @@ const generateDoors = ({ x, y, width, height, worldX, worldY, worldZ }: IRoomBla
|
||||||
const doorX = randInt(x + 1, x + width - 2);
|
const doorX = randInt(x + 1, x + width - 2);
|
||||||
const doorY = randInt(y + 1, y + height - 2);
|
const doorY = randInt(y + 1, y + height - 2);
|
||||||
const [, , zoff] = DIRECTION_OFFSETS[Direction.DOWN];
|
const [, , zoff] = DIRECTION_OFFSETS[Direction.DOWN];
|
||||||
doors[Direction.DOWN] = new Door(doorX, doorY, Direction.DOWN, 0, 0, worldZ + zoff);
|
doors[Direction.DOWN] = new Door(display, doorX, doorY, Direction.DOWN, 0, 0, worldZ + zoff);
|
||||||
|
|
||||||
if (worldZ !== 0) {
|
if (worldZ !== 0) {
|
||||||
let upDoorX: number;
|
let upDoorX: number;
|
||||||
|
|
@ -134,7 +136,7 @@ const generateDoors = ({ x, y, width, height, worldX, worldY, worldZ }: IRoomBla
|
||||||
upDoorY = randInt(y + 1, y + height - 2);
|
upDoorY = randInt(y + 1, y + height - 2);
|
||||||
} while (doorX === upDoorX && doorY === upDoorY);
|
} while (doorX === upDoorX && doorY === upDoorY);
|
||||||
const [, , upZoff] = DIRECTION_OFFSETS[Direction.UP];
|
const [, , upZoff] = DIRECTION_OFFSETS[Direction.UP];
|
||||||
doors[Direction.UP] = new Door(upDoorX, upDoorY, Direction.UP, 0, 0, worldZ + upZoff);
|
doors[Direction.UP] = new Door(display, upDoorX, upDoorY, Direction.UP, 0, 0, worldZ + upZoff);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -143,7 +145,7 @@ const generateDoors = ({ x, y, width, height, worldX, worldY, worldZ }: IRoomBla
|
||||||
|
|
||||||
const generatedItems = new Set<number>();
|
const generatedItems = new Set<number>();
|
||||||
|
|
||||||
const generateItems = ({ x, y, width, height, doors }: IRoomBlank): Item[] => {
|
const generateItems = (display: TextDisplay, { x, y, width, height, doors }: IRoomBlank): Item[] => {
|
||||||
const items: Item[] = [];
|
const items: Item[] = [];
|
||||||
if (typeof x === 'undefined' || typeof y === 'undefined' || typeof width === 'undefined' || typeof height === 'undefined') {
|
if (typeof x === 'undefined' || typeof y === 'undefined' || typeof width === 'undefined' || typeof height === 'undefined') {
|
||||||
console.error('Screen coordinates not defined for items generation');
|
console.error('Screen coordinates not defined for items generation');
|
||||||
|
|
@ -170,7 +172,7 @@ const generateItems = ({ x, y, width, height, doors }: IRoomBlank): Item[] => {
|
||||||
newItemX = randInt(x + 1, x + width - 2);
|
newItemX = randInt(x + 1, x + width - 2);
|
||||||
newItemY = randInt(y + 1, y + height - 2);
|
newItemY = randInt(y + 1, y + height - 2);
|
||||||
} while (hasObjectAt(newItemX, newItemY));
|
} while (hasObjectAt(newItemX, newItemY));
|
||||||
const item = new Item(id, 1, newItemX, newItemY);
|
const item = new Item(display, id, 1, newItemX, newItemY);
|
||||||
|
|
||||||
items.push(item);
|
items.push(item);
|
||||||
generatedItems.add(id);
|
generatedItems.add(id);
|
||||||
|
|
@ -179,7 +181,7 @@ const generateItems = ({ x, y, width, height, doors }: IRoomBlank): Item[] => {
|
||||||
return items;
|
return items;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getRoom(worldX: number, worldY: number, worldZ: number) {
|
export function getRoom(display: TextDisplay, worldX: number, worldY: number, worldZ: number) {
|
||||||
const id = generateRoomId(worldX, worldY, worldZ);
|
const id = generateRoomId(worldX, worldY, worldZ);
|
||||||
const existingRoom = rooms.get(id);
|
const existingRoom = rooms.get(id);
|
||||||
if (existingRoom) {
|
if (existingRoom) {
|
||||||
|
|
@ -193,10 +195,10 @@ export function getRoom(worldX: number, worldY: number, worldZ: number) {
|
||||||
|
|
||||||
const roomBlank: IRoomBlank = { x, y, width, height, worldX, worldY, worldZ };
|
const roomBlank: IRoomBlank = { x, y, width, height, worldX, worldY, worldZ };
|
||||||
|
|
||||||
roomBlank.doors = generateDoors(roomBlank);
|
roomBlank.doors = generateDoors(display, roomBlank);
|
||||||
roomBlank.items = generateItems(roomBlank);
|
roomBlank.items = generateItems(display, roomBlank);
|
||||||
|
|
||||||
const room = new Room(x, y, width, height, worldX, worldY, worldZ, roomBlank.doors, roomBlank.items);
|
const room = new Room(display, x, y, width, height, worldX, worldY, worldZ, roomBlank.doors, roomBlank.items);
|
||||||
|
|
||||||
rooms.set(id, room);
|
rooms.set(id, room);
|
||||||
return room;
|
return room;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { getRegion, setRegion } from "./display";
|
import type { TextDisplay } from "@common/display/text";
|
||||||
import { Figure } from "./figure";
|
import { Figure } from "./figure";
|
||||||
import { PLAYER_SPRITE } from "./images";
|
import { PLAYER_SPRITE } from "./images";
|
||||||
import type { IRegion } from "./types";
|
import type { IRegion } from "./types";
|
||||||
|
|
@ -11,23 +11,24 @@ export class Sprite extends Figure {
|
||||||
public skipNextBackgroundRestore = false;
|
public skipNextBackgroundRestore = false;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
display: TextDisplay,
|
||||||
x: number,
|
x: number,
|
||||||
y: number,
|
y: number,
|
||||||
) {
|
) {
|
||||||
super(x, y, PLAYER_SPRITE);
|
super(display, x, y, PLAYER_SPRITE);
|
||||||
|
|
||||||
this.prevX = x | 0;
|
this.prevX = x | 0;
|
||||||
this.prevY = y | 0;
|
this.prevY = y | 0;
|
||||||
this.prevImage = getRegion(x, y, this.width, this.height);
|
this.prevImage = this.display.getRegion(x, y, this.width, this.height);
|
||||||
}
|
}
|
||||||
|
|
||||||
override doDraw() {
|
override doDraw() {
|
||||||
if (this.skipNextBackgroundRestore) {
|
if (this.skipNextBackgroundRestore) {
|
||||||
this.skipNextBackgroundRestore = false;
|
this.skipNextBackgroundRestore = false;
|
||||||
} else {
|
} else {
|
||||||
setRegion(this.prevX, this.prevY, this.width, this.height, this.prevImage);
|
this.display.setRegion(this.prevX, this.prevY, this.width, this.height, this.prevImage);
|
||||||
}
|
}
|
||||||
this.prevImage = getRegion(this.x, this.y, this.width, this.height);
|
this.prevImage = this.display.getRegion(this.x, this.y, this.width, this.height);
|
||||||
|
|
||||||
super.doDraw();
|
super.doDraw();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Color } from "const";
|
import { Color } from "./const";
|
||||||
|
|
||||||
export type IColorLike = string | number | Color;
|
export type IColorLike = string | number | Color;
|
||||||
export type IChar = [string, IColorLike?, IColorLike?];
|
export type IChar = [string, IColorLike?, IColorLike?];
|
||||||
|
|
|
||||||
|
|
@ -225,7 +225,7 @@ describe('Inventory — equip', () => {
|
||||||
sword.add('equippable', new Equippable('weapon'));
|
sword.add('equippable', new Equippable('weapon'));
|
||||||
const player = w.createEntity('player');
|
const player = w.createEntity('player');
|
||||||
player.add('str', new Stat({ value: 10 }));
|
player.add('str', new Stat({ value: 10 }));
|
||||||
player.add('equipment', new Equipment([{ slotName: 'weapon', type: 'weapon' }]));
|
player.add('equipment', new Equipment({ slotName: 'weapon', type: 'weapon' }));
|
||||||
player.add('inv', new Inventory());
|
player.add('inv', new Inventory());
|
||||||
const inv = player.get(Inventory)!;
|
const inv = player.get(Inventory)!;
|
||||||
inv.add({ itemId: 'sword', amount: 1 });
|
inv.add({ itemId: 'sword', amount: 1 });
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue