diff --git a/assets/testentity.js b/assets/testentity.js
index 273cd11e..8d3623bb 100644
--- a/assets/testentity.js
+++ b/assets/testentity.js
@@ -1,4 +1,8 @@
-const e = Entity.create();
-const pos = e.add(Component.POSITION);
-pos.localPosition = new Vec3(-1, 0, 1);
-Console.print('Entity ID: ' + e.toString());
+const cam = Entity.create();
+const pos = cam.add(Component.POSITION);
+cam.add(Component.CAMERA);
+
+pos.localPosition = new Vec3(3, 3, 3);
+pos.lookAt(new Vec3(0, 0, 0));
+
+Console.print('Camera entity ID: ' + cam.toString());
diff --git a/src/dusk/scene/initial/initialscene.c b/src/dusk/scene/initial/initialscene.c
index ce7575d5..7c70abdd 100644
--- a/src/dusk/scene/initial/initialscene.c
+++ b/src/dusk/scene/initial/initialscene.c
@@ -9,29 +9,27 @@
#include "console/console.h"
#include "scene/scene.h"
#include "script/script.h"
-#include "time/time.h"
-#include "ui/uiloading.h"
+#include "entity/entitymanager.h"
+#include "entity/entity.h"
+#include "entity/component/display/entityposition.h"
+#include "entity/component/display/entityrenderable.h"
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")));
- // SCENE.data.initial.timer = 0.0f;
- // SCENE.data.initial.hiding = false;
- // uiLoadingShow(NULL, NULL);
}
errorret_t initialSceneUpdate(void) {
- // initialscene_t *scene = &SCENE.data.initial;
- // if(scene->hiding) errorOk();
-
- // scene->timer += TIME.delta;
- // if(scene->timer >= INITIAL_SCENE_WAIT) {
- // scene->hiding = true;
- // uiLoadingHide(NULL, NULL);
- // }
-
errorOk();
}
void initialSceneDispose(void) {
+ entityDispose(SCENE.data.initial.cubeEntityId);
}
diff --git a/src/dusk/scene/initial/initialscene.h b/src/dusk/scene/initial/initialscene.h
index 966eec4d..fb3d255d 100644
--- a/src/dusk/scene/initial/initialscene.h
+++ b/src/dusk/scene/initial/initialscene.h
@@ -7,12 +7,10 @@
#pragma once
#include "error/error.h"
-
-#define INITIAL_SCENE_WAIT 2.0f
+#include "entity/entitybase.h"
typedef struct {
- float_t timer;
- bool_t hiding;
+ entityid_t cubeEntityId;
} initialscene_t;
void initialSceneInit(void);
diff --git a/src/dusk/script/module/entity/component/camera/modulecamera.h b/src/dusk/script/module/entity/component/camera/modulecamera.h
new file mode 100644
index 00000000..d12f0dc6
--- /dev/null
+++ b/src/dusk/script/module/entity/component/camera/modulecamera.h
@@ -0,0 +1,174 @@
+/**
+ * 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/entity/modulecomponent.h"
+#include "entity/component/display/entitycamera.h"
+
+static scriptproto_t MODULE_CAMERA_PROTO;
+
+moduleBaseFunction(moduleCameraCtor) {
+ (void)callInfo; (void)args; (void)argc;
+ return moduleBaseThrow("Camera cannot be instantiated with new");
+}
+
+static inline jscomponent_t *moduleCameraSelf(
+ const jerry_call_info_t *callInfo
+) {
+ return (jscomponent_t *)scriptProtoGetValue(
+ &MODULE_CAMERA_PROTO, callInfo->this_value
+ );
+}
+
+static inline entitycamera_t *moduleCameraData(const jscomponent_t *c) {
+ return (entitycamera_t *)componentGetData(
+ c->entityId, c->componentId, COMPONENT_TYPE_CAMERA
+ );
+}
+
+moduleBaseFunction(moduleCameraGetEntity) {
+ jscomponent_t *c = moduleCameraSelf(callInfo);
+ if(!c) return jerry_undefined();
+ return jerry_number((double)c->entityId);
+}
+
+moduleBaseFunction(moduleCameraGetId) {
+ jscomponent_t *c = moduleCameraSelf(callInfo);
+ if(!c) return jerry_undefined();
+ return jerry_number((double)c->componentId);
+}
+
+moduleBaseFunction(moduleCameraGetFov) {
+ jscomponent_t *c = moduleCameraSelf(callInfo);
+ if(!c) return jerry_undefined();
+ entitycamera_t *cam = moduleCameraData(c);
+ if(!cam) return jerry_undefined();
+ return jerry_number((double)cam->perspective.fov);
+}
+
+moduleBaseFunction(moduleCameraSetFov) {
+ moduleBaseRequireArgs(1);
+ jscomponent_t *c = moduleCameraSelf(callInfo);
+ if(!c) return jerry_undefined();
+ entitycamera_t *cam = moduleCameraData(c);
+ if(!cam) return jerry_undefined();
+ cam->perspective.fov = moduleBaseArgFloat(0);
+ return jerry_undefined();
+}
+
+moduleBaseFunction(moduleCameraGetNearClip) {
+ jscomponent_t *c = moduleCameraSelf(callInfo);
+ if(!c) return jerry_undefined();
+ entitycamera_t *cam = moduleCameraData(c);
+ if(!cam) return jerry_undefined();
+ return jerry_number((double)cam->nearClip);
+}
+
+moduleBaseFunction(moduleCameraSetNearClip) {
+ moduleBaseRequireArgs(1);
+ jscomponent_t *c = moduleCameraSelf(callInfo);
+ if(!c) return jerry_undefined();
+ entitycamera_t *cam = moduleCameraData(c);
+ if(!cam) return jerry_undefined();
+ cam->nearClip = moduleBaseArgFloat(0);
+ return jerry_undefined();
+}
+
+moduleBaseFunction(moduleCameraGetFarClip) {
+ jscomponent_t *c = moduleCameraSelf(callInfo);
+ if(!c) return jerry_undefined();
+ entitycamera_t *cam = moduleCameraData(c);
+ if(!cam) return jerry_undefined();
+ return jerry_number((double)cam->farClip);
+}
+
+moduleBaseFunction(moduleCameraSetFarClip) {
+ moduleBaseRequireArgs(1);
+ jscomponent_t *c = moduleCameraSelf(callInfo);
+ if(!c) return jerry_undefined();
+ entitycamera_t *cam = moduleCameraData(c);
+ if(!cam) return jerry_undefined();
+ cam->farClip = moduleBaseArgFloat(0);
+ return jerry_undefined();
+}
+
+moduleBaseFunction(moduleCameraGetProjType) {
+ jscomponent_t *c = moduleCameraSelf(callInfo);
+ if(!c) return jerry_undefined();
+ entitycamera_t *cam = moduleCameraData(c);
+ if(!cam) return jerry_undefined();
+ return jerry_number((double)cam->projType);
+}
+
+moduleBaseFunction(moduleCameraSetProjType) {
+ moduleBaseRequireArgs(1);
+ jscomponent_t *c = moduleCameraSelf(callInfo);
+ if(!c) return jerry_undefined();
+ entitycamera_t *cam = moduleCameraData(c);
+ if(!cam) return jerry_undefined();
+ cam->projType = (entitycameraprojectiontype_t)moduleBaseArgInt(0);
+ return jerry_undefined();
+}
+
+moduleBaseFunction(moduleCameraToString) {
+ jscomponent_t *c = moduleCameraSelf(callInfo);
+ if(!c) return jerry_string_sz("Camera:invalid");
+ char_t buf[32];
+ snprintf(buf, sizeof(buf), "Camera(%u)", (unsigned)c->componentId);
+ return jerry_string_sz(buf);
+}
+
+static void moduleCameraInit(void) {
+ scriptProtoInit(
+ &MODULE_CAMERA_PROTO, "Camera",
+ sizeof(jscomponent_t), moduleCameraCtor
+ );
+
+ scriptProtoDefineProp(
+ &MODULE_CAMERA_PROTO, "entity", moduleCameraGetEntity, NULL
+ );
+ scriptProtoDefineProp(
+ &MODULE_CAMERA_PROTO, "id", moduleCameraGetId, NULL
+ );
+ scriptProtoDefineProp(
+ &MODULE_CAMERA_PROTO, "fov", moduleCameraGetFov, moduleCameraSetFov
+ );
+ scriptProtoDefineProp(
+ &MODULE_CAMERA_PROTO, "nearClip",
+ moduleCameraGetNearClip, moduleCameraSetNearClip
+ );
+ scriptProtoDefineProp(
+ &MODULE_CAMERA_PROTO, "farClip",
+ moduleCameraGetFarClip, moduleCameraSetFarClip
+ );
+ scriptProtoDefineProp(
+ &MODULE_CAMERA_PROTO, "projType",
+ moduleCameraGetProjType, moduleCameraSetProjType
+ );
+ scriptProtoDefineToString(&MODULE_CAMERA_PROTO, moduleCameraToString);
+
+ /* Camera.PERSPECTIVE, Camera.PERSPECTIVE_FLIPPED, Camera.ORTHOGRAPHIC */
+ jerry_value_t ctor = MODULE_CAMERA_PROTO.constructor;
+ struct { const char_t *name; int val; } projtypes[] = {
+ { "PERSPECTIVE", ENTITY_CAMERA_PROJECTION_TYPE_PERSPECTIVE },
+ { "PERSPECTIVE_FLIPPED", ENTITY_CAMERA_PROJECTION_TYPE_PERSPECTIVE_FLIPPED },
+ { "ORTHOGRAPHIC", ENTITY_CAMERA_PROJECTION_TYPE_ORTHOGRAPHIC },
+ };
+ for(int i = 0; i < 3; i++) {
+ jerry_value_t k = jerry_string_sz(projtypes[i].name);
+ jerry_value_t v = jerry_number((double)projtypes[i].val);
+ jerry_object_set(ctor, k, v);
+ jerry_value_free(v);
+ jerry_value_free(k);
+ }
+}
+
+static void moduleCameraDispose(void) {
+ scriptProtoDispose(&MODULE_CAMERA_PROTO);
+}
diff --git a/src/dusk/script/module/entity/component/modulecomponentlist.h b/src/dusk/script/module/entity/component/modulecomponentlist.h
index 02182b7e..463bfdd8 100644
--- a/src/dusk/script/module/entity/component/modulecomponentlist.h
+++ b/src/dusk/script/module/entity/component/modulecomponentlist.h
@@ -7,6 +7,7 @@
#pragma once
#include "script/module/entity/modulecomponent.h"
+#include "camera/modulecamera.h"
#include "position/moduleposition.h"
/**
@@ -20,6 +21,8 @@ static jerry_value_t moduleComponentListCreateInstance(
switch(type) {
case COMPONENT_TYPE_POSITION:
return scriptProtoCreateValue(&MODULE_POSITION_PROTO, comp);
+ case COMPONENT_TYPE_CAMERA:
+ return scriptProtoCreateValue(&MODULE_CAMERA_PROTO, comp);
default:
return scriptProtoCreateValue(&MODULE_COMPONENT_PROTO, comp);
}
@@ -27,8 +30,10 @@ static jerry_value_t moduleComponentListCreateInstance(
static void moduleComponentListInit(void) {
modulePositionInit();
+ moduleCameraInit();
}
static void moduleComponentListDispose(void) {
+ moduleCameraDispose();
modulePositionDispose();
}
diff --git a/src/dusk/script/module/entity/component/position/moduleposition.h b/src/dusk/script/module/entity/component/position/moduleposition.h
index 85723dc9..db9a89b3 100644
--- a/src/dusk/script/module/entity/component/position/moduleposition.h
+++ b/src/dusk/script/module/entity/component/position/moduleposition.h
@@ -147,6 +147,26 @@ moduleBaseFunction(modulePositionSetWorldScale) {
return jerry_undefined();
}
+moduleBaseFunction(modulePositionLookAt) {
+ jscomponent_t *c = modulePositionSelf(callInfo);
+ if(!c) return jerry_undefined();
+ moduleBaseRequireArgs(1);
+ float_t *target = moduleVec3From(args[0]);
+ if(!target) return moduleBaseThrow("Position.lookAt: expected Vec3 target");
+
+ vec3 eye;
+ entityPositionGetLocalPosition(c->entityId, c->componentId, eye);
+
+ vec3 up = { 0.0f, 1.0f, 0.0f };
+ if(argc >= 2) {
+ float_t *upArg = moduleVec3From(args[1]);
+ if(upArg) glm_vec3_copy(upArg, up);
+ }
+
+ entityPositionLookAt(c->entityId, c->componentId, eye, target, up);
+ return jerry_undefined();
+}
+
moduleBaseFunction(modulePositionSetParent) {
jscomponent_t *c = modulePositionSelf(callInfo);
if(!c) return jerry_undefined();
@@ -214,6 +234,9 @@ static void modulePositionInit(void) {
&MODULE_POSITION_PROTO, "worldScale",
modulePositionGetWorldScale, modulePositionSetWorldScale
);
+ scriptProtoDefineFunc(
+ &MODULE_POSITION_PROTO, "lookAt", modulePositionLookAt
+ );
scriptProtoDefineFunc(
&MODULE_POSITION_PROTO, "setParent", modulePositionSetParent
);
diff --git a/src/dusk/script/module/scene/modulescene.h b/src/dusk/script/module/scene/modulescene.h
index 4ea309cd..20f576ce 100644
--- a/src/dusk/script/module/scene/modulescene.h
+++ b/src/dusk/script/module/scene/modulescene.h
@@ -12,8 +12,43 @@
static scriptproto_t MODULE_SCENE_PROTO;
+moduleBaseFunction(moduleSceneGetCurrent) {
+ return jerry_number((double)SCENE.type);
+}
+
+moduleBaseFunction(moduleSceneSet) {
+ moduleBaseRequireArgs(1);
+ moduleBaseRequireNumber(0);
+ const scenetype_t type = (scenetype_t)moduleBaseArgInt(0);
+ if(type <= SCENE_TYPE_NULL || type >= SCENE_TYPE_COUNT) {
+ return moduleBaseThrow("Scene.set: invalid scene type");
+ }
+ sceneSet(type);
+ return jerry_undefined();
+}
+
static void moduleSceneInit(void) {
scriptProtoInit(&MODULE_SCENE_PROTO, "Scene", sizeof(uint8_t), NULL);
+
+ scriptProtoDefineStaticProp(
+ &MODULE_SCENE_PROTO, "current", moduleSceneGetCurrent, NULL
+ );
+ scriptProtoDefineStaticFunc(
+ &MODULE_SCENE_PROTO, "set", moduleSceneSet
+ );
+
+ /* Scene.INITIAL, Scene.TEST, Scene.OVERWORLD, ... */
+ jerry_value_t global = MODULE_SCENE_PROTO.prototype;
+ #define X(structName, varName, varNameUpper, initFunc, updateFunc, disposeFunc) \
+ do { \
+ jerry_value_t _key = jerry_string_sz(#varNameUpper); \
+ jerry_value_t _val = jerry_number((double)SCENE_TYPE_##varNameUpper); \
+ jerry_object_set(global, _key, _val); \
+ jerry_value_free(_val); \
+ jerry_value_free(_key); \
+ } while(0);
+ #include "scene/scenelist.h"
+ #undef X
}
static void moduleSceneDispose(void) {
diff --git a/types/entity.d.ts b/types/entity.d.ts
new file mode 100644
index 00000000..dbb72ae0
--- /dev/null
+++ b/types/entity.d.ts
@@ -0,0 +1,129 @@
+/**
+ * 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/index.d.ts b/types/index.d.ts
index 6da3d3ba..cebc47f4 100644
--- a/types/index.d.ts
+++ b/types/index.d.ts
@@ -19,3 +19,6 @@
///
///
///
+///
+///
+///
diff --git a/types/scene.d.ts b/types/scene.d.ts
new file mode 100644
index 00000000..79f1dc15
--- /dev/null
+++ b/types/scene.d.ts
@@ -0,0 +1,30 @@
+/**
+ * Copyright (c) 2026 Dominic Masters
+ *
+ * This software is released under the MIT License.
+ * https://opensource.org/licenses/MIT
+ */
+
+/** Scene management — request scene transitions and query the active scene. */
+interface SceneNamespace {
+ /** The 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;
+}
+
+declare var Scene: SceneNamespace;
diff --git a/types/vec3.d.ts b/types/vec3.d.ts
new file mode 100644
index 00000000..bf8b6811
--- /dev/null
+++ b/types/vec3.d.ts
@@ -0,0 +1,15 @@
+/**
+ * Copyright (c) 2026 Dominic Masters
+ *
+ * This software is released under the MIT License.
+ * https://opensource.org/licenses/MIT
+ */
+
+/** A three-component float vector (x, y, z). */
+declare class Vec3 {
+ constructor(x?: number, y?: number, z?: number);
+ x: number;
+ y: number;
+ z: number;
+ toString(): string;
+}