Add awoodungeon as subproject, add favicons and font styles
This commit is contained in:
parent
ed9ddd5e89
commit
9ae3c40368
|
|
@ -27,6 +27,7 @@
|
|||
margin: auto;
|
||||
}
|
||||
</style>
|
||||
$ICON$
|
||||
</head>
|
||||
<body>
|
||||
<canvas id="canvas"></canvas>
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ const dataUrlPlugin: BunPlugin = {
|
|||
const buffer = Buffer.from(arrayBuffer);
|
||||
|
||||
return {
|
||||
contents: `data:image/png;base64,${buffer.toString('base64')}`,
|
||||
contents: `data:;base64,${buffer.toString('base64')}`,
|
||||
loader: 'text',
|
||||
};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,41 @@
|
|||
import { plugin, type BunPlugin } from "bun";
|
||||
import path from 'path';
|
||||
|
||||
const fontPlugin: BunPlugin = {
|
||||
name: "Font loader",
|
||||
async setup(build) {
|
||||
build.onLoad({ filter: /\.font\.css$/ }, async (args) => {
|
||||
const css = await Bun.file(args.path).text();
|
||||
|
||||
const match = css.match(/url\(['"]?([^'")]*)['"]?\)/);
|
||||
if (match?.[1]) {
|
||||
const fontName = match[1];
|
||||
const fontPath = path.resolve(path.dirname(args.path), fontName);
|
||||
const fontFile = Bun.file(fontPath);
|
||||
if (await fontFile.exists()) {
|
||||
const buffer = Buffer.from(await fontFile.arrayBuffer());
|
||||
const url = `data:;base64,${buffer.toString('base64')}`;
|
||||
const updatedCSS = css.replace(fontName, url)
|
||||
.replace(/(\n\s*)*/g, '')
|
||||
.replace(/;\s*\}/g, '}')
|
||||
.replace(/\s*([{:])\s*/g, '$1');
|
||||
|
||||
return {
|
||||
contents: `import { injectStyle } from '__style_helper__';
|
||||
injectStyle(${JSON.stringify(updatedCSS)})`,
|
||||
loader: 'js',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
contents: '',
|
||||
loader: 'js',
|
||||
};
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
plugin(fontPlugin);
|
||||
|
||||
export default fontPlugin;
|
||||
|
|
@ -2,13 +2,17 @@ import path from 'path';
|
|||
import { minify } from 'html-minifier';
|
||||
|
||||
import dataUrlPlugin from './dataUrlPlugin';
|
||||
import fontPlugin from './fontPlugin';
|
||||
import lightningcss from 'bun-lightningcss';
|
||||
|
||||
import { getGames } from './isGame';
|
||||
|
||||
const transpiler = new Bun.Transpiler();
|
||||
|
||||
export async function buildHTML(game: string, production = false) {
|
||||
const html = await Bun.file(path.resolve(import.meta.dir, '..', 'assets', 'index.html')).text();
|
||||
const bundle = await Bun.build({
|
||||
outdir: '/tmp',
|
||||
entrypoints: [path.resolve(import.meta.dir, '..', 'index.ts')],
|
||||
sourcemap: production ? 'none' : 'inline',
|
||||
minify: production,
|
||||
|
|
@ -19,15 +23,27 @@ export async function buildHTML(game: string, production = false) {
|
|||
},
|
||||
plugins: [
|
||||
dataUrlPlugin,
|
||||
fontPlugin,
|
||||
lightningcss(),
|
||||
]
|
||||
});
|
||||
|
||||
if (bundle.success && bundle.outputs.length === 1) {
|
||||
if (bundle.success) {
|
||||
if (bundle.outputs.length > 1) {
|
||||
console.log(bundle.outputs);
|
||||
return;
|
||||
}
|
||||
const iconFile = Bun.file(path.resolve(import.meta.dir, '..', 'games', game, 'assets', 'favicon.ico'));
|
||||
let icon = '';
|
||||
if (await iconFile.exists()) {
|
||||
icon = `<link rel="icon" href="data:;base64,${Buffer.from(await iconFile.arrayBuffer()).toString('base64')}" />`;
|
||||
}
|
||||
const script = await bundle.outputs[0].text();
|
||||
|
||||
const resultHTML = html
|
||||
.replace('$SCRIPT$', `<script>${script}</script>`)
|
||||
.replace('$TITLE$', game[0].toUpperCase() + game.slice(1).toLowerCase());
|
||||
.replace('$TITLE$', game[0].toUpperCase() + game.slice(1).toLowerCase())
|
||||
.replace('$ICON$', icon);
|
||||
|
||||
return minify(resultHTML, {
|
||||
collapseWhitespace: true,
|
||||
|
|
@ -35,6 +51,6 @@ export async function buildHTML(game: string, production = false) {
|
|||
minifyCSS: true,
|
||||
});
|
||||
} else {
|
||||
console.error('Multiple assets: ', bundle.outputs, 'or fail: ', !bundle.success, bundle);
|
||||
console.error('Failed: ', !bundle.success, bundle);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
interface IKeyState {
|
||||
state: boolean;
|
||||
prevState?: boolean;
|
||||
|
||||
released?: boolean;
|
||||
pressed?: boolean;
|
||||
held?: boolean;
|
||||
}
|
||||
const KEYS: Record<string, IKeyState> = {};
|
||||
|
||||
document.body.addEventListener('keydown', (e) => {
|
||||
const keyId = e.key.toLowerCase();
|
||||
if (KEYS[keyId]) {
|
||||
KEYS[keyId].state = true;
|
||||
} else {
|
||||
KEYS[keyId] = { state: true };
|
||||
}
|
||||
});
|
||||
|
||||
document.body.addEventListener('keyup', (e) => {
|
||||
const keyId = e.key.toLowerCase();
|
||||
if (KEYS[keyId]) {
|
||||
KEYS[keyId].state = false;
|
||||
}
|
||||
});
|
||||
|
||||
export const isPressed = (key: string): boolean => KEYS[key.toLowerCase()]?.pressed ?? false;
|
||||
export const isReleased = (key: string): boolean => KEYS[key.toLowerCase()]?.released ?? false;
|
||||
export const isHeld = (key: string): boolean => KEYS[key.toLowerCase()]?.held ?? false;
|
||||
|
||||
export function updateKeys() {
|
||||
for (const key of Object.values(KEYS)) {
|
||||
key.released = false;
|
||||
key.pressed = false;
|
||||
|
||||
if (key.state) {
|
||||
key.pressed = !key.held;
|
||||
key.held = true;
|
||||
} else {
|
||||
key.released = true;
|
||||
key.held = false;
|
||||
}
|
||||
|
||||
key.prevState = key.state;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
export const delay = async (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
export const nextFrame = async (): Promise<number> => new Promise((resolve) => requestAnimationFrame(resolve));
|
||||
export const randInt = (min: number, max: number) => Math.round(min + (max - min - 1) * Math.random());
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import Binario from "./game";
|
||||
|
||||
export default function runGame(canvas: HTMLCanvasElement) {
|
||||
const game = new Binario(canvas);
|
||||
game.run();
|
||||
}
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
import { render } from "preact";
|
||||
import styles from './game.module.css';
|
||||
|
||||
declare const GAMES: string[];
|
||||
|
||||
export default class GameIndex implements IGame {
|
||||
constructor(canvas: HTMLCanvasElement) {
|
||||
canvas.remove();
|
||||
}
|
||||
run() {
|
||||
const root = <div class={styles.games}>
|
||||
{GAMES.map(g => <a key={g} href={`?game=${g}`}>{g}</a>)}
|
||||
</div>;
|
||||
|
||||
render(root, document.body);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
import { render } from "preact";
|
||||
import styles from './game.module.css';
|
||||
|
||||
declare const GAMES: string[];
|
||||
|
||||
export default function run(canvas: HTMLCanvasElement) {
|
||||
canvas.remove();
|
||||
const root = <div class={styles.games}>
|
||||
{GAMES.map(g => <a key={g} href={`?game=${g}`}>{g}</a>)}
|
||||
</div>;
|
||||
|
||||
render(root, document.body);
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 318 B |
|
|
@ -0,0 +1,21 @@
|
|||
html, body {
|
||||
background: black;
|
||||
color: white;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
font-family: 'Verite 8x16', 'IBM VGA 8x16', monospace;
|
||||
font-size: 16px;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
#fps {
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
left: 5px;
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
@font-face {
|
||||
font-family: 'IBM VGA 8x16';
|
||||
src: url("./WebPlus_IBM_VGA_8x16.woff") format('woff');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
export const GAME_WIDTH = 80;
|
||||
export const GAME_HEIGHT = 25;
|
||||
|
||||
export const ROOM_AREA_WIDTH = 40;
|
||||
export const ROOM_AREA_HEIGHT = GAME_HEIGHT;
|
||||
|
||||
export const ROOM_AREA_X = GAME_WIDTH - ROOM_AREA_WIDTH;
|
||||
export const ROOM_AREA_Y = Math.round((GAME_HEIGHT - ROOM_AREA_HEIGHT) / 2);
|
||||
|
||||
export const INVENTORY_HEIGHT = 3;
|
||||
export const INVENTORY_X = 0;
|
||||
export const INVENTORY_Y = GAME_HEIGHT - INVENTORY_HEIGHT;
|
||||
|
||||
export const MAP_X = 16;
|
||||
export const MAP_Y = 0;
|
||||
export const MAP_WIDTH = GAME_WIDTH - ROOM_AREA_WIDTH - MAP_X;
|
||||
export const MAP_HEIGHT = 10;
|
||||
|
||||
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 DIRECTION_OFFSETS: Record<number, [number, number, number]> = {
|
||||
[Direction.NORTH]: [0, -1, 0],
|
||||
[Direction.EAST]: [1, 0, 0],
|
||||
[Direction.SOUTH]: [0, 1, 0],
|
||||
[Direction.WEST]: [-1, 0, 0],
|
||||
[Direction.UP]: [0, 0, -1],
|
||||
[Direction.DOWN]: [0, 0, 1],
|
||||
};
|
||||
|
||||
export const getOppositeDirection = (d: Direction): Direction => {
|
||||
switch (d) {
|
||||
case Direction.NORTH: return Direction.SOUTH;
|
||||
case Direction.WEST: return Direction.EAST;
|
||||
case Direction.SOUTH: return Direction.NORTH;
|
||||
case Direction.EAST: return Direction.WEST;
|
||||
case Direction.UP: return Direction.DOWN;
|
||||
case Direction.DOWN: return Direction.UP;
|
||||
}
|
||||
}
|
||||
|
||||
export const MAP_ROOM_CHARS: Record<number, string> = {
|
||||
[0b0000]: ' ',
|
||||
[0b0001]: '█',
|
||||
[0b0010]: '█',
|
||||
[0b0100]: '█',
|
||||
[0b1000]: '█',
|
||||
[0b0011]: '╚',
|
||||
[0b0110]: '╔',
|
||||
[0b1100]: '╗',
|
||||
[0b1001]: '╝',
|
||||
[0b1010]: '═',
|
||||
[0b0101]: '║',
|
||||
[0b0111]: '╠',
|
||||
[0b1110]: '╦',
|
||||
[0b1101]: '╣',
|
||||
[0b1011]: '╩',
|
||||
[0b1111]: '╬',
|
||||
};
|
||||
|
|
@ -0,0 +1,230 @@
|
|||
import { delay, 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 delay(remainingDelay);
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('resize', handleResize);
|
||||
handleResize();
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
export abstract class Drawable {
|
||||
protected dirty: boolean = true;
|
||||
abstract doDraw(): void;
|
||||
|
||||
draw() {
|
||||
if (this.dirty) {
|
||||
this.doDraw();
|
||||
this.dirty = false;
|
||||
}
|
||||
}
|
||||
|
||||
invalidate() {
|
||||
this.dirty = true;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
import { setRegion } from "./display";
|
||||
import { Drawable } from "./drawable";
|
||||
import type { IRegion, ISpriteDefinition } from "./types";
|
||||
|
||||
export class Figure extends Drawable {
|
||||
private frames: IRegion[];
|
||||
private animationCounter = 0;
|
||||
private animationPeriod;
|
||||
|
||||
constructor(
|
||||
public x: number,
|
||||
public y: number,
|
||||
definition: ISpriteDefinition,
|
||||
public frame: number = 0,
|
||||
) {
|
||||
super();
|
||||
this.x = x | 0;
|
||||
this.y = y | 0;
|
||||
this.frames = definition.frames;
|
||||
this.animationCounter = 0;
|
||||
this.animationPeriod = definition.animationPeriod ?? Number.POSITIVE_INFINITY;
|
||||
}
|
||||
|
||||
override doDraw() {
|
||||
this.animationCounter++;
|
||||
if (this.animationCounter >= this.animationPeriod) {
|
||||
this.animationCounter = 0;
|
||||
this.frame = (this.frame + 1) % this.numFrames;
|
||||
}
|
||||
|
||||
setRegion(this.x, this.y, this.width, this.height, this.image);
|
||||
}
|
||||
|
||||
get width() {
|
||||
const frame = this.frames[(this.frame % this.numFrames)];
|
||||
const line = frame[0];
|
||||
return line?.length ?? 0;
|
||||
}
|
||||
|
||||
get height() {
|
||||
const frame = this.frames[(this.frame % this.numFrames)];
|
||||
return frame.length;
|
||||
}
|
||||
|
||||
get numFrames() {
|
||||
return this.frames.length;
|
||||
}
|
||||
|
||||
get image() {
|
||||
return this.frames[(this.frame % this.numFrames)];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,141 @@
|
|||
import { Color, Direction, getOppositeDirection } from './const';
|
||||
import { drawTextInBox, tick } from './display';
|
||||
import { isHeld, isPressed, updateKeys } 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 = isPressed(' ');
|
||||
|
||||
if (Date.now() - lastMove < 75 && !isHeld('shift') && !isSpacePressed) return;
|
||||
lastMove = Date.now();
|
||||
let newX = player.x;
|
||||
let newY = player.y;
|
||||
let moved = isSpacePressed;
|
||||
|
||||
if (isHeld('arrowup')) {
|
||||
newY--;
|
||||
moved = true;
|
||||
}
|
||||
|
||||
if (isHeld('arrowdown')) {
|
||||
newY++;
|
||||
moved = true;
|
||||
}
|
||||
|
||||
if (isHeld('arrowleft')) {
|
||||
newX--;
|
||||
moved = true;
|
||||
}
|
||||
|
||||
if (isHeld('arrowright')) {
|
||||
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) {
|
||||
updateKeys();
|
||||
handleInput();
|
||||
handleLogic();
|
||||
draw();
|
||||
|
||||
await tick(60);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
import { Color } from "./const";
|
||||
import type { IChar, ISpriteDefinition } from "./types";
|
||||
|
||||
const l = (line: string, fg = Color.WHITE, bg = Color.BLACK): IChar[] =>
|
||||
Array.from(line).map((c) => [c, fg, bg]);
|
||||
|
||||
export const PLAYER_SPRITE: ISpriteDefinition = {
|
||||
frames: [
|
||||
[
|
||||
l('@', Color.YELLOW),
|
||||
],
|
||||
],
|
||||
};
|
||||
|
||||
export const DOOR_SPRITE: ISpriteDefinition = {
|
||||
frames: [
|
||||
[
|
||||
l('┘ └'),
|
||||
],
|
||||
[
|
||||
l('└'),
|
||||
l(' '),
|
||||
l('┌'),
|
||||
],
|
||||
[
|
||||
l('┐ ┌'),
|
||||
],
|
||||
[
|
||||
l('┘'),
|
||||
l(' '),
|
||||
l('┐'),
|
||||
],
|
||||
[
|
||||
l('^', Color.CYAN),
|
||||
],
|
||||
[
|
||||
l('_', Color.GREEN),
|
||||
],
|
||||
],
|
||||
}
|
||||
|
||||
export const ITEM_KEY_SPRITE: ISpriteDefinition = {
|
||||
frames: [
|
||||
[
|
||||
l('ъ', Color.MAGENTA),
|
||||
],
|
||||
[
|
||||
l('ъ', Color.YELLOW),
|
||||
],
|
||||
[
|
||||
l('ъ', Color.CYAN),
|
||||
],
|
||||
[
|
||||
l('ъ', Color.GREEN),
|
||||
],
|
||||
],
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
|
||||
|
||||
import './assets/style.css';
|
||||
import './assets/vga.font.css';
|
||||
|
||||
export default async function run(canvas: HTMLCanvasElement) {
|
||||
canvas.remove();
|
||||
const root = document.createElement('div');
|
||||
root.id = 'root';
|
||||
document.body.append(root);
|
||||
|
||||
const fps = document.createElement('div');
|
||||
fps.id = 'fps';
|
||||
document.body.append(fps);
|
||||
|
||||
const { main } = await import('./game');
|
||||
await main();
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
import { Drawable } from "./drawable";
|
||||
import { Figure } from "./figure";
|
||||
import { ITEM_KEY_SPRITE } from "./images";
|
||||
|
||||
let globalItemId = 0;
|
||||
const idMap = new Map<number, Figure>();
|
||||
|
||||
export const getItemsCount = () => idMap.size;
|
||||
|
||||
export function createItems() {
|
||||
for (let frame = 0; frame < ITEM_KEY_SPRITE.frames.length; frame++) {
|
||||
idMap.set(
|
||||
globalItemId++,
|
||||
new Figure(0, 0, ITEM_KEY_SPRITE, frame),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class Item extends Drawable {
|
||||
constructor(
|
||||
public id: number,
|
||||
public count: number = 1,
|
||||
public x: number = -1,
|
||||
public y: number = -1,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
override doDraw() {
|
||||
const figure = idMap.get(this.id);
|
||||
if (figure) {
|
||||
figure.x = this.x;
|
||||
figure.y = this.y;
|
||||
figure.doDraw();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
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 { getRoomsForLayer, Room } from "./room";
|
||||
|
||||
export class GameMap extends Drawable {
|
||||
constructor(public currentRoom: Room) {
|
||||
super();
|
||||
}
|
||||
|
||||
override doDraw() {
|
||||
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 centerY = this.currentRoom.worldY - MAP_Y - MAP_HEIGHT / 2;
|
||||
|
||||
for (const room of getRoomsForLayer(this.currentRoom.worldZ)) {
|
||||
const x = Math.round(room.worldX - centerX);
|
||||
const y = Math.round(room.worldY - centerY);
|
||||
|
||||
if (x > MAP_X && x < MAP_X + MAP_WIDTH - 1 && y > MAP_Y && y < MAP_Y + MAP_HEIGHT - 1) {
|
||||
const char = getMapRoomChar(room);
|
||||
setChar(x, y, [char, room === this.currentRoom ? Color.YELLOW : Color.WHITE]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getMapRoomChar(room: Room): string {
|
||||
let doorsMask = 0;
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const mask = 1 << i;
|
||||
if (room.doors[i]) {
|
||||
doorsMask |= mask;
|
||||
}
|
||||
}
|
||||
|
||||
return MAP_ROOM_CHARS[doorsMask];
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
import { INVENTORY_X, INVENTORY_Y } from "./const";
|
||||
import { drawBox } from "./display";
|
||||
import { getItemsCount, Item } from "./item";
|
||||
import { Sprite } from "./sprite";
|
||||
|
||||
export class Player extends Sprite {
|
||||
private inventory: Item[] = [];
|
||||
|
||||
addItem(item: Item) {
|
||||
for (const i of this.inventory) {
|
||||
if (i.id === item.id) {
|
||||
i.count += item.count;
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.inventory.push(new Item(
|
||||
item.id,
|
||||
item.count,
|
||||
INVENTORY_X + 1 + this.inventory.length,
|
||||
INVENTORY_Y + 1,
|
||||
));
|
||||
}
|
||||
|
||||
hasItem(item: Item) {
|
||||
for (const i of this.inventory) {
|
||||
if (i.id === item.id) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
get foundItems() {
|
||||
return this.inventory.length;
|
||||
}
|
||||
|
||||
override doDraw() {
|
||||
super.doDraw();
|
||||
|
||||
drawBox(INVENTORY_X, INVENTORY_Y, getItemsCount(), 1, { fill: [' '], title: 'Inv' });
|
||||
this.inventory.forEach((item) => {
|
||||
if (item.count > 0) {
|
||||
item.doDraw();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,220 @@
|
|||
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 { drawBox } from "./display";
|
||||
import { Drawable } from "./drawable";
|
||||
import { Figure } from "./figure";
|
||||
import { DOOR_SPRITE } from "./images";
|
||||
import { getItemsCount, Item } from "./item";
|
||||
|
||||
type IDoors = [Door | null, Door | null, Door | null, Door | null, Door | null, Door | null];
|
||||
|
||||
export class Door extends Figure {
|
||||
constructor(
|
||||
x: number,
|
||||
y: number,
|
||||
direction: Direction,
|
||||
public worldX: number,
|
||||
public worldY: number,
|
||||
public worldZ: number,
|
||||
) {
|
||||
super(x, y, DOOR_SPRITE);
|
||||
this.frame = direction;
|
||||
}
|
||||
|
||||
isActivated(x: number, y: number): boolean {
|
||||
const cx = Math.floor(this.x + this.width / 2);
|
||||
const cy = Math.floor(this.y + this.height / 2);
|
||||
|
||||
return (cx === x && cy === y);
|
||||
}
|
||||
|
||||
get direction(): Direction {
|
||||
return this.frame as Direction;
|
||||
}
|
||||
}
|
||||
|
||||
export class Room extends Drawable {
|
||||
constructor(
|
||||
public readonly x: number,
|
||||
public readonly y: number,
|
||||
public readonly width: number,
|
||||
public readonly height: number,
|
||||
public readonly worldX: number,
|
||||
public readonly worldY: number,
|
||||
public readonly worldZ: number,
|
||||
public readonly doors: IDoors = [null, null, null, null, null, null],
|
||||
public readonly items: Item[] = [],
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
override doDraw() {
|
||||
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.doors.forEach((door) => {
|
||||
if (door) {
|
||||
door.doDraw();
|
||||
}
|
||||
});
|
||||
|
||||
this.items.forEach((item) => item.doDraw());
|
||||
}
|
||||
|
||||
getActivatedDoor(x: number, y: number): Door | null {
|
||||
return this.doors.find((door) => door?.isActivated(x, y)) ?? null;
|
||||
}
|
||||
|
||||
pickItem(x: number, y: number): Item | null {
|
||||
const itemIndex = this.items.findIndex((item) => item.x === x && item.y === y);
|
||||
if (itemIndex >= 0) {
|
||||
const [item] = this.items.splice(itemIndex, 1);
|
||||
this.invalidate();
|
||||
return item;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
type IRoomBlank = {
|
||||
-readonly [P in keyof Room]?: Room[P]
|
||||
};
|
||||
|
||||
const rooms = new Map<string, Room>();
|
||||
|
||||
const generateRoomId = (worldX: number, worldY: number, worldZ: number) => `${worldX},${worldY},${worldZ}`;
|
||||
|
||||
const generateDoors = ({ x, y, width, height, worldX, worldY, worldZ }: IRoomBlank): IDoors => {
|
||||
const doors: IDoors = [null, null, null, null, null, null];
|
||||
if (typeof worldX === 'undefined' || typeof worldY === 'undefined' || typeof worldZ === 'undefined') {
|
||||
console.error('World coordinates not defined for doors generation');
|
||||
return doors;
|
||||
}
|
||||
if (typeof x === 'undefined' || typeof y === 'undefined' || typeof width === 'undefined' || typeof height === 'undefined') {
|
||||
console.error('Screen coordinates not defined for doors generation');
|
||||
return doors;
|
||||
}
|
||||
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const [xoff, yoff] = DIRECTION_OFFSETS[i];
|
||||
const doorWorldX = worldX + xoff;
|
||||
const doorWorldY = worldY + yoff;
|
||||
const doorId = generateRoomId(doorWorldX, doorWorldY, worldZ);
|
||||
const doorRoom = rooms.get(doorId);
|
||||
if (doorRoom && !doorRoom.doors[getOppositeDirection(i)]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ((worldX === 0 && worldY === 0) || doorRoom || Math.random() < 0.3) {
|
||||
const doorX = randInt(x + 1, x + width - 2);
|
||||
const doorY = randInt(y + 1, y + height - 2);
|
||||
let dx = 0;
|
||||
let dy = 0;
|
||||
switch (i) {
|
||||
case Direction.NORTH: dx = doorX; dy = y; break;
|
||||
case Direction.SOUTH: dx = doorX; dy = y + height + 1; break;
|
||||
case Direction.EAST: dx = x + width + 1; dy = doorY; break;
|
||||
case Direction.WEST: dx = x; dy = doorY; break;
|
||||
}
|
||||
doors[i] = new Door(dx, dy, i as Direction, doorWorldX, doorWorldY, worldZ);
|
||||
}
|
||||
}
|
||||
|
||||
if (worldX === 0 && worldY === 0) {
|
||||
const doorX = randInt(x + 1, x + width - 2);
|
||||
const doorY = randInt(y + 1, y + height - 2);
|
||||
const [, , zoff] = DIRECTION_OFFSETS[Direction.DOWN];
|
||||
doors[Direction.DOWN] = new Door(doorX, doorY, Direction.DOWN, 0, 0, worldZ + zoff);
|
||||
|
||||
if (worldZ !== 0) {
|
||||
let upDoorX: number;
|
||||
let upDoorY: number;
|
||||
do {
|
||||
upDoorX = randInt(x + 1, x + width - 2);
|
||||
upDoorY = randInt(y + 1, y + height - 2);
|
||||
} while (doorX === upDoorX && doorY === upDoorY);
|
||||
const [, , upZoff] = DIRECTION_OFFSETS[Direction.UP];
|
||||
doors[Direction.UP] = new Door(upDoorX, upDoorY, Direction.UP, 0, 0, worldZ + upZoff);
|
||||
}
|
||||
}
|
||||
|
||||
return doors;
|
||||
};
|
||||
|
||||
const generatedItems = new Set<number>();
|
||||
|
||||
const generateItems = ({ x, y, width, height, doors }: IRoomBlank): Item[] => {
|
||||
const items: Item[] = [];
|
||||
if (typeof x === 'undefined' || typeof y === 'undefined' || typeof width === 'undefined' || typeof height === 'undefined') {
|
||||
console.error('Screen coordinates not defined for items generation');
|
||||
return items;
|
||||
}
|
||||
if (typeof doors === 'undefined') {
|
||||
console.error('Doors not defined for items generation');
|
||||
return items;
|
||||
}
|
||||
|
||||
const hasObjectAt = (x: number, y: number) =>
|
||||
doors.some((d) => d?.x === x && d?.y === y) ||
|
||||
items.some((i) => i.x === x && i.y === y);
|
||||
|
||||
const oneDoor = doors.filter((d) => Boolean(d)).length === 1;
|
||||
|
||||
const id = randInt(0, getItemsCount());
|
||||
|
||||
if (Math.random() < 0.4 && oneDoor && !generatedItems.has(id)) {
|
||||
let newItemX: number;
|
||||
let newItemY: number;
|
||||
|
||||
do {
|
||||
newItemX = randInt(x + 1, x + width - 2);
|
||||
newItemY = randInt(y + 1, y + height - 2);
|
||||
} while (hasObjectAt(newItemX, newItemY));
|
||||
const item = new Item(id, 1, newItemX, newItemY);
|
||||
|
||||
items.push(item);
|
||||
generatedItems.add(id);
|
||||
}
|
||||
|
||||
return items;
|
||||
};
|
||||
|
||||
export function getRoom(worldX: number, worldY: number, worldZ: number) {
|
||||
const id = generateRoomId(worldX, worldY, worldZ);
|
||||
const existingRoom = rooms.get(id);
|
||||
if (existingRoom) {
|
||||
return existingRoom;
|
||||
}
|
||||
const width = randInt(5, ROOM_AREA_WIDTH - 5);
|
||||
const height = randInt(5, ROOM_AREA_HEIGHT - 5);
|
||||
|
||||
const x = Math.max(ROOM_AREA_X + randInt(2, ROOM_AREA_WIDTH - width - 2), ROOM_AREA_X + 2);
|
||||
const y = Math.max(ROOM_AREA_Y + randInt(2, ROOM_AREA_HEIGHT - height - 2), ROOM_AREA_Y + 2);
|
||||
|
||||
const roomBlank: IRoomBlank = { x, y, width, height, worldX, worldY, worldZ };
|
||||
|
||||
roomBlank.doors = generateDoors(roomBlank);
|
||||
roomBlank.items = generateItems(roomBlank);
|
||||
|
||||
const room = new Room(x, y, width, height, worldX, worldY, worldZ, roomBlank.doors, roomBlank.items);
|
||||
|
||||
rooms.set(id, room);
|
||||
return room;
|
||||
}
|
||||
|
||||
export const getRoomsCount = () => rooms.size;
|
||||
export const getPossibleRoomsCount = () => {
|
||||
const roomsSet = new Set(rooms.keys());
|
||||
for (const room of rooms.values()) {
|
||||
room.doors.forEach((d) => {
|
||||
if (d) {
|
||||
const id = generateRoomId(d.worldX, d.worldY, d.worldZ);
|
||||
roomsSet.add(id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return roomsSet.size;
|
||||
};
|
||||
|
||||
export const getRoomsForLayer = (z: number): Room[] => Array.from(rooms.values()).filter((r) => r.worldZ === z);
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
import { getRegion, setRegion } from "./display";
|
||||
import { Figure } from "./figure";
|
||||
import { PLAYER_SPRITE } from "./images";
|
||||
import type { IRegion } from "./types";
|
||||
|
||||
export class Sprite extends Figure {
|
||||
private prevX: number;
|
||||
private prevY: number;
|
||||
private prevImage: IRegion;
|
||||
|
||||
public skipNextBackgroundRestore = false;
|
||||
|
||||
constructor(
|
||||
x: number,
|
||||
y: number,
|
||||
) {
|
||||
super(x, y, PLAYER_SPRITE);
|
||||
|
||||
this.prevX = x | 0;
|
||||
this.prevY = y | 0;
|
||||
this.prevImage = getRegion(x, y, this.width, this.height);
|
||||
}
|
||||
|
||||
override doDraw() {
|
||||
if (this.skipNextBackgroundRestore) {
|
||||
this.skipNextBackgroundRestore = false;
|
||||
} else {
|
||||
setRegion(this.prevX, this.prevY, this.width, this.height, this.prevImage);
|
||||
}
|
||||
this.prevImage = getRegion(this.x, this.y, this.width, this.height);
|
||||
|
||||
super.doDraw();
|
||||
|
||||
this.prevX = this.x;
|
||||
this.prevY = this.y;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
import { Color } from "const";
|
||||
|
||||
export type IColorLike = string | number | Color;
|
||||
export type IChar = [string, IColorLike?, IColorLike?];
|
||||
export type IRegion = IChar[][];
|
||||
|
||||
export interface ISpriteDefinition {
|
||||
frames: IRegion[];
|
||||
animationPeriod?: number;
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
import { randInt } from "@common/utils";
|
||||
|
||||
export const randChar = (min = ' ', max = '~') =>
|
||||
String.fromCharCode(randInt(
|
||||
min.charCodeAt(0),
|
||||
max.charCodeAt(0) + 1,
|
||||
));
|
||||
|
||||
export const choice = (array: any[]) => array[randInt(0, array.length)];
|
||||
|
||||
export 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;
|
||||
};
|
||||
|
|
@ -1,11 +1,10 @@
|
|||
declare const GAME: string;
|
||||
|
||||
async function main() {
|
||||
const { default: Game }: { default: IGameConstructor } = await import(`./games/${GAME}/game`);
|
||||
const { default: runGame }: { default: RunGame } = await import(`./games/${GAME}`);
|
||||
const canvas = document.getElementById('canvas');
|
||||
if (canvas instanceof HTMLCanvasElement) {
|
||||
const game = new Game(canvas);
|
||||
game.run();
|
||||
await runGame(canvas);
|
||||
} else {
|
||||
alert('Something wrong with your canvas!');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,7 @@
|
|||
type Point = [number, number];
|
||||
type Rect = [number, number, number, number];
|
||||
|
||||
interface IGame {
|
||||
run();
|
||||
}
|
||||
|
||||
interface IGameConstructor {
|
||||
new(canvas: HTMLCanvasElement): IGame;
|
||||
}
|
||||
type RunGame = (canvas: HTMLCanvasElement) => Promise<void>;
|
||||
|
||||
declare module "*.png" {
|
||||
const content: string;
|
||||
|
|
@ -16,4 +10,8 @@ declare module "*.png" {
|
|||
declare module '*.module.css' {
|
||||
const classes: { [key: string]: string };
|
||||
export default classes;
|
||||
}
|
||||
declare module '*.module.scss' {
|
||||
const classes: { [key: string]: string };
|
||||
export default classes;
|
||||
}
|
||||
|
|
@ -24,5 +24,8 @@
|
|||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noPropertyAccessFromIndexSignature": false,
|
||||
"paths": {
|
||||
"@common/*": ["./src/common/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue