diff --git a/assets/testentity.js b/assets/testentity.js
index 8d3623bb..5a1ef6ea 100644
--- a/assets/testentity.js
+++ b/assets/testentity.js
@@ -1,8 +1,26 @@
+// Load rosa.
+Console.print('Asset time');
+const entry = Asset.lock('rosa.png', Asset.TYPE_TEXTURE, Texture.FORMAT_RGBA);
+Asset.requireLoaded(entry);
+Console.print('Asset loaded');
+
+// Camera at (3,3,3) looking at origin
const cam = Entity.create();
-const pos = cam.add(Component.POSITION);
+const camPos = cam.add(Component.POSITION);
cam.add(Component.CAMERA);
+camPos.localPosition = new Vec3(3, 3, 3);
+camPos.lookAt(new Vec3(0, 0, 0));
-pos.localPosition = new Vec3(3, 3, 3);
-pos.lookAt(new Vec3(0, 0, 0));
+// Test entity at origin
+const testEntity = Entity.create();
+const testPos = testEntity.add(Component.POSITION);
-Console.print('Camera entity ID: ' + cam.toString());
+/** @type {RenderableSpritebatch} */
+const testRenderable = testEntity.add(Component.RENDERABLE);
+testRenderable.type = Renderable.SPRITEBATCH;
+testRenderable.setTexture(entry.texture);
+testRenderable.addSprite(
+ 0, 0, 1, 1,
+ 0, 0, 1, 1
+);
+testPos.localPosition = new Vec3(0, 0, 0);
diff --git a/src/dusk/asset/asset.c b/src/dusk/asset/asset.c
index 791d016c..424495b8 100644
--- a/src/dusk/asset/asset.c
+++ b/src/dusk/asset/asset.c
@@ -197,8 +197,12 @@ errorret_t assetUpdate(void) {
switch(loading->entry->state) {
// This thing is pending synchronous loading.
case ASSET_ENTRY_STATE_PENDING_SYNC:
- // Perform sync load.
loading->entry->state = ASSET_ENTRY_STATE_LOADING_SYNC;
+ // Unlock before calling loadSync. The sync loader may re-enter
+ // assetUpdate (e.g. a script loading another asset), and the async
+ // thread never touches LOADING_SYNC entries, so this is safe.
+ threadMutexUnlock(&loading->mutex);
+
errorret_t ret = (
ASSET_LOADER_CALLBACKS[loading->type].loadSync(loading)
);
@@ -212,8 +216,6 @@ errorret_t assetUpdate(void) {
"Loader did not set entry state to loaded or error on finished load."
);
- // If an error occured these things need to be true, basically just
- // ensuring the sync loader is setting the error correctly.
if(errorIsNotOk(ret)) {
errorCatch(errorPrint(ret));
assertTrue(
@@ -222,15 +224,15 @@ errorret_t assetUpdate(void) {
);
}
- threadMutexUnlock(&loading->mutex);
loading++;
break;
case ASSET_ENTRY_STATE_LOADING_SYNC:
- assertUnreachable(
- "Entry is in a pending sync state still?"
- );
- break;
+ // A re-entrant assetUpdate call (e.g. from a script loading another
+ // asset) will see this entry mid-sync-load. Skip it.
+ threadMutexUnlock(&loading->mutex);
+ loading++;
+ continue;
// Done loading, we can just free it up.
case ASSET_ENTRY_STATE_LOADED:
diff --git a/src/dusk/asset/loader/assetentry.c b/src/dusk/asset/loader/assetentry.c
index c8a815b7..7ceb7b1f 100644
--- a/src/dusk/asset/loader/assetentry.c
+++ b/src/dusk/asset/loader/assetentry.c
@@ -25,8 +25,11 @@ void assetEntryInit(
memoryZero(entry, sizeof(assetentry_t));
stringCopy(entry->name, name, ASSET_FILE_NAME_MAX);
entry->type = type;
- entry->input = input;
entry->state = ASSET_ENTRY_STATE_NOT_STARTED;
+ if(input) {
+ entry->inputData = *input;
+ entry->input = &entry->inputData;
+ }
refInit(&entry->refs, entry, NULL, NULL, NULL);
}
diff --git a/src/dusk/asset/loader/assetentry.h b/src/dusk/asset/loader/assetentry.h
index ab8c38ca..176379f2 100644
--- a/src/dusk/asset/loader/assetentry.h
+++ b/src/dusk/asset/loader/assetentry.h
@@ -35,7 +35,9 @@ typedef struct assetentry_s {
// zero). Entries that nobody has ever locked are left alone so raw-pointer
// callers (tests, requireLoaded before locking) are not surprised.
bool_t wasLocked;
- // Data that will be passed to the loader about how it should load.
+ // Owned copy of the loader input. input points here when non-NULL.
+ assetloaderinput_t inputData;
+ // Pointer to inputData, or NULL if no input was provided.
assetloaderinput_t *input;
} assetentry_t;
diff --git a/src/dusk/entity/component/display/entityrenderable.c b/src/dusk/entity/component/display/entityrenderable.c
index 74de17af..a30fa1be 100644
--- a/src/dusk/entity/component/display/entityrenderable.c
+++ b/src/dusk/entity/component/display/entityrenderable.c
@@ -36,7 +36,6 @@ void entityRenderableDispose(
const entityid_t entityId,
const componentid_t componentId
) {
-
}
void entityRenderableSetType(
@@ -80,6 +79,49 @@ void entityRenderableSetDraw(
r->data.custom.drawUser = user;
}
+static errorret_t entityRenderableDrawSpritebatch(
+ const entityrenderablespritebatch_t *sb
+) {
+ if(sb->spriteCount == 0) errorOk();
+
+ errorChain(displaySetState((displaystate_t){
+ .flags = DISPLAY_STATE_FLAG_BLEND
+ }));
+
+ spriteBatchClear();
+ shadermaterial_t mat;
+ memoryZero(&mat, sizeof(shadermaterial_t));
+ mat.unlit.texture = sb->texture;
+ mat.unlit.color = COLOR_WHITE;
+ errorChain(spriteBatchBuffer(
+ sb->sprites, sb->spriteCount,
+ SHADER_LIST_DEFS[SHADER_LIST_SHADER_UNLIT].shader, mat
+ ));
+ return spriteBatchFlush();
+}
+
+static errorret_t entityRenderableDrawMaterial(
+ const entityrenderablematerial_t *m
+) {
+ errorChain(displaySetState(m->state));
+ shader_t *shader = SHADER_LIST_DEFS[m->shaderType].shader;
+ assertNotNull(shader, "Shader cannot be null for material type");
+ errorChain(shaderBind(shader));
+ errorChain(shaderSetMaterial(shader, &m->material));
+ for(uint8_t i = 0; i < m->meshCount; i++) {
+ errorChain(meshDraw(m->meshes[i], m->meshOffsets[i], m->meshCounts[i]));
+ }
+ errorOk();
+}
+
+static errorret_t entityRenderableDrawCustom(
+ const entityid_t entityId,
+ const componentid_t componentId,
+ const entityrenderablecustom_t *custom
+) {
+ return custom->draw(entityId, componentId, custom->drawUser);
+}
+
errorret_t entityRenderableDraw(
const entityid_t entityId,
const componentid_t componentId
@@ -87,41 +129,13 @@ errorret_t entityRenderableDraw(
entityrenderable_t *r = componentGetData(
entityId, componentId, COMPONENT_TYPE_RENDERABLE
);
-
switch(r->type) {
- case ENTITY_RENDERABLE_TYPE_SPRITEBATCH: {
- const entityrenderablespritebatch_t *sb = &r->data.spritebatch;
- errorChain(displaySetState((displaystate_t){
- .flags = DISPLAY_STATE_FLAG_BLEND
- }));
- spriteBatchClear();
- shadermaterial_t mat;
- memoryZero(&mat, sizeof(shadermaterial_t));
- mat.unlit.texture = sb->texture;
- mat.unlit.color = COLOR_WHITE;
- errorChain(spriteBatchBuffer(
- sb->sprites, sb->spriteCount,
- SHADER_LIST_DEFS[SHADER_LIST_SHADER_UNLIT].shader, mat
- ));
- return spriteBatchFlush();
- }
-
- case ENTITY_RENDERABLE_TYPE_SHADER_MATERIAL: {
- const entityrenderablematerial_t *m = &r->data.material;
- errorChain(displaySetState(m->state));
- shader_t *shader = SHADER_LIST_DEFS[m->shaderType].shader;
- assertNotNull(shader, "Shader cannot be null for material type");
- errorChain(shaderBind(shader));
- errorChain(shaderSetMaterial(shader, &m->material));
- for(uint8_t i = 0; i < m->meshCount; i++) {
- errorChain(meshDraw(m->meshes[i], m->meshOffsets[i], m->meshCounts[i]));
- }
- errorOk();
- }
-
+ case ENTITY_RENDERABLE_TYPE_SPRITEBATCH:
+ return entityRenderableDrawSpritebatch(&r->data.spritebatch);
+ case ENTITY_RENDERABLE_TYPE_SHADER_MATERIAL:
+ return entityRenderableDrawMaterial(&r->data.material);
case ENTITY_RENDERABLE_TYPE_CUSTOM:
- return r->data.custom.draw(entityId, componentId, r->data.custom.drawUser);
-
+ return entityRenderableDrawCustom(entityId, componentId, &r->data.custom);
default:
assertUnreachable("Invalid renderable type");
}
diff --git a/src/dusk/scene/initial/initialscene.c b/src/dusk/scene/initial/initialscene.c
index 7c70abdd..6fe4a39c 100644
--- a/src/dusk/scene/initial/initialscene.c
+++ b/src/dusk/scene/initial/initialscene.c
@@ -16,13 +16,6 @@
void initialSceneInit(void) {
consolePrint("Initial scene initialized");
-
- // Cube entity — RENDERABLE init defaults to a white unit cube at origin
- entityid_t cubeId = entityManagerAdd();
- SCENE.data.initial.cubeEntityId = cubeId;
- entityAddComponent(cubeId, COMPONENT_TYPE_POSITION);
- entityAddComponent(cubeId, COMPONENT_TYPE_RENDERABLE);
-
errorCatch(errorPrint(scriptExecFile("testentity.js")));
}
diff --git a/src/dusk/script/module/asset/moduleasset.h b/src/dusk/script/module/asset/moduleasset.h
new file mode 100644
index 00000000..d83ff21f
--- /dev/null
+++ b/src/dusk/script/module/asset/moduleasset.h
@@ -0,0 +1,138 @@
+/**
+ * Copyright (c) 2026 Dominic Masters
+ *
+ * This software is released under the MIT License.
+ * https://opensource.org/licenses/MIT
+ */
+
+#pragma once
+#include "script/module/modulebase.h"
+#include "script/scriptproto.h"
+#include "script/module/display/moduletexture.h"
+#include "script/module/asset/moduleassetentry.h"
+#include "asset/asset.h"
+#include "asset/loader/assetloader.h"
+
+static scriptproto_t MODULE_ASSET_PROTO;
+
+moduleBaseFunction(moduleAssetExists) {
+ moduleBaseRequireArgs(1);
+ moduleBaseRequireString(0);
+
+ char_t buf[256];
+ moduleBaseToString(args[0], buf, sizeof(buf));
+
+ return jerry_boolean(assetFileExists(buf));
+}
+
+moduleBaseFunction(moduleAssetLock) {
+ moduleBaseRequireArgs(2);
+ moduleBaseRequireString(0);
+ moduleBaseRequireNumber(1);
+
+ char_t buf[256];
+ moduleBaseToString(args[0], buf, sizeof(buf));
+ assetloadertype_t type = (assetloadertype_t)moduleBaseArgInt(1);
+
+ assetloaderinput_t input;
+ assetloaderinput_t *inputPtr = NULL;
+
+ if(argc >= 3 && jerry_value_is_number(args[2])) {
+ int32_t inputVal = moduleBaseArgInt(2);
+ switch(type) {
+ case ASSET_LOADER_TYPE_TEXTURE:
+ input.texture = (textureformat_t)inputVal;
+ inputPtr = &input;
+ break;
+
+ case ASSET_LOADER_TYPE_MESH:
+ input.mesh = (assetmeshinputaxis_t)inputVal;
+ inputPtr = &input;
+ break;
+
+ default:
+ break;
+ }
+ }
+
+ assetentry_t *entry = assetLock(buf, type, inputPtr);
+ if(!entry) return moduleBaseThrow("Asset.lock: failed to lock asset");
+ jsassetentry_t e = { .entry = entry };
+
+ return scriptProtoCreateValue(&MODULE_ASSET_ENTRY_PROTO, &e);
+}
+
+moduleBaseFunction(moduleAssetRequireLoaded) {
+ moduleBaseRequireArgs(1);
+
+ jsassetentry_t *e = (jsassetentry_t *)scriptProtoGetValue(
+ &MODULE_ASSET_ENTRY_PROTO, args[0]
+ );
+ if(!e || !e->entry) {
+ return moduleBaseThrow("Asset.requireLoaded: expected AssetEntry");
+ }
+ errorret_t err = assetRequireLoaded(e->entry);
+ if(errorIsNotOk(err)) return moduleBaseThrowError(err);
+ jerry_value_t ref = jerry_value_copy(args[0]);
+
+ return ref;
+}
+
+moduleBaseFunction(moduleAssetUnlock) {
+ moduleBaseRequireArgs(1);
+ moduleBaseRequireString(0);
+ char_t buf[256];
+ moduleBaseToString(args[0], buf, sizeof(buf));
+ assetUnlock(buf);
+ return jerry_undefined();
+}
+
+static void moduleAssetInit(void) {
+ moduleAssetEntryInit();
+ scriptProtoInit(&MODULE_ASSET_PROTO, "Asset", sizeof(uint8_t), NULL);
+ scriptProtoDefineStaticFunc(&MODULE_ASSET_PROTO, "exists", moduleAssetExists);
+ scriptProtoDefineStaticFunc(&MODULE_ASSET_PROTO, "lock", moduleAssetLock);
+ scriptProtoDefineStaticFunc(&MODULE_ASSET_PROTO, "unlock", moduleAssetUnlock);
+ scriptProtoDefineStaticFunc(&MODULE_ASSET_PROTO, "requireLoaded", moduleAssetRequireLoaded);
+
+ jerry_value_t global = MODULE_ASSET_PROTO.prototype;
+
+ /* Asset.TYPE_* loader type constants */
+ struct { const char_t *name; int val; } types[] = {
+ { "TYPE_MESH", ASSET_LOADER_TYPE_MESH },
+ { "TYPE_TEXTURE", ASSET_LOADER_TYPE_TEXTURE },
+ { "TYPE_TILESET", ASSET_LOADER_TYPE_TILESET },
+ { "TYPE_LOCALE", ASSET_LOADER_TYPE_LOCALE },
+ { "TYPE_JSON", ASSET_LOADER_TYPE_JSON },
+ { "TYPE_SCRIPT", ASSET_LOADER_TYPE_SCRIPT },
+ };
+ for(int i = 0; i < 6; i++) {
+ jerry_value_t k = jerry_string_sz(types[i].name);
+ jerry_value_t v = jerry_number((double)types[i].val);
+ jerry_object_set(global, k, v);
+ jerry_value_free(v);
+ jerry_value_free(k);
+ }
+
+ /* Asset.MESH_AXIS_* input constants for TYPE_MESH */
+ struct { const char_t *name; int val; } axes[] = {
+ { "MESH_AXIS_Y_UP", MESH_INPUT_AXIS_Y_UP },
+ { "MESH_AXIS_Z_UP", MESH_INPUT_AXIS_Z_UP },
+ { "MESH_AXIS_X_UP", MESH_INPUT_AXIS_X_UP },
+ { "MESH_AXIS_Y_DOWN", MESH_INPUT_AXIS_Y_DOWN },
+ { "MESH_AXIS_Z_DOWN", MESH_INPUT_AXIS_Z_DOWN },
+ { "MESH_AXIS_X_DOWN", MESH_INPUT_AXIS_X_DOWN },
+ };
+ for(int i = 0; i < 6; i++) {
+ jerry_value_t k = jerry_string_sz(axes[i].name);
+ jerry_value_t v = jerry_number((double)axes[i].val);
+ jerry_object_set(global, k, v);
+ jerry_value_free(v);
+ jerry_value_free(k);
+ }
+}
+
+static void moduleAssetDispose(void) {
+ scriptProtoDispose(&MODULE_ASSET_PROTO);
+ moduleAssetEntryDispose();
+}
diff --git a/src/dusk/script/module/asset/moduleassetentry.h b/src/dusk/script/module/asset/moduleassetentry.h
new file mode 100644
index 00000000..d61dfb5a
--- /dev/null
+++ b/src/dusk/script/module/asset/moduleassetentry.h
@@ -0,0 +1,171 @@
+/**
+ * Copyright (c) 2026 Dominic Masters
+ *
+ * This software is released under the MIT License.
+ * https://opensource.org/licenses/MIT
+ */
+
+#pragma once
+#include "script/module/modulebase.h"
+#include "script/scriptproto.h"
+#include "script/module/display/moduletexture.h"
+#include "asset/asset.h"
+#include "asset/loader/assetloader.h"
+#include "asset/loader/assetentry.h"
+#include "util/memory.h"
+
+static scriptproto_t MODULE_ASSET_ENTRY_PROTO;
+
+typedef struct {
+ assetentry_t *entry;
+} jsassetentry_t;
+
+/** Releases the asset lock when the JS object is GC'd. */
+static void moduleAssetEntryFree(
+ void *ptr,
+ jerry_object_native_info_t *info
+) {
+ (void)info;
+ jsassetentry_t *e = (jsassetentry_t *)ptr;
+ if(e && e->entry) {
+ assetUnlockEntry(e->entry);
+ e->entry = NULL;
+ }
+ memoryFree(ptr);
+}
+
+moduleBaseFunction(moduleAssetEntryCtor) {
+ (void)callInfo; (void)args; (void)argc;
+ return moduleBaseThrow("AssetEntry cannot be instantiated with new");
+}
+
+static inline jsassetentry_t *moduleAssetEntrySelf(
+ const jerry_call_info_t *callInfo
+) {
+ return (jsassetentry_t *)scriptProtoGetValue(
+ &MODULE_ASSET_ENTRY_PROTO, callInfo->this_value
+ );
+}
+
+moduleBaseFunction(moduleAssetEntryGetName) {
+ jsassetentry_t *e = moduleAssetEntrySelf(callInfo);
+ if(!e || !e->entry) return jerry_undefined();
+ return jerry_string_sz(e->entry->name);
+}
+
+moduleBaseFunction(moduleAssetEntryGetState) {
+ jsassetentry_t *e = moduleAssetEntrySelf(callInfo);
+ if(!e || !e->entry) return jerry_undefined();
+ return jerry_number((double)e->entry->state);
+}
+
+moduleBaseFunction(moduleAssetEntryGetType) {
+ jsassetentry_t *e = moduleAssetEntrySelf(callInfo);
+ if(!e || !e->entry) return jerry_undefined();
+ return jerry_number((double)e->entry->type);
+}
+
+moduleBaseFunction(moduleAssetEntryGetIsLoaded) {
+ jsassetentry_t *e = moduleAssetEntrySelf(callInfo);
+ if(!e || !e->entry) return jerry_boolean(false);
+ return jerry_boolean(e->entry->state == ASSET_ENTRY_STATE_LOADED);
+}
+
+/* requireLoaded() — blocks until fully loaded, returns this for chaining. */
+moduleBaseFunction(moduleAssetEntryRequireLoaded) {
+ jsassetentry_t *e = moduleAssetEntrySelf(callInfo);
+ if(!e || !e->entry) return moduleBaseThrow("AssetEntry.requireLoaded: invalid entry");
+ errorret_t err = assetRequireLoaded(e->entry);
+ if(errorIsNotOk(err)) return moduleBaseThrowError(err);
+ jerry_value_t self = jerry_value_copy(callInfo->this_value);
+ return self;
+}
+
+/*
+ * texture — returns a Texture wrapping this entry's loaded texture data.
+ * Returns undefined if the entry is not a texture or not yet loaded.
+ * Locks the entry a second time so the Texture holds its own independent
+ * reference; the lock is released when the Texture is GC'd.
+ */
+moduleBaseFunction(moduleAssetEntryGetTexture) {
+ jsassetentry_t *e = moduleAssetEntrySelf(callInfo);
+ if(!e || !e->entry) return jerry_undefined();
+ if(e->entry->type != ASSET_LOADER_TYPE_TEXTURE) return jerry_undefined();
+ if(e->entry->state != ASSET_ENTRY_STATE_LOADED) return jerry_undefined();
+ assetEntryLock(e->entry);
+ jstexture_t tex = { .entry = e->entry };
+ return scriptProtoCreateValue(&MODULE_TEXTURE_PROTO, &tex);
+}
+
+/* unlock() — releases the lock early; subsequent access is undefined. */
+moduleBaseFunction(moduleAssetEntryUnlock) {
+ jsassetentry_t *e = moduleAssetEntrySelf(callInfo);
+ if(!e || !e->entry) return jerry_undefined();
+ assetUnlockEntry(e->entry);
+ e->entry = NULL;
+ return jerry_undefined();
+}
+
+moduleBaseFunction(moduleAssetEntryToString) {
+ jsassetentry_t *e = moduleAssetEntrySelf(callInfo);
+ if(!e || !e->entry) return jerry_string_sz("AssetEntry:invalid");
+ char_t buf[64];
+ snprintf(buf, sizeof(buf), "AssetEntry(%s)", e->entry->name);
+ return jerry_string_sz(buf);
+}
+
+static void moduleAssetEntryInit(void) {
+ scriptProtoInit(
+ &MODULE_ASSET_ENTRY_PROTO, "AssetEntry",
+ sizeof(jsassetentry_t), moduleAssetEntryCtor
+ );
+ MODULE_ASSET_ENTRY_PROTO.info.free_cb = moduleAssetEntryFree;
+
+ scriptProtoDefineProp(
+ &MODULE_ASSET_ENTRY_PROTO, "name", moduleAssetEntryGetName, NULL
+ );
+ scriptProtoDefineProp(
+ &MODULE_ASSET_ENTRY_PROTO, "state", moduleAssetEntryGetState, NULL
+ );
+ scriptProtoDefineProp(
+ &MODULE_ASSET_ENTRY_PROTO, "type", moduleAssetEntryGetType, NULL
+ );
+ scriptProtoDefineProp(
+ &MODULE_ASSET_ENTRY_PROTO, "isLoaded", moduleAssetEntryGetIsLoaded, NULL
+ );
+ scriptProtoDefineProp(
+ &MODULE_ASSET_ENTRY_PROTO, "texture", moduleAssetEntryGetTexture, NULL
+ );
+ scriptProtoDefineFunc(
+ &MODULE_ASSET_ENTRY_PROTO, "requireLoaded", moduleAssetEntryRequireLoaded
+ );
+ scriptProtoDefineFunc(
+ &MODULE_ASSET_ENTRY_PROTO, "unlock", moduleAssetEntryUnlock
+ );
+ scriptProtoDefineToString(
+ &MODULE_ASSET_ENTRY_PROTO, moduleAssetEntryToString
+ );
+
+ /* State constants */
+ jerry_value_t ctor = MODULE_ASSET_ENTRY_PROTO.constructor;
+ struct { const char_t *name; int val; } states[] = {
+ { "NOT_STARTED", ASSET_ENTRY_STATE_NOT_STARTED },
+ { "PENDING", ASSET_ENTRY_STATE_PENDING_ASYNC },
+ { "LOADING", ASSET_ENTRY_STATE_LOADING_ASYNC },
+ { "LOADED", ASSET_ENTRY_STATE_LOADED },
+ { "ERROR", ASSET_ENTRY_STATE_ERROR },
+ };
+ for(int i = 0; i < 5; i++) {
+ jerry_value_t k = jerry_string_sz(states[i].name);
+ jerry_value_t v = jerry_number((double)states[i].val);
+ jerry_object_set(ctor, k, v);
+ jerry_value_free(v);
+ jerry_value_free(k);
+ }
+
+
+}
+
+static void moduleAssetEntryDispose(void) {
+ scriptProtoDispose(&MODULE_ASSET_ENTRY_PROTO);
+}
diff --git a/src/dusk/script/module/display/modulecolor.h b/src/dusk/script/module/display/modulecolor.h
new file mode 100644
index 00000000..f62ddbc4
--- /dev/null
+++ b/src/dusk/script/module/display/modulecolor.h
@@ -0,0 +1,154 @@
+/**
+ * Copyright (c) 2026 Dominic Masters
+ *
+ * This software is released under the MIT License.
+ * https://opensource.org/licenses/MIT
+ */
+
+#pragma once
+#include "script/module/modulebase.h"
+#include "script/scriptproto.h"
+#include "util/memory.h"
+#include "display/color.h"
+
+static scriptproto_t MODULE_COLOR_PROTO;
+
+/**
+ * Returns the native color_t pointer from a Color JS instance.
+ * Returns NULL if the value is not a Color.
+ */
+static inline color_t *moduleColorFrom(const jerry_value_t val) {
+ return (color_t *)scriptProtoGetValue(&MODULE_COLOR_PROTO, val);
+}
+
+/**
+ * Creates a Color JS object from a C color_t value.
+ */
+static inline jerry_value_t moduleColorPush(const color_t c) {
+ return scriptProtoCreateValue(&MODULE_COLOR_PROTO, &c);
+}
+
+moduleBaseFunction(moduleColorConstructor) {
+ color_t *ptr = (color_t *)memoryAllocate(sizeof(color_t));
+ ptr->r = (uint8_t)moduleBaseOptInt(0, 0);
+ ptr->g = (uint8_t)moduleBaseOptInt(1, 0);
+ ptr->b = (uint8_t)moduleBaseOptInt(2, 0);
+ ptr->a = (uint8_t)moduleBaseOptInt(3, 255);
+ jerry_object_set_native_ptr(
+ callInfo->this_value, &MODULE_COLOR_PROTO.info, ptr
+ );
+ return jerry_undefined();
+}
+
+moduleBaseFunction(moduleColorGetR) {
+ color_t *c = moduleColorFrom(callInfo->this_value);
+ if(!c) return jerry_undefined();
+ return jerry_number((double)c->r);
+}
+
+moduleBaseFunction(moduleColorSetR) {
+ moduleBaseRequireArgs(1);
+ color_t *c = moduleColorFrom(callInfo->this_value);
+ if(!c) return jerry_undefined();
+ c->r = (uint8_t)moduleBaseArgInt(0);
+ return jerry_undefined();
+}
+
+moduleBaseFunction(moduleColorGetG) {
+ color_t *c = moduleColorFrom(callInfo->this_value);
+ if(!c) return jerry_undefined();
+ return jerry_number((double)c->g);
+}
+
+moduleBaseFunction(moduleColorSetG) {
+ moduleBaseRequireArgs(1);
+ color_t *c = moduleColorFrom(callInfo->this_value);
+ if(!c) return jerry_undefined();
+ c->g = (uint8_t)moduleBaseArgInt(0);
+ return jerry_undefined();
+}
+
+moduleBaseFunction(moduleColorGetB) {
+ color_t *c = moduleColorFrom(callInfo->this_value);
+ if(!c) return jerry_undefined();
+ return jerry_number((double)c->b);
+}
+
+moduleBaseFunction(moduleColorSetB) {
+ moduleBaseRequireArgs(1);
+ color_t *c = moduleColorFrom(callInfo->this_value);
+ if(!c) return jerry_undefined();
+ c->b = (uint8_t)moduleBaseArgInt(0);
+ return jerry_undefined();
+}
+
+moduleBaseFunction(moduleColorGetA) {
+ color_t *c = moduleColorFrom(callInfo->this_value);
+ if(!c) return jerry_undefined();
+ return jerry_number((double)c->a);
+}
+
+moduleBaseFunction(moduleColorSetA) {
+ moduleBaseRequireArgs(1);
+ color_t *c = moduleColorFrom(callInfo->this_value);
+ if(!c) return jerry_undefined();
+ c->a = (uint8_t)moduleBaseArgInt(0);
+ return jerry_undefined();
+}
+
+moduleBaseFunction(moduleColorToString) {
+ color_t *c = moduleColorFrom(callInfo->this_value);
+ if(!c) return jerry_string_sz("Color:invalid");
+ char_t buf[32];
+ snprintf(buf, sizeof(buf), "Color(%u,%u,%u,%u)",
+ (unsigned)c->r, (unsigned)c->g,
+ (unsigned)c->b, (unsigned)c->a
+ );
+ return jerry_string_sz(buf);
+}
+
+static void moduleColorInit(void) {
+ scriptProtoInit(
+ &MODULE_COLOR_PROTO, "Color",
+ sizeof(color_t), moduleColorConstructor
+ );
+
+ scriptProtoDefineProp(&MODULE_COLOR_PROTO, "r", moduleColorGetR, moduleColorSetR);
+ scriptProtoDefineProp(&MODULE_COLOR_PROTO, "g", moduleColorGetG, moduleColorSetG);
+ scriptProtoDefineProp(&MODULE_COLOR_PROTO, "b", moduleColorGetB, moduleColorSetB);
+ scriptProtoDefineProp(&MODULE_COLOR_PROTO, "a", moduleColorGetA, moduleColorSetA);
+ scriptProtoDefineToString(&MODULE_COLOR_PROTO, moduleColorToString);
+
+ /* Static named color constants on the constructor. */
+ struct { const char_t *name; color_t val; } constants[] = {
+ { "WHITE", COLOR_WHITE },
+ { "BLACK", COLOR_BLACK },
+ { "RED", COLOR_RED },
+ { "GREEN", COLOR_GREEN },
+ { "BLUE", COLOR_BLUE },
+ { "YELLOW", COLOR_YELLOW },
+ { "CYAN", COLOR_CYAN },
+ { "MAGENTA", COLOR_MAGENTA },
+ { "TRANSPARENT", COLOR_TRANSPARENT },
+ { "GRAY", COLOR_GRAY },
+ { "LIGHT_GRAY", COLOR_LIGHT_GRAY },
+ { "DARK_GRAY", COLOR_DARK_GRAY },
+ { "ORANGE", COLOR_ORANGE },
+ { "PURPLE", COLOR_PURPLE },
+ { "PINK", COLOR_PINK },
+ { "TEAL", COLOR_TEAL },
+ { "CORNFLOWER_BLUE", COLOR_CORNFLOWER_BLUE },
+ };
+ jerry_value_t ctor = MODULE_COLOR_PROTO.constructor;
+ for(int i = 0; i < (int)(sizeof(constants)/sizeof(constants[0])); i++) {
+ jerry_value_t k = jerry_string_sz(constants[i].name);
+ jerry_value_t v = moduleColorPush(constants[i].val);
+ jerry_object_set(ctor, k, v);
+ jerry_value_free(v);
+ jerry_value_free(k);
+ }
+}
+
+static void moduleColorDispose(void) {
+ scriptProtoDispose(&MODULE_COLOR_PROTO);
+}
diff --git a/src/dusk/script/module/display/moduletexture.h b/src/dusk/script/module/display/moduletexture.h
new file mode 100644
index 00000000..da66fbc3
--- /dev/null
+++ b/src/dusk/script/module/display/moduletexture.h
@@ -0,0 +1,108 @@
+/**
+ * Copyright (c) 2026 Dominic Masters
+ *
+ * This software is released under the MIT License.
+ * https://opensource.org/licenses/MIT
+ */
+
+#pragma once
+#include "script/module/modulebase.h"
+#include "script/scriptproto.h"
+#include "asset/asset.h"
+#include "asset/loader/assetloader.h"
+#include "display/texture/texture.h"
+#include "util/memory.h"
+
+static scriptproto_t MODULE_TEXTURE_PROTO;
+
+typedef struct {
+ assetentry_t *entry;
+} jstexture_t;
+
+/**
+ * Custom free callback — unlocks the asset entry so it can be reclaimed
+ * once the JS Texture object is garbage collected.
+ */
+static void moduleTextureFree(
+ void *ptr,
+ jerry_object_native_info_t *info
+) {
+ (void)info;
+ jstexture_t *tex = (jstexture_t *)ptr;
+ if(tex && tex->entry) {
+ assetUnlockEntry(tex->entry);
+ tex->entry = NULL;
+ }
+ memoryFree(ptr);
+}
+
+moduleBaseFunction(moduleTextureCtor) {
+ (void)callInfo; (void)args; (void)argc;
+ return moduleBaseThrow("Texture cannot be instantiated with new");
+}
+
+static inline jstexture_t *moduleTextureSelf(
+ const jerry_call_info_t *callInfo
+) {
+ return (jstexture_t *)scriptProtoGetValue(
+ &MODULE_TEXTURE_PROTO, callInfo->this_value
+ );
+}
+
+moduleBaseFunction(moduleTextureGetWidth) {
+ jstexture_t *t = moduleTextureSelf(callInfo);
+ if(!t || !t->entry) return jerry_undefined();
+ return jerry_number((double)t->entry->data.texture.width);
+}
+
+moduleBaseFunction(moduleTextureGetHeight) {
+ jstexture_t *t = moduleTextureSelf(callInfo);
+ if(!t || !t->entry) return jerry_undefined();
+ return jerry_number((double)t->entry->data.texture.height);
+}
+
+moduleBaseFunction(moduleTextureToString) {
+ jstexture_t *t = moduleTextureSelf(callInfo);
+ if(!t || !t->entry) return jerry_string_sz("Texture:invalid");
+ char_t buf[64];
+ snprintf(buf, sizeof(buf), "Texture(%dx%d)",
+ t->entry->data.texture.width,
+ t->entry->data.texture.height
+ );
+ return jerry_string_sz(buf);
+}
+
+static void moduleTextureInit(void) {
+ scriptProtoInit(
+ &MODULE_TEXTURE_PROTO, "Texture",
+ sizeof(jstexture_t), moduleTextureCtor
+ );
+ /* Override the default free callback so the asset lock is released on GC. */
+ MODULE_TEXTURE_PROTO.info.free_cb = moduleTextureFree;
+
+ scriptProtoDefineProp(
+ &MODULE_TEXTURE_PROTO, "width", moduleTextureGetWidth, NULL
+ );
+ scriptProtoDefineProp(
+ &MODULE_TEXTURE_PROTO, "height", moduleTextureGetHeight, NULL
+ );
+ scriptProtoDefineToString(&MODULE_TEXTURE_PROTO, moduleTextureToString);
+
+ /* Texture.FORMAT_* constants */
+ jerry_value_t ctor = MODULE_TEXTURE_PROTO.constructor;
+ struct { const char_t *name; int val; } formats[] = {
+ { "FORMAT_RGBA", TEXTURE_FORMAT_RGBA },
+ { "FORMAT_PALETTE", TEXTURE_FORMAT_PALETTE },
+ };
+ for(int i = 0; i < 2; i++) {
+ jerry_value_t k = jerry_string_sz(formats[i].name);
+ jerry_value_t v = jerry_number((double)formats[i].val);
+ jerry_object_set(ctor, k, v);
+ jerry_value_free(v);
+ jerry_value_free(k);
+ }
+}
+
+static void moduleTextureDispose(void) {
+ scriptProtoDispose(&MODULE_TEXTURE_PROTO);
+}
diff --git a/src/dusk/script/module/entity/component/modulecomponentlist.h b/src/dusk/script/module/entity/component/modulecomponentlist.h
index 463bfdd8..c5531ef9 100644
--- a/src/dusk/script/module/entity/component/modulecomponentlist.h
+++ b/src/dusk/script/module/entity/component/modulecomponentlist.h
@@ -8,7 +8,10 @@
#pragma once
#include "script/module/entity/modulecomponent.h"
#include "camera/modulecamera.h"
+#include "physics/modulephysics.h"
#include "position/moduleposition.h"
+#include "renderable/modulerenderable.h"
+#include "trigger/moduletrigger.h"
/**
* Returns a typed JS instance for a newly-added component. Falls back to the
@@ -19,21 +22,33 @@ static jerry_value_t moduleComponentListCreateInstance(
const jscomponent_t *comp
) {
switch(type) {
- case COMPONENT_TYPE_POSITION:
- return scriptProtoCreateValue(&MODULE_POSITION_PROTO, comp);
case COMPONENT_TYPE_CAMERA:
return scriptProtoCreateValue(&MODULE_CAMERA_PROTO, comp);
+ case COMPONENT_TYPE_PHYSICS:
+ return scriptProtoCreateValue(&MODULE_PHYSICS_PROTO, comp);
+ case COMPONENT_TYPE_POSITION:
+ return scriptProtoCreateValue(&MODULE_POSITION_PROTO, comp);
+ case COMPONENT_TYPE_RENDERABLE:
+ return scriptProtoCreateValue(&MODULE_RENDERABLE_PROTO, comp);
+ case COMPONENT_TYPE_TRIGGER:
+ return scriptProtoCreateValue(&MODULE_TRIGGER_PROTO, comp);
default:
return scriptProtoCreateValue(&MODULE_COMPONENT_PROTO, comp);
}
}
static void moduleComponentListInit(void) {
- modulePositionInit();
moduleCameraInit();
+ modulePhysicsInit();
+ modulePositionInit();
+ moduleRenderableInit();
+ moduleTriggerInit();
}
static void moduleComponentListDispose(void) {
- moduleCameraDispose();
+ moduleTriggerDispose();
+ moduleRenderableDispose();
modulePositionDispose();
+ modulePhysicsDispose();
+ moduleCameraDispose();
}
diff --git a/src/dusk/script/module/entity/component/physics/modulephysics.h b/src/dusk/script/module/entity/component/physics/modulephysics.h
new file mode 100644
index 00000000..8e7cffd6
--- /dev/null
+++ b/src/dusk/script/module/entity/component/physics/modulephysics.h
@@ -0,0 +1,204 @@
+/**
+ * Copyright (c) 2026 Dominic Masters
+ *
+ * This software is released under the MIT License.
+ * https://opensource.org/licenses/MIT
+ */
+
+#pragma once
+#include "script/module/modulebase.h"
+#include "script/scriptproto.h"
+#include "script/module/math/modulevec3.h"
+#include "script/module/entity/modulecomponent.h"
+#include "entity/component/physics/entityphysics.h"
+
+static scriptproto_t MODULE_PHYSICS_PROTO;
+
+moduleBaseFunction(modulePhysicsCtor) {
+ (void)callInfo; (void)args; (void)argc;
+ return moduleBaseThrow("Physics cannot be instantiated with new");
+}
+
+static inline jscomponent_t *modulePhysicsSelf(
+ const jerry_call_info_t *callInfo
+) {
+ return (jscomponent_t *)scriptProtoGetValue(
+ &MODULE_PHYSICS_PROTO, callInfo->this_value
+ );
+}
+
+moduleBaseFunction(modulePhysicsGetEntity) {
+ jscomponent_t *c = modulePhysicsSelf(callInfo);
+ if(!c) return jerry_undefined();
+ return jerry_number((double)c->entityId);
+}
+
+moduleBaseFunction(modulePhysicsGetId) {
+ jscomponent_t *c = modulePhysicsSelf(callInfo);
+ if(!c) return jerry_undefined();
+ return jerry_number((double)c->componentId);
+}
+
+moduleBaseFunction(modulePhysicsGetBodyType) {
+ jscomponent_t *c = modulePhysicsSelf(callInfo);
+ if(!c) return jerry_undefined();
+ return jerry_number((double)entityPhysicsGetBodyType(c->entityId, c->componentId));
+}
+
+moduleBaseFunction(modulePhysicsSetBodyType) {
+ moduleBaseRequireArgs(1);
+ jscomponent_t *c = modulePhysicsSelf(callInfo);
+ if(!c) return jerry_undefined();
+ entityPhysicsSetBodyType(
+ c->entityId, c->componentId,
+ (physicsbodytype_t)moduleBaseArgInt(0)
+ );
+ return jerry_undefined();
+}
+
+moduleBaseFunction(modulePhysicsGetShape) {
+ jscomponent_t *c = modulePhysicsSelf(callInfo);
+ if(!c) return jerry_undefined();
+ return jerry_number((double)entityPhysicsGetShape(c->entityId, c->componentId).type);
+}
+
+moduleBaseFunction(modulePhysicsSetShape) {
+ moduleBaseRequireArgs(1);
+ jscomponent_t *c = modulePhysicsSelf(callInfo);
+ if(!c) return jerry_undefined();
+ physicsshape_t shape = entityPhysicsGetShape(c->entityId, c->componentId);
+ shape.type = (physicshapetype_t)moduleBaseArgInt(0);
+ entityPhysicsSetShape(c->entityId, c->componentId, shape);
+ return jerry_undefined();
+}
+
+moduleBaseFunction(modulePhysicsGetVelocity) {
+ jscomponent_t *c = modulePhysicsSelf(callInfo);
+ if(!c) return jerry_undefined();
+ vec3 v;
+ entityPhysicsGetVelocity(c->entityId, c->componentId, v);
+ return moduleVec3Push(v);
+}
+
+moduleBaseFunction(modulePhysicsSetVelocity) {
+ moduleBaseRequireArgs(1);
+ jscomponent_t *c = modulePhysicsSelf(callInfo);
+ if(!c) return jerry_undefined();
+ float_t *v = moduleVec3From(args[0]);
+ if(!v) return moduleBaseThrow("Physics.velocity: expected Vec3");
+ entityPhysicsSetVelocity(c->entityId, c->componentId, v);
+ return jerry_undefined();
+}
+
+moduleBaseFunction(modulePhysicsGetGravityScale) {
+ jscomponent_t *c = modulePhysicsSelf(callInfo);
+ if(!c) return jerry_undefined();
+ entityphysics_t *p = entityPhysicsGet(c->entityId, c->componentId);
+ if(!p) return jerry_undefined();
+ return jerry_number((double)p->gravityScale);
+}
+
+moduleBaseFunction(modulePhysicsSetGravityScale) {
+ moduleBaseRequireArgs(1);
+ jscomponent_t *c = modulePhysicsSelf(callInfo);
+ if(!c) return jerry_undefined();
+ entityphysics_t *p = entityPhysicsGet(c->entityId, c->componentId);
+ if(!p) return jerry_undefined();
+ p->gravityScale = moduleBaseArgFloat(0);
+ return jerry_undefined();
+}
+
+moduleBaseFunction(modulePhysicsGetOnGround) {
+ jscomponent_t *c = modulePhysicsSelf(callInfo);
+ if(!c) return jerry_undefined();
+ return jerry_boolean(entityPhysicsIsOnGround(c->entityId, c->componentId));
+}
+
+moduleBaseFunction(modulePhysicsApplyImpulse) {
+ moduleBaseRequireArgs(1);
+ jscomponent_t *c = modulePhysicsSelf(callInfo);
+ if(!c) return jerry_undefined();
+ float_t *v = moduleVec3From(args[0]);
+ if(!v) return moduleBaseThrow("Physics.applyImpulse: expected Vec3");
+ entityPhysicsApplyImpulse(c->entityId, c->componentId, v);
+ return jerry_undefined();
+}
+
+moduleBaseFunction(modulePhysicsToString) {
+ jscomponent_t *c = modulePhysicsSelf(callInfo);
+ if(!c) return jerry_string_sz("Physics:invalid");
+ char_t buf[32];
+ snprintf(buf, sizeof(buf), "Physics(%u)", (unsigned)c->componentId);
+ return jerry_string_sz(buf);
+}
+
+static void modulePhysicsInit(void) {
+ scriptProtoInit(
+ &MODULE_PHYSICS_PROTO, "Physics",
+ sizeof(jscomponent_t), modulePhysicsCtor
+ );
+
+ scriptProtoDefineProp(
+ &MODULE_PHYSICS_PROTO, "entity", modulePhysicsGetEntity, NULL
+ );
+ scriptProtoDefineProp(
+ &MODULE_PHYSICS_PROTO, "id", modulePhysicsGetId, NULL
+ );
+ scriptProtoDefineProp(
+ &MODULE_PHYSICS_PROTO, "bodyType",
+ modulePhysicsGetBodyType, modulePhysicsSetBodyType
+ );
+ scriptProtoDefineProp(
+ &MODULE_PHYSICS_PROTO, "shape",
+ modulePhysicsGetShape, modulePhysicsSetShape
+ );
+ scriptProtoDefineProp(
+ &MODULE_PHYSICS_PROTO, "velocity",
+ modulePhysicsGetVelocity, modulePhysicsSetVelocity
+ );
+ scriptProtoDefineProp(
+ &MODULE_PHYSICS_PROTO, "gravityScale",
+ modulePhysicsGetGravityScale, modulePhysicsSetGravityScale
+ );
+ scriptProtoDefineProp(
+ &MODULE_PHYSICS_PROTO, "onGround", modulePhysicsGetOnGround, NULL
+ );
+ scriptProtoDefineFunc(
+ &MODULE_PHYSICS_PROTO, "applyImpulse", modulePhysicsApplyImpulse
+ );
+ scriptProtoDefineToString(&MODULE_PHYSICS_PROTO, modulePhysicsToString);
+
+ /* Body type constants */
+ jerry_value_t ctor = MODULE_PHYSICS_PROTO.constructor;
+ struct { const char_t *name; int val; } bodyTypes[] = {
+ { "STATIC", PHYSICS_BODY_STATIC },
+ { "DYNAMIC", PHYSICS_BODY_DYNAMIC },
+ { "KINEMATIC", PHYSICS_BODY_KINEMATIC },
+ };
+ for(int i = 0; i < 3; i++) {
+ jerry_value_t k = jerry_string_sz(bodyTypes[i].name);
+ jerry_value_t v = jerry_number((double)bodyTypes[i].val);
+ jerry_object_set(ctor, k, v);
+ jerry_value_free(v);
+ jerry_value_free(k);
+ }
+
+ /* Shape type constants */
+ struct { const char_t *name; int val; } shapes[] = {
+ { "SHAPE_CUBE", PHYSICS_SHAPE_CUBE },
+ { "SHAPE_SPHERE", PHYSICS_SHAPE_SPHERE },
+ { "SHAPE_CAPSULE", PHYSICS_SHAPE_CAPSULE },
+ { "SHAPE_PLANE", PHYSICS_SHAPE_PLANE },
+ };
+ for(int i = 0; i < 4; i++) {
+ jerry_value_t k = jerry_string_sz(shapes[i].name);
+ jerry_value_t v = jerry_number((double)shapes[i].val);
+ jerry_object_set(ctor, k, v);
+ jerry_value_free(v);
+ jerry_value_free(k);
+ }
+}
+
+static void modulePhysicsDispose(void) {
+ scriptProtoDispose(&MODULE_PHYSICS_PROTO);
+}
diff --git a/src/dusk/script/module/entity/component/renderable/modulerenderable.h b/src/dusk/script/module/entity/component/renderable/modulerenderable.h
new file mode 100644
index 00000000..0e132117
--- /dev/null
+++ b/src/dusk/script/module/entity/component/renderable/modulerenderable.h
@@ -0,0 +1,243 @@
+/**
+ * Copyright (c) 2026 Dominic Masters
+ *
+ * This software is released under the MIT License.
+ * https://opensource.org/licenses/MIT
+ */
+
+#pragma once
+#include "script/module/modulebase.h"
+#include "script/module/display/modulecolor.h"
+#include "script/module/display/moduletexture.h"
+#include "script/module/entity/modulecomponent.h"
+#include "script/scriptproto.h"
+#include "entity/component/display/entityrenderable.h"
+
+static scriptproto_t MODULE_RENDERABLE_PROTO;
+
+moduleBaseFunction(moduleRenderableCtor) {
+ (void)callInfo; (void)args; (void)argc;
+ return moduleBaseThrow("Renderable cannot be instantiated with new");
+}
+
+static inline jscomponent_t *moduleRenderableSelf(
+ const jerry_call_info_t *callInfo
+) {
+ return (jscomponent_t *)scriptProtoGetValue(
+ &MODULE_RENDERABLE_PROTO, callInfo->this_value
+ );
+}
+
+static inline entityrenderable_t *moduleRenderableData(const jscomponent_t *c) {
+ return (entityrenderable_t *)componentGetData(
+ c->entityId, c->componentId, COMPONENT_TYPE_RENDERABLE
+ );
+}
+
+moduleBaseFunction(moduleRenderableGetEntity) {
+ jscomponent_t *c = moduleRenderableSelf(callInfo);
+ if(!c) return jerry_undefined();
+ return jerry_number((double)c->entityId);
+}
+
+moduleBaseFunction(moduleRenderableGetId) {
+ jscomponent_t *c = moduleRenderableSelf(callInfo);
+ if(!c) return jerry_undefined();
+ return jerry_number((double)c->componentId);
+}
+
+moduleBaseFunction(moduleRenderableGetType) {
+ jscomponent_t *c = moduleRenderableSelf(callInfo);
+ if(!c) return jerry_undefined();
+ entityrenderable_t *r = moduleRenderableData(c);
+ if(!r) return jerry_undefined();
+ return jerry_number((double)r->type);
+}
+
+moduleBaseFunction(moduleRenderableSetType) {
+ moduleBaseRequireArgs(1);
+ jscomponent_t *c = moduleRenderableSelf(callInfo);
+ if(!c) return jerry_undefined();
+ entityRenderableSetType(
+ c->entityId, c->componentId,
+ (entityrenderabletype_t)moduleBaseArgInt(0)
+ );
+ return jerry_undefined();
+}
+
+moduleBaseFunction(moduleRenderableGetPriority) {
+ jscomponent_t *c = moduleRenderableSelf(callInfo);
+ if(!c) return jerry_undefined();
+ entityrenderable_t *r = moduleRenderableData(c);
+ if(!r) return jerry_undefined();
+ return jerry_number((double)r->priority);
+}
+
+moduleBaseFunction(moduleRenderableSetPriority) {
+ moduleBaseRequireArgs(1);
+ jscomponent_t *c = moduleRenderableSelf(callInfo);
+ if(!c) return jerry_undefined();
+ entityRenderableSetPriority(
+ c->entityId, c->componentId,
+ (int8_t)moduleBaseArgInt(0)
+ );
+ return jerry_undefined();
+}
+
+moduleBaseFunction(moduleRenderableGetColor) {
+ jscomponent_t *c = moduleRenderableSelf(callInfo);
+ if(!c) return jerry_undefined();
+ entityrenderable_t *r = moduleRenderableData(c);
+ if(!r) return jerry_undefined();
+ return moduleColorPush(r->data.material.material.unlit.color);
+}
+
+moduleBaseFunction(moduleRenderableSetColor) {
+ moduleBaseRequireArgs(1);
+ jscomponent_t *c = moduleRenderableSelf(callInfo);
+ if(!c) return jerry_undefined();
+ entityrenderable_t *r = moduleRenderableData(c);
+ if(!r) return jerry_undefined();
+ color_t *col = moduleColorFrom(args[0]);
+ if(!col) return moduleBaseThrow("Renderable.color: expected Color");
+ r->data.material.material.unlit.color = *col;
+ return jerry_undefined();
+}
+
+/* setTexture(tex: Texture) — switches to SPRITEBATCH and stores the texture. */
+moduleBaseFunction(moduleRenderableSetTexture) {
+ moduleBaseRequireArgs(1);
+ jscomponent_t *c = moduleRenderableSelf(callInfo);
+ if(!c) return jerry_undefined();
+ entityrenderable_t *r = moduleRenderableData(c);
+ if(!r) return jerry_undefined();
+ jstexture_t *tex = (jstexture_t *)scriptProtoGetValue(
+ &MODULE_TEXTURE_PROTO, args[0]
+ );
+ if(!tex || !tex->entry) {
+ return moduleBaseThrow("Renderable.setTexture: expected Texture");
+ }
+ r->type = ENTITY_RENDERABLE_TYPE_SPRITEBATCH;
+ r->data.spritebatch.texture = &tex->entry->data.texture;
+ /* Pin the Texture object so GC won't free the asset while we hold a pointer. */
+ jerry_value_t pinKey = jerry_string_sz("_tex");
+ jerry_object_set(callInfo->this_value, pinKey, args[0]);
+ jerry_value_free(pinKey);
+ return jerry_undefined();
+}
+
+/*
+ * addSprite(x1,y1,z1, x2,y2,z2, u1,v1, u2,v2)
+ * addSprite(x1,y1, x2,y2, u1,v1, u2,v2) — z defaults to 0
+ */
+moduleBaseFunction(moduleRenderableAddSprite) {
+ jscomponent_t *c = moduleRenderableSelf(callInfo);
+ if(!c) return jerry_undefined();
+ entityrenderable_t *r = moduleRenderableData(c);
+ if(!r) return jerry_undefined();
+ entityrenderablespritebatch_t *sb = &r->data.spritebatch;
+ if(sb->spriteCount >= ENTITY_RENDERABLE_SPRITEBATCH_SPRITES_MAX) {
+ return moduleBaseThrow("Renderable.addSprite: sprite capacity reached");
+ }
+ spritebatchsprite_t s;
+ if(argc >= 10) {
+ moduleBaseRequireArgs(10);
+ s.min[0] = moduleBaseArgFloat(0);
+ s.min[1] = moduleBaseArgFloat(1);
+ s.min[2] = moduleBaseArgFloat(2);
+ s.max[0] = moduleBaseArgFloat(3);
+ s.max[1] = moduleBaseArgFloat(4);
+ s.max[2] = moduleBaseArgFloat(5);
+ s.uvMin[0] = moduleBaseArgFloat(6);
+ s.uvMin[1] = moduleBaseArgFloat(7);
+ s.uvMax[0] = moduleBaseArgFloat(8);
+ s.uvMax[1] = moduleBaseArgFloat(9);
+ } else {
+ moduleBaseRequireArgs(8);
+ s.min[0] = moduleBaseArgFloat(0);
+ s.min[1] = moduleBaseArgFloat(1);
+ s.min[2] = 0.0f;
+ s.max[0] = moduleBaseArgFloat(2);
+ s.max[1] = moduleBaseArgFloat(3);
+ s.max[2] = 0.0f;
+ s.uvMin[0] = moduleBaseArgFloat(4);
+ s.uvMin[1] = moduleBaseArgFloat(5);
+ s.uvMax[0] = moduleBaseArgFloat(6);
+ s.uvMax[1] = moduleBaseArgFloat(7);
+ }
+ sb->sprites[sb->spriteCount++] = s;
+ return jerry_undefined();
+}
+
+/* clearSprites() — resets the sprite count to 0. */
+moduleBaseFunction(moduleRenderableClearSprites) {
+ jscomponent_t *c = moduleRenderableSelf(callInfo);
+ if(!c) return jerry_undefined();
+ entityrenderable_t *r = moduleRenderableData(c);
+ if(!r) return jerry_undefined();
+ r->data.spritebatch.spriteCount = 0;
+ return jerry_undefined();
+}
+
+moduleBaseFunction(moduleRenderableToString) {
+ jscomponent_t *c = moduleRenderableSelf(callInfo);
+ if(!c) return jerry_string_sz("Renderable:invalid");
+ char_t buf[32];
+ snprintf(buf, sizeof(buf), "Renderable(%u)", (unsigned)c->componentId);
+ return jerry_string_sz(buf);
+}
+
+static void moduleRenderableInit(void) {
+ scriptProtoInit(
+ &MODULE_RENDERABLE_PROTO, "Renderable",
+ sizeof(jscomponent_t), moduleRenderableCtor
+ );
+
+ scriptProtoDefineProp(
+ &MODULE_RENDERABLE_PROTO, "entity", moduleRenderableGetEntity, NULL
+ );
+ scriptProtoDefineProp(
+ &MODULE_RENDERABLE_PROTO, "id", moduleRenderableGetId, NULL
+ );
+ scriptProtoDefineProp(
+ &MODULE_RENDERABLE_PROTO, "type",
+ moduleRenderableGetType, moduleRenderableSetType
+ );
+ scriptProtoDefineProp(
+ &MODULE_RENDERABLE_PROTO, "priority",
+ moduleRenderableGetPriority, moduleRenderableSetPriority
+ );
+ scriptProtoDefineProp(
+ &MODULE_RENDERABLE_PROTO, "color",
+ moduleRenderableGetColor, moduleRenderableSetColor
+ );
+ scriptProtoDefineFunc(
+ &MODULE_RENDERABLE_PROTO, "setTexture", moduleRenderableSetTexture
+ );
+ scriptProtoDefineFunc(
+ &MODULE_RENDERABLE_PROTO, "addSprite", moduleRenderableAddSprite
+ );
+ scriptProtoDefineFunc(
+ &MODULE_RENDERABLE_PROTO, "clearSprites", moduleRenderableClearSprites
+ );
+ scriptProtoDefineToString(&MODULE_RENDERABLE_PROTO, moduleRenderableToString);
+
+ /* Renderable.SHADER_MATERIAL, .SPRITEBATCH, .CUSTOM */
+ jerry_value_t ctor = MODULE_RENDERABLE_PROTO.constructor;
+ struct { const char_t *name; int val; } types[] = {
+ { "SHADER_MATERIAL", ENTITY_RENDERABLE_TYPE_SHADER_MATERIAL },
+ { "SPRITEBATCH", ENTITY_RENDERABLE_TYPE_SPRITEBATCH },
+ { "CUSTOM", ENTITY_RENDERABLE_TYPE_CUSTOM },
+ };
+ for(int i = 0; i < 3; i++) {
+ jerry_value_t k = jerry_string_sz(types[i].name);
+ jerry_value_t v = jerry_number((double)types[i].val);
+ jerry_object_set(ctor, k, v);
+ jerry_value_free(v);
+ jerry_value_free(k);
+ }
+}
+
+static void moduleRenderableDispose(void) {
+ scriptProtoDispose(&MODULE_RENDERABLE_PROTO);
+}
diff --git a/src/dusk/script/module/entity/component/trigger/moduletrigger.h b/src/dusk/script/module/entity/component/trigger/moduletrigger.h
new file mode 100644
index 00000000..0039aad0
--- /dev/null
+++ b/src/dusk/script/module/entity/component/trigger/moduletrigger.h
@@ -0,0 +1,140 @@
+/**
+ * Copyright (c) 2026 Dominic Masters
+ *
+ * This software is released under the MIT License.
+ * https://opensource.org/licenses/MIT
+ */
+
+#pragma once
+#include "script/module/modulebase.h"
+#include "script/scriptproto.h"
+#include "script/module/math/modulevec3.h"
+#include "script/module/entity/modulecomponent.h"
+#include "entity/component/trigger/entitytrigger.h"
+
+static scriptproto_t MODULE_TRIGGER_PROTO;
+
+moduleBaseFunction(moduleTriggerCtor) {
+ (void)callInfo; (void)args; (void)argc;
+ return moduleBaseThrow("Trigger cannot be instantiated with new");
+}
+
+static inline jscomponent_t *moduleTriggerSelf(
+ const jerry_call_info_t *callInfo
+) {
+ return (jscomponent_t *)scriptProtoGetValue(
+ &MODULE_TRIGGER_PROTO, callInfo->this_value
+ );
+}
+
+moduleBaseFunction(moduleTriggerGetEntity) {
+ jscomponent_t *c = moduleTriggerSelf(callInfo);
+ if(!c) return jerry_undefined();
+ return jerry_number((double)c->entityId);
+}
+
+moduleBaseFunction(moduleTriggerGetId) {
+ jscomponent_t *c = moduleTriggerSelf(callInfo);
+ if(!c) return jerry_undefined();
+ return jerry_number((double)c->componentId);
+}
+
+moduleBaseFunction(moduleTriggerGetMin) {
+ jscomponent_t *c = moduleTriggerSelf(callInfo);
+ if(!c) return jerry_undefined();
+ entitytrigger_t *t = entityTriggerGet(c->entityId, c->componentId);
+ if(!t) return jerry_undefined();
+ return moduleVec3Push(t->min);
+}
+
+moduleBaseFunction(moduleTriggerSetMin) {
+ moduleBaseRequireArgs(1);
+ jscomponent_t *c = moduleTriggerSelf(callInfo);
+ if(!c) return jerry_undefined();
+ float_t *v = moduleVec3From(args[0]);
+ if(!v) return moduleBaseThrow("Trigger.min: expected Vec3");
+ entitytrigger_t *t = entityTriggerGet(c->entityId, c->componentId);
+ if(!t) return jerry_undefined();
+ glm_vec3_copy(v, t->min);
+ return jerry_undefined();
+}
+
+moduleBaseFunction(moduleTriggerGetMax) {
+ jscomponent_t *c = moduleTriggerSelf(callInfo);
+ if(!c) return jerry_undefined();
+ entitytrigger_t *t = entityTriggerGet(c->entityId, c->componentId);
+ if(!t) return jerry_undefined();
+ return moduleVec3Push(t->max);
+}
+
+moduleBaseFunction(moduleTriggerSetMax) {
+ moduleBaseRequireArgs(1);
+ jscomponent_t *c = moduleTriggerSelf(callInfo);
+ if(!c) return jerry_undefined();
+ float_t *v = moduleVec3From(args[0]);
+ if(!v) return moduleBaseThrow("Trigger.max: expected Vec3");
+ entitytrigger_t *t = entityTriggerGet(c->entityId, c->componentId);
+ if(!t) return jerry_undefined();
+ glm_vec3_copy(v, t->max);
+ return jerry_undefined();
+}
+
+moduleBaseFunction(moduleTriggerSetBounds) {
+ moduleBaseRequireArgs(2);
+ jscomponent_t *c = moduleTriggerSelf(callInfo);
+ if(!c) return jerry_undefined();
+ float_t *minV = moduleVec3From(args[0]);
+ float_t *maxV = moduleVec3From(args[1]);
+ if(!minV) return moduleBaseThrow("Trigger.setBounds: expected Vec3 for min");
+ if(!maxV) return moduleBaseThrow("Trigger.setBounds: expected Vec3 for max");
+ entityTriggerSetBounds(c->entityId, c->componentId, minV, maxV);
+ return jerry_undefined();
+}
+
+moduleBaseFunction(moduleTriggerContains) {
+ moduleBaseRequireArgs(1);
+ jscomponent_t *c = moduleTriggerSelf(callInfo);
+ if(!c) return jerry_undefined();
+ float_t *v = moduleVec3From(args[0]);
+ if(!v) return moduleBaseThrow("Trigger.contains: expected Vec3");
+ return jerry_boolean(entityTriggerContains(c->entityId, c->componentId, v));
+}
+
+moduleBaseFunction(moduleTriggerToString) {
+ jscomponent_t *c = moduleTriggerSelf(callInfo);
+ if(!c) return jerry_string_sz("Trigger:invalid");
+ char_t buf[32];
+ snprintf(buf, sizeof(buf), "Trigger(%u)", (unsigned)c->componentId);
+ return jerry_string_sz(buf);
+}
+
+static void moduleTriggerInit(void) {
+ scriptProtoInit(
+ &MODULE_TRIGGER_PROTO, "Trigger",
+ sizeof(jscomponent_t), moduleTriggerCtor
+ );
+
+ scriptProtoDefineProp(
+ &MODULE_TRIGGER_PROTO, "entity", moduleTriggerGetEntity, NULL
+ );
+ scriptProtoDefineProp(
+ &MODULE_TRIGGER_PROTO, "id", moduleTriggerGetId, NULL
+ );
+ scriptProtoDefineProp(
+ &MODULE_TRIGGER_PROTO, "min", moduleTriggerGetMin, moduleTriggerSetMin
+ );
+ scriptProtoDefineProp(
+ &MODULE_TRIGGER_PROTO, "max", moduleTriggerGetMax, moduleTriggerSetMax
+ );
+ scriptProtoDefineFunc(
+ &MODULE_TRIGGER_PROTO, "setBounds", moduleTriggerSetBounds
+ );
+ scriptProtoDefineFunc(
+ &MODULE_TRIGGER_PROTO, "contains", moduleTriggerContains
+ );
+ scriptProtoDefineToString(&MODULE_TRIGGER_PROTO, moduleTriggerToString);
+}
+
+static void moduleTriggerDispose(void) {
+ scriptProtoDispose(&MODULE_TRIGGER_PROTO);
+}
diff --git a/src/dusk/script/module/modulelist.h b/src/dusk/script/module/modulelist.h
index f0000402..0ddb4b24 100644
--- a/src/dusk/script/module/modulelist.h
+++ b/src/dusk/script/module/modulelist.h
@@ -6,7 +6,9 @@
*/
#pragma once
+#include "script/module/asset/moduleasset.h"
#include "script/module/console/moduleconsole.h"
+#include "script/module/display/modulecolor.h"
#include "script/module/display/modulescreen.h"
#include "script/module/engine/moduleengine.h"
#include "script/module/entity/component/modulecomponentlist.h"
@@ -18,6 +20,9 @@
#include "script/module/system/modulesystem.h"
static void moduleListInit(void) {
+ moduleTextureInit();
+ moduleColorInit();
+ moduleAssetInit();
moduleConsoleInit();
moduleScreenInit();
moduleEngineInit();
@@ -41,4 +46,7 @@ static void moduleListDispose(void) {
moduleEngineDispose();
moduleScreenDispose();
moduleConsoleDispose();
+ moduleAssetDispose();
+ moduleColorDispose();
+ moduleTextureDispose();
}
diff --git a/test/asset/test_asset.c b/test/asset/test_asset.c
index 7f6d4b59..fdcefea8 100644
--- a/test/asset/test_asset.c
+++ b/test/asset/test_asset.c
@@ -267,6 +267,73 @@ static void test_update_error_slot_stays_occupied(void **state) {
assert_int_equal(memoryGetAllocatedCount(), 0);
}
+// ============================================================
+// assetEntryInit — input copy
+// ============================================================
+
+static void test_getEntry_null_input_stays_null(void **state) {
+ assetentry_t *entry = assetGetEntry("test.locale", ASSET_LOADER_TYPE_LOCALE, NULL);
+
+ assert_null(entry->input);
+ assert_int_equal(memoryGetAllocatedCount(), 0);
+}
+
+static void test_getEntry_input_copied_into_entry(void **state) {
+ assetloaderinput_t input;
+ memoryZero(&input, sizeof(input));
+ input.texture = (textureformat_t)42;
+
+ assetentry_t *entry = assetGetEntry("test.texture", ASSET_LOADER_TYPE_TEXTURE, &input);
+
+ // input must have been copied — entry->input must point inside the entry.
+ assert_non_null(entry->input);
+ assert_ptr_equal(entry->input, &entry->inputData);
+ assert_int_equal((int)entry->input->texture, 42);
+
+ assert_int_equal(memoryGetAllocatedCount(), 0);
+}
+
+// ============================================================
+// assetUpdate — re-entrant sync loader
+// ============================================================
+
+static assetentry_t *reentrant_inner_entry = NULL;
+static bool_t reentrant_inner_loaded = false;
+
+static errorret_t reentrant_stub_load(assetloading_t *loading) {
+ if(reentrant_inner_entry == NULL) {
+ // Simulate a script loading another asset from within its own sync step.
+ reentrant_inner_entry = assetGetEntry(
+ "inner.json", ASSET_LOADER_TYPE_JSON, NULL
+ );
+ errorret_t inner_ret = assetRequireLoaded(reentrant_inner_entry);
+ reentrant_inner_loaded = errorIsOk(inner_ret);
+ if(errorIsNotOk(inner_ret)) errorChain(inner_ret);
+ }
+ loading->entry->state = ASSET_ENTRY_STATE_LOADED;
+ errorOk();
+}
+
+static void test_update_reentrant_sync_loader(void **state) {
+ reentrant_inner_entry = NULL;
+ reentrant_inner_loaded = false;
+ // LOCALE uses the re-entrant loader; JSON keeps the default stub_load_success.
+ ASSET_LOADER_CALLBACKS[ASSET_LOADER_TYPE_LOCALE].loadSync = reentrant_stub_load;
+
+ assetentry_t *outer = assetGetEntry("outer.locale", ASSET_LOADER_TYPE_LOCALE, NULL);
+
+ errorret_t ret = assetRequireLoaded(outer);
+ assert_true(errorIsOk(ret));
+ assert_int_equal(outer->state, ASSET_ENTRY_STATE_LOADED);
+ // Verify the re-entrant load ran and completed successfully.
+ // (The inner entry may have been reaped by the time we check here,
+ // so we capture the result inside the callback rather than reading state.)
+ assert_non_null(reentrant_inner_entry);
+ assert_true(reentrant_inner_loaded);
+
+ assert_int_equal(memoryGetAllocatedCount(), 0);
+}
+
// ============================================================
// assetGetEntry — dedup against non-NOT_STARTED entries
// ============================================================
@@ -377,6 +444,8 @@ int main(void) {
cmocka_unit_test_setup_teardown(test_getEntry_dedup, asset_setup, asset_teardown),
cmocka_unit_test_setup_teardown(test_getEntry_distinct_names, asset_setup, asset_teardown),
cmocka_unit_test_setup_teardown(test_getEntry_returns_loaded_entry, asset_setup, asset_teardown),
+ cmocka_unit_test_setup_teardown(test_getEntry_null_input_stays_null, asset_setup, asset_teardown),
+ cmocka_unit_test_setup_teardown(test_getEntry_input_copied_into_entry, asset_setup, asset_teardown),
// assetUpdate — state machine
cmocka_unit_test_setup_teardown(test_update_noop_on_empty_table, asset_setup, asset_teardown),
@@ -388,6 +457,7 @@ int main(void) {
cmocka_unit_test_setup_teardown(test_update_overflow_queues_entries, asset_setup, asset_teardown),
cmocka_unit_test_setup_teardown(test_update_error_state, asset_setup, asset_teardown),
cmocka_unit_test_setup_teardown(test_update_error_slot_stays_occupied, asset_setup, asset_teardown),
+ cmocka_unit_test_setup_teardown(test_update_reentrant_sync_loader, asset_setup, asset_teardown),
// assetEntryDispose
cmocka_unit_test_setup_teardown(test_entry_dispose_clears_entry, asset_setup, asset_teardown),
diff --git a/types/asset/asset.d.ts b/types/asset/asset.d.ts
new file mode 100644
index 00000000..1389b06a
--- /dev/null
+++ b/types/asset/asset.d.ts
@@ -0,0 +1,72 @@
+/**
+ * Copyright (c) 2026 Dominic Masters
+ *
+ * This software is released under the MIT License.
+ * https://opensource.org/licenses/MIT
+ */
+
+/** Asset archive queries and cache management. */
+interface AssetNamespace {
+ // Loader type constants
+ readonly TYPE_MESH: number;
+ readonly TYPE_TEXTURE: number;
+ readonly TYPE_TILESET: number;
+ readonly TYPE_LOCALE: number;
+ readonly TYPE_JSON: number;
+ readonly TYPE_SCRIPT: number;
+
+ // Mesh axis input constants (pass as `input` to lock with TYPE_MESH)
+ readonly MESH_AXIS_Y_UP: number;
+ readonly MESH_AXIS_Z_UP: number;
+ readonly MESH_AXIS_X_UP: number;
+ readonly MESH_AXIS_Y_DOWN: number;
+ readonly MESH_AXIS_Z_DOWN: number;
+ readonly MESH_AXIS_X_DOWN: number;
+
+ /**
+ * Returns `true` if the given path exists in the asset archive (`dusk.dsk`).
+ *
+ * @param path - Archive-relative path, e.g. `"init.js"` or `"ui/hud.png"`.
+ */
+ exists(path: string): boolean;
+
+ /**
+ * Locks an entry in the asset cache and returns an `AssetEntry`.
+ * The entry begins loading in the background. Call `entry.requireLoaded()`
+ * to block until it is ready.
+ *
+ * The lock is released when the `AssetEntry` is GC'd or `entry.unlock()`
+ * is called explicitly.
+ *
+ * @param path - Archive-relative path.
+ * @param type - Loader type constant (`Asset.TYPE_*`).
+ * @param input - Optional loader-specific input constant.
+ * `TYPE_TEXTURE` → `Texture.FORMAT_*`
+ * `TYPE_MESH` → `Asset.MESH_AXIS_*`
+ *
+ * @example
+ * const entry = Asset.lock('data/map.json');
+ * entry.requireLoaded();
+ */
+ lock(path: string, type: number, input?: number): AssetEntry;
+
+ /**
+ * Blocks until the given entry is fully loaded.
+ * Returns the entry for chaining.
+ * @throws If the load fails.
+ *
+ * @example
+ * const entry = Asset.requireLoaded(Asset.lock('map.json', Asset.TYPE_JSON));
+ */
+ requireLoaded(entry: AssetEntry): AssetEntry;
+
+ /**
+ * Releases the lock on an asset by path.
+ * Prefer calling `entry.unlock()` on the `AssetEntry` object directly.
+ *
+ * @param path - The path originally passed to `lock`.
+ */
+ unlock(path: string): void;
+}
+
+declare var Asset: AssetNamespace;
diff --git a/types/asset/assetentry.d.ts b/types/asset/assetentry.d.ts
new file mode 100644
index 00000000..7d93918e
--- /dev/null
+++ b/types/asset/assetentry.d.ts
@@ -0,0 +1,54 @@
+/**
+ * Copyright (c) 2026 Dominic Masters
+ *
+ * This software is released under the MIT License.
+ * https://opensource.org/licenses/MIT
+ */
+
+/**
+ * A live reference to an entry in the asset cache.
+ * Holds a lock that keeps the entry alive; the lock is released automatically
+ * when the object is garbage collected, or immediately via `unlock()`.
+ */
+interface AssetEntry {
+ /** Archive-relative path used as the cache key. */
+ readonly name: string;
+ /** Current loading state — compare against `AssetEntry.*` state constants. */
+ readonly state: number;
+ /** Loader type — one of the `AssetEntry.TYPE_*` constants. */
+ readonly type: number;
+ /** `true` when the entry has fully loaded (`state === AssetEntry.LOADED`). */
+ readonly isLoaded: boolean;
+ /**
+ * Returns a `Texture` for this entry when it is a loaded texture asset.
+ * The `Texture` holds its own asset lock — independent of this `AssetEntry`.
+ * Returns `undefined` if the entry is not of type `Asset.TYPE_TEXTURE` or
+ * is not yet loaded.
+ */
+ readonly texture: Texture | undefined;
+ /**
+ * Blocks until the entry reaches `LOADED` (or `ERROR`).
+ * Returns `this` for chaining.
+ * @throws If the load fails.
+ */
+ requireLoaded(): this;
+ /**
+ * Releases the lock immediately.
+ * After this call the object is invalid — do not use it again.
+ */
+ unlock(): void;
+ toString(): string;
+}
+
+interface AssetEntryConstructor {
+ // Loading state constants
+ readonly NOT_STARTED: number;
+ readonly PENDING: number;
+ readonly LOADING: number;
+ readonly LOADED: number;
+ readonly ERROR: number;
+
+ new(): never;
+}
+
+declare var AssetEntry: AssetEntryConstructor;
diff --git a/types/console.d.ts b/types/console/console.d.ts
similarity index 81%
rename from types/console.d.ts
rename to types/console/console.d.ts
index c55e4f2f..dc5e1cc7 100644
--- a/types/console.d.ts
+++ b/types/console/console.d.ts
@@ -5,16 +5,12 @@
* https://opensource.org/licenses/MIT
*/
-/**
- * Interface for the in-game developer console.
- */
+/** Interface for the in-game developer console. */
interface ConsoleNamespace {
/**
* Prints one or more values to the in-game console, separated by tabs.
* Each argument is coerced to a string before printing.
*
- * @param args - Values to print.
- *
* @example
* Console.print("x =", player.x, "y =", player.y);
*/
@@ -23,9 +19,6 @@ interface ConsoleNamespace {
/**
* Whether the in-game console overlay is currently visible.
* Set to `true` to show the console, `false` to hide it.
- *
- * @example
- * Console.visible = true;
*/
visible: boolean;
}
diff --git a/types/display/color.d.ts b/types/display/color.d.ts
new file mode 100644
index 00000000..f6a0807e
--- /dev/null
+++ b/types/display/color.d.ts
@@ -0,0 +1,41 @@
+/**
+ * Copyright (c) 2026 Dominic Masters
+ *
+ * This software is released under the MIT License.
+ * https://opensource.org/licenses/MIT
+ */
+
+/** An RGBA color with channels in the range 0–255. */
+declare class Color {
+ /** @param r Red 0–255 (default 0) */
+ /** @param g Green 0–255 (default 0) */
+ /** @param b Blue 0–255 (default 0) */
+ /** @param a Alpha 0–255 (default 255) */
+ constructor(r?: number, g?: number, b?: number, a?: number);
+
+ r: number;
+ g: number;
+ b: number;
+ a: number;
+
+ toString(): string;
+
+ // Named color constants
+ static readonly WHITE: Color;
+ static readonly BLACK: Color;
+ static readonly RED: Color;
+ static readonly GREEN: Color;
+ static readonly BLUE: Color;
+ static readonly YELLOW: Color;
+ static readonly CYAN: Color;
+ static readonly MAGENTA: Color;
+ static readonly TRANSPARENT: Color;
+ static readonly GRAY: Color;
+ static readonly LIGHT_GRAY: Color;
+ static readonly DARK_GRAY: Color;
+ static readonly ORANGE: Color;
+ static readonly PURPLE: Color;
+ static readonly PINK: Color;
+ static readonly TEAL: Color;
+ static readonly CORNFLOWER_BLUE: Color;
+}
diff --git a/types/display/screen.d.ts b/types/display/screen.d.ts
new file mode 100644
index 00000000..1bbd8696
--- /dev/null
+++ b/types/display/screen.d.ts
@@ -0,0 +1,19 @@
+/**
+ * Copyright (c) 2026 Dominic Masters
+ *
+ * This software is released under the MIT License.
+ * https://opensource.org/licenses/MIT
+ */
+
+/** Read-only information about the current display surface. */
+interface ScreenNamespace {
+ /** Current render-target width in pixels. */
+ readonly width: number;
+ /** Current render-target height in pixels. */
+ readonly height: number;
+ /** Aspect ratio: `width / height`. */
+ readonly aspect: number;
+}
+
+/** Current display / render-target dimensions. */
+declare var Screen: ScreenNamespace;
diff --git a/types/display/texture.d.ts b/types/display/texture.d.ts
new file mode 100644
index 00000000..665ec7ad
--- /dev/null
+++ b/types/display/texture.d.ts
@@ -0,0 +1,28 @@
+/**
+ * Copyright (c) 2026 Dominic Masters
+ *
+ * This software is released under the MIT License.
+ * https://opensource.org/licenses/MIT
+ */
+
+/**
+ * A loaded texture asset. Holds a lock on the underlying asset entry —
+ * the lock is released automatically when the object is garbage collected.
+ */
+interface Texture {
+ /** Pixel width of the texture. */
+ readonly width: number;
+ /** Pixel height of the texture. */
+ readonly height: number;
+ toString(): string;
+}
+
+interface TextureConstructor {
+ /** RGBA 32-bit format (4 channels × 8 bits). */
+ readonly FORMAT_RGBA: number;
+ /** Paletted format. */
+ readonly FORMAT_PALETTE: number;
+ new(): never;
+}
+
+declare var Texture: TextureConstructor;
diff --git a/types/engine.d.ts b/types/engine/engine.d.ts
similarity index 50%
rename from types/engine.d.ts
rename to types/engine/engine.d.ts
index 55391d02..504ad743 100644
--- a/types/engine.d.ts
+++ b/types/engine/engine.d.ts
@@ -5,26 +5,17 @@
* https://opensource.org/licenses/MIT
*/
-/**
- * Controls over the engine main loop.
- */
+/** Controls over the engine main loop. */
interface EngineNamespace {
/**
- * Whether the engine main loop is still running (read-only).
+ * Whether the engine main loop is still running.
* Becomes `false` after `Engine.exit()` is called.
- *
- * @example
- * while (Engine.running) { ... }
*/
readonly running: boolean;
/**
- * Requests an orderly shutdown of the engine.
- * Sets the internal running flag to `false`; the main loop exits at the end
- * of the current tick.
- *
- * @example
- * Engine.exit();
+ * Requests an orderly shutdown. Sets `running` to `false`; the main loop
+ * exits at the end of the current tick.
*/
exit(): void;
}
diff --git a/types/entity.d.ts b/types/entity.d.ts
deleted file mode 100644
index dbb72ae0..00000000
--- a/types/entity.d.ts
+++ /dev/null
@@ -1,129 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-// ---------------------------------------------------------------------------
-// Component (generic base returned by entity.add() for untyped components)
-// ---------------------------------------------------------------------------
-
-/** Base component returned by entity.add() for component types without a specific module. */
-interface Component {
- /** Entity ID this component belongs to. */
- readonly entity: number;
- /** Component slot ID. */
- readonly id: number;
- toString(): string;
-}
-
-interface ComponentConstructor {
- /** Sentinel value for an invalid component ID. */
- readonly INVALID: number;
-
- // Component type constants (generated from componentlist.h)
- readonly POSITION: number;
- readonly CAMERA: number;
- readonly RENDERABLE: number;
- readonly PHYSICS: number;
- readonly TRIGGER: number;
- readonly OVERWORLD: number;
- readonly PLAYER: number;
- readonly INTERACTABLE: number;
- readonly OVERWORLD_CAMERA: number;
- readonly OVERWORLD_TRIGGER: number;
-
- new(): never;
-}
-
-declare var Component: ComponentConstructor;
-
-// ---------------------------------------------------------------------------
-// Position
-// ---------------------------------------------------------------------------
-
-/** Position/rotation/scale transform component with optional parent hierarchy. */
-interface Position extends Component {
- /**
- * Local-space position. Assigning a Vec3 writes to the C transform and
- * marks the world transform dirty. Reading returns a fresh Vec3 copy.
- */
- localPosition: Vec3;
- /** World-space position (accounts for all parent transforms). */
- worldPosition: Vec3;
- /** Local-space euler rotation in radians (XYZ). */
- localRotation: Vec3;
- /** World-space euler rotation in radians. */
- worldRotation: Vec3;
- /** Local-space scale. */
- localScale: Vec3;
- /** World-space scale. */
- worldScale: Vec3;
- /**
- * Orients the transform to look at a world-space target point.
- * Uses the current local position as the eye. Optionally specify an up
- * vector (defaults to Y-up).
- */
- lookAt(target: Vec3, up?: Vec3): void;
- /**
- * Sets this component's parent in the transform hierarchy.
- * Pass `null` or `undefined` to detach.
- */
- setParent(parent: Position | null | undefined): void;
- toString(): string;
-}
-
-interface PositionConstructor {
- new(): never;
-}
-
-declare var Position: PositionConstructor;
-
-// ---------------------------------------------------------------------------
-// Camera
-// ---------------------------------------------------------------------------
-
-/** Camera projection component. */
-interface Camera extends Component {
- /** Field of view in radians (perspective projections only). */
- fov: number;
- /** Near clip plane distance. */
- nearClip: number;
- /** Far clip plane distance. */
- farClip: number;
- /** Projection type — one of the Camera.PERSPECTIVE / Camera.ORTHOGRAPHIC constants. */
- projType: number;
- toString(): string;
-}
-
-interface CameraConstructor {
- readonly PERSPECTIVE: number;
- readonly PERSPECTIVE_FLIPPED: number;
- readonly ORTHOGRAPHIC: number;
- new(): never;
-}
-
-declare var Camera: CameraConstructor;
-
-// ---------------------------------------------------------------------------
-// Entity
-// ---------------------------------------------------------------------------
-
-interface Entity {
- /** Add a component of the given type and return a typed instance. */
- add(type: CameraConstructor["PERSPECTIVE"] | number): Component;
- toString(): string;
-}
-
-interface EntityConstructor {
- /** Sentinel value for an invalid entity ID. */
- readonly INVALID: number;
- /** Creates a new entity and returns it. Returns Entity.INVALID slot if the pool is full. */
- create(): Entity;
- /** Disposes the entity and all its components. */
- dispose(entity: Entity): void;
- new(): never;
-}
-
-declare var Entity: EntityConstructor;
diff --git a/types/entity/component.d.ts b/types/entity/component.d.ts
new file mode 100644
index 00000000..2820532c
--- /dev/null
+++ b/types/entity/component.d.ts
@@ -0,0 +1,39 @@
+/**
+ * Copyright (c) 2026 Dominic Masters
+ *
+ * This software is released under the MIT License.
+ * https://opensource.org/licenses/MIT
+ */
+
+/**
+ * Base type for all components. `entity.add()` returns a subtype when the
+ * component type has a dedicated module; cast with `as Position` etc. when
+ * you need the specific API.
+ */
+interface Component {
+ /** Entity ID this component belongs to. */
+ readonly entity: number;
+ /** Component slot index. */
+ readonly id: number;
+ toString(): string;
+}
+
+interface ComponentConstructor {
+ /** Sentinel for an invalid component ID. */
+ readonly INVALID: number;
+
+ readonly POSITION: number;
+ readonly CAMERA: number;
+ readonly RENDERABLE: number;
+ readonly PHYSICS: number;
+ readonly TRIGGER: number;
+ readonly OVERWORLD: number;
+ readonly PLAYER: number;
+ readonly INTERACTABLE: number;
+ readonly OVERWORLD_CAMERA: number;
+ readonly OVERWORLD_TRIGGER: number;
+
+ new(): never;
+}
+
+declare var Component: ComponentConstructor;
diff --git a/types/entity/component/camera.d.ts b/types/entity/component/camera.d.ts
new file mode 100644
index 00000000..d7771796
--- /dev/null
+++ b/types/entity/component/camera.d.ts
@@ -0,0 +1,28 @@
+/**
+ * Copyright (c) 2026 Dominic Masters
+ *
+ * This software is released under the MIT License.
+ * https://opensource.org/licenses/MIT
+ */
+
+/** Camera projection component. Only one camera is active at a time. */
+interface Camera extends Component {
+ /** Field of view in radians (perspective only). */
+ fov: number;
+ /** Near clip plane distance. */
+ nearClip: number;
+ /** Far clip plane distance. */
+ farClip: number;
+ /** One of `Camera.PERSPECTIVE`, `Camera.PERSPECTIVE_FLIPPED`, or `Camera.ORTHOGRAPHIC`. */
+ projType: number;
+ toString(): string;
+}
+
+interface CameraConstructor {
+ readonly PERSPECTIVE: number;
+ readonly PERSPECTIVE_FLIPPED: number;
+ readonly ORTHOGRAPHIC: number;
+ new(): never;
+}
+
+declare var Camera: CameraConstructor;
diff --git a/types/entity/component/physics.d.ts b/types/entity/component/physics.d.ts
new file mode 100644
index 00000000..c4616a24
--- /dev/null
+++ b/types/entity/component/physics.d.ts
@@ -0,0 +1,39 @@
+/**
+ * Copyright (c) 2026 Dominic Masters
+ *
+ * This software is released under the MIT License.
+ * https://opensource.org/licenses/MIT
+ */
+
+/** Physics body component. */
+interface Physics extends Component {
+ /** Body simulation type — `Physics.STATIC`, `DYNAMIC`, or `KINEMATIC`. */
+ bodyType: number;
+ /**
+ * Collision shape type — one of the `Physics.SHAPE_*` constants.
+ * Changing the type preserves existing velocity and ground state.
+ */
+ shape: number;
+ /** Current linear velocity (Vec3). */
+ velocity: Vec3;
+ /** Gravity multiplier. 0 = no gravity, 1 = full, negative = inverted. */
+ gravityScale: number;
+ /** `true` if the body rested on a surface during the last simulation step. */
+ readonly onGround: boolean;
+ /** Applies an instantaneous velocity change. No-op on STATIC bodies. */
+ applyImpulse(impulse: Vec3): void;
+ toString(): string;
+}
+
+interface PhysicsConstructor {
+ readonly STATIC: number;
+ readonly DYNAMIC: number;
+ readonly KINEMATIC: number;
+ readonly SHAPE_CUBE: number;
+ readonly SHAPE_SPHERE: number;
+ readonly SHAPE_CAPSULE: number;
+ readonly SHAPE_PLANE: number;
+ new(): never;
+}
+
+declare var Physics: PhysicsConstructor;
diff --git a/types/entity/component/position.d.ts b/types/entity/component/position.d.ts
new file mode 100644
index 00000000..5f7addfe
--- /dev/null
+++ b/types/entity/component/position.d.ts
@@ -0,0 +1,41 @@
+/**
+ * Copyright (c) 2026 Dominic Masters
+ *
+ * This software is released under the MIT License.
+ * https://opensource.org/licenses/MIT
+ */
+
+/** Transform component — local/world PRS with an optional parent hierarchy. */
+interface Position extends Component {
+ /**
+ * Local-space position. Reading returns a Vec3 copy; assigning a Vec3
+ * writes through to the C transform and marks descendants dirty.
+ */
+ localPosition: Vec3;
+ /** World-space position (full parent-chain applied). */
+ worldPosition: Vec3;
+ /** Local euler rotation in radians (XYZ order). */
+ localRotation: Vec3;
+ /** World euler rotation in radians. */
+ worldRotation: Vec3;
+ /** Local scale. */
+ localScale: Vec3;
+ /** World scale (extracted from parent-chain matrix). */
+ worldScale: Vec3;
+ /**
+ * Orients the transform so it faces `target`. Uses the current local
+ * position as the eye. `up` defaults to world Y-up.
+ */
+ lookAt(target: Vec3, up?: Vec3): void;
+ /**
+ * Attaches this transform to a parent. Pass `null` / `undefined` to detach.
+ */
+ setParent(parent: Position | null | undefined): void;
+ toString(): string;
+}
+
+interface PositionConstructor {
+ new(): never;
+}
+
+declare var Position: PositionConstructor;
diff --git a/types/entity/component/renderable.d.ts b/types/entity/component/renderable.d.ts
new file mode 100644
index 00000000..c20b30b0
--- /dev/null
+++ b/types/entity/component/renderable.d.ts
@@ -0,0 +1,71 @@
+/**
+ * Copyright (c) 2026 Dominic Masters
+ *
+ * This software is released under the MIT License.
+ * https://opensource.org/licenses/MIT
+ */
+
+/** Fields shared by every renderable type. */
+interface Renderable extends Component {
+ /** Current render type — one of the `Renderable.*` type constants. */
+ type: number;
+ /** Render priority. 0 = auto. Higher = drawn later. */
+ priority: number;
+ toString(): string;
+}
+
+/**
+ * Renderable in `SHADER_MATERIAL` mode (default after `entity.add`).
+ * Renders a mesh with the unlit shader.
+ */
+interface RenderableMaterial extends Renderable {
+ /**
+ * Unlit material color. Reading returns a fresh `Color` copy; assigning
+ * a `Color` instance writes through to the C material.
+ *
+ * @example
+ * r.color = Color.RED;
+ * r.color = new Color(255, 128, 0);
+ */
+ color: Color;
+}
+
+/**
+ * Renderable in `SPRITEBATCH` mode.
+ * Activated by calling `setTexture`.
+ */
+interface RenderableSpritebatch extends Renderable {
+ /**
+ * Assigns a texture and switches to `SPRITEBATCH` mode.
+ * The Texture object is pinned (GC-safe) for the component's lifetime.
+ */
+ setTexture(texture: Texture): void;
+ /**
+ * Adds a sprite quad to the spritebatch.
+ *
+ * 3D form (10 args): `addSprite(x1,y1,z1, x2,y2,z2, u1,v1, u2,v2)`
+ * 2D form (8 args): `addSprite(x1,y1, x2,y2, u1,v1, u2,v2)` — z defaults to 0
+ */
+ addSprite(
+ x1: number, y1: number, z1OrX2: number,
+ x2OrY2: number, y2OrZ2?: number, z2?: number,
+ u1?: number, v1?: number, u2?: number, v2?: number
+ ): void;
+ /** Resets the sprite count to zero. */
+ clearSprites(): void;
+}
+
+/**
+ * Renderable in `CUSTOM` mode.
+ * Draw logic is provided by a C callback set via `entityRenderableSetDraw`.
+ */
+interface RenderableCustom extends Renderable {}
+
+interface RenderableConstructor {
+ readonly SHADER_MATERIAL: number;
+ readonly SPRITEBATCH: number;
+ readonly CUSTOM: number;
+ new(): never;
+}
+
+declare var Renderable: RenderableConstructor;
diff --git a/types/entity/component/trigger.d.ts b/types/entity/component/trigger.d.ts
new file mode 100644
index 00000000..ac86c20b
--- /dev/null
+++ b/types/entity/component/trigger.d.ts
@@ -0,0 +1,25 @@
+/**
+ * Copyright (c) 2026 Dominic Masters
+ *
+ * This software is released under the MIT License.
+ * https://opensource.org/licenses/MIT
+ */
+
+/** AABB trigger volume. `contains` tests whether a world point is inside. */
+interface Trigger extends Component {
+ /** Minimum corner of the AABB (Vec3). */
+ min: Vec3;
+ /** Maximum corner of the AABB (Vec3). */
+ max: Vec3;
+ /** Sets both corners at once. */
+ setBounds(min: Vec3, max: Vec3): void;
+ /** Returns `true` if `point` is inside `[min, max]`. */
+ contains(point: Vec3): boolean;
+ toString(): string;
+}
+
+interface TriggerConstructor {
+ new(): never;
+}
+
+declare var Trigger: TriggerConstructor;
diff --git a/types/entity/entity.d.ts b/types/entity/entity.d.ts
new file mode 100644
index 00000000..46d0808c
--- /dev/null
+++ b/types/entity/entity.d.ts
@@ -0,0 +1,29 @@
+/**
+ * Copyright (c) 2026 Dominic Masters
+ *
+ * This software is released under the MIT License.
+ * https://opensource.org/licenses/MIT
+ */
+
+interface Entity {
+ /**
+ * Adds a component of the given type and returns it.
+ * Returns a typed subclass when the component has a dedicated module
+ * (`Position`, `Camera`, `Renderable`, `Trigger`, `Physics`); otherwise
+ * returns the base `Component`. Cast with `as Position` etc. when needed.
+ */
+ add(type: number): Component;
+ toString(): string;
+}
+
+interface EntityConstructor {
+ /** Sentinel for an invalid entity ID. */
+ readonly INVALID: number;
+ /** Allocates a new entity from the fixed pool (max 64). */
+ create(): Entity;
+ /** Disposes the entity and all of its components. */
+ dispose(entity: Entity): void;
+ new(): never;
+}
+
+declare var Entity: EntityConstructor;
diff --git a/types/index.d.ts b/types/index.d.ts
index cebc47f4..f9a497df 100644
--- a/types/index.d.ts
+++ b/types/index.d.ts
@@ -14,11 +14,30 @@
* { "compilerOptions": { "typeRoots": ["./types"] } }
*/
-///
-///
-///
-///
-///
-///
-///
-///
+// math
+///
+
+// display
+///
+///
+///
+
+// asset
+///
+///
+
+// engine systems
+///
+///
+///
+///
+///
+
+// entity / components
+///
+///
+///
+///
+///
+///
+///
diff --git a/types/input.d.ts b/types/input.d.ts
deleted file mode 100644
index 07c645bd..00000000
--- a/types/input.d.ts
+++ /dev/null
@@ -1,115 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-/**
- * Opaque type alias for input action identifiers.
- * Use the `INPUT_ACTION_*` constants rather than raw numbers.
- */
-type InputAction = number;
-
-/**
- * Polling-based input queries and button rebinding.
- */
-interface InputNamespace {
- /**
- * Returns `true` while the given action is held down this frame.
- *
- * @param action - An `INPUT_ACTION_*` constant.
- *
- * @example
- * if (Input.isDown(INPUT_ACTION_UP)) { player.moveUp(); }
- */
- isDown(action: InputAction): boolean;
-
- /**
- * Returns `true` if the given action was held down in the previous frame.
- *
- * @param action - An `INPUT_ACTION_*` constant.
- */
- wasDown(action: InputAction): boolean;
-
- /**
- * Returns `true` on the first frame the action transitions from up → down.
- *
- * @param action - An `INPUT_ACTION_*` constant.
- *
- * @example
- * if (Input.pressed(INPUT_ACTION_ACCEPT)) { confirmSelection(); }
- */
- pressed(action: InputAction): boolean;
-
- /**
- * Returns `true` on the first frame the action transitions from down → up.
- *
- * @param action - An `INPUT_ACTION_*` constant.
- */
- released(action: InputAction): boolean;
-
- /**
- * Returns the continuous (analog) value of an action in the range `0.0–1.0`.
- * Digital buttons return either `0` or `1`.
- *
- * @param action - An `INPUT_ACTION_*` constant.
- *
- * @example
- * const speed = Input.getValue(INPUT_ACTION_UP) * MAX_SPEED;
- */
- getValue(action: InputAction): number;
-
- /**
- * Returns a signed axis value in the range `-1.0–1.0`, derived from two
- * opposing actions: `getValue(pos) - getValue(neg)`.
- *
- * @param neg - Action mapped to the negative direction.
- * @param pos - Action mapped to the positive direction.
- *
- * @example
- * const moveX = Input.axis(INPUT_ACTION_LEFT, INPUT_ACTION_RIGHT);
- */
- axis(neg: InputAction, pos: InputAction): number;
-
- /**
- * Rebinds a physical button (by name) to a logical input action at runtime.
- * Throws if `buttonName` is unknown or `action` is out of range.
- *
- * @param buttonName - Platform-specific button identifier string (e.g. `"A"`, `"START"`).
- * @param action - Target `INPUT_ACTION_*` constant.
- *
- * @example
- * Input.bind("A", INPUT_ACTION_ACCEPT);
- */
- bind(buttonName: string, action: InputAction): void;
-}
-
-/** Polling-based input system. */
-declare var Input: InputNamespace;
-
-// ---------------------------------------------------------------------------
-// Input action constants.
-// Injected as plain global variables by the engine at startup.
-// ---------------------------------------------------------------------------
-
-/** Move / navigate upward. */
-declare var INPUT_ACTION_UP: InputAction;
-/** Move / navigate downward. */
-declare var INPUT_ACTION_DOWN: InputAction;
-/** Move / navigate left. */
-declare var INPUT_ACTION_LEFT: InputAction;
-/** Move / navigate right. */
-declare var INPUT_ACTION_RIGHT: InputAction;
-/** Confirm / accept the current selection. */
-declare var INPUT_ACTION_ACCEPT: InputAction;
-/** Cancel / go back. */
-declare var INPUT_ACTION_CANCEL: InputAction;
-/** Emergency quit (e.g. hold-to-exit on embedded platforms). */
-declare var INPUT_ACTION_RAGEQUIT: InputAction;
-/** Toggle the developer console overlay. */
-declare var INPUT_ACTION_CONSOLE: InputAction;
-/** Pointer / cursor horizontal position (analog). */
-declare var INPUT_ACTION_POINTERX: InputAction;
-/** Pointer / cursor vertical position (analog). */
-declare var INPUT_ACTION_POINTERY: InputAction;
diff --git a/types/input/input.d.ts b/types/input/input.d.ts
new file mode 100644
index 00000000..e39a2f24
--- /dev/null
+++ b/types/input/input.d.ts
@@ -0,0 +1,50 @@
+/**
+ * Copyright (c) 2026 Dominic Masters
+ *
+ * This software is released under the MIT License.
+ * https://opensource.org/licenses/MIT
+ */
+
+/** Opaque type alias for input action identifiers. */
+type InputAction = number;
+
+/** Polling-based input queries and button rebinding. */
+interface InputNamespace {
+ /** Returns `true` while the given action is held down this frame. */
+ isDown(action: InputAction): boolean;
+ /** Returns `true` if the given action was held down last frame. */
+ wasDown(action: InputAction): boolean;
+ /** Returns `true` on the first frame the action transitions up → down. */
+ pressed(action: InputAction): boolean;
+ /** Returns `true` on the first frame the action transitions down → up. */
+ released(action: InputAction): boolean;
+ /** Continuous (analog) value in `0.0–1.0`. Digital buttons return 0 or 1. */
+ getValue(action: InputAction): number;
+ /**
+ * Signed axis value `-1.0–1.0`: `getValue(pos) - getValue(neg)`.
+ * @param neg - Action mapped to the negative direction.
+ * @param pos - Action mapped to the positive direction.
+ */
+ axis(neg: InputAction, pos: InputAction): number;
+ /**
+ * Rebinds a physical button to a logical action at runtime.
+ * @param buttonName - Platform-specific button name, e.g. `"A"`, `"START"`.
+ * @param action - Target `INPUT_ACTION_*` constant.
+ */
+ bind(buttonName: string, action: InputAction): void;
+}
+
+/** Polling-based input system. */
+declare var Input: InputNamespace;
+
+// Input action constants — injected as globals by the engine at startup.
+declare var INPUT_ACTION_UP: InputAction;
+declare var INPUT_ACTION_DOWN: InputAction;
+declare var INPUT_ACTION_LEFT: InputAction;
+declare var INPUT_ACTION_RIGHT: InputAction;
+declare var INPUT_ACTION_ACCEPT: InputAction;
+declare var INPUT_ACTION_CANCEL: InputAction;
+declare var INPUT_ACTION_RAGEQUIT: InputAction;
+declare var INPUT_ACTION_CONSOLE: InputAction;
+declare var INPUT_ACTION_POINTERX: InputAction;
+declare var INPUT_ACTION_POINTERY: InputAction;
diff --git a/types/vec3.d.ts b/types/math/vec3.d.ts
similarity index 100%
rename from types/vec3.d.ts
rename to types/math/vec3.d.ts
diff --git a/types/scene.d.ts b/types/scene/scene.d.ts
similarity index 77%
rename from types/scene.d.ts
rename to types/scene/scene.d.ts
index 79f1dc15..1769b3a4 100644
--- a/types/scene.d.ts
+++ b/types/scene/scene.d.ts
@@ -7,21 +7,18 @@
/** Scene management — request scene transitions and query the active scene. */
interface SceneNamespace {
- /** The type constant of the currently active scene, or 0 if none. */
+ /** Type constant of the currently active scene, or 0 if none. */
readonly current: number;
/**
* Requests a scene transition. The change takes effect at the start of the
* next safe update tick (current scene is disposed, new scene is initialized).
*
- * @param type - A `Scene.*` scene type constant.
- *
* @example
* Scene.set(Scene.OVERWORLD);
*/
set(type: number): void;
- // Scene type constants (generated from scenelist.h)
readonly INITIAL: number;
readonly TEST: number;
readonly OVERWORLD: number;
diff --git a/types/screen.d.ts b/types/screen.d.ts
deleted file mode 100644
index 1b303ffa..00000000
--- a/types/screen.d.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-/**
- * Read-only information about the current display surface.
- */
-interface ScreenNamespace {
- /** Current render-target width in pixels (read-only). */
- readonly width: number;
-
- /** Current render-target height in pixels (read-only). */
- readonly height: number;
-
- /**
- * Aspect ratio of the current render target: `width / height` (read-only).
- *
- * @example
- * if (Screen.aspect > 1) { /* landscape *\/ }
- */
- readonly aspect: number;
-}
-
-/** Current display / render-target dimensions. */
-declare var Screen: ScreenNamespace;
diff --git a/types/system.d.ts b/types/system/system.d.ts
similarity index 67%
rename from types/system.d.ts
rename to types/system/system.d.ts
index beceddf4..870e0ca1 100644
--- a/types/system.d.ts
+++ b/types/system/system.d.ts
@@ -5,12 +5,10 @@
* https://opensource.org/licenses/MIT
*/
-/**
- * Runtime platform detection and system-level information.
- */
+/** Runtime platform detection. */
interface SystemNamespace {
/**
- * Numeric identifier for the platform the engine is running on (read-only).
+ * Numeric identifier for the current platform.
* Compare against the `System.PLATFORM_*` constants.
*
* @example
@@ -18,19 +16,10 @@ interface SystemNamespace {
*/
readonly platform: number;
- /** Linux desktop. */
readonly PLATFORM_LINUX: number;
-
- /** Knulli handheld (Linux-based). */
readonly PLATFORM_KNULLI: number;
-
- /** Sony PlayStation Portable. */
readonly PLATFORM_PSP: number;
-
- /** Nintendo GameCube. */
readonly PLATFORM_GAMECUBE: number;
-
- /** Nintendo Wii. */
readonly PLATFORM_WII: number;
}