add runtime type-checking

This commit is contained in:
Reimar 2025-10-16 11:03:41 +02:00
parent d2a746cbe4
commit 43c8c0e535
4 changed files with 420 additions and 281 deletions

View File

@ -1,3 +1,5 @@
import { GamelibAdapter } from "./gamelib_adapter.js";
export class Gamelib { export class Gamelib {
constructor(console, assetProvider, canvasElement) { constructor(console, assetProvider, canvasElement) {
this.console = console; this.console = console;
@ -274,280 +276,3 @@ export class Gamelib {
return new GamelibAdapter(this); return new GamelibAdapter(this);
} }
} }
/**
* A function that is called whenever the Key it is registered to is pressed or released.
* @callback OnKeyEventHandler
*/
/**
* A function that is called whenever the MouseButton it is registered to is pressed or released.
* @callback OnClickEventHandler
*/
/**
* A function that is called whenever the mouse is moved.
* @callback OnMouseMoveEventHandler
* @param {number} positionX The mouse x position
* @param {number} positionY The mouse y position
* @param {number} deltaX The difference in x position between this and the last onMouseMove event
* @param {number} deltaY The difference in y position between this and the last onMouseMove event
*/
/**
* A loaded sprite
* @typedef {object} Sprite
*/
/**
* A html color
* @typedef {string} Color
*/
/**
* A component of `[x, y]` of a path.
* @typedef {[number, number]} PathComponent
*/
/**
* Text style
* @typedef TextStyle
* @type {object}
* @property {number} fontWeight Font weight
* @property {number} fontStyle !!!!!!!!!!!!!!!!!!!!! no idea what this does, needs better docs
* @property {number} fontSize Font size in pixels
* @property {string} fontFamily Font family
* @property {"left"|"center"|"right"} align Text alignment
* @property {string} baseline !!!!!!!!!!!!!!!!!!!!! no idea what this does, needs better docs
* @property {string} direction !!!!!!!!!!!!!!!!!!!!! no idea what this does, needs better docs
* @property {Color} color
*/
export class GamelibAdapter {
/* Enum for MouseButton values
* @enum {number}
*/
MouseButton = {
Left: 0,
Right: 1,
Middle: 2,
};
constructor(gamelib) {
this.gamelib = gamelib;
}
get width() {
return this.gamelib.width;
}
get height() {
return this.gamelib.height;
}
get mouseX() {
return this.gamelib.mouseX;
}
get mouseY() {
return this.gamelib.mouseY;
}
println(msg) {
this.gamelib.console.log(msg);
}
startGameLoop(loopFunction) {
this.gamelib.startGameLoop(loopFunction);
}
/**
* Returns whether or not `key` is currently pressed.
* `Key` can any one of [KeyBoardEvent.key](https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values)'s values
* @param {Key} key
* @return {boolean} Whether `key` is pressed.
*/
isPressed(key) {
return this.gamelib.keysPressed.has(key);
}
/**
* Registers a `handlerFunction` that is called when `key` is pressed.
* Key can be any of [KeyBoardEvent.key](https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values)'s values
* @param {Key} key
* @param {OnKeyEventHandler} handlerFunction
* @return {void}
*/
onPress(key, handlerFunction) {
this.gamelib.keyPressHandlers.set(key, handlerFunction);
}
/**
* Registers a `handlerFunction` that is called when `key` is pressed.
* Key can be any of [KeyBoardEvent.key](https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values)'s values
* @param {Key} key
* @param {OnKeyEventHandler} handlerFunction
*/
onRelease(key, handlerFunction) {
this.gamelib.keyReleaseHandlers.set(key, handlerFunction);
}
/**
* Registers a `handlerFunction` that is called when mouse is moved.
* @param {OnMouseMoveEventHandler} handlerFunction
*/
onMouseMove(handlerFunction) {
this.gamelib.mouseMoveHandler = handlerFunction;
}
/**
* Returns whether or not `button` is currently clicked.
* MouseButton is accessed from `lib.MouseButton`
* @param {GamelibAdapter.MouseButton} button defaults to {GamelibAdapter.MouseButton.Left}
* @return {boolean} Whether `key` is pressed.
*/
isClicking(button = this.MouseButton.Left) {
return this.gamelib.mouseButtonsPressed.has(button);
}
/**
* Registers a `handlerFunction` that is called when `button` is pressed.
* MouseButton is accessed from `lib.MouseButton`
* @param {OnClickEventHandler} handlerFunction
* @param {GamelibAdapter.MouseButton} button defaults to {GamelibAdapter.MouseButton.Left}
*/
onClick(handlerFunction, button = this.MouseButton.Left) {
this.gamelib.mouseDownHandlers.set(button, handlerFunction);
}
/**
* Registers a `handlerFunction` that is called when `button` is released.
* MouseButton is accessed from `lib.MouseButton`
* @param {OnClickEventHandler} handlerFunction
* @param {GamelibAdapter.MouseButton} button defaults to {GamelibAdapter.MouseButton.Left}
*/
onClickRelease(handlerFunction, button = this.MouseButton.Left) {
this.gamelib.mouseUpHandlers.set(button, handlerFunction);
}
/**
* Loads a sprite with `name`, rendered with specified `width` and `height`
*
* @param {string} name The sprite name
* @param {number} width
* @param {number} height
* @return {Promise<Sprite>} The loaded sprite
*/
async loadSprite(name, width, height) {
return await this.gamelib.loadSprite(name, width, height);
}
/**
* Draws a sprite loaded by {GamelibAdapter.loadSprite}
*
* @param {number} x
* @param {number} y
* @param {Sprite} A loaded sprite
*/
drawSprite(x, y, sprite) {
this.gamelib.drawSprite(x, y, sprite);
}
/**
* Draws a rotated sprite loaded by {GamelibAdapter.loadSprite}
*
* @param {number} x
* @param {number} y
* @param {Sprite} sprite A loaded sprite
* @param {number} angle An angle in radians
*/
drawSpriteRotated(x, y, sprite, angle) {
this.gamelib.drawSpriteRotated(x, y, sprite, angle);
}
/**
* Generates an rgb Color.
*
* @param {number} red
* @param {number} green
* @param {number} blue
* @return {Color} Formatted {Color} string
*/
rgb(red, green, blue) {
return `rgb(${red}, ${green}, ${blue})`;
}
/**
* Clears the screen with `color`
* @param {Color} color
*/
clear(color) {
this.gamelib.clear(color);
}
/**
* Draws a rect at `(x,y)` with a size of `(width,height)` in `color`
* @param {number} x
* @param {number} y
* @param {number} width
* @param {number} height
* @param {Color} color
*/
drawRect(x, y, width, height, color) {
this.gamelib.drawRect(x, y, width, height, color);
}
/**
* Draws a circle at `(x,y)` with a radius of `r` in `color`
* @param {number} x
* @param {number} y
* @param {number} r Radius of circle
* @param {Color} color
*/
drawCircle(x, y, r, color) {
this.gamelib.drawCircle(x, y, r, color);
}
/**
* Draws a line from `(x0,y0)` to `(x1,y1)` with a thickness of `thickness` in `color`
* @param {number} x0
* @param {number} y0
* @param {number} x1
* @param {number} y1
* @param {number} thickness
* @param {Color} color
*/
drawLine(x0, y0, x1, y1, thickness, color) {
this.gamelib.drawLine(x0, y0, x1, y1, thickness, color);
}
/**
* Draws a polygon in `color` based off of `path` components
* @param {PathComponent[]} path
* @param {Color} color
*/
drawPath(path, color) {
this.gamelib.drawPath(path, color);
}
/**
* Draws an outline in `color` based off of `path` components, with a thickness of `thickness`
* @param {PathComponent[]} path
* @param {number} thickness
* @param {Color} color
*/
drawPathLine(path, thickness, color) {
this.gamelib.drawPathLine(path, thickness, color);
}
/**
* Draw text
* @param {number} x
* @param {number} y
* @param {string} text
* @param {TextStyle} style
*/
drawText(x, y, text, style = {}) {
this.gamelib.drawText(x, y, text, style);
}
}

View File

@ -0,0 +1,402 @@
/**
* A function that is called whenever the Key it is registered to is pressed or released.
* @callback OnKeyEventHandler
*/
/**
* A function that is called whenever the MouseButton it is registered to is pressed or released.
* @callback OnClickEventHandler
*/
/**
* A function that is called whenever the mouse is moved.
* @callback OnMouseMoveEventHandler
* @param {number} positionX The mouse x position
* @param {number} positionY The mouse y position
* @param {number} deltaX The difference in x position between this and the last onMouseMove event
* @param {number} deltaY The difference in y position between this and the last onMouseMove event
*/
/**
* A loaded sprite
* @typedef {object} Sprite
*/
/**
* A html color
* @typedef {string} Color
*/
/**
* A component of `[x, y]` of a path.
* @typedef {[number, number]} PathComponent
*/
/**
* A keyboard key
* @typedef {string} Key
*/
/**
* Text style
* @typedef TextStyle
* @type {object}
* @property {number|string} [fontWeight="normal"] Font weight as a number or a string, e.g. 400 or "bold"
* @property {"normal"|"italic"} [fontStyle="normal"] Style to apply to the font, e.g. "italic"
* @property {number} [fontSize=16] Font size in pixels
* @property {string} [fontFamily="inherit"] Font family, e.g. "sans-serif" or "serif"
* @property {"left"|"center"|"right"} [align="left"] Horizontal text alignment
* @property {"top"|"hanging"|"middle"|"alphabetic"|"ideographic"|"bottom"} [baseline="alphabetic"] Vertical text alignment
* @property {"ltr"|"rtl"} [direction="ltr"] Whether the text goes left-to-right or right-to-left
* @property {Color} [color="white"] Text color
*/
export class GamelibAdapter {
/* Enum for MouseButton values
* @enum {number}
*/
MouseButton = {
Left: 0,
Right: 1,
Middle: 2,
};
constructor(gamelib) {
this.gamelib = gamelib;
}
#error(message, func) {
const error = new Error(`${func.name}: ${message}`);
Error.captureStackTrace(error, func);
this.gamelib.console.error(error);
throw error;
}
#checkParams(func, args, expectedTypes) {
console.log(func, args, expectedTypes);
if (args.length > expectedTypes.length) {
this.#error(
`Too many arguments to function. Expected ${expectedTypes.length}, got ${args.length}`,
func,
);
}
const typeAliases = {
Sprite: OffscreenCanvas,
MouseButton: "number",
};
for (let i = 0; i < args.length; i++) {
const expectedType = typeAliases[expectedTypes[i]] ?? expectedTypes[i];
if (typeof expectedType === "string" && typeof args[i] !== expectedType) {
this.#error(
`Expected parameter #${
i + 1
} to be of type ${expectedType}, got ${typeof args[i]}`,
func,
);
}
if (expectedType instanceof Object && !(args[i] instanceof expectedType)) {
const expectedTypeName = expectedType.prototype.constructor.name;
this.#error(
`Expected parameter #${
i + 1
} to be of type ${expectedTypeName}, got ${typeof args[i]}`,
func,
);
}
}
}
get width() {
return this.gamelib.width;
}
get height() {
return this.gamelib.height;
}
get mouseX() {
return this.gamelib.mouseX;
}
get mouseY() {
return this.gamelib.mouseY;
}
/**
* Prints a line to the console
* @param {any} msg The message or value to print
* @return {void}
*/
println(msg) {
this.gamelib.console.log(msg);
}
startGameLoop(loopFunction) {
this.#checkParams(this.startGameLoop, [loopFunction], [Function]);
this.gamelib.startGameLoop(loopFunction);
}
/**
* Returns whether or not `key` is currently pressed.
* `Key` can any one of [KeyBoardEvent.key](https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values)'s values
* @param {Key} key
* @return {boolean} Whether `key` is pressed.
*/
isPressed(key) {
this.#checkParams(this.isPressed, [key], ["string"]);
return this.gamelib.keysPressed.has(key);
}
/**
* Registers a `handlerFunction` that is called when `key` is pressed.
* Key can be any of [KeyBoardEvent.key](https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values)'s values
* @param {Key} key
* @param {OnKeyEventHandler} handlerFunction
* @return {void}
*/
onPress(key, handlerFunction) {
this.#checkParams(this.onPress, [key, handlerFunction], ["string", Function]);
this.gamelib.keyPressHandlers.set(key, handlerFunction);
}
/**
* Registers a `handlerFunction` that is called when `key` is pressed.
* Key can be any of [KeyBoardEvent.key](https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values)'s values
* @param {Key} key
* @param {OnKeyEventHandler} handlerFunction
*/
onRelease(key, handlerFunction) {
this.#checkParams(this.onRelease, [key, handlerFunction], ["string", Function]);
this.gamelib.keyReleaseHandlers.set(key, handlerFunction);
}
/**
* Registers a `handlerFunction` that is called when mouse is moved.
* @param {OnMouseMoveEventHandler} handlerFunction
*/
onMouseMove(handlerFunction) {
this.#checkParams(this.onMouseMove, [handlerFunction], [Function]);
this.gamelib.mouseMoveHandler = handlerFunction;
}
/**
* Returns whether or not `button` is currently clicked.
* MouseButton is accessed from `lib.MouseButton`
* @param {GamelibAdapter.MouseButton} button defaults to {GamelibAdapter.MouseButton.Left}
* @return {boolean} Whether `key` is pressed.
*/
isClicking(button = this.MouseButton.Left) {
this.#checkParams(this.isClicking, [button], ["MouseButton"]);
return this.gamelib.mouseButtonsPressed.has(button);
}
/**
* Registers a `handlerFunction` that is called when `button` is pressed.
* MouseButton is accessed from `lib.MouseButton`
* @param {OnClickEventHandler} handlerFunction
* @param {GamelibAdapter.MouseButton} button defaults to {GamelibAdapter.MouseButton.Left}
*/
onClick(handlerFunction, button = this.MouseButton.Left) {
this.#checkParams(this.onClick, [handlerFunction, button], [Function, "MouseButton"]);
this.gamelib.mouseDownHandlers.set(button, handlerFunction);
}
/**
* Registers a `handlerFunction` that is called when `button` is released.
* MouseButton is accessed from `lib.MouseButton`
* @param {OnClickEventHandler} handlerFunction
* @param {GamelibAdapter.MouseButton} button defaults to {GamelibAdapter.MouseButton.Left}
*/
onClickRelease(handlerFunction, button = this.MouseButton.Left) {
this.#checkParams(this.onClickRelease, [handlerFunction, button], [
Function,
"MouseButton",
]);
this.gamelib.mouseUpHandlers.set(button, handlerFunction);
}
/**
* Loads a sprite with `name`, rendered with specified `width` and `height`
*
* @param {string} name The sprite name
* @param {number} width
* @param {number} height
* @return {Promise<Sprite>} The loaded sprite
*/
async loadSprite(name, width, height) {
this.#checkParams(this.loadSprite, [name, width, height], ["string", "number", "number"]);
return await this.gamelib.loadSprite(name, width, height);
}
/**
* Draws a sprite loaded by {GamelibAdapter.loadSprite}
*
* @param {number} x
* @param {number} y
* @param {Sprite} sprite A loaded sprite
*/
drawSprite(x, y, sprite) {
this.#checkParams(this.drawSprite, [x, y, sprite], ["number", "number", "Sprite"]);
this.gamelib.drawSprite(x, y, sprite);
}
/**
* Draws a rotated sprite loaded by {GamelibAdapter.loadSprite}
*
* @param {number} x
* @param {number} y
* @param {Sprite} sprite A loaded sprite
* @param {number} angle An angle in radians
*/
drawSpriteRotated(x, y, sprite, angle) {
this.#checkParams(this.drawSpriteRotated, [x, y, sprite, angle], [
"number",
"number",
"Sprite",
"number",
]);
this.gamelib.drawSpriteRotated(x, y, sprite, angle);
}
/**
* Generates an rgb Color.
*
* @param {number} red
* @param {number} green
* @param {number} blue
* @return {Color} Formatted {Color} string
*/
rgb(red, green, blue) {
this.#checkParams(this.rgb, [red, green, blue], ["number", "number", "number"]);
return `rgb(${red}, ${green}, ${blue})`;
}
/**
* Clears the screen with `color`
* @param {Color} color
*/
clear(color) {
this.#checkParams(this.clear, [color], ["string"]);
this.gamelib.clear(color);
}
/**
* Draws a rect at `(x,y)` with a size of `(width,height)` in `color`
* @param {number} x
* @param {number} y
* @param {number} width
* @param {number} height
* @param {Color} color
*/
drawRect(x, y, width, height, color) {
this.#checkParams(this.drawRect, [x, y, width, height, color], [
"number",
"number",
"number",
"number",
"string",
]);
this.gamelib.drawRect(x, y, width, height, color);
}
/**
* Draws a circle at `(x,y)` with a radius of `r` in `color`
* @param {number} x
* @param {number} y
* @param {number} r Radius of circle
* @param {Color} color
*/
drawCircle(x, y, r, color) {
this.#checkParams(this.drawCircle, [x, y, r, color], [
"number",
"number",
"number",
"string",
]);
this.gamelib.drawCircle(x, y, r, color);
}
/**
* Draws a line from `(x0,y0)` to `(x1,y1)` with a thickness of `thickness` in `color`
* @param {number} x0
* @param {number} y0
* @param {number} x1
* @param {number} y1
* @param {number} thickness
* @param {Color} color
*/
drawLine(x0, y0, x1, y1, thickness, color) {
this.#checkParams(this.drawLine, [x0, y0, x1, y1, thickness, color], [
"number",
"number",
"number",
"number",
"number",
"string",
]);
this.gamelib.drawLine(x0, y0, x1, y1, thickness, color);
}
/**
* Draws a polygon in `color` based off of `path` components
* @param {PathComponent[]} path
* @param {Color} color
*/
drawPath(path, color) {
this.#checkParams(this.drawPath, [path, color], [Array, "string"]);
this.gamelib.drawPath(path, color);
}
/**
* Draws an outline in `color` based off of `path` components, with a thickness of `thickness`
* @param {PathComponent[]} path
* @param {number} thickness
* @param {Color} color
*/
drawPathLine(path, thickness, color) {
this.#checkParams(this.drawPathLine, [path, thickness, color], [Array, "number", "string"]);
this.gamelib.drawPathLine(path, thickness, color);
}
/**
* Draw text
* @param {number} x
* @param {number} y
* @param {string} text
* @param {TextStyle} style
*/
drawText(x, y, text, style = {}) {
this.#checkParams(this.drawText, [x, y, text, style], [
"number",
"number",
"string",
Object,
]);
this.gamelib.drawText(x, y, text, style);
}
}

View File

@ -30,13 +30,13 @@ const languageProvider = LanguageProvider.fromCdn(
languageProvider.registerEditor(editor); languageProvider.registerEditor(editor);
languageProvider.setGlobalOptions("typescript", { languageProvider.setGlobalOptions("typescript", {
extraLibs: { extraLibs: {
"gamelib.js": { "gamelib_adapter.js": {
content: await (await fetch("/src/gamelib.js")).text(), content: await (await fetch("/src/gamelib_adapter.js")).text(),
version: 1, version: 1,
}, },
"karlkoder.js": { "karlkoder.js": {
content: ` content: `
import { GamelibAdapter } from "./gamelib.js"; import { GamelibAdapter } from "./gamelib_adapter.js";
declare global { declare global {
const karlkoder: { lib: () => GamelibAdapter } const karlkoder: { lib: () => GamelibAdapter }
} }

View File

@ -64,7 +64,13 @@ export class PlaygroundConsole {
details.appendChild(summary); details.appendChild(summary);
if (arg instanceof Error) { if (arg instanceof Error) {
// Add error stack trace // On Chrome, the first line of the stack trace is the error message repeated
if (globalThis.chrome) {
const trace = arg.stack.split("\n");
trace.shift();
arg.stack = trace.join("\n");
}
const el = document.createElement("p"); const el = document.createElement("p");
el.innerHTML = this.formatStacktrace(arg.stack); el.innerHTML = this.formatStacktrace(arg.stack);
details.appendChild(el); details.appendChild(el);
@ -126,26 +132,32 @@ class PlaygroundConsoleAdapter {
} }
log() { log() {
console.log(...arguments);
this.#console.addTopLevelEntry("log", ...arguments); this.#console.addTopLevelEntry("log", ...arguments);
} }
dir() { dir() {
console.dir(...arguments);
this.#console.addTopLevelEntry("dir", ...arguments); this.#console.addTopLevelEntry("dir", ...arguments);
} }
debug() { debug() {
console.debug(...arguments);
this.#console.addTopLevelEntry("debug", ...arguments); this.#console.addTopLevelEntry("debug", ...arguments);
} }
info() { info() {
console.info(...arguments);
this.#console.addTopLevelEntry("info", ...arguments); this.#console.addTopLevelEntry("info", ...arguments);
} }
warn() { warn() {
console.warn(...arguments);
this.#console.addTopLevelEntry("warn", ...arguments); this.#console.addTopLevelEntry("warn", ...arguments);
} }
error() { error() {
console.error(...arguments);
this.#console.addTopLevelEntry("error", ...arguments); this.#console.addTopLevelEntry("error", ...arguments);
} }