From 07f98c119af6f9921604887867120d2ee657dac8 Mon Sep 17 00:00:00 2001 From: Dominic Masters Date: Fri, 31 Jul 2026 09:40:53 -0500 Subject: [PATCH] Script stuff --- assets/scripts/overworldscene.js | 72 +++++ src/dusk/engine/engine.c | 1 + .../component/display/entityrenderable.c | 26 ++ .../component/display/entityrenderable.h | 20 ++ src/dusk/script/module/CMakeLists.txt | 3 + src/dusk/script/module/display/CMakeLists.txt | 9 + src/dusk/script/module/display/modulemesh.c | 27 ++ src/dusk/script/module/display/modulemesh.h | 20 ++ .../module/entity/component/CMakeLists.txt | 4 + .../entity/component/display/CMakeLists.txt | 10 + .../display/modulepositioncomponent.c | 150 ++++++++++ .../display/modulepositioncomponent.h | 46 +++ .../display/modulerenderablecomponent.c | 123 ++++++++ .../display/modulerenderablecomponent.h | 46 +++ .../entity/component/modulecomponentlist.c | 38 +++ .../entity/component/modulecomponentlist.h | 34 +++ .../entity/component/physics/CMakeLists.txt | 9 + .../physics/modulephysicscomponent.c | 211 ++++++++++++++ .../physics/modulephysicscomponent.h | 48 ++++ src/dusk/script/module/entity/moduleentity.c | 5 +- src/dusk/script/module/modulebase.h | 27 ++ src/dusk/script/module/modulelist.c | 12 + src/dusk/script/module/modulelist.h | 5 +- src/dusk/script/module/require/CMakeLists.txt | 9 + .../script/module/require/modulerequire.c | 271 ++++++++++++++++++ .../script/module/require/modulerequire.h | 150 ++++++++++ src/dusk/script/module/time/CMakeLists.txt | 9 + src/dusk/script/module/time/moduletime.c | 29 ++ src/dusk/script/module/time/moduletime.h | 20 ++ src/duskrpg/game/game.c | 6 +- src/duskrpg/scene/CMakeLists.txt | 5 - src/duskrpg/scene/overworldscene.c | 138 --------- src/duskrpg/scene/overworldscene.h | 45 --- test/CMakeLists.txt | 1 + test/script/CMakeLists.txt | 13 + test/script/test_modulerequire.c | 228 +++++++++++++++ test/script/test_overworldscene.c | 243 ++++++++++++++++ types/display/mesh.d.ts | 15 + types/entity/component/component.d.ts | 12 +- types/entity/component/physics.d.ts | 54 ++++ types/entity/component/position.d.ts | 34 +++ types/entity/component/renderable.d.ts | 26 ++ types/index.d.ts | 6 + types/require.d.ts | 20 ++ types/time/time.d.ts | 15 + 45 files changed, 2094 insertions(+), 201 deletions(-) create mode 100644 assets/scripts/overworldscene.js create mode 100644 src/dusk/script/module/display/CMakeLists.txt create mode 100644 src/dusk/script/module/display/modulemesh.c create mode 100644 src/dusk/script/module/display/modulemesh.h create mode 100644 src/dusk/script/module/entity/component/display/CMakeLists.txt create mode 100644 src/dusk/script/module/entity/component/display/modulepositioncomponent.c create mode 100644 src/dusk/script/module/entity/component/display/modulepositioncomponent.h create mode 100644 src/dusk/script/module/entity/component/display/modulerenderablecomponent.c create mode 100644 src/dusk/script/module/entity/component/display/modulerenderablecomponent.h create mode 100644 src/dusk/script/module/entity/component/modulecomponentlist.c create mode 100644 src/dusk/script/module/entity/component/modulecomponentlist.h create mode 100644 src/dusk/script/module/entity/component/physics/CMakeLists.txt create mode 100644 src/dusk/script/module/entity/component/physics/modulephysicscomponent.c create mode 100644 src/dusk/script/module/entity/component/physics/modulephysicscomponent.h create mode 100644 src/dusk/script/module/require/CMakeLists.txt create mode 100644 src/dusk/script/module/require/modulerequire.c create mode 100644 src/dusk/script/module/require/modulerequire.h create mode 100644 src/dusk/script/module/time/CMakeLists.txt create mode 100644 src/dusk/script/module/time/moduletime.c create mode 100644 src/dusk/script/module/time/moduletime.h delete mode 100644 src/duskrpg/scene/overworldscene.c delete mode 100644 src/duskrpg/scene/overworldscene.h create mode 100644 test/script/CMakeLists.txt create mode 100644 test/script/test_modulerequire.c create mode 100644 test/script/test_overworldscene.c create mode 100644 types/display/mesh.d.ts create mode 100644 types/entity/component/physics.d.ts create mode 100644 types/entity/component/position.d.ts create mode 100644 types/entity/component/renderable.d.ts create mode 100644 types/require.d.ts create mode 100644 types/time/time.d.ts diff --git a/assets/scripts/overworldscene.js b/assets/scripts/overworldscene.js new file mode 100644 index 00000000..5b426728 --- /dev/null +++ b/assets/scripts/overworldscene.js @@ -0,0 +1,72 @@ +// Radius must stay well outside the floor's footprint (a 20x20 plane has a +// corner-to-center distance of 10*sqrt(2) =~ 14.1) -- orbiting inside that +// puts parts of the floor's own geometry near/behind the camera's view +// direction, which the PSP's legacy GU pipeline can't clip properly and +// drops the whole triangle instead of clipping it. +var CAMERA_ORBIT_RADIUS = 18.0; +var CAMERA_ORBIT_HEIGHT = 10.0; +var CAMERA_ORBIT_SPEED = 0.5; + +var cameraOrbitAngle = 0.0; +var cameraPosition = null; + +// Orbits the camera around the world origin at a fixed radius/height/ +// speed, always looking back at the origin. Called once up front (so the +// very first rendered frame is already positioned correctly) and then +// once per frame via update() below. +function updateCameraOrbit() { + cameraOrbitAngle += Time.delta * CAMERA_ORBIT_SPEED; + + var eyeX = Math.cos(cameraOrbitAngle) * CAMERA_ORBIT_RADIUS; + var eyeY = CAMERA_ORBIT_HEIGHT; + var eyeZ = Math.sin(cameraOrbitAngle) * CAMERA_ORBIT_RADIUS; + + cameraPosition.lookAt(eyeX, eyeY, eyeZ, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0); +} + +// Called once per engine frame (see engineUpdate() -> scriptManagerCall +// Global("update")). +function update() { + if(cameraPosition) updateCameraOrbit(); +} + +(function setup() { + var scene = new Scene(); + scene.setActive(); + + // Camera, orbiting the origin (see updateCameraOrbit above). + var camera = new Entity(); + cameraPosition = camera.add(POSITION); + camera.add(CAMERA); + updateCameraOrbit(); + + // Static ground plane. Physics ignores the entity's position component -- + // the shape's own normal/distance fully define the plane in world space. + var plane = new Entity(); + var planePosition = plane.add(POSITION); + planePosition.setLocalPosition(-10.0, 0.0, -10.0); + planePosition.setLocalScale(20.0, 1.0, 20.0); + + var planePhysics = plane.add(PHYSICS); + planePhysics.setBodyType(PHYSICS_BODY_STATIC); + planePhysics.setShape(PHYSICS_SHAPE_PLANE, 0.0, 1.0, 0.0, 0.0); + + var planeRenderable = plane.add(RENDERABLE); + planeRenderable.setMesh(0, MESH_PLANE); + planeRenderable.setColor(128, 128, 128, 255); + + // Player: dynamic capsule body, moved relative to the camera by + // PLAYER's own update callback (see entityplayer.c). + var player = new Entity(); + var playerPosition = player.add(POSITION); + playerPosition.setLocalPosition(0.0, 2.0, 0.0); + + var playerPhysics = player.add(PHYSICS); + playerPhysics.setShape(PHYSICS_SHAPE_CAPSULE, 0.5, 0.5); + + var playerRenderable = player.add(RENDERABLE); + playerRenderable.setMesh(0, MESH_CAPSULE); + playerRenderable.setColor(0, 0, 255, 255); + + player.add(PLAYER); +})(); diff --git a/src/dusk/engine/engine.c b/src/dusk/engine/engine.c index f571b7a0..08d371d7 100644 --- a/src/dusk/engine/engine.c +++ b/src/dusk/engine/engine.c @@ -74,6 +74,7 @@ errorret_t engineUpdate(void) { consoleUpdate(); errorChain(gameUpdate()); + errorChain(scriptManagerCallGlobal("update")); errorChain(sceneUpdate()); errorChain(assetUpdate()); errorChain(uiUpdate()); diff --git a/src/dusk/entity/component/display/entityrenderable.c b/src/dusk/entity/component/display/entityrenderable.c index 03af2edf..0f510d2f 100644 --- a/src/dusk/entity/component/display/entityrenderable.c +++ b/src/dusk/entity/component/display/entityrenderable.c @@ -80,6 +80,32 @@ void entityRenderableSetColor( r->data.material.material.unlit.color = color; } +void entityRenderableSetMesh( + entitymanager_t *mgr, + const entityid_t entityId, + const componentid_t componentId, + const uint8_t slot, + mesh_t *mesh +) { + assertNotNull(mesh, "Mesh cannot be null"); + assertTrue(slot < ENTITY_RENDERABLE_MESHES_MAX, "Mesh slot out of bounds"); + + entityrenderable_t *r = componentGetData( + mgr, entityId, componentId, COMPONENT_TYPE_RENDERABLE + ); + assertTrue( + r->type == ENTITY_RENDERABLE_TYPE_SHADER_MATERIAL, + "Renderable must be ENTITY_RENDERABLE_TYPE_SHADER_MATERIAL to set a mesh" + ); + + r->data.material.meshes[slot] = mesh; + r->data.material.meshOffsets[slot] = 0; + r->data.material.meshCounts[slot] = -1; + if(slot >= r->data.material.meshCount) { + r->data.material.meshCount = slot + 1; + } +} + void entityRenderableSetDraw( entitymanager_t *mgr, const entityid_t entityId, diff --git a/src/dusk/entity/component/display/entityrenderable.h b/src/dusk/entity/component/display/entityrenderable.h index 7c0b2d15..a4acee32 100644 --- a/src/dusk/entity/component/display/entityrenderable.h +++ b/src/dusk/entity/component/display/entityrenderable.h @@ -142,6 +142,26 @@ void entityRenderableSetColor( const color_t color ); +/** + * Sets one of the material renderable's meshes, drawn in full (no offset/ + * count override). Only meaningful when the renderable is (or defaults + * to) ENTITY_RENDERABLE_TYPE_SHADER_MATERIAL -- asserts otherwise. + * + * @param mgr The entity manager that owns the entity. + * @param entityId The entity to configure. + * @param componentId The renderable component. + * @param slot Index into the material's meshes array (0 to + * ENTITY_RENDERABLE_MESHES_MAX - 1). + * @param mesh The mesh to draw in that slot. + */ +void entityRenderableSetMesh( + entitymanager_t *mgr, + const entityid_t entityId, + const componentid_t componentId, + const uint8_t slot, + mesh_t *mesh +); + /** * Sets the draw callback, switching the type to * ENTITY_RENDERABLE_TYPE_CUSTOM. diff --git a/src/dusk/script/module/CMakeLists.txt b/src/dusk/script/module/CMakeLists.txt index d9aecb8a..cddc069e 100644 --- a/src/dusk/script/module/CMakeLists.txt +++ b/src/dusk/script/module/CMakeLists.txt @@ -10,3 +10,6 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME} add_subdirectory(entity) add_subdirectory(scene) +add_subdirectory(require) +add_subdirectory(display) +add_subdirectory(time) diff --git a/src/dusk/script/module/display/CMakeLists.txt b/src/dusk/script/module/display/CMakeLists.txt new file mode 100644 index 00000000..be46483c --- /dev/null +++ b/src/dusk/script/module/display/CMakeLists.txt @@ -0,0 +1,9 @@ +# Copyright (c) 2026 Dominic Masters +# +# This software is released under the MIT License. +# https://opensource.org/licenses/MIT + +target_sources(${DUSK_LIBRARY_TARGET_NAME} + PUBLIC + modulemesh.c +) diff --git a/src/dusk/script/module/display/modulemesh.c b/src/dusk/script/module/display/modulemesh.c new file mode 100644 index 00000000..f7492170 --- /dev/null +++ b/src/dusk/script/module/display/modulemesh.c @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "modulemesh.h" +#include "script/module/modulebase.h" +#include "display/mesh/cube.h" +#include "display/mesh/plane.h" +#include "display/mesh/sphere.h" +#include "display/mesh/capsule.h" +#include "display/mesh/quad.h" +#include "display/mesh/triprism.h" + +void moduleMeshInit(void) { + moduleBaseSetWrappedPointer("MESH_CUBE", &CUBE_MESH_SIMPLE); + moduleBaseSetWrappedPointer("MESH_PLANE", &PLANE_MESH_SIMPLE); + moduleBaseSetWrappedPointer("MESH_SPHERE", &SPHERE_MESH_SIMPLE); + moduleBaseSetWrappedPointer("MESH_CAPSULE", &CAPSULE_MESH_SIMPLE); + moduleBaseSetWrappedPointer("MESH_TRIPRISM", &TRIPRISM_MESH_SIMPLE); + moduleBaseSetWrappedPointer("MESH_QUAD", &QUAD_MESH_SIMPLE); +} + +void moduleMeshDispose(void) { +} diff --git a/src/dusk/script/module/display/modulemesh.h b/src/dusk/script/module/display/modulemesh.h new file mode 100644 index 00000000..572d0201 --- /dev/null +++ b/src/dusk/script/module/display/modulemesh.h @@ -0,0 +1,20 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once + +/** + * Registers the built-in primitive meshes (MESH_CUBE, MESH_PLANE, + * MESH_SPHERE, MESH_CAPSULE, MESH_TRIPRISM, MESH_QUAD) as global, + * engine-owned pointer values usable with Renderable.setMesh(). + */ +void moduleMeshInit(void); + +/** + * Disposes the mesh module's script resources. + */ +void moduleMeshDispose(void); diff --git a/src/dusk/script/module/entity/component/CMakeLists.txt b/src/dusk/script/module/entity/component/CMakeLists.txt index 60460869..34424814 100644 --- a/src/dusk/script/module/entity/component/CMakeLists.txt +++ b/src/dusk/script/module/entity/component/CMakeLists.txt @@ -6,4 +6,8 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME} PUBLIC modulecomponent.c + modulecomponentlist.c ) + +add_subdirectory(display) +add_subdirectory(physics) diff --git a/src/dusk/script/module/entity/component/display/CMakeLists.txt b/src/dusk/script/module/entity/component/display/CMakeLists.txt new file mode 100644 index 00000000..a64778de --- /dev/null +++ b/src/dusk/script/module/entity/component/display/CMakeLists.txt @@ -0,0 +1,10 @@ +# Copyright (c) 2026 Dominic Masters +# +# This software is released under the MIT License. +# https://opensource.org/licenses/MIT + +target_sources(${DUSK_LIBRARY_TARGET_NAME} + PUBLIC + modulepositioncomponent.c + modulerenderablecomponent.c +) diff --git a/src/dusk/script/module/entity/component/display/modulepositioncomponent.c b/src/dusk/script/module/entity/component/display/modulepositioncomponent.c new file mode 100644 index 00000000..2ea35fe1 --- /dev/null +++ b/src/dusk/script/module/entity/component/display/modulepositioncomponent.c @@ -0,0 +1,150 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "modulepositioncomponent.h" +#include "script/module/modulebase.h" +#include "entity/component/display/entityposition.h" +#include "entity/component.h" +#include "util/string.h" + +scriptproto_t MODULE_POSITION_PROTO; + +moduleBaseFunction(modulePositionGetType) { + moduleBaseGetOrReturn(modulecomponenthandle_t, h, moduleEntityPositionGet); + return jerry_number(h->type); +} + +moduleBaseFunction(modulePositionGetEntityId) { + moduleBaseGetOrReturn(modulecomponenthandle_t, h, moduleEntityPositionGet); + return jerry_number(h->entityId); +} + +moduleBaseFunction(modulePositionDisposeMethod) { + moduleBaseGetOrReturn(modulecomponenthandle_t, h, moduleEntityPositionGet); + componentDispose(h->mgr, h->entityId, h->componentId); + return jerry_undefined(); +} + +moduleBaseFunction(modulePositionToString) { + modulecomponenthandle_t *h = moduleEntityPositionGet(callInfo); + if(!h) return jerry_string_sz("Position(?)"); + + char_t buf[32]; + stringFormat(buf, sizeof(buf), "Position(entityId=%d)", h->entityId); + return jerry_string_sz(buf); +} + +moduleBaseFunction(modulePositionSetLocalPosition) { + moduleBaseRequireArgs(3); + moduleBaseRequireNumber(0); moduleBaseRequireNumber(1); + moduleBaseRequireNumber(2); + moduleBaseGetOrReturn(modulecomponenthandle_t, h, moduleEntityPositionGet); + + entityPositionSetLocalPosition(h->mgr, h->entityId, h->componentId, (vec3){ + moduleBaseArgFloat(0), moduleBaseArgFloat(1), moduleBaseArgFloat(2) + }); + return jerry_undefined(); +} + +moduleBaseFunction(modulePositionGetLocalPosition) { + moduleBaseGetOrReturn(modulecomponenthandle_t, h, moduleEntityPositionGet); + + vec3 pos; + entityPositionGetLocalPosition(h->mgr, h->entityId, h->componentId, pos); + return moduleBaseVec3ToObject(pos); +} + +moduleBaseFunction(modulePositionSetLocalScale) { + moduleBaseRequireArgs(3); + moduleBaseRequireNumber(0); moduleBaseRequireNumber(1); + moduleBaseRequireNumber(2); + moduleBaseGetOrReturn(modulecomponenthandle_t, h, moduleEntityPositionGet); + + entityPositionSetLocalScale(h->mgr, h->entityId, h->componentId, (vec3){ + moduleBaseArgFloat(0), moduleBaseArgFloat(1), moduleBaseArgFloat(2) + }); + return jerry_undefined(); +} + +moduleBaseFunction(modulePositionGetLocalScale) { + moduleBaseGetOrReturn(modulecomponenthandle_t, h, moduleEntityPositionGet); + + vec3 scale; + entityPositionGetLocalScale(h->mgr, h->entityId, h->componentId, scale); + return moduleBaseVec3ToObject(scale); +} + +moduleBaseFunction(modulePositionLookAt) { + moduleBaseRequireArgs(9); + for(jerry_length_t i = 0; i < 9; i++) moduleBaseRequireNumber(i); + moduleBaseGetOrReturn(modulecomponenthandle_t, h, moduleEntityPositionGet); + + entityPositionLookAt( + h->mgr, h->entityId, h->componentId, + (vec3){ + moduleBaseArgFloat(0), moduleBaseArgFloat(1), moduleBaseArgFloat(2) + }, + (vec3){ + moduleBaseArgFloat(3), moduleBaseArgFloat(4), moduleBaseArgFloat(5) + }, + (vec3){ + moduleBaseArgFloat(6), moduleBaseArgFloat(7), moduleBaseArgFloat(8) + } + ); + return jerry_undefined(); +} + +void moduleEntityPositionInit(void) { + scriptProtoInit( + &MODULE_POSITION_PROTO, "Position", sizeof(modulecomponenthandle_t), NULL + ); + + scriptProtoDefineToString(&MODULE_POSITION_PROTO, modulePositionToString); + + scriptProtoDefineProp( + &MODULE_POSITION_PROTO, "type", modulePositionGetType, NULL + ); + scriptProtoDefineProp( + &MODULE_POSITION_PROTO, "entityId", modulePositionGetEntityId, NULL + ); + scriptProtoDefineFunc( + &MODULE_POSITION_PROTO, "dispose", modulePositionDisposeMethod + ); + + scriptProtoDefineFunc( + &MODULE_POSITION_PROTO, "setLocalPosition", modulePositionSetLocalPosition + ); + scriptProtoDefineFunc( + &MODULE_POSITION_PROTO, "getLocalPosition", modulePositionGetLocalPosition + ); + scriptProtoDefineFunc( + &MODULE_POSITION_PROTO, "setLocalScale", modulePositionSetLocalScale + ); + scriptProtoDefineFunc( + &MODULE_POSITION_PROTO, "getLocalScale", modulePositionGetLocalScale + ); + scriptProtoDefineFunc( + &MODULE_POSITION_PROTO, "lookAt", modulePositionLookAt + ); +} + +void moduleEntityPositionDispose(void) { +} + +jerry_value_t moduleEntityPositionCreate( + const modulecomponenthandle_t *handle +) { + return scriptProtoCreateValue(&MODULE_POSITION_PROTO, handle); +} + +modulecomponenthandle_t *moduleEntityPositionGet( + const jerry_call_info_t *callInfo +) { + return (modulecomponenthandle_t *)scriptProtoGetValue( + &MODULE_POSITION_PROTO, callInfo->this_value + ); +} diff --git a/src/dusk/script/module/entity/component/display/modulepositioncomponent.h b/src/dusk/script/module/entity/component/display/modulepositioncomponent.h new file mode 100644 index 00000000..99b30a25 --- /dev/null +++ b/src/dusk/script/module/entity/component/display/modulepositioncomponent.h @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "script/scriptproto.h" +#include "script/module/entity/component/modulecomponent.h" +#include + +extern scriptproto_t MODULE_POSITION_PROTO; + +/** + * Registers the Position class: a typed wrapper over COMPONENT_TYPE_ + * POSITION. Has no constructor -- instances are only ever created via + * moduleEntityPositionCreate(), e.g. from Entity.add()/getComponent(). + */ +void moduleEntityPositionInit(void); + +/** + * Disposes the Position class's script resources. + */ +void moduleEntityPositionDispose(void); + +/** + * Wraps a component handle as a new JS Position instance. + * + * @param handle The handle to copy into the new instance. + * @return The new JS object. + */ +jerry_value_t moduleEntityPositionCreate( + const modulecomponenthandle_t *handle +); + +/** + * Internal. Gets the native handle wrapped by a Position instance's + * `this` value. + * + * @param callInfo The JS call info, whose this_value is the instance. + * @return The wrapped handle, or NULL if this_value isn't a Position. + */ +modulecomponenthandle_t *moduleEntityPositionGet( + const jerry_call_info_t *callInfo +); diff --git a/src/dusk/script/module/entity/component/display/modulerenderablecomponent.c b/src/dusk/script/module/entity/component/display/modulerenderablecomponent.c new file mode 100644 index 00000000..656a8b12 --- /dev/null +++ b/src/dusk/script/module/entity/component/display/modulerenderablecomponent.c @@ -0,0 +1,123 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "modulerenderablecomponent.h" +#include "script/module/modulebase.h" +#include "entity/component/display/entityrenderable.h" +#include "entity/component.h" +#include "display/color.h" +#include "util/string.h" + +scriptproto_t MODULE_RENDERABLE_PROTO; + +moduleBaseFunction(moduleRenderableGetType) { + moduleBaseGetOrReturn(modulecomponenthandle_t, h, moduleEntityRenderableGet); + return jerry_number(h->type); +} + +moduleBaseFunction(moduleRenderableGetEntityId) { + moduleBaseGetOrReturn(modulecomponenthandle_t, h, moduleEntityRenderableGet); + return jerry_number(h->entityId); +} + +moduleBaseFunction(moduleRenderableDisposeMethod) { + moduleBaseGetOrReturn(modulecomponenthandle_t, h, moduleEntityRenderableGet); + componentDispose(h->mgr, h->entityId, h->componentId); + return jerry_undefined(); +} + +moduleBaseFunction(moduleRenderableToString) { + modulecomponenthandle_t *h = moduleEntityRenderableGet(callInfo); + if(!h) return jerry_string_sz("Renderable(?)"); + + char_t buf[32]; + stringFormat(buf, sizeof(buf), "Renderable(entityId=%d)", h->entityId); + return jerry_string_sz(buf); +} + +moduleBaseFunction(moduleRenderableSetColor) { + moduleBaseRequireArgs(4); + moduleBaseRequireNumber(0); moduleBaseRequireNumber(1); + moduleBaseRequireNumber(2); moduleBaseRequireNumber(3); + moduleBaseGetOrReturn(modulecomponenthandle_t, h, moduleEntityRenderableGet); + + entityRenderableSetColor(h->mgr, h->entityId, h->componentId, color( + (uint8_t)moduleBaseArgInt(0), (uint8_t)moduleBaseArgInt(1), + (uint8_t)moduleBaseArgInt(2), (uint8_t)moduleBaseArgInt(3) + )); + return jerry_undefined(); +} + +moduleBaseFunction(moduleRenderableSetMesh) { + moduleBaseRequireArgs(2); moduleBaseRequireNumber(0); + moduleBaseRequireObject(1); + moduleBaseGetOrReturn(modulecomponenthandle_t, h, moduleEntityRenderableGet); + + mesh_t *mesh = (mesh_t *)moduleBaseUnwrapPointer(args[1]); + if(!mesh) return moduleBaseThrow("Renderable.setMesh: invalid mesh"); + + entityRenderableSetMesh( + h->mgr, h->entityId, h->componentId, (uint8_t)moduleBaseArgInt(0), mesh + ); + return jerry_undefined(); +} + +moduleBaseFunction(moduleRenderableSetPriority) { + moduleBaseRequireArgs(1); moduleBaseRequireNumber(0); + moduleBaseGetOrReturn(modulecomponenthandle_t, h, moduleEntityRenderableGet); + + entityRenderableSetPriority( + h->mgr, h->entityId, h->componentId, (int8_t)moduleBaseArgInt(0) + ); + return jerry_undefined(); +} + +void moduleEntityRenderableInit(void) { + scriptProtoInit( + &MODULE_RENDERABLE_PROTO, + "Renderable", sizeof(modulecomponenthandle_t), NULL + ); + + scriptProtoDefineToString(&MODULE_RENDERABLE_PROTO, moduleRenderableToString); + + scriptProtoDefineProp( + &MODULE_RENDERABLE_PROTO, "type", moduleRenderableGetType, NULL + ); + scriptProtoDefineProp( + &MODULE_RENDERABLE_PROTO, "entityId", moduleRenderableGetEntityId, NULL + ); + scriptProtoDefineFunc( + &MODULE_RENDERABLE_PROTO, "dispose", moduleRenderableDisposeMethod + ); + + scriptProtoDefineFunc( + &MODULE_RENDERABLE_PROTO, "setColor", moduleRenderableSetColor + ); + scriptProtoDefineFunc( + &MODULE_RENDERABLE_PROTO, "setMesh", moduleRenderableSetMesh + ); + scriptProtoDefineFunc( + &MODULE_RENDERABLE_PROTO, "setPriority", moduleRenderableSetPriority + ); +} + +void moduleEntityRenderableDispose(void) { +} + +jerry_value_t moduleEntityRenderableCreate( + const modulecomponenthandle_t *handle +) { + return scriptProtoCreateValue(&MODULE_RENDERABLE_PROTO, handle); +} + +modulecomponenthandle_t *moduleEntityRenderableGet( + const jerry_call_info_t *callInfo +) { + return (modulecomponenthandle_t *)scriptProtoGetValue( + &MODULE_RENDERABLE_PROTO, callInfo->this_value + ); +} diff --git a/src/dusk/script/module/entity/component/display/modulerenderablecomponent.h b/src/dusk/script/module/entity/component/display/modulerenderablecomponent.h new file mode 100644 index 00000000..83a5a2de --- /dev/null +++ b/src/dusk/script/module/entity/component/display/modulerenderablecomponent.h @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "script/scriptproto.h" +#include "script/module/entity/component/modulecomponent.h" +#include + +extern scriptproto_t MODULE_RENDERABLE_PROTO; + +/** + * Registers the Renderable class: a typed wrapper over COMPONENT_TYPE_ + * RENDERABLE. Has no constructor -- instances are only ever created via + * moduleEntityRenderableCreate(), e.g. from Entity.add()/getComponent(). + */ +void moduleEntityRenderableInit(void); + +/** + * Disposes the Renderable class's script resources. + */ +void moduleEntityRenderableDispose(void); + +/** + * Wraps a component handle as a new JS Renderable instance. + * + * @param handle The handle to copy into the new instance. + * @return The new JS object. + */ +jerry_value_t moduleEntityRenderableCreate( + const modulecomponenthandle_t *handle +); + +/** + * Internal. Gets the native handle wrapped by a Renderable instance's + * `this` value. + * + * @param callInfo The JS call info, whose this_value is the instance. + * @return The wrapped handle, or NULL if this_value isn't a Renderable. + */ +modulecomponenthandle_t *moduleEntityRenderableGet( + const jerry_call_info_t *callInfo +); diff --git a/src/dusk/script/module/entity/component/modulecomponentlist.c b/src/dusk/script/module/entity/component/modulecomponentlist.c new file mode 100644 index 00000000..e443f688 --- /dev/null +++ b/src/dusk/script/module/entity/component/modulecomponentlist.c @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "modulecomponentlist.h" +#include "script/module/entity/component/display/modulepositioncomponent.h" +#include "script/module/entity/component/physics/modulephysicscomponent.h" +#include "script/module/entity/component/display/modulerenderablecomponent.h" + +void moduleComponentListInit(void) { + moduleEntityPositionInit(); + moduleEntityPhysicsInit(); + moduleEntityRenderableInit(); +} + +void moduleComponentListDispose(void) { + moduleEntityRenderableDispose(); + moduleEntityPhysicsDispose(); + moduleEntityPositionDispose(); +} + +jerry_value_t moduleComponentListCreate( + const modulecomponenthandle_t *handle +) { + switch(handle->type) { + case COMPONENT_TYPE_POSITION: + return moduleEntityPositionCreate(handle); + case COMPONENT_TYPE_PHYSICS: + return moduleEntityPhysicsCreate(handle); + case COMPONENT_TYPE_RENDERABLE: + return moduleEntityRenderableCreate(handle); + default: + return moduleComponentCreate(handle); + } +} diff --git a/src/dusk/script/module/entity/component/modulecomponentlist.h b/src/dusk/script/module/entity/component/modulecomponentlist.h new file mode 100644 index 00000000..85bb54c4 --- /dev/null +++ b/src/dusk/script/module/entity/component/modulecomponentlist.h @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "script/module/entity/component/modulecomponent.h" +#include + +/** + * Registers every typed per-component-type JS wrapper module (Position, + * Physics, Renderable, ...). Called once by moduleListInit(). + */ +void moduleComponentListInit(void); + +/** + * Disposes every typed component wrapper module. Called once by + * moduleListDispose(). + */ +void moduleComponentListDispose(void); + +/** + * Wraps a component handle using the most specific JS class available + * for its type (e.g. Position for COMPONENT_TYPE_POSITION), falling back + * to the generic Component wrapper for types with no typed wrapper yet. + * + * @param handle The handle to wrap. + * @return The new JS object. + */ +jerry_value_t moduleComponentListCreate( + const modulecomponenthandle_t *handle +); diff --git a/src/dusk/script/module/entity/component/physics/CMakeLists.txt b/src/dusk/script/module/entity/component/physics/CMakeLists.txt new file mode 100644 index 00000000..e34e629a --- /dev/null +++ b/src/dusk/script/module/entity/component/physics/CMakeLists.txt @@ -0,0 +1,9 @@ +# Copyright (c) 2026 Dominic Masters +# +# This software is released under the MIT License. +# https://opensource.org/licenses/MIT + +target_sources(${DUSK_LIBRARY_TARGET_NAME} + PUBLIC + modulephysicscomponent.c +) diff --git a/src/dusk/script/module/entity/component/physics/modulephysicscomponent.c b/src/dusk/script/module/entity/component/physics/modulephysicscomponent.c new file mode 100644 index 00000000..b5e7c0a1 --- /dev/null +++ b/src/dusk/script/module/entity/component/physics/modulephysicscomponent.c @@ -0,0 +1,211 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "modulephysicscomponent.h" +#include "script/module/modulebase.h" +#include "entity/component/physics/entityphysics.h" +#include "entity/component.h" +#include "util/string.h" +#include "util/memory.h" + +scriptproto_t MODULE_PHYSICS_PROTO; + +moduleBaseFunction(modulePhysicsGetType) { + moduleBaseGetOrReturn(modulecomponenthandle_t, h, moduleEntityPhysicsGet); + return jerry_number(h->type); +} + +moduleBaseFunction(modulePhysicsGetEntityId) { + moduleBaseGetOrReturn(modulecomponenthandle_t, h, moduleEntityPhysicsGet); + return jerry_number(h->entityId); +} + +moduleBaseFunction(modulePhysicsDisposeMethod) { + moduleBaseGetOrReturn(modulecomponenthandle_t, h, moduleEntityPhysicsGet); + componentDispose(h->mgr, h->entityId, h->componentId); + return jerry_undefined(); +} + +moduleBaseFunction(modulePhysicsToString) { + modulecomponenthandle_t *h = moduleEntityPhysicsGet(callInfo); + if(!h) return jerry_string_sz("Physics(?)"); + + char_t buf[32]; + stringFormat(buf, sizeof(buf), "Physics(entityId=%d)", h->entityId); + return jerry_string_sz(buf); +} + +moduleBaseFunction(modulePhysicsSetBodyType) { + moduleBaseRequireArgs(1); moduleBaseRequireNumber(0); + moduleBaseGetOrReturn(modulecomponenthandle_t, h, moduleEntityPhysicsGet); + + entityPhysicsSetBodyType( + h->mgr, h->entityId, h->componentId, + (physicsbodytype_t)moduleBaseArgInt(0) + ); + return jerry_undefined(); +} + +moduleBaseFunction(modulePhysicsGetBodyType) { + moduleBaseGetOrReturn(modulecomponenthandle_t, h, moduleEntityPhysicsGet); + return jerry_number( + entityPhysicsGetBodyType(h->mgr, h->entityId, h->componentId) + ); +} + +moduleBaseFunction(modulePhysicsSetShape) { + moduleBaseRequireArgs(1); moduleBaseRequireNumber(0); + moduleBaseGetOrReturn(modulecomponenthandle_t, h, moduleEntityPhysicsGet); + + physicsshape_t shape; + memoryZero(&shape, sizeof(shape)); + shape.type = (physicshapetype_t)moduleBaseArgInt(0); + + switch(shape.type) { + case PHYSICS_SHAPE_CUBE: + moduleBaseRequireArgs(4); + moduleBaseRequireNumber(1); moduleBaseRequireNumber(2); + moduleBaseRequireNumber(3); + shape.data.cube.halfExtents[0] = moduleBaseArgFloat(1); + shape.data.cube.halfExtents[1] = moduleBaseArgFloat(2); + shape.data.cube.halfExtents[2] = moduleBaseArgFloat(3); + break; + + case PHYSICS_SHAPE_SPHERE: + moduleBaseRequireArgs(2); moduleBaseRequireNumber(1); + shape.data.sphere.radius = moduleBaseArgFloat(1); + break; + + case PHYSICS_SHAPE_CAPSULE: + moduleBaseRequireArgs(3); + moduleBaseRequireNumber(1); moduleBaseRequireNumber(2); + shape.data.capsule.radius = moduleBaseArgFloat(1); + shape.data.capsule.halfHeight = moduleBaseArgFloat(2); + break; + + case PHYSICS_SHAPE_PLANE: + moduleBaseRequireArgs(5); + moduleBaseRequireNumber(1); moduleBaseRequireNumber(2); + moduleBaseRequireNumber(3); moduleBaseRequireNumber(4); + shape.data.plane.normal[0] = moduleBaseArgFloat(1); + shape.data.plane.normal[1] = moduleBaseArgFloat(2); + shape.data.plane.normal[2] = moduleBaseArgFloat(3); + shape.data.plane.distance = moduleBaseArgFloat(4); + break; + + default: + return moduleBaseThrow("Physics.setShape: unsupported shape type"); + } + + entityPhysicsSetShape(h->mgr, h->entityId, h->componentId, shape); + return jerry_undefined(); +} + +moduleBaseFunction(modulePhysicsSetVelocity) { + moduleBaseRequireArgs(3); + moduleBaseRequireNumber(0); moduleBaseRequireNumber(1); + moduleBaseRequireNumber(2); + moduleBaseGetOrReturn(modulecomponenthandle_t, h, moduleEntityPhysicsGet); + + entityPhysicsSetVelocity(h->mgr, h->entityId, h->componentId, (vec3){ + moduleBaseArgFloat(0), moduleBaseArgFloat(1), moduleBaseArgFloat(2) + }); + return jerry_undefined(); +} + +moduleBaseFunction(modulePhysicsGetVelocity) { + moduleBaseGetOrReturn(modulecomponenthandle_t, h, moduleEntityPhysicsGet); + + vec3 velocity; + entityPhysicsGetVelocity(h->mgr, h->entityId, h->componentId, velocity); + return moduleBaseVec3ToObject(velocity); +} + +moduleBaseFunction(modulePhysicsApplyImpulse) { + moduleBaseRequireArgs(3); + moduleBaseRequireNumber(0); moduleBaseRequireNumber(1); + moduleBaseRequireNumber(2); + moduleBaseGetOrReturn(modulecomponenthandle_t, h, moduleEntityPhysicsGet); + + entityPhysicsApplyImpulse(h->mgr, h->entityId, h->componentId, (vec3){ + moduleBaseArgFloat(0), moduleBaseArgFloat(1), moduleBaseArgFloat(2) + }); + return jerry_undefined(); +} + +moduleBaseFunction(modulePhysicsIsOnGround) { + moduleBaseGetOrReturn(modulecomponenthandle_t, h, moduleEntityPhysicsGet); + return jerry_boolean( + entityPhysicsIsOnGround(h->mgr, h->entityId, h->componentId) + ); +} + +void moduleEntityPhysicsInit(void) { + scriptProtoInit( + &MODULE_PHYSICS_PROTO, "Physics", sizeof(modulecomponenthandle_t), NULL + ); + + scriptProtoDefineToString(&MODULE_PHYSICS_PROTO, modulePhysicsToString); + + scriptProtoDefineProp( + &MODULE_PHYSICS_PROTO, "type", modulePhysicsGetType, NULL + ); + scriptProtoDefineProp( + &MODULE_PHYSICS_PROTO, "entityId", modulePhysicsGetEntityId, NULL + ); + scriptProtoDefineFunc( + &MODULE_PHYSICS_PROTO, "dispose", modulePhysicsDisposeMethod + ); + + scriptProtoDefineFunc( + &MODULE_PHYSICS_PROTO, "setBodyType", modulePhysicsSetBodyType + ); + scriptProtoDefineFunc( + &MODULE_PHYSICS_PROTO, "getBodyType", modulePhysicsGetBodyType + ); + scriptProtoDefineFunc( + &MODULE_PHYSICS_PROTO, "setShape", modulePhysicsSetShape + ); + scriptProtoDefineFunc( + &MODULE_PHYSICS_PROTO, "setVelocity", modulePhysicsSetVelocity + ); + scriptProtoDefineFunc( + &MODULE_PHYSICS_PROTO, "getVelocity", modulePhysicsGetVelocity + ); + scriptProtoDefineFunc( + &MODULE_PHYSICS_PROTO, "applyImpulse", modulePhysicsApplyImpulse + ); + scriptProtoDefineFunc( + &MODULE_PHYSICS_PROTO, "isOnGround", modulePhysicsIsOnGround + ); + + moduleBaseSetInt("PHYSICS_BODY_STATIC", PHYSICS_BODY_STATIC); + moduleBaseSetInt("PHYSICS_BODY_DYNAMIC", PHYSICS_BODY_DYNAMIC); + moduleBaseSetInt("PHYSICS_BODY_KINEMATIC", PHYSICS_BODY_KINEMATIC); + + moduleBaseSetInt("PHYSICS_SHAPE_CUBE", PHYSICS_SHAPE_CUBE); + moduleBaseSetInt("PHYSICS_SHAPE_SPHERE", PHYSICS_SHAPE_SPHERE); + moduleBaseSetInt("PHYSICS_SHAPE_CAPSULE", PHYSICS_SHAPE_CAPSULE); + moduleBaseSetInt("PHYSICS_SHAPE_PLANE", PHYSICS_SHAPE_PLANE); +} + +void moduleEntityPhysicsDispose(void) { +} + +jerry_value_t moduleEntityPhysicsCreate( + const modulecomponenthandle_t *handle +) { + return scriptProtoCreateValue(&MODULE_PHYSICS_PROTO, handle); +} + +modulecomponenthandle_t *moduleEntityPhysicsGet( + const jerry_call_info_t *callInfo +) { + return (modulecomponenthandle_t *)scriptProtoGetValue( + &MODULE_PHYSICS_PROTO, callInfo->this_value + ); +} diff --git a/src/dusk/script/module/entity/component/physics/modulephysicscomponent.h b/src/dusk/script/module/entity/component/physics/modulephysicscomponent.h new file mode 100644 index 00000000..c36ce8a6 --- /dev/null +++ b/src/dusk/script/module/entity/component/physics/modulephysicscomponent.h @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "script/scriptproto.h" +#include "script/module/entity/component/modulecomponent.h" +#include + +extern scriptproto_t MODULE_PHYSICS_PROTO; + +/** + * Registers the Physics class: a typed wrapper over COMPONENT_TYPE_ + * PHYSICS. Also registers PHYSICS_BODY_* and PHYSICS_SHAPE_* as global + * integer constants for use with setBodyType()/setShape(). Has no + * constructor -- instances are only ever created via + * moduleEntityPhysicsCreate(), e.g. from Entity.add()/getComponent(). + */ +void moduleEntityPhysicsInit(void); + +/** + * Disposes the Physics class's script resources. + */ +void moduleEntityPhysicsDispose(void); + +/** + * Wraps a component handle as a new JS Physics instance. + * + * @param handle The handle to copy into the new instance. + * @return The new JS object. + */ +jerry_value_t moduleEntityPhysicsCreate( + const modulecomponenthandle_t *handle +); + +/** + * Internal. Gets the native handle wrapped by a Physics instance's + * `this` value. + * + * @param callInfo The JS call info, whose this_value is the instance. + * @return The wrapped handle, or NULL if this_value isn't a Physics. + */ +modulecomponenthandle_t *moduleEntityPhysicsGet( + const jerry_call_info_t *callInfo +); diff --git a/src/dusk/script/module/entity/moduleentity.c b/src/dusk/script/module/entity/moduleentity.c index 8cb8fc2d..2d116446 100644 --- a/src/dusk/script/module/entity/moduleentity.c +++ b/src/dusk/script/module/entity/moduleentity.c @@ -8,6 +8,7 @@ #include "moduleentity.h" #include "script/module/modulebase.h" #include "script/module/entity/component/modulecomponent.h" +#include "script/module/entity/component/modulecomponentlist.h" #include "scene/scene.h" #include "entity/entitymanager.h" #include "entity/entity.h" @@ -67,7 +68,7 @@ moduleBaseFunction(moduleEntityAddComponent) { modulecomponenthandle_t h = { .mgr = inst->mgr, .entityId = inst->id, .componentId = id, .type = type }; - return moduleComponentCreate(&h); + return moduleComponentListCreate(&h); } moduleBaseFunction(moduleEntityGetComponentMethod) { @@ -85,7 +86,7 @@ moduleBaseFunction(moduleEntityGetComponentMethod) { modulecomponenthandle_t h = { .mgr = inst->mgr, .entityId = inst->id, .componentId = id, .type = type }; - return moduleComponentCreate(&h); + return moduleComponentListCreate(&h); } moduleBaseFunction(moduleEntityDisposeMethod) { diff --git a/src/dusk/script/module/modulebase.h b/src/dusk/script/module/modulebase.h index 1367f470..8a955d2b 100644 --- a/src/dusk/script/module/modulebase.h +++ b/src/dusk/script/module/modulebase.h @@ -394,3 +394,30 @@ static inline float_t moduleBaseValueFloat(jerry_value_t val) { static inline int32_t moduleBaseValueInt(jerry_value_t val) { return (int32_t)jerry_value_as_number(val); } + +/** + * Wraps a vec3 as a plain JS {x, y, z} object. + */ +static inline jerry_value_t moduleBaseVec3ToObject(const vec3 v) { + jerry_value_t obj = jerry_object(); + + jerry_value_t xKey = jerry_string_sz("x"); + jerry_value_t xVal = jerry_number(v[0]); + jerry_object_set(obj, xKey, xVal); + jerry_value_free(xVal); + jerry_value_free(xKey); + + jerry_value_t yKey = jerry_string_sz("y"); + jerry_value_t yVal = jerry_number(v[1]); + jerry_object_set(obj, yKey, yVal); + jerry_value_free(yVal); + jerry_value_free(yKey); + + jerry_value_t zKey = jerry_string_sz("z"); + jerry_value_t zVal = jerry_number(v[2]); + jerry_object_set(obj, zKey, zVal); + jerry_value_free(zVal); + jerry_value_free(zKey); + + return obj; +} diff --git a/src/dusk/script/module/modulelist.c b/src/dusk/script/module/modulelist.c index 6cce6ec6..b79c8ae8 100644 --- a/src/dusk/script/module/modulelist.c +++ b/src/dusk/script/module/modulelist.c @@ -7,13 +7,21 @@ #include "modulelist.h" #include "script/module/moduleplatform.h" +#include "script/module/require/modulerequire.h" +#include "script/module/display/modulemesh.h" +#include "script/module/time/moduletime.h" #include "script/module/entity/component/modulecomponent.h" +#include "script/module/entity/component/modulecomponentlist.h" #include "script/module/entity/moduleentity.h" #include "script/module/scene/modulescene.h" void moduleListInit(void) { modulePlatform(); + moduleRequireInit(); + moduleMeshInit(); + moduleTimeInit(); moduleComponentInit(); + moduleComponentListInit(); moduleEntityInit(); moduleSceneInit(); } @@ -21,5 +29,9 @@ void moduleListInit(void) { void moduleListDispose(void) { moduleSceneDispose(); moduleEntityDispose(); + moduleComponentListDispose(); moduleComponentDispose(); + moduleTimeDispose(); + moduleMeshDispose(); + moduleRequireDispose(); } diff --git a/src/dusk/script/module/modulelist.h b/src/dusk/script/module/modulelist.h index 30cf2a26..6419e33b 100644 --- a/src/dusk/script/module/modulelist.h +++ b/src/dusk/script/module/modulelist.h @@ -8,8 +8,9 @@ #pragma once /** - * Registers every script module (Component, Entity, Scene, the platform - * globals). Called once by scriptManagerInit(). + * Registers every script module (Component + its typed per-type + * wrappers, Entity, Scene, require(), Mesh, Time, the platform globals). + * Called once by scriptManagerInit(). */ void moduleListInit(void); diff --git a/src/dusk/script/module/require/CMakeLists.txt b/src/dusk/script/module/require/CMakeLists.txt new file mode 100644 index 00000000..c063ce82 --- /dev/null +++ b/src/dusk/script/module/require/CMakeLists.txt @@ -0,0 +1,9 @@ +# Copyright (c) 2026 Dominic Masters +# +# This software is released under the MIT License. +# https://opensource.org/licenses/MIT + +target_sources(${DUSK_LIBRARY_TARGET_NAME} + PUBLIC + modulerequire.c +) diff --git a/src/dusk/script/module/require/modulerequire.c b/src/dusk/script/module/require/modulerequire.c new file mode 100644 index 00000000..ca56c5b3 --- /dev/null +++ b/src/dusk/script/module/require/modulerequire.c @@ -0,0 +1,271 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "modulerequire.h" +#include "script/module/modulebase.h" +#include "assert/assert.h" +#include "util/memory.h" +#include "util/string.h" + +#define MODULE_REQUIRE_WRAP_PREFIX "(function(module){\n" +#define MODULE_REQUIRE_WRAP_SUFFIX "\n})" + +modulerequire_t MODULE_REQUIRE; + +moduleBaseFunction(moduleRequireFn) { + moduleBaseRequireArgs(1); moduleBaseRequireString(0); + + char_t requested[ASSET_FILE_NAME_MAX]; + moduleBaseToString(args[0], requested, sizeof(requested)); + + char_t resolved[ASSET_FILE_NAME_MAX]; + moduleRequireResolvePath(requested, resolved, sizeof(resolved)); + + jerry_value_t cachedModule; + if(moduleRequireCacheGet(resolved, &cachedModule)) { + return moduleBaseGetProp(cachedModule, "exports"); + } + + assetfile_t file; + errorret_t err = assetFileInit(&file, resolved, NULL, NULL); + if(errorIsNotOk(err)) return moduleBaseThrowError(err); + + uint8_t *buffer = NULL; + size_t size = 0; + err = assetFileReadEntire(&file, &buffer, &size); + if(errorIsNotOk(err)) return moduleBaseThrowError(err); + + err = assetFileDispose(&file); + if(errorIsNotOk(err)) { + memoryFree(buffer); + return moduleBaseThrowError(err); + } + + // Wrap the source in a function so `module` is a real parameter, not a + // global - each required file's scope stays isolated from the others. + const size_t prefixLen = sizeof(MODULE_REQUIRE_WRAP_PREFIX) - 1; + const size_t suffixLen = sizeof(MODULE_REQUIRE_WRAP_SUFFIX) - 1; + const size_t wrappedLen = prefixLen + size + suffixLen; + char_t *wrapped = (char_t *)memoryAllocate(wrappedLen + 1); + memoryCopy(wrapped, MODULE_REQUIRE_WRAP_PREFIX, prefixLen); + memoryCopy(wrapped + prefixLen, buffer, size); + memoryCopy(wrapped + prefixLen + size, MODULE_REQUIRE_WRAP_SUFFIX, suffixLen); + wrapped[wrappedLen] = '\0'; + memoryFree(buffer); + + jerry_value_t fnValue = jerry_eval( + (const jerry_char_t *)wrapped, wrappedLen, JERRY_PARSE_NO_OPTS + ); + memoryFree(wrapped); + + if(jerry_value_is_exception(fnValue)) return fnValue; + + // Cache before calling so a circular require() sees this module object + // (with whatever `module.exports` has been set so far) instead of + // recursing forever. + jerry_value_t moduleObj = jerry_object(); + moduleRequireCacheSet(resolved, moduleObj); + + char_t dir[ASSET_FILE_NAME_MAX]; + moduleRequireDirname(resolved, dir, sizeof(dir)); + moduleRequireDirPush(dir); + + jerry_value_t callArgs[1] = { moduleObj }; + jerry_value_t callResult = jerry_call(fnValue, jerry_undefined(), callArgs, 1); + jerry_value_free(fnValue); + + moduleRequireDirPop(); + + if(jerry_value_is_exception(callResult)) { + moduleRequireCacheDelete(resolved); + jerry_value_free(moduleObj); + return callResult; + } + jerry_value_free(callResult); + + return moduleBaseGetProp(moduleObj, "exports"); +} + +void moduleRequireInit(void) { + memoryZero(&MODULE_REQUIRE, sizeof(modulerequire_t)); + moduleBaseDefineGlobalMethod("require", moduleRequireFn); +} + +void moduleRequireDispose(void) { + for(uint8_t i = 0; i < MODULE_REQUIRE.cacheCount; i++) { + jerry_value_free(MODULE_REQUIRE.cache[i].moduleObj); + } + MODULE_REQUIRE.cacheCount = 0; + MODULE_REQUIRE.dirStackCount = 0; +} + +void moduleRequireResolvePath( + const char_t *requested, + char_t *outResolved, + const size_t outResolvedSize +) { + assertNotNull(requested, "Requested path cannot be NULL"); + assertNotNull(outResolved, "Output buffer cannot be NULL"); + + bool_t isRelative = ( + (requested[0] == '.' && requested[1] == '/') || + (requested[0] == '.' && requested[1] == '.' && requested[2] == '/') + ); + + char_t combined[MODULE_REQUIRE_PATH_MAX]; + if(isRelative) { + stringFormat( + combined, sizeof(combined) - 1, "%s%s", + moduleRequireDirStackTop(), requested + ); + } else { + stringCopy(combined, requested, sizeof(combined) - 1); + } + + moduleRequireNormalize(combined, outResolved, outResolvedSize); +} + +void moduleRequireNormalize( + const char_t *path, + char_t *outNormalized, + const size_t outSize +) { + assertNotNull(path, "Path cannot be NULL"); + assertNotNull(outNormalized, "Output buffer cannot be NULL"); + + char_t scratch[MODULE_REQUIRE_PATH_MAX]; + stringCopy(scratch, path, sizeof(scratch) - 1); + + char_t *segments[MODULE_REQUIRE_SEGMENT_MAX]; + uint8_t segmentCount = 0; + + char_t *cursor = scratch; + bool_t more = true; + while(more) { + char_t *segment = cursor; + while(*cursor != '/' && *cursor != '\0') cursor++; + more = (*cursor == '/'); + if(more) { *cursor = '\0'; cursor++; } + + if(segment[0] == '\0' || stringEquals(segment, ".")) { + // Skip empty (e.g. leading/double slash) and "." segments. + } else if(stringEquals(segment, "..")) { + if(segmentCount > 0) segmentCount--; + } else { + assertTrue( + segmentCount < MODULE_REQUIRE_SEGMENT_MAX, + "require() path has too many segments" + ); + segments[segmentCount++] = segment; + } + } + + size_t written = 0; + outNormalized[0] = '\0'; + for(uint8_t i = 0; i < segmentCount; i++) { + if(i > 0) { + assertTrue(written + 1 < outSize, "require() path too long"); + outNormalized[written++] = '/'; + } + + size_t segLen = 0; + while(segments[i][segLen] != '\0') segLen++; + assertTrue(written + segLen < outSize, "require() path too long"); + memoryCopy(outNormalized + written, segments[i], segLen); + written += segLen; + } + outNormalized[written] = '\0'; +} + +void moduleRequireDirname( + const char_t *path, + char_t *outDir, + const size_t outDirSize +) { + assertNotNull(path, "Path cannot be NULL"); + assertNotNull(outDir, "Output buffer cannot be NULL"); + + char_t *lastSlash = stringFindLastChar(path, '/'); + if(lastSlash == NULL) { + outDir[0] = '\0'; + return; + } + + size_t len = (size_t)(lastSlash - path) + 1; + assertTrue(len < outDirSize, "Directory path too long"); + memoryCopy(outDir, path, len); + outDir[len] = '\0'; +} + +bool_t moduleRequireCacheGet( + const char_t *resolvedPath, + jerry_value_t *outModuleObj +) { + assertNotNull(resolvedPath, "Resolved path cannot be NULL"); + assertNotNull(outModuleObj, "Output module object cannot be NULL"); + + for(uint8_t i = 0; i < MODULE_REQUIRE.cacheCount; i++) { + if(stringEquals(MODULE_REQUIRE.cache[i].path, resolvedPath)) { + *outModuleObj = MODULE_REQUIRE.cache[i].moduleObj; + return true; + } + } + return false; +} + +void moduleRequireCacheSet( + const char_t *resolvedPath, + const jerry_value_t moduleObj +) { + assertNotNull(resolvedPath, "Resolved path cannot be NULL"); + assertTrue( + MODULE_REQUIRE.cacheCount < MODULE_REQUIRE_CACHE_MAX, + "require() module cache is full" + ); + + modulerequirecacheentry_t *entry = + &MODULE_REQUIRE.cache[MODULE_REQUIRE.cacheCount++]; + stringCopy(entry->path, resolvedPath, ASSET_FILE_NAME_MAX - 1); + entry->moduleObj = moduleObj; +} + +void moduleRequireCacheDelete(const char_t *resolvedPath) { + assertNotNull(resolvedPath, "Resolved path cannot be NULL"); + + for(uint8_t i = 0; i < MODULE_REQUIRE.cacheCount; i++) { + if(!stringEquals(MODULE_REQUIRE.cache[i].path, resolvedPath)) continue; + + MODULE_REQUIRE.cacheCount--; + MODULE_REQUIRE.cache[i] = MODULE_REQUIRE.cache[MODULE_REQUIRE.cacheCount]; + return; + } +} + +void moduleRequireDirPush(const char_t *dir) { + assertNotNull(dir, "Directory cannot be NULL"); + assertTrue( + MODULE_REQUIRE.dirStackCount < MODULE_REQUIRE_STACK_MAX, + "require() nesting too deep" + ); + + stringCopy( + MODULE_REQUIRE.dirStack[MODULE_REQUIRE.dirStackCount++], + dir, ASSET_FILE_NAME_MAX - 1 + ); +} + +void moduleRequireDirPop(void) { + assertTrue( + MODULE_REQUIRE.dirStackCount > 0, "require() dir stack underflow" + ); + MODULE_REQUIRE.dirStackCount--; +} + +const char_t * moduleRequireDirStackTop(void) { + if(MODULE_REQUIRE.dirStackCount == 0) return ""; + return MODULE_REQUIRE.dirStack[MODULE_REQUIRE.dirStackCount - 1]; +} diff --git a/src/dusk/script/module/require/modulerequire.h b/src/dusk/script/module/require/modulerequire.h new file mode 100644 index 00000000..a68f86b4 --- /dev/null +++ b/src/dusk/script/module/require/modulerequire.h @@ -0,0 +1,150 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "error/error.h" +#include "asset/assetfile.h" +#include + +/** Max number of distinct files require() will keep resolved/cached. */ +#define MODULE_REQUIRE_CACHE_MAX 32 + +/** Max require() nesting depth (a file requiring a file requiring...). */ +#define MODULE_REQUIRE_STACK_MAX 16 + +/** Max path segments handled while resolving a single require() call. */ +#define MODULE_REQUIRE_SEGMENT_MAX 16 + +/** Scratch buffer size used while joining a base dir with a request. */ +#define MODULE_REQUIRE_PATH_MAX (ASSET_FILE_NAME_MAX * 2) + +typedef struct { + char_t path[ASSET_FILE_NAME_MAX]; + jerry_value_t moduleObj; +} modulerequirecacheentry_t; + +typedef struct { + modulerequirecacheentry_t cache[MODULE_REQUIRE_CACHE_MAX]; + uint8_t cacheCount; + + /** Directories of files currently mid-require(), innermost last. */ + char_t dirStack[MODULE_REQUIRE_STACK_MAX][ASSET_FILE_NAME_MAX]; + uint8_t dirStackCount; +} modulerequire_t; + +extern modulerequire_t MODULE_REQUIRE; + +/** + * Registers the global require() function. + */ +void moduleRequireInit(void); + +/** + * Frees every cached module object. Called once by moduleListDispose(). + */ +void moduleRequireDispose(void); + +/** + * Resolves a require() argument to an asset-root-relative path. Paths + * starting with "./" or "../" are resolved against the directory of the + * file currently being required (or the asset root if require() is not + * currently nested); any other path is treated as already asset-root- + * relative. "." and ".." segments are collapsed either way. + * + * @param requested The raw string passed to require(). + * @param outResolved Output buffer for the resolved path. + * @param outResolvedSize Capacity of outResolved, including the null + * terminator. + */ +void moduleRequireResolvePath( + const char_t *requested, + char_t *outResolved, + const size_t outResolvedSize +); + +/** + * Collapses "." and ".." segments out of a slash-separated path. + * + * @param path The path to normalize. + * @param outNormalized Output buffer for the normalized path. + * @param outSize Capacity of outNormalized, including the null + * terminator. + */ +void moduleRequireNormalize( + const char_t *path, + char_t *outNormalized, + const size_t outSize +); + +/** + * Extracts the directory portion (including trailing slash) of a + * resolved path, or an empty string if the path has no directory. + * + * @param path The resolved path. + * @param outDir Output buffer for the directory. + * @param outDirSize Capacity of outDir, including the null terminator. + */ +void moduleRequireDirname( + const char_t *path, + char_t *outDir, + const size_t outDirSize +); + +/** + * Looks up a previously require()'d module by its resolved path. + * + * @param resolvedPath The resolved path to look up. + * @param outModuleObj Receives the cached module object if found. + * @return true if a cache entry was found. + */ +bool_t moduleRequireCacheGet( + const char_t *resolvedPath, + jerry_value_t *outModuleObj +); + +/** + * Registers a module object under a resolved path. Takes ownership of + * the passed value - it is only freed by moduleRequireDispose() or + * moduleRequireCacheDelete(). + * + * @param resolvedPath The resolved path to cache under. + * @param moduleObj The module object to cache. + */ +void moduleRequireCacheSet( + const char_t *resolvedPath, + const jerry_value_t moduleObj +); + +/** + * Evicts and frees a cache entry. Used to roll back a failed load so a + * later require() of the same path can retry instead of being stuck + * with a broken cache entry. + * + * @param resolvedPath The resolved path to evict. + */ +void moduleRequireCacheDelete(const char_t *resolvedPath); + +/** + * Pushes a directory onto the require() nesting stack, becoming the new + * base for resolving relative requires until popped. + * + * @param dir The directory (including trailing slash, or empty) to push. + */ +void moduleRequireDirPush(const char_t *dir); + +/** + * Pops the require() nesting stack. + */ +void moduleRequireDirPop(void); + +/** + * Gets the current top of the require() nesting stack. + * + * @return The current base directory, or an empty string if the stack is + * empty (i.e. require() is not currently nested). + */ +const char_t * moduleRequireDirStackTop(void); diff --git a/src/dusk/script/module/time/CMakeLists.txt b/src/dusk/script/module/time/CMakeLists.txt new file mode 100644 index 00000000..337e2c82 --- /dev/null +++ b/src/dusk/script/module/time/CMakeLists.txt @@ -0,0 +1,9 @@ +# Copyright (c) 2026 Dominic Masters +# +# This software is released under the MIT License. +# https://opensource.org/licenses/MIT + +target_sources(${DUSK_LIBRARY_TARGET_NAME} + PUBLIC + moduletime.c +) diff --git a/src/dusk/script/module/time/moduletime.c b/src/dusk/script/module/time/moduletime.c new file mode 100644 index 00000000..e184b9b5 --- /dev/null +++ b/src/dusk/script/module/time/moduletime.c @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "moduletime.h" +#include "script/module/modulebase.h" +#include "time/time.h" + +moduleBaseFunction(moduleTimeGetDelta) { + return jerry_number(TIME.delta); +} + +moduleBaseFunction(moduleTimeGetTime) { + return jerry_number(TIME.time); +} + +void moduleTimeInit(void) { + jerry_value_t timeObj = jerry_object(); + moduleBaseDefineProperty(timeObj, "delta", moduleTimeGetDelta, NULL); + moduleBaseDefineProperty(timeObj, "time", moduleTimeGetTime, NULL); + moduleBaseSetValue("Time", timeObj); + jerry_value_free(timeObj); +} + +void moduleTimeDispose(void) { +} diff --git a/src/dusk/script/module/time/moduletime.h b/src/dusk/script/module/time/moduletime.h new file mode 100644 index 00000000..c9be15d4 --- /dev/null +++ b/src/dusk/script/module/time/moduletime.h @@ -0,0 +1,20 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once + +/** + * Registers the global `Time` object, exposing `Time.delta` and + * `Time.time` as live getters over the engine's TIME state (see + * time/time.h). Values always reflect the most recent timeUpdate(). + */ +void moduleTimeInit(void); + +/** + * Disposes the time module's script resources. + */ +void moduleTimeDispose(void); diff --git a/src/duskrpg/game/game.c b/src/duskrpg/game/game.c index b76b7fa9..a2dd2d54 100644 --- a/src/duskrpg/game/game.c +++ b/src/duskrpg/game/game.c @@ -6,12 +6,10 @@ */ #include "game/game.h" -#include "scene/scene.h" -#include "scene/overworldscene.h" +#include "script/scriptmanager.h" errorret_t gameInit(void) { - sceneid_t testSceneId = overworldSceneCreate(); - sceneSetActive(testSceneId); + errorChain(scriptManagerExecFile("scripts/overworldscene.js", NULL)); errorOk(); } diff --git a/src/duskrpg/scene/CMakeLists.txt b/src/duskrpg/scene/CMakeLists.txt index 40e0832b..e079b5ed 100644 --- a/src/duskrpg/scene/CMakeLists.txt +++ b/src/duskrpg/scene/CMakeLists.txt @@ -3,9 +3,4 @@ # This software is released under the MIT License. # https://opensource.org/licenses/MIT -target_sources(${DUSK_LIBRARY_TARGET_NAME} - PUBLIC - overworldscene.c -) - add_subdirectory(prefab) diff --git a/src/duskrpg/scene/overworldscene.c b/src/duskrpg/scene/overworldscene.c deleted file mode 100644 index 4f7347b9..00000000 --- a/src/duskrpg/scene/overworldscene.c +++ /dev/null @@ -1,138 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "overworldscene.h" -#include "scene/scene.h" -#include "entity/entitymanager.h" -#include "entity/component/display/entityposition.h" -#include "entity/component/display/entitycamera.h" -#include "entity/component/display/entityrenderable.h" -#include "entity/component/physics/entityphysics.h" -#include "display/mesh/plane.h" -#include "display/mesh/capsule.h" -#include "display/color.h" -#include "time/time.h" - -// Radius must stay well outside the floor's footprint (a 20x20 plane has a -// corner-to-center distance of 10*sqrt(2) =~ 14.1) -- orbiting inside that -// puts parts of the floor's own geometry near/behind the camera's view -// direction, which the PSP's legacy GU pipeline can't clip properly and -// drops the whole triangle instead of clipping it. -#define OVERWORLD_SCENE_CAMERA_ORBIT_RADIUS 18.0f -#define OVERWORLD_SCENE_CAMERA_ORBIT_HEIGHT 10.0f -#define OVERWORLD_SCENE_CAMERA_ORBIT_SPEED 0.5f - -static overworldcameraorbit_t OVERWORLD_SCENE_CAMERA_ORBIT; - -sceneid_t overworldSceneCreate(void) { - sceneid_t sceneId = sceneCreate(); - entitymanager_t *mgr = sceneGetEntities(sceneId); - - // Camera, orbiting the origin (see overworldSceneCameraOrbitUpdate). - entityid_t camEntity = entityManagerAdd(mgr); - componentid_t camPosition = entityAddComponent( - mgr, camEntity, COMPONENT_TYPE_POSITION - ); - entityAddComponent(mgr, camEntity, COMPONENT_TYPE_CAMERA); - - OVERWORLD_SCENE_CAMERA_ORBIT = (overworldcameraorbit_t){ - .angle = 0.0f, - .radius = OVERWORLD_SCENE_CAMERA_ORBIT_RADIUS, - .height = OVERWORLD_SCENE_CAMERA_ORBIT_HEIGHT, - .speed = OVERWORLD_SCENE_CAMERA_ORBIT_SPEED - }; - entityUpdateAdd( - mgr, camEntity, overworldSceneCameraOrbitUpdate, camPosition, - &OVERWORLD_SCENE_CAMERA_ORBIT - ); - // Position it correctly for the very first rendered frame, rather than - // waiting for the first fixed tick to run. - overworldSceneCameraOrbitUpdate( - mgr, camEntity, camPosition, &OVERWORLD_SCENE_CAMERA_ORBIT - ); - - // Static ground plane. Physics ignores the entity's position component -- - // the shape's own normal/distance fully define the plane in world space. - entityid_t planeEntity = entityManagerAdd(mgr); - componentid_t planePosition = entityAddComponent( - mgr, planeEntity, COMPONENT_TYPE_POSITION - ); - entityPositionSetLocalPosition( - mgr, planeEntity, planePosition, (vec3){ -10.0f, 0.0f, -10.0f } - ); - entityPositionSetLocalScale( - mgr, planeEntity, planePosition, (vec3){ 20.0f, 1.0f, 20.0f } - ); - componentid_t planePhysics = entityAddComponent( - mgr, planeEntity, COMPONENT_TYPE_PHYSICS - ); - entityPhysicsSetBodyType( - mgr, planeEntity, planePhysics, PHYSICS_BODY_STATIC - ); - entityPhysicsSetShape(mgr, planeEntity, planePhysics, (physicsshape_t){ - .type = PHYSICS_SHAPE_PLANE, - .data.plane = { .normal = { 0.0f, 1.0f, 0.0f }, .distance = 0.0f } - }); - componentid_t planeRenderable = entityAddComponent( - mgr, planeEntity, COMPONENT_TYPE_RENDERABLE - ); - entityrenderable_t *planeR = componentGetData( - mgr, planeEntity, planeRenderable, COMPONENT_TYPE_RENDERABLE - ); - planeR->data.material.meshes[0] = &PLANE_MESH_SIMPLE; - entityRenderableSetColor(mgr, planeEntity, planeRenderable, COLOR_GRAY); - - // Player: dynamic capsule body, moved relative to the camera by - // COMPONENT_TYPE_PLAYER's own update callback (see entityplayer.c). - entityid_t playerEntity = entityManagerAdd(mgr); - componentid_t playerPosition = entityAddComponent( - mgr, playerEntity, COMPONENT_TYPE_POSITION - ); - entityPositionSetLocalPosition( - mgr, playerEntity, playerPosition, (vec3){ 0.0f, 2.0f, 0.0f } - ); - componentid_t playerPhysics = entityAddComponent( - mgr, playerEntity, COMPONENT_TYPE_PHYSICS - ); - entityPhysicsSetShape(mgr, playerEntity, playerPhysics, (physicsshape_t){ - .type = PHYSICS_SHAPE_CAPSULE, - .data.capsule = { .radius = 0.5f, .halfHeight = 0.5f } - }); - componentid_t playerRenderable = entityAddComponent( - mgr, playerEntity, COMPONENT_TYPE_RENDERABLE - ); - entityrenderable_t *playerR = componentGetData( - mgr, playerEntity, playerRenderable, COMPONENT_TYPE_RENDERABLE - ); - playerR->data.material.meshes[0] = &CAPSULE_MESH_SIMPLE; - entityRenderableSetColor(mgr, playerEntity, playerRenderable, COLOR_BLUE); - entityAddComponent(mgr, playerEntity, COMPONENT_TYPE_PLAYER); - - return sceneId; -} - -void overworldSceneCameraOrbitUpdate( - entitymanager_t *mgr, - const entityid_t entityId, - const componentid_t componentId, - void *user -) { - overworldcameraorbit_t *orbit = (overworldcameraorbit_t *)user; - orbit->angle += TIME.delta * orbit->speed; - - vec3 eye = { - cosf(orbit->angle) * orbit->radius, - orbit->height, - sinf(orbit->angle) * orbit->radius - }; - entityPositionLookAt( - mgr, entityId, componentId, - eye, - (vec3){ 0.0f, 0.0f, 0.0f }, - (vec3){ 0.0f, 1.0f, 0.0f } - ); -} diff --git a/src/duskrpg/scene/overworldscene.h b/src/duskrpg/scene/overworldscene.h deleted file mode 100644 index 74ef6d45..00000000 --- a/src/duskrpg/scene/overworldscene.h +++ /dev/null @@ -1,45 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "scene/scenebase.h" -#include "entity/entitybase.h" - -typedef struct { - float_t angle; - float_t radius; - float_t height; - float_t speed; -} overworldcameraorbit_t; - -/** - * Creates the overworld scene: a static ground plane, a player entity - * (dynamic capsule body, moved relative to the camera by - * COMPONENT_TYPE_PLAYER's own update callback), and a camera orbiting the - * origin (see overworldSceneCameraOrbitUpdate). Does not make the scene - * active -- call sceneSetActive with the returned ID. - * - * @return The ID of the newly created scene. - */ -sceneid_t overworldSceneCreate(void); - -/** - * Per-tick update for the test camera: orbits it around the world origin - * at a fixed radius/height/speed, always looking back at the origin. - * Registered automatically by overworldSceneCreate. - * - * @param mgr The entity manager that owns the entity. - * @param entityId The camera entity ID. - * @param componentId The camera's position component ID. - * @param user Pointer to this camera's overworldcameraorbit_t state. - */ -void overworldSceneCameraOrbitUpdate( - entitymanager_t *mgr, - const entityid_t entityId, - const componentid_t componentId, - void *user -); diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index de2439a2..67a99d04 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -14,5 +14,6 @@ add_subdirectory(entity) add_subdirectory(physics) add_subdirectory(scene) # add_subdirectory(item) +add_subdirectory(script) add_subdirectory(time) add_subdirectory(util) \ No newline at end of file diff --git a/test/script/CMakeLists.txt b/test/script/CMakeLists.txt new file mode 100644 index 00000000..877e32b7 --- /dev/null +++ b/test/script/CMakeLists.txt @@ -0,0 +1,13 @@ +# Copyright (c) 2026 Dominic Masters +# +# This software is released under the MIT License. +# https://opensource.org/licenses/MIT + +include(dusktest) + +dusktest(test_modulerequire.c) + +dusktest(test_overworldscene.c) +target_compile_definitions(test_overworldscene PRIVATE + DUSK_ASSETS_DIR="${DUSK_ASSETS_DIR}" +) diff --git a/test/script/test_modulerequire.c b/test/script/test_modulerequire.c new file mode 100644 index 00000000..3c54eb40 --- /dev/null +++ b/test/script/test_modulerequire.c @@ -0,0 +1,228 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "dusktest.h" +#include "asset/asset.h" +#include "script/scriptmanager.h" +#include "script/module/modulebase.h" +#include "util/memory.h" +#include +#include +#include + +// ============================================================ +// In-memory ZIP fixture + script manager lifecycle +// ============================================================ + +static zip_t *g_zip = NULL; + +static int require_setup(void **state) { + zip_error_t err; + zip_error_init(&err); + + zip_source_t *write_src = zip_source_buffer_create(NULL, 0, 1, &err); + if(!write_src) return -1; + + zip_t *za = zip_open_from_source(write_src, ZIP_TRUNCATE, &err); + if(!za) { zip_source_free(write_src); return -1; } + + #define ADD_FILE(name, content) do { \ + zip_source_t *s = zip_source_buffer(za, content, strlen(content), 0); \ + if(zip_file_add(za, name, s, ZIP_FL_OVERWRITE) < 0) { \ + zip_close(za); return -1; \ + } \ + } while(0) + + ADD_FILE( + "lib.js", + "module.exports = { test: function() { return 'hello world'; } };" + ); + ADD_FILE( + "counted.js", + "GLOBAL_COUNTER = " + "(typeof GLOBAL_COUNTER === 'undefined' ? 0 : GLOBAL_COUNTER) + 1;\n" + "module.exports = { count: GLOBAL_COUNTER };" + ); + ADD_FILE( + "dir/parent.js", + "var child = require('./child.js');\n" + "module.exports = { childValue: child.value };" + ); + ADD_FILE("dir/child.js", "module.exports = { value: 42 };"); + ADD_FILE( + "circular-a.js", + "var b = require('./circular-b.js');\n" + "module.exports = " + "{ name: 'a', bName: b.name, bSawUndefinedA: b.aWasUndefined };" + ); + ADD_FILE( + "circular-b.js", + "var a = require('./circular-a.js');\n" + "module.exports = " + "{ name: 'b', aWasUndefined: (typeof a === 'undefined') };" + ); + + #undef ADD_FILE + + zip_source_keep(write_src); + if(zip_close(za) != 0) { zip_source_free(write_src); return -1; } + + zip_stat_t zs; + memset(&zs, 0, sizeof(zs)); + if(zip_source_stat(write_src, &zs) != 0 || !(zs.valid & ZIP_STAT_SIZE)) { + zip_source_free(write_src); return -1; + } + + void *zipbuf = malloc((size_t)zs.size); + if(!zipbuf) { zip_source_free(write_src); return -1; } + + if(zip_source_open(write_src) != 0) { + free(zipbuf); zip_source_free(write_src); return -1; + } + zip_source_read(write_src, zipbuf, (zip_uint64_t)zs.size); + zip_source_close(write_src); + zip_source_free(write_src); + + zip_error_init(&err); + zip_source_t *read_src = zip_source_buffer_create( + zipbuf, (zip_uint64_t)zs.size, 1, &err + ); + if(!read_src) { free(zipbuf); return -1; } + + g_zip = zip_open_from_source(read_src, 0, &err); + if(!g_zip) { zip_source_free(read_src); return -1; } + + ASSET.zip = g_zip; + + errorret_t ret = scriptManagerInit(); + if(errorIsNotOk(ret)) { errorCatch(ret); return -1; } + + return 0; +} + +static int require_teardown(void **state) { + errorret_t ret = scriptManagerDispose(); + if(errorIsNotOk(ret)) errorCatch(ret); + + if(g_zip) { zip_close(g_zip); g_zip = NULL; } + ASSET.zip = NULL; + return 0; +} + +// ============================================================ +// Tests +// ============================================================ + +static void test_require_basic(void **state) { + jerry_value_t result; + errorret_t ret = scriptManagerExec( + "var something = require('./lib.js'); something.test();", &result + ); + assert_true(errorIsOk(ret)); + assert_true(jerry_value_is_string(result)); + + char_t buf[64]; + moduleBaseToString(result, buf, sizeof(buf)); + assert_string_equal(buf, "hello world"); + + jerry_value_free(result); + assert_int_equal(memoryGetAllocatedCount(), 0); +} + +static void test_require_caches_and_executes_once(void **state) { + jerry_value_t result; + errorret_t ret = scriptManagerExec( + "require('./counted.js'); require('./counted.js');" + "require('./counted.js').count;", + &result + ); + assert_true(errorIsOk(ret)); + assert_true(jerry_value_is_number(result)); + assert_int_equal(moduleBaseValueInt(result), 1); + + jerry_value_free(result); + assert_int_equal(memoryGetAllocatedCount(), 0); +} + +static void test_require_relative_path_resolves_to_requiring_files_dir( + void **state +) { + jerry_value_t result; + errorret_t ret = scriptManagerExec( + "require('dir/parent.js').childValue;", &result + ); + assert_true(errorIsOk(ret)); + assert_true(jerry_value_is_number(result)); + assert_int_equal(moduleBaseValueInt(result), 42); + + jerry_value_free(result); + assert_int_equal(memoryGetAllocatedCount(), 0); +} + +static void test_require_circular_does_not_hang(void **state) { + jerry_value_t result; + errorret_t ret = scriptManagerExec("require('./circular-a.js');", &result); + assert_true(errorIsOk(ret)); + assert_true(jerry_value_is_object(result)); + + jerry_value_t nameVal = moduleBaseGetProp(result, "name"); + char_t nameBuf[16]; + moduleBaseToString(nameVal, nameBuf, sizeof(nameBuf)); + assert_string_equal(nameBuf, "a"); + jerry_value_free(nameVal); + + jerry_value_t bNameVal = moduleBaseGetProp(result, "bName"); + char_t bNameBuf[16]; + moduleBaseToString(bNameVal, bNameBuf, sizeof(bNameBuf)); + assert_string_equal(bNameBuf, "b"); + jerry_value_free(bNameVal); + + // circular-b.js's require('./circular-a.js') ran while circular-a.js was + // still mid-execution (before it set module.exports), so it must have + // seen the in-progress module's exports as still undefined. + jerry_value_t sawVal = moduleBaseGetProp(result, "bSawUndefinedA"); + assert_true(jerry_value_is_true(sawVal)); + jerry_value_free(sawVal); + + jerry_value_free(result); + assert_int_equal(memoryGetAllocatedCount(), 0); +} + +static void test_require_missing_file_throws(void **state) { + errorret_t ret = scriptManagerExec("require('./does-not-exist.js');", NULL); + assert_true(errorIsNotOk(ret)); + errorCatch(ret); + + assert_int_equal(memoryGetAllocatedCount(), 0); +} + +// ============================================================ +// main +// ============================================================ + +int main(void) { + assertInit(); + const struct CMUnitTest tests[] = { + cmocka_unit_test_setup_teardown( + test_require_basic, require_setup, require_teardown + ), + cmocka_unit_test_setup_teardown( + test_require_caches_and_executes_once, require_setup, require_teardown + ), + cmocka_unit_test_setup_teardown( + test_require_relative_path_resolves_to_requiring_files_dir, + require_setup, require_teardown + ), + cmocka_unit_test_setup_teardown( + test_require_circular_does_not_hang, require_setup, require_teardown + ), + cmocka_unit_test_setup_teardown( + test_require_missing_file_throws, require_setup, require_teardown + ), + }; + return cmocka_run_group_tests(tests, NULL, NULL); +} diff --git a/test/script/test_overworldscene.c b/test/script/test_overworldscene.c new file mode 100644 index 00000000..4657de4c --- /dev/null +++ b/test/script/test_overworldscene.c @@ -0,0 +1,243 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "dusktest.h" +#include "util/memory.h" +#include "time/time.h" +#include "scene/scene.h" +#include "entity/entitymanager.h" +#include "entity/component.h" +#include "entity/component/display/entityposition.h" +#include "entity/component/physics/entityphysics.h" +#include "entity/component/display/entityrenderable.h" +#include "display/mesh/plane.h" +#include "display/mesh/capsule.h" +#include "script/scriptmanager.h" +#include +#include + +#ifndef DUSK_ASSETS_DIR + #error "DUSK_ASSETS_DIR must be defined" +#endif + +// Reads the real, shipped overworldscene.js from disk (not a copy embedded +// in this test) so this test actually verifies what ships. +static char_t *readScriptSource(void) { + char_t path[512]; + snprintf( + path, sizeof(path), "%s/scripts/overworldscene.js", DUSK_ASSETS_DIR + ); + + FILE *f = fopen(path, "rb"); + assert_non_null(f); + + fseek(f, 0, SEEK_END); + long size = ftell(f); + fseek(f, 0, SEEK_SET); + + char_t *buf = (char_t *)memoryAllocate((size_t)size + 1); + size_t read = fread(buf, 1, (size_t)size, f); + fclose(f); + buf[read] = '\0'; + + return buf; +} + +static int overworld_setup(void **state) { + sceneInit(); + + errorret_t ret = scriptManagerInit(); + if(errorIsNotOk(ret)) { errorCatch(ret); return -1; } + + return 0; +} + +static int overworld_teardown(void **state) { + errorret_t ret = scriptManagerDispose(); + if(errorIsNotOk(ret)) errorCatch(ret); + + sceneDispose(); + + // JerryScript defers freeing native-wrapped handles (Entity/Scene/ + // Position/Physics/Renderable instances) until GC/jerry_cleanup() runs, + // so the leak check can only be meaningful after scriptManagerDispose() + // has actually run above -- not inside the test body. + assert_int_equal(memoryGetAllocatedCount(), 0); + return 0; +} + +static void test_overworldscene_builds_expected_entities(void **state) { + char_t *src = readScriptSource(); + errorret_t ret = scriptManagerExec(src, NULL); + memoryFree(src); + assert_true(errorIsOk(ret)); + + sceneid_t sceneId = sceneGetActive(); + assert_true(sceneId != SCENE_ID_INVALID); + entitymanager_t *mgr = sceneGetEntities(sceneId); + + // Entity 0: camera. Position + Camera components, initial orbit + // position placed with angle=0 (Time.delta defaults to 0 with no + // timeInit()/timeUpdate() in this test), i.e. eye=(radius, height, 0). + entityid_t camEntity = 0; + componentid_t camPos = entityGetComponent( + mgr, camEntity, COMPONENT_TYPE_POSITION + ); + assert_true(camPos != COMPONENT_ID_INVALID); + assert_true( + entityGetComponent(mgr, camEntity, COMPONENT_TYPE_CAMERA) != + COMPONENT_ID_INVALID + ); + + vec3 camPosition; + entityPositionGetLocalPosition(mgr, camEntity, camPos, camPosition); + assert_float_equal(camPosition[0], 18.0f, 0.0001f); + assert_float_equal(camPosition[1], 10.0f, 0.0001f); + assert_float_equal(camPosition[2], 0.0f, 0.0001f); + + // Entity 1: static ground plane. + entityid_t planeEntity = 1; + componentid_t planePos = entityGetComponent( + mgr, planeEntity, COMPONENT_TYPE_POSITION + ); + assert_true(planePos != COMPONENT_ID_INVALID); + + vec3 planePosition, planeScale; + entityPositionGetLocalPosition(mgr, planeEntity, planePos, planePosition); + entityPositionGetLocalScale(mgr, planeEntity, planePos, planeScale); + assert_float_equal(planePosition[0], -10.0f, 0.0001f); + assert_float_equal(planePosition[1], 0.0f, 0.0001f); + assert_float_equal(planePosition[2], -10.0f, 0.0001f); + assert_float_equal(planeScale[0], 20.0f, 0.0001f); + assert_float_equal(planeScale[1], 1.0f, 0.0001f); + assert_float_equal(planeScale[2], 20.0f, 0.0001f); + + componentid_t planePhysics = entityGetComponent( + mgr, planeEntity, COMPONENT_TYPE_PHYSICS + ); + assert_true(planePhysics != COMPONENT_ID_INVALID); + assert_int_equal( + entityPhysicsGetBodyType(mgr, planeEntity, planePhysics), + PHYSICS_BODY_STATIC + ); + physicsshape_t planeShape = entityPhysicsGetShape( + mgr, planeEntity, planePhysics + ); + assert_int_equal(planeShape.type, PHYSICS_SHAPE_PLANE); + assert_float_equal(planeShape.data.plane.normal[0], 0.0f, 0.0001f); + assert_float_equal(planeShape.data.plane.normal[1], 1.0f, 0.0001f); + assert_float_equal(planeShape.data.plane.normal[2], 0.0f, 0.0001f); + assert_float_equal(planeShape.data.plane.distance, 0.0f, 0.0001f); + + componentid_t planeRenderable = entityGetComponent( + mgr, planeEntity, COMPONENT_TYPE_RENDERABLE + ); + assert_true(planeRenderable != COMPONENT_ID_INVALID); + entityrenderable_t *planeR = componentGetData( + mgr, planeEntity, planeRenderable, COMPONENT_TYPE_RENDERABLE + ); + assert_ptr_equal(planeR->data.material.meshes[0], &PLANE_MESH_SIMPLE); + assert_int_equal(planeR->data.material.material.unlit.color.r, 128); + assert_int_equal(planeR->data.material.material.unlit.color.g, 128); + assert_int_equal(planeR->data.material.material.unlit.color.b, 128); + assert_int_equal(planeR->data.material.material.unlit.color.a, 255); + + // Entity 2: player. Dynamic capsule body (default body type), PLAYER + // component present. + entityid_t playerEntity = 2; + componentid_t playerPos = entityGetComponent( + mgr, playerEntity, COMPONENT_TYPE_POSITION + ); + assert_true(playerPos != COMPONENT_ID_INVALID); + + vec3 playerPosition; + entityPositionGetLocalPosition(mgr, playerEntity, playerPos, playerPosition); + assert_float_equal(playerPosition[0], 0.0f, 0.0001f); + assert_float_equal(playerPosition[1], 2.0f, 0.0001f); + assert_float_equal(playerPosition[2], 0.0f, 0.0001f); + + componentid_t playerPhysics = entityGetComponent( + mgr, playerEntity, COMPONENT_TYPE_PHYSICS + ); + assert_true(playerPhysics != COMPONENT_ID_INVALID); + assert_int_equal( + entityPhysicsGetBodyType(mgr, playerEntity, playerPhysics), + PHYSICS_BODY_DYNAMIC + ); + physicsshape_t playerShape = entityPhysicsGetShape( + mgr, playerEntity, playerPhysics + ); + assert_int_equal(playerShape.type, PHYSICS_SHAPE_CAPSULE); + assert_float_equal(playerShape.data.capsule.radius, 0.5f, 0.0001f); + assert_float_equal(playerShape.data.capsule.halfHeight, 0.5f, 0.0001f); + + componentid_t playerRenderable = entityGetComponent( + mgr, playerEntity, COMPONENT_TYPE_RENDERABLE + ); + assert_true(playerRenderable != COMPONENT_ID_INVALID); + entityrenderable_t *playerR = componentGetData( + mgr, playerEntity, playerRenderable, COMPONENT_TYPE_RENDERABLE + ); + assert_ptr_equal(playerR->data.material.meshes[0], &CAPSULE_MESH_SIMPLE); + assert_int_equal(playerR->data.material.material.unlit.color.r, 0); + assert_int_equal(playerR->data.material.material.unlit.color.g, 0); + assert_int_equal(playerR->data.material.material.unlit.color.b, 255); + assert_int_equal(playerR->data.material.material.unlit.color.a, 255); + + assert_true( + entityGetComponent(mgr, playerEntity, COMPONENT_TYPE_PLAYER) != + COMPONENT_ID_INVALID + ); +} + +static void test_overworldscene_update_orbits_camera(void **state) { + char_t *src = readScriptSource(); + errorret_t ret = scriptManagerExec(src, NULL); + memoryFree(src); + assert_true(errorIsOk(ret)); + + sceneid_t sceneId = sceneGetActive(); + entitymanager_t *mgr = sceneGetEntities(sceneId); + entityid_t camEntity = 0; + componentid_t camPos = entityGetComponent( + mgr, camEntity, COMPONENT_TYPE_POSITION + ); + + // Drive one frame with a known delta and confirm the camera orbited by + // exactly angle = delta * speed (0.1 * 0.5 = 0.05 rad). + TIME.delta = 0.1f; + ret = scriptManagerCallGlobal("update"); + assert_true(errorIsOk(ret)); + + vec3 camPosition; + entityPositionGetLocalPosition(mgr, camEntity, camPos, camPosition); + const float_t expectedAngle = 0.1f * 0.5f; + assert_float_equal( + camPosition[0], cosf(expectedAngle) * 18.0f, 0.0001f + ); + assert_float_equal(camPosition[1], 10.0f, 0.0001f); + assert_float_equal( + camPosition[2], sinf(expectedAngle) * 18.0f, 0.0001f + ); + + TIME.delta = 0.0f; +} + +int main(void) { + assertInit(); + const struct CMUnitTest tests[] = { + cmocka_unit_test_setup_teardown( + test_overworldscene_builds_expected_entities, + overworld_setup, overworld_teardown + ), + cmocka_unit_test_setup_teardown( + test_overworldscene_update_orbits_camera, + overworld_setup, overworld_teardown + ), + }; + return cmocka_run_group_tests(tests, NULL, NULL); +} diff --git a/types/display/mesh.d.ts b/types/display/mesh.d.ts new file mode 100644 index 00000000..c41cf19c --- /dev/null +++ b/types/display/mesh.d.ts @@ -0,0 +1,15 @@ +// Copyright (c) 2026 Dominic Masters +// +// This software is released under the MIT License. +// https://opensource.org/licenses/MIT + +/** + * Built-in primitive meshes, usable with Renderable.setMesh(). Opaque + * engine-owned handles -- do not inspect or mutate. + */ +declare const MESH_CUBE: object; +declare const MESH_PLANE: object; +declare const MESH_SPHERE: object; +declare const MESH_CAPSULE: object; +declare const MESH_TRIPRISM: object; +declare const MESH_QUAD: object; diff --git a/types/entity/component/component.d.ts b/types/entity/component/component.d.ts index 92ee779e..1fd36250 100644 --- a/types/entity/component/component.d.ts +++ b/types/entity/component/component.d.ts @@ -4,11 +4,13 @@ // https://opensource.org/licenses/MIT /** - * A component attached to an Entity. Generic across every component - * type for now -- there are no typed per-type properties yet (e.g. no - * `.position` on a POSITION component), just the operations every - * component supports regardless of type. Only ever obtained via - * Entity.add()/Entity.getComponent(), never constructed directly. + * A component attached to an Entity: the operations every component + * supports regardless of type. Entity.add()/Entity.getComponent() return + * a typed subclass for component types that have one -- see position.d.ts + * (POSITION), physics.d.ts (PHYSICS), and renderable.d.ts (RENDERABLE). + * Every other component type (CAMERA, TRIGGER, ANIMATION, PLAYER, + * INTERACTABLE) still only returns this generic Component. Never + * constructed directly. */ declare class Component { /** The componenttype_t this component was added as, e.g. POSITION. */ diff --git a/types/entity/component/physics.d.ts b/types/entity/component/physics.d.ts new file mode 100644 index 00000000..35ebb6c2 --- /dev/null +++ b/types/entity/component/physics.d.ts @@ -0,0 +1,54 @@ +// Copyright (c) 2026 Dominic Masters +// +// This software is released under the MIT License. +// https://opensource.org/licenses/MIT + +/// + +/** Physics body type constants, usable with Physics.setBodyType(). */ +declare const PHYSICS_BODY_STATIC: number; +declare const PHYSICS_BODY_DYNAMIC: number; +declare const PHYSICS_BODY_KINEMATIC: number; + +/** + * Physics shape type constants, usable with Physics.setShape(). Argument + * shape per type: + * - PHYSICS_SHAPE_CUBE: (type, halfExtentX, halfExtentY, halfExtentZ) + * - PHYSICS_SHAPE_SPHERE: (type, radius) + * - PHYSICS_SHAPE_CAPSULE: (type, radius, halfHeight) + * - PHYSICS_SHAPE_PLANE: (type, normalX, normalY, normalZ, distance) + */ +declare const PHYSICS_SHAPE_CUBE: number; +declare const PHYSICS_SHAPE_SPHERE: number; +declare const PHYSICS_SHAPE_CAPSULE: number; +declare const PHYSICS_SHAPE_PLANE: number; + +/** + * Typed wrapper for a PHYSICS component. Returned by Entity.add(PHYSICS) + * / Entity.getComponent(PHYSICS) instead of the generic Component. + */ +declare class Physics extends Component { + /** Sets the body type (see PHYSICS_BODY_* constants). */ + setBodyType(type: number): void; + + /** Gets the body type. */ + getBodyType(): number; + + /** + * Sets the collision shape. See PHYSICS_SHAPE_* constants for the + * expected trailing arguments per shape type. + */ + setShape(type: number, ...args: number[]): void; + + /** Sets the body's velocity (not an impulse -- affected by mass/drag). */ + setVelocity(x: number, y: number, z: number): void; + + /** Gets the body's velocity. */ + getVelocity(): { x: number; y: number; z: number }; + + /** Applies an immediate velocity change, unaffected by mass/drag. */ + applyImpulse(x: number, y: number, z: number): void; + + /** True if the body rested on a surface during the last physics step. */ + isOnGround(): boolean; +} diff --git a/types/entity/component/position.d.ts b/types/entity/component/position.d.ts new file mode 100644 index 00000000..a5410b43 --- /dev/null +++ b/types/entity/component/position.d.ts @@ -0,0 +1,34 @@ +// Copyright (c) 2026 Dominic Masters +// +// This software is released under the MIT License. +// https://opensource.org/licenses/MIT + +/// + +/** + * Typed wrapper for a POSITION component. Returned by Entity.add(POSITION) + * / Entity.getComponent(POSITION) instead of the generic Component. + */ +declare class Position extends Component { + /** Sets the local position (not world position -- see entityposition.h). */ + setLocalPosition(x: number, y: number, z: number): void; + + /** Gets the cached local position. */ + getLocalPosition(): { x: number; y: number; z: number }; + + /** Sets the local scale. */ + setLocalScale(x: number, y: number, z: number): void; + + /** Gets the cached local scale. */ + getLocalScale(): { x: number; y: number; z: number }; + + /** + * Positions and orients the entity at (ex, ey, ez), facing + * (tx, ty, tz), with (ux, uy, uz) as up. + */ + lookAt( + ex: number, ey: number, ez: number, + tx: number, ty: number, tz: number, + ux: number, uy: number, uz: number + ): void; +} diff --git a/types/entity/component/renderable.d.ts b/types/entity/component/renderable.d.ts new file mode 100644 index 00000000..2b55fc96 --- /dev/null +++ b/types/entity/component/renderable.d.ts @@ -0,0 +1,26 @@ +// Copyright (c) 2026 Dominic Masters +// +// This software is released under the MIT License. +// https://opensource.org/licenses/MIT + +/// + +/** + * Typed wrapper for a RENDERABLE component. Returned by + * Entity.add(RENDERABLE) / Entity.getComponent(RENDERABLE) instead of + * the generic Component. Only meaningful for the default shader-material + * renderable type. + */ +declare class Renderable extends Component { + /** Sets the unlit material color (each channel 0-255). */ + setColor(r: number, g: number, b: number, a: number): void; + + /** + * Sets one of the material's mesh slots. mesh must be one of the + * built-in MESH_* constants (see mesh.d.ts). + */ + setMesh(slot: number, mesh: object): void; + + /** Sets the render priority. 0 = auto (derived from type/flags). */ + setPriority(priority: number): void; +} diff --git a/types/index.d.ts b/types/index.d.ts index e84bf671..e5ef4677 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -3,6 +3,12 @@ // This software is released under the MIT License. // https://opensource.org/licenses/MIT +/// /// /// +/// +/// +/// /// +/// +/// diff --git a/types/require.d.ts b/types/require.d.ts new file mode 100644 index 00000000..8c899218 --- /dev/null +++ b/types/require.d.ts @@ -0,0 +1,20 @@ +// Copyright (c) 2026 Dominic Masters +// +// This software is released under the MIT License. +// https://opensource.org/licenses/MIT + +/** + * Loads and executes another script file (relative to the asset root, + * or to the requiring file's own directory if path starts with "./" or + * "../"), returning its `module.exports`. Each resolved path is only + * executed once - subsequent require() calls for the same file return + * the cached exports. + */ +declare function require(path: string): any; + +/** + * Only defined inside a file loaded via require() - not present in a + * top-level script executed directly. Set `module.exports` to whatever + * value require() of this file should return. + */ +declare const module: { exports: any }; diff --git a/types/time/time.d.ts b/types/time/time.d.ts new file mode 100644 index 00000000..dbd259ce --- /dev/null +++ b/types/time/time.d.ts @@ -0,0 +1,15 @@ +// Copyright (c) 2026 Dominic Masters +// +// This software is released under the MIT License. +// https://opensource.org/licenses/MIT + +/** + * Live view over the engine's fixed-timestep clock (see time/time.h). + * `delta`/`time` always reflect the most recent timeUpdate() call. + */ +declare const Time: { + /** Fixed simulation timestep, in seconds (always DUSK_TIME_STEP). */ + readonly delta: number; + /** Total elapsed fixed-simulation time, in seconds. */ + readonly time: number; +};