Refactor
This commit is contained in:
parent
043378cfbb
commit
31a3ee213e
|
|
@ -29,8 +29,8 @@ void image_draw_point(image_data_t image, int16_t x, int16_t y, image_color_t co
|
|||
void image_draw_hline(image_data_t image, int16_t x1, int16_t x2, int16_t y, image_color_t color);
|
||||
void image_draw_vline(image_data_t image, int16_t x, int16_t y1, int16_t y2, image_color_t color);
|
||||
void image_draw_line(image_data_t image, int16_t x1, int16_t y1, int16_t x2, int16_t y2, image_color_t color);
|
||||
void image_draw_rect(image_data_t image, int16_t x, int16_t y, uint16_t w, uint16_t h, image_color_t color);
|
||||
void image_fill_rect(image_data_t image, int16_t x, int16_t y, uint16_t w, uint16_t h, image_color_t color);
|
||||
void image_draw_rect(image_data_t image, int16_t x, int16_t y, int16_t w, int16_t h, image_color_t color);
|
||||
void image_fill_rect(image_data_t image, int16_t x, int16_t y, int16_t w, int16_t h, image_color_t color);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
#include <graphics.h>
|
||||
#include <memory.h>
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
static inline int max(int a, int b) {
|
||||
static inline int32_t max(int32_t a, int32_t b) {
|
||||
return a > b ? a : b;
|
||||
}
|
||||
|
||||
static inline int min(int a, int b) {
|
||||
static inline int32_t min(int32_t a, int32_t b) {
|
||||
return a < b ? a : b;
|
||||
}
|
||||
|
||||
|
|
@ -28,7 +29,7 @@ void image_free(image_data_t image) {
|
|||
}
|
||||
}
|
||||
|
||||
static inline void set_point(image_data_t image, uint16_t x, uint16_t y, image_color_t color) {
|
||||
static inline void set_point(image_data_t image, int16_t x, int16_t y, image_color_t color) {
|
||||
image.pixels[x + y * image.width] = color;
|
||||
}
|
||||
|
||||
|
|
@ -44,15 +45,15 @@ void image_draw_hline(image_data_t image, int16_t x1, int16_t x2, int16_t y, ima
|
|||
return;
|
||||
}
|
||||
if (x1 > x2) {
|
||||
uint16_t temp = x1;
|
||||
int16_t temp = x1;
|
||||
x1 = x2;
|
||||
x2 = temp;
|
||||
}
|
||||
if (x1 > image.width || x2 < 0) {
|
||||
return;
|
||||
}
|
||||
x1 = max(0, x1);
|
||||
x2 = min(x2, image.width);
|
||||
x1 = (int16_t)max(0, x1);
|
||||
x2 = (int16_t)min(x2, image.width);
|
||||
|
||||
do {
|
||||
set_point(image, x1++, y, color);
|
||||
|
|
@ -64,15 +65,15 @@ void image_draw_vline(image_data_t image, int16_t x, int16_t y1, int16_t y2, ima
|
|||
return;
|
||||
}
|
||||
if (y1 > y2) {
|
||||
uint16_t temp = y1;
|
||||
int16_t temp = y1;
|
||||
y1 = y2;
|
||||
y2 = temp;
|
||||
}
|
||||
if (y1 > image.width || y2 < 0) {
|
||||
return;
|
||||
}
|
||||
y1 = max(0, y1);
|
||||
y2 = min(y2, image.height);
|
||||
y1 = (int16_t)max(0, y1);
|
||||
y2 = (int16_t)min(y2, image.height);
|
||||
|
||||
do {
|
||||
set_point(image, x, y1++, color);
|
||||
|
|
@ -110,13 +111,13 @@ void image_draw_line(image_data_t image, int16_t x1, int16_t y1, int16_t x2, int
|
|||
}
|
||||
}
|
||||
|
||||
void image_draw_rect(image_data_t image, int16_t x, int16_t y, uint16_t w, uint16_t h, image_color_t color) {
|
||||
if (w == 0 || h == 0) {
|
||||
void image_draw_rect(image_data_t image, int16_t x, int16_t y, int16_t w, int16_t h, image_color_t color) {
|
||||
if (w <= 0 || h <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
w = min(image.width - x, w);
|
||||
h = min(image.height - y, h);
|
||||
w = (int16_t)min(image.width - x, w);
|
||||
h = (int16_t)min(image.height - y, h);
|
||||
|
||||
image_draw_hline(image, x, x + w - 1, y, color);
|
||||
image_draw_hline(image, x, x + w, y + h - 1, color);
|
||||
|
|
@ -125,14 +126,14 @@ void image_draw_rect(image_data_t image, int16_t x, int16_t y, uint16_t w, uint1
|
|||
image_draw_vline(image, x + w - 1, y, y + h - 1, color);
|
||||
}
|
||||
|
||||
void image_fill_rect(image_data_t image, int16_t x, int16_t y, uint16_t w, uint16_t h, image_color_t color) {
|
||||
if (w == 0 || h == 0) {
|
||||
void image_fill_rect(image_data_t image, int16_t x, int16_t y, int16_t w, int16_t h, image_color_t color) {
|
||||
if (w <= 0 || h <= 0) {
|
||||
return;
|
||||
}
|
||||
x = max(0, x);
|
||||
y = max(0, y);
|
||||
w = min(image.width - x, w);
|
||||
h = min(image.height - y, h);
|
||||
x = (int16_t)max(0, x);
|
||||
y = (int16_t)max(0, y);
|
||||
w = (int16_t)min(image.width - x, w);
|
||||
h = (int16_t)min(image.height - y, h);
|
||||
|
||||
while (h--) {
|
||||
image_draw_hline(image, x, x + w, y++, color);
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ await fs.mkdir(outDir, { recursive: true });
|
|||
const publish = process.env.PUBLISH_LOCATION;
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const itch = args.includes('--itch');
|
||||
const local = args.includes('--local');
|
||||
let game = args.find(a => !a.startsWith('-')) ?? '';
|
||||
|
||||
while (!await isGame(game)) {
|
||||
|
|
@ -22,7 +22,7 @@ while (!await isGame(game)) {
|
|||
});
|
||||
}
|
||||
console.log(`Building ${game}...`);
|
||||
const html = await buildHTML(game, { production: true, itch });
|
||||
const html = await buildHTML(game, { production: true, local });
|
||||
|
||||
if (!html) {
|
||||
process.exit(1);
|
||||
|
|
@ -30,7 +30,7 @@ if (!html) {
|
|||
const filePath = path.resolve(outDir, `${game}.html`);
|
||||
await Bun.write(filePath, html);
|
||||
|
||||
if (publish && !itch) {
|
||||
if (publish && !local) {
|
||||
console.log(`Publishing ${game}...`);
|
||||
const result = await $`scp "${filePath}" "${publish}${game}.html"`;
|
||||
if (result.exitCode === 0) {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import type { BunFile } from 'bun';
|
||||
import path from 'path';
|
||||
import { minify } from 'html-minifier';
|
||||
import UglifyJS from 'uglify-js';
|
||||
|
|
@ -10,17 +11,27 @@ import filePlugin from './filePlugin';
|
|||
|
||||
import { getGames } from './isGame';
|
||||
|
||||
const b64 = async (file: string | BunFile) => (
|
||||
typeof file === 'string'
|
||||
? Buffer.from(file)
|
||||
: Buffer.from(await file.arrayBuffer())
|
||||
).toString('base64');
|
||||
|
||||
interface Args {
|
||||
production?: boolean;
|
||||
portable?: boolean;
|
||||
mobile?: boolean;
|
||||
itch?: boolean;
|
||||
local?: boolean;
|
||||
}
|
||||
export async function buildHTML(game: string, { production = false, portable = false, mobile = false, itch = false }: Args = {}) {
|
||||
const html = await Bun.file(path.resolve(import.meta.dir, 'assets', itch ? 'index-itch.html' : 'index.html')).text();
|
||||
export async function buildHTML(game: string, { production = false, mobile = false, local = false }: Args = {}) {
|
||||
const assetsDir = path.resolve(import.meta.dir, 'assets');
|
||||
const srcDir = path.resolve(import.meta.dir, '..', 'src');
|
||||
const gameDir = path.resolve(srcDir, 'games', game);
|
||||
const gameAssetsDir = path.resolve(gameDir, 'assets');
|
||||
|
||||
const html = await Bun.file(path.resolve(assetsDir, local ? 'index-local.html' : 'index.html')).text();
|
||||
const bundle = await Bun.build({
|
||||
outdir: '/tmp',
|
||||
entrypoints: [path.resolve(import.meta.dir, '..', 'src', 'index.ts')],
|
||||
entrypoints: [path.resolve(srcDir, 'index.ts')],
|
||||
sourcemap: production ? 'none' : 'inline',
|
||||
define: {
|
||||
global: 'window',
|
||||
|
|
@ -31,7 +42,7 @@ export async function buildHTML(game: string, { production = false, portable = f
|
|||
imagePlugin,
|
||||
audioPlugin,
|
||||
fontPlugin,
|
||||
wasmPlugin({ production, portable }),
|
||||
wasmPlugin({ production }),
|
||||
filePlugin,
|
||||
]
|
||||
});
|
||||
|
|
@ -42,24 +53,24 @@ export async function buildHTML(game: string, { production = false, portable = f
|
|||
console.error('No entry point found:', bundle.outputs);
|
||||
return;
|
||||
}
|
||||
const iconFile = Bun.file(path.resolve(import.meta.dir, '..', 'src', 'games', game, 'assets', 'favicon.ico'));
|
||||
const iconFile = Bun.file(path.resolve(gameAssetsDir, 'favicon.ico'));
|
||||
let icon = '';
|
||||
if (await iconFile.exists()) {
|
||||
icon = `<link rel="icon" href="data:;base64,${Buffer.from(await iconFile.arrayBuffer()).toString('base64')}" />`;
|
||||
icon = `<link rel="icon" href="data:;base64,${await b64(iconFile)}" />`;
|
||||
}
|
||||
let style = '';
|
||||
let styleFile = bundle.outputs.find(a => a.kind === 'asset' && a.path.endsWith('.css'));
|
||||
if (styleFile) {
|
||||
style = await styleFile.text();
|
||||
let styleFiles = bundle.outputs.filter(a => a.kind === 'asset' && a.path.endsWith('.css'));
|
||||
for (let styleFile of styleFiles) {
|
||||
style += await styleFile.text();
|
||||
}
|
||||
const title = game[0].toUpperCase() + game.slice(1).toLowerCase();
|
||||
let pwaIconFile = Bun.file(path.resolve(import.meta.dir, '..', 'src', 'games', game, 'assets', 'pwa_icon.png'));
|
||||
let pwaIconFile = Bun.file(path.resolve(gameAssetsDir, 'pwa_icon.png'));
|
||||
if (!await pwaIconFile.exists()) {
|
||||
pwaIconFile = Bun.file(path.resolve(import.meta.dir, 'assets', 'pwa_icon.png'));
|
||||
pwaIconFile = Bun.file(path.resolve(assetsDir, 'pwa_icon.png'));
|
||||
}
|
||||
let manifest = '';
|
||||
if (production) {
|
||||
const pwaIcon = `data:;base64,${Buffer.from(await pwaIconFile.arrayBuffer()).toString('base64')}`;
|
||||
if (production && !local) {
|
||||
const pwaIcon = `data:;base64,${await b64(pwaIconFile)}`;
|
||||
const publishURL = process.env.PUBLISH_URL ? `${process.env.PUBLISH_URL}${game}` : '.';
|
||||
const manifestJSON = JSON.stringify({
|
||||
name: title,
|
||||
|
|
@ -76,7 +87,7 @@ export async function buildHTML(game: string, { production = false, portable = f
|
|||
type: 'image/png'
|
||||
}]
|
||||
});
|
||||
manifest = `<link rel="manifest" href="data:;base64,${Buffer.from(manifestJSON).toString('base64')}" />`;
|
||||
manifest = `<link rel="manifest" href="data:;base64,${await b64(manifestJSON)}" />`;
|
||||
}
|
||||
let script = await scriptFile.text();
|
||||
const inits = new Set<string>();
|
||||
|
|
@ -106,8 +117,10 @@ export async function buildHTML(game: string, { production = false, portable = f
|
|||
scriptPrefix = `<script>${eruda};\neruda.init();</script>`;
|
||||
}
|
||||
|
||||
// function to avoid $& being replaced
|
||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace#specifying_a_string_as_the_replacement
|
||||
const resultHTML = html
|
||||
.replace('<!--$SCRIPT$-->', () => `${scriptPrefix}<script type="module">${script}</script>`) // to avoid $& being replaced
|
||||
.replace('<!--$SCRIPT$-->', () => `${scriptPrefix}<script type="module">${script}</script>`)
|
||||
.replace('<!--$TITLE$-->', () => title)
|
||||
.replace('<!--$ICON$-->', () => icon)
|
||||
.replace('<!--$MANIFEST$-->', () => manifest)
|
||||
|
|
|
|||
|
|
@ -21,8 +21,8 @@ Bun.serve({
|
|||
game,
|
||||
{
|
||||
production: url.searchParams.get('production') === 'true', // to debug production builds
|
||||
portable: url.searchParams.get('portable') === 'true', // to skip AssemblyScript compilation,
|
||||
mobile: detectedBrowser.mobile || url.searchParams.get('mobile') === 'true',
|
||||
local: true,
|
||||
}
|
||||
);
|
||||
if (html) {
|
||||
|
|
|
|||
|
|
@ -151,7 +151,6 @@ const wasmPlugin = ({ production }: WasmLoaderConfig = {}): BunPlugin => {
|
|||
'nontrapping-fptoint',
|
||||
'reference-types',
|
||||
'multivalue',
|
||||
|
||||
].map(f => `-m${f}`);
|
||||
|
||||
const flags = [
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
export const formatError = (error: unknown, message: string = ''): string => {
|
||||
const prefix = message ? `${message}: ` : '';
|
||||
const suffix = (error && typeof error === 'object' ) ? (('stack' in error) ? `\n${error.stack}` : '') : '';
|
||||
|
||||
const errorMessage = formatErrorMessage(error).trim();
|
||||
|
||||
return `${prefix}${errorMessage}${suffix}`;
|
||||
}
|
||||
|
||||
export const formatErrorMessage = (error: unknown): string => {
|
||||
if (error && typeof error === 'object' && 'message' in error) {
|
||||
return `${error.message}`;
|
||||
} else if (error) {
|
||||
return error.toString();
|
||||
} else {
|
||||
return 'Unknown error';
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,9 @@
|
|||
import { formatError, formatErrorMessage } from "./errors";
|
||||
import Input from "./input";
|
||||
import { nextFrame } from "./utils";
|
||||
|
||||
type Setup<T extends Record<string, unknown> | void> = () => Promise<T> | T;
|
||||
type Frame<T extends Record<string, unknown> | void> = (dt: number, state: T) => Promise<void> | void;
|
||||
type Frame<T extends Record<string, unknown> | void> = (dt: number, state: T) => Promise<T | void> | T | void;
|
||||
type GameMain = () => void;
|
||||
|
||||
export function gameLoop<T extends Record<string, unknown> | void>(frame: Frame<T>): GameMain;
|
||||
|
|
@ -10,24 +11,39 @@ export function gameLoop<T extends Record<string, unknown> | void>(setup: Setup<
|
|||
export function gameLoop<T extends Record<string, unknown> | void>(setupOrFrame: Setup<T> | Frame<T>, frame?: Frame<T>): GameMain {
|
||||
return async () => {
|
||||
let state: T;
|
||||
if (frame) {
|
||||
state = await (setupOrFrame as Setup<T>)();
|
||||
} else {
|
||||
frame = setupOrFrame as Frame<T>;
|
||||
state = {} as T;
|
||||
try {
|
||||
if (frame) {
|
||||
state = await (setupOrFrame as Setup<T>)();
|
||||
} else {
|
||||
frame = setupOrFrame as Frame<T>;
|
||||
state = {} as T;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(formatError(e, 'Error in game setup'));
|
||||
alert(formatErrorMessage(e));
|
||||
return;
|
||||
}
|
||||
|
||||
let prevFrame = performance.now();
|
||||
while (true) {
|
||||
await nextFrame();
|
||||
Input.updateKeys();
|
||||
try {
|
||||
let prevFrame = performance.now();
|
||||
while (true) {
|
||||
await nextFrame();
|
||||
Input.updateKeys();
|
||||
|
||||
const now = performance.now();
|
||||
const dt = (now - prevFrame) / 1000;
|
||||
const now = performance.now();
|
||||
const dt = (now - prevFrame) / 1000;
|
||||
|
||||
await frame(dt, state);
|
||||
const newState = await frame(dt, state);
|
||||
if (newState) {
|
||||
state = newState;
|
||||
}
|
||||
|
||||
prevFrame = performance.now();
|
||||
prevFrame = performance.now();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(formatError(e, 'Error in game loop'));
|
||||
alert(formatErrorMessage(e));
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
export const nextFrame = async (): Promise<number> => new Promise((resolve) => requestAnimationFrame(resolve));
|
||||
export const delay = async (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
|
||||
|
||||
export const randInt = (min: number, max: number) => Math.round(min + (max - min - 1) * Math.random());
|
||||
export const randBool = () => Math.random() < 0.5;
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 158 B After Width: | Height: | Size: 158 B |
|
|
@ -207,7 +207,6 @@ async function loop() {
|
|||
}
|
||||
rowsToClear.clear();
|
||||
colsToClear.clear();
|
||||
|
||||
}
|
||||
|
||||
display.clear();
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
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);
|
||||
Loading…
Reference in New Issue