1
0
Fork 0
This commit is contained in:
Pabloader 2025-08-25 12:25:05 +00:00
parent 043378cfbb
commit 31a3ee213e
16 changed files with 131 additions and 58 deletions

View File

@ -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
}

View File

@ -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);

View File

@ -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) {

View File

@ -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)

View File

@ -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) {

View File

@ -151,7 +151,6 @@ const wasmPlugin = ({ production }: WasmLoaderConfig = {}): BunPlugin => {
'nontrapping-fptoint',
'reference-types',
'multivalue',
].map(f => `-m${f}`);
const flags = [

18
src/common/errors.ts Normal file
View File

@ -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';
}
}

View File

@ -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;
}
}
};

View File

@ -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;

View File

Before

Width:  |  Height:  |  Size: 158 B

After

Width:  |  Height:  |  Size: 158 B

View File

@ -207,7 +207,6 @@ async function loop() {
}
rowsToClear.clear();
colsToClear.clear();
}
display.clear();

View File

@ -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);