diff --git a/assets/cutscenes/MoveCubeCutscene.js b/assets/cutscenes/MoveCubeCutscene.js index 6038a99b..418da840 100644 --- a/assets/cutscenes/MoveCubeCutscene.js +++ b/assets/cutscenes/MoveCubeCutscene.js @@ -7,30 +7,23 @@ function MoveCubeCutscene(params) { var startX = this.cube.position.position.x; - // this.animLeft = new Animation([ - // { time: 0.0, value: startX, easing: Easing.outQuad }, - // { time: DURATION, value: startX - SPEED * DURATION } - // ]); + this.anim = new Animation([ + [ + { time: 0.0, value: startX, easing: Easing.outQuad }, + { time: DURATION, value: startX - SPEED * DURATION, easing: Easing.inOutQuad }, + { time: DURATION * 2, value: startX } + ] + ]); - // this.animRight = new Animation([ - // { time: 0.0, value: startX - SPEED * DURATION, easing: Easing.inOutQuad }, - // { time: DURATION, value: startX } - // ]); + this.anim.onComplete = function() { Cutscene.finish(); }; } MoveCubeCutscene.prototype = Object.create(Cutscene.prototype); MoveCubeCutscene.prototype.constructor = MoveCubeCutscene; MoveCubeCutscene.prototype.update = function() { - Cutscene.finish(); - // if(!this.animLeft.complete) { - // this.cube.position.position.x = this.animLeft.update(TIME.delta); - // } else { - // this.cube.position.position.x = this.animRight.update(TIME.delta); - // if(this.animRight.complete) { - // Cutscene.finish(); - // } - // } + this.anim.update(TIME.delta); + this.cube.position.position.x = this.anim.properties[0].value; }; module = MoveCubeCutscene; diff --git a/src/dusk/CMakeLists.txt b/src/dusk/CMakeLists.txt index 1512d200..8d110da8 100644 --- a/src/dusk/CMakeLists.txt +++ b/src/dusk/CMakeLists.txt @@ -55,6 +55,7 @@ target_sources(${DUSK_BINARY_TARGET_NAME} # Subdirs add_subdirectory(animation) +add_subdirectory(event) add_subdirectory(assert) add_subdirectory(asset) add_subdirectory(cutscene) diff --git a/src/dusk/animation/animation.c b/src/dusk/animation/animation.c index 91e070e5..0007eacc 100644 --- a/src/dusk/animation/animation.c +++ b/src/dusk/animation/animation.c @@ -10,42 +10,56 @@ void animationInit(animation_t *anim) { memoryZero(anim, sizeof(animation_t)); + eventInit(&anim->onStart); + eventInit(&anim->onComplete); } -animationproperty_t *animationAddProperty(animation_t *anim) { +animationproperty_t *animationAddProperty(animation_t *anim, float_t *target) { assertTrue( anim->propertyCount < ANIMATION_PROPERTY_COUNT_MAX, "Property count exceeds ANIMATION_PROPERTY_COUNT_MAX" ); animationproperty_t *prop = &anim->properties[anim->propertyCount++]; prop->keyframeCount = 0; + prop->target = target; return prop; } void animationUpdate(animation_t *anim, float_t delta) { assertNotNull(anim, "Animation cannot be null"); - // Anim done? if( (anim->flags & ANIMATION_FLAG_FINISHED) && !(anim->flags & ANIMATION_FLAG_LOOP_ENABLED) ) return; - // Start + bool_t wasStarted = (anim->flags & ANIMATION_FLAG_STARTED) != 0; anim->flags |= ANIMATION_FLAG_STARTED; anim->time += delta; - // Tick float_t duration = animationGetDuration(anim); + bool_t justFinished = false; if(anim->time >= duration) { - // Done/Loop if(anim->flags & ANIMATION_FLAG_LOOP_ENABLED) { anim->time = mathModFloat(anim->time, duration); } else { anim->time = duration; - anim->flags |= ANIMATION_FLAG_FINISHED; + if(!(anim->flags & ANIMATION_FLAG_FINISHED)) { + anim->flags |= ANIMATION_FLAG_FINISHED; + justFinished = true; + } } } + + for(uint8_t i = 0; i < anim->propertyCount; i++) { + animationproperty_t *prop = &anim->properties[i]; + if(prop->target != NULL) { + *prop->target = animationPropertyGetValue(prop, anim->time); + } + } + + if(!wasStarted) eventInvoke(&anim->onStart, anim); + if(justFinished) eventInvoke(&anim->onComplete, anim); } diff --git a/src/dusk/animation/animation.h b/src/dusk/animation/animation.h index afeee823..90b81528 100644 --- a/src/dusk/animation/animation.h +++ b/src/dusk/animation/animation.h @@ -5,6 +5,7 @@ #pragma once #include "animationproperty.h" +#include "event/event.h" #define ANIMATION_PROPERTY_COUNT_MAX 8 @@ -17,6 +18,8 @@ typedef struct { uint8_t propertyCount; float_t time; uint8_t flags; + event_t onStart; + event_t onComplete; } animation_t; /** @@ -31,9 +34,10 @@ void animationInit(animation_t *anim); * for the lifetime of the animation. * * @param anim The animation to add the property to. + * @param target Pointer to the float to write interpolated values into. * @return A pointer to the new property. */ -animationproperty_t *animationAddProperty(animation_t *anim); +animationproperty_t *animationAddProperty(animation_t *anim, float_t *target); /** * Advances the animation by delta seconds and writes interpolated values to diff --git a/src/dusk/animation/animationproperty.h b/src/dusk/animation/animationproperty.h index 9f989024..f2be7fbe 100644 --- a/src/dusk/animation/animationproperty.h +++ b/src/dusk/animation/animationproperty.h @@ -13,6 +13,7 @@ typedef struct { keyframe_t keyframes[ANIMATION_PROPERTY_KEYFRAME_COUNT_MAX]; uint8_t keyframeCount; + float_t *target; } animationproperty_t; /** diff --git a/src/dusk/display/spritebatch/spritebatch.c b/src/dusk/display/spritebatch/spritebatch.c index d46d152d..8f1a4015 100644 --- a/src/dusk/display/spritebatch/spritebatch.c +++ b/src/dusk/display/spritebatch/spritebatch.c @@ -66,40 +66,54 @@ errorret_t spriteBatchPushv( const float_t *sMaxX = maxX + offset; const float_t *sMinY = minY + offset; const float_t *sMaxY = maxY + offset; - const float_t *sZ = z + offset; - const float_t *sU0 = u0 + offset; - const float_t *sU1 = u1 + offset; - const float_t *sV0 = v0 + offset; - const float_t *sV1 = v1 + offset; + const float_t *sZ = z + offset; + const float_t *sU0 = u0 + offset; + const float_t *sU1 = u1 + offset; + const float_t *sV0 = v0 + offset; + const float_t *sV1 = v1 + offset; - #define COPY_FIELD(vi, field, srcArr) \ - memoryCopyInterleaved(&start[vi].field, dstStride, srcArr, fSz, fSz, toPush) + #define memshVertCopy(vi, field, srcArr) \ + memoryCopyInterleaved( \ + &start[vi].field, dstStride, srcArr, fSz, fSz, toPush \ + ) - COPY_FIELD(0, pos[0], sMinX); COPY_FIELD(0, pos[1], sMinY); - COPY_FIELD(0, pos[2], sZ); COPY_FIELD(0, uv[0], sU0); - COPY_FIELD(0, uv[1], sV0); + memshVertCopy(0, pos[0], sMinX); + memshVertCopy(0, pos[1], sMinY); + memshVertCopy(0, pos[2], sZ); + memshVertCopy(0, uv[0], sU0); + memshVertCopy(0, uv[1], sV0); - COPY_FIELD(1, pos[0], sMaxX); COPY_FIELD(1, pos[1], sMinY); - COPY_FIELD(1, pos[2], sZ); COPY_FIELD(1, uv[0], sU1); - COPY_FIELD(1, uv[1], sV0); + memshVertCopy(1, pos[0], sMaxX); + memshVertCopy(1, pos[1], sMinY); + memshVertCopy(1, pos[2], sZ); + memshVertCopy(1, uv[0], sU1); + memshVertCopy(1, uv[1], sV0); - COPY_FIELD(2, pos[0], sMaxX); COPY_FIELD(2, pos[1], sMaxY); - COPY_FIELD(2, pos[2], sZ); COPY_FIELD(2, uv[0], sU1); - COPY_FIELD(2, uv[1], sV1); + memshVertCopy(2, pos[0], sMaxX); + memshVertCopy(2, pos[1], sMaxY); + memshVertCopy(2, pos[2], sZ); + memshVertCopy(2, uv[0], sU1); + memshVertCopy(2, uv[1], sV1); - COPY_FIELD(3, pos[0], sMinX); COPY_FIELD(3, pos[1], sMinY); - COPY_FIELD(3, pos[2], sZ); COPY_FIELD(3, uv[0], sU0); - COPY_FIELD(3, uv[1], sV0); + memshVertCopy(3, pos[0], sMinX); + memshVertCopy(3, pos[1], sMinY); + memshVertCopy(3, pos[2], sZ); + memshVertCopy(3, uv[0], sU0); + memshVertCopy(3, uv[1], sV0); - COPY_FIELD(4, pos[0], sMaxX); COPY_FIELD(4, pos[1], sMaxY); - COPY_FIELD(4, pos[2], sZ); COPY_FIELD(4, uv[0], sU1); - COPY_FIELD(4, uv[1], sV1); + memshVertCopy(4, pos[0], sMaxX); + memshVertCopy(4, pos[1], sMaxY); + memshVertCopy(4, pos[2], sZ); + memshVertCopy(4, uv[0], sU1); + memshVertCopy(4, uv[1], sV1); - COPY_FIELD(5, pos[0], sMinX); COPY_FIELD(5, pos[1], sMaxY); - COPY_FIELD(5, pos[2], sZ); COPY_FIELD(5, uv[0], sU0); - COPY_FIELD(5, uv[1], sV1); + memshVertCopy(5, pos[0], sMinX); + memshVertCopy(5, pos[1], sMaxY); + memshVertCopy(5, pos[2], sZ); + memshVertCopy(5, uv[0], sU0); + memshVertCopy(5, uv[1], sV1); - #undef COPY_FIELD + #undef memshVertCopy #if MESH_ENABLE_COLOR for(uint8_t vi = 0; vi < QUAD_VERTEX_COUNT; vi++) { diff --git a/src/dusk/event/CMakeLists.txt b/src/dusk/event/CMakeLists.txt new file mode 100644 index 00000000..69958ead --- /dev/null +++ b/src/dusk/event/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 + event.c +) diff --git a/src/dusk/event/event.c b/src/dusk/event/event.c new file mode 100644 index 00000000..a6b12977 --- /dev/null +++ b/src/dusk/event/event.c @@ -0,0 +1,59 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "event.h" +#include "assert/assert.h" +#include "util/memory.h" + +void eventInit(event_t *event) { + assertNotNull(event, "event must not be NULL"); + memoryZero(event, sizeof(event_t)); +} + +void eventSubscribe(event_t *event, eventcallback_t callback, void *user) { + assertNotNull(event, "event must not be NULL"); + assertNotNull((void *)callback, "callback must not be NULL"); + + for(uint8_t i = 0; i < event->count; i++) { + if(event->callbacks[i] == callback && event->users[i] == user) return; + } + + assertTrue( + event->count < EVENT_SUBSCRIBER_COUNT_MAX, + "EVENT_SUBSCRIBER_COUNT_MAX exceeded" + ); + + event->callbacks[event->count] = callback; + event->users[event->count] = user; + event->count++; +} + +void eventUnsubscribe(event_t *event, eventcallback_t callback, void *user) { + assertNotNull(event, "event must not be NULL"); + assertNotNull((void *)callback, "callback must not be NULL"); + + for(uint8_t i = 0; i < event->count; i++) { + if(event->callbacks[i] != callback || event->users[i] != user) continue; + + uint8_t last = event->count - 1; + if(i != last) { + event->callbacks[i] = event->callbacks[last]; + event->users[i] = event->users[last]; + } + event->callbacks[last] = NULL; + event->users[last] = NULL; + event->count--; + return; + } +} + +void eventInvoke(const event_t *event, void *params) { + assertNotNull(event, "event must not be NULL"); + for(uint8_t i = 0; i < event->count; i++) { + event->callbacks[i](params, event->users[i]); + } +} diff --git a/src/dusk/event/event.h b/src/dusk/event/event.h new file mode 100644 index 00000000..340ad3d1 --- /dev/null +++ b/src/dusk/event/event.h @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "dusk.h" + +#define EVENT_SUBSCRIBER_COUNT_MAX 8 + +typedef void (*eventcallback_t)(void *params, void *user); + +typedef struct { + eventcallback_t callbacks[EVENT_SUBSCRIBER_COUNT_MAX]; + void *users[EVENT_SUBSCRIBER_COUNT_MAX]; + uint8_t count; +} event_t; + +/** + * Initializes an event, clearing all subscribers. + * + * @param event The event to initialize. + */ +void eventInit(event_t *event); + +/** + * Subscribes a callback to an event. The callback is invoked with params and + * the provided user pointer each time the event fires. The same (callback, + * user) pair may only be subscribed once. + * + * @param event The event to subscribe to. + * @param callback The function to call when the event fires. + * @param user Arbitrary pointer forwarded to the callback unchanged. + */ +void eventSubscribe(event_t *event, eventcallback_t callback, void *user); + +/** + * Removes a previously subscribed (callback, user) pair. Does nothing if the + * pair is not currently subscribed. + * + * @param event The event to unsubscribe from. + * @param callback The callback that was passed to eventSubscribe. + * @param user The user pointer that was passed to eventSubscribe. + */ +void eventUnsubscribe(event_t *event, eventcallback_t callback, void *user); + +/** + * Invokes all subscribed callbacks, passing params and each subscriber's user + * pointer. + * + * @param event The event to invoke. + * @param params Arbitrary pointer forwarded to every callback unchanged. + */ +void eventInvoke(const event_t *event, void *params); diff --git a/src/dusk/input/input.c b/src/dusk/input/input.c index 5e3f98db..c67d9a2f 100644 --- a/src/dusk/input/input.c +++ b/src/dusk/input/input.c @@ -11,6 +11,7 @@ #include "util/string.h" #include "util/math.h" #include "time/time.h" +#include "event/event.h" input_t INPUT; @@ -21,6 +22,8 @@ errorret_t inputInit(void) { INPUT.actions[i].action = (inputaction_t)i; INPUT.actions[i].lastValue = 0.0f; INPUT.actions[i].currentValue = 0.0f; + eventInit(&INPUT.actions[i].onPressed); + eventInit(&INPUT.actions[i].onReleased); } #ifdef inputInitPlatform @@ -83,10 +86,17 @@ void inputUpdate(void) { cur++; } - // Do we need to fire off events? #ifdef DUSK_TIME_DYNAMIC if(TIME.dynamicUpdate) return; #endif + + for(uint8_t i = INPUT_ACTION_NULL + 1; i < INPUT_ACTION_COUNT; i++) { + inputactiondata_t *act = &INPUT.actions[i]; + bool_t isDown = act->currentValue > 0.0f; + bool_t wasDown = act->lastValue > 0.0f; + if(isDown && !wasDown) eventInvoke(&act->onPressed, act); + if(!isDown && wasDown) eventInvoke(&act->onReleased, act); + } } float_t inputGetCurrentValue(const inputaction_t action) { diff --git a/src/dusk/input/inputaction.h b/src/dusk/input/inputaction.h index 77a5f913..867f3ace 100644 --- a/src/dusk/input/inputaction.h +++ b/src/dusk/input/inputaction.h @@ -8,6 +8,7 @@ #pragma once #include "time/time.h" #include "input/inputactiondefs.h" +#include "event/event.h" typedef struct { inputaction_t action; @@ -18,6 +19,9 @@ typedef struct { float_t lastDynamicValue; float_t currentDynamicValue; #endif + + event_t onPressed; + event_t onReleased; } inputactiondata_t; /** diff --git a/src/dusk/script/module/animation/moduleanimation.h b/src/dusk/script/module/animation/moduleanimation.h index 3f4ff9eb..a5ab2b84 100644 --- a/src/dusk/script/module/animation/moduleanimation.h +++ b/src/dusk/script/module/animation/moduleanimation.h @@ -9,307 +9,396 @@ #include "animation/animation.h" #include "util/memory.h" -// static scriptproto_t MODULE_ANIMATION_PROTO; +static scriptproto_t MODULE_ANIMATION_PROP_PROTO; +static scriptproto_t MODULE_ANIMATION_PROTO; -// // JS animation wraps a single-property animation; value is the managed target. -// typedef struct { -// animation_t anim; -// float_t value; -// } jsAnimation_t; +typedef struct { + float_t value; +} jsAnimationProperty_t; -// static inline jsAnimation_t *moduleAnimationGetWrapper( -// const jerry_call_info_t *callInfo -// ) { -// return (jsAnimation_t *)scriptProtoGetValue( -// &MODULE_ANIMATION_PROTO, callInfo->this_value -// ); -// } +typedef struct { + animation_t anim; + jerry_value_t onStartCallback; + jerry_value_t onCompleteCallback; +} jsAnimation_t; -// // Returns the animation_t pointer (first field of jsAnimation_t — same address). -// static inline animation_t *moduleAnimationGet( -// const jerry_call_info_t *callInfo -// ) { -// return (animation_t *)moduleAnimationGetWrapper(callInfo); -// } +static inline jsAnimation_t *moduleAnimationGetWrapper( + const jerry_call_info_t *callInfo +) { + return (jsAnimation_t *)scriptProtoGetValue( + &MODULE_ANIMATION_PROTO, callInfo->this_value + ); +} -// // Extracts an easingtype_t from a JS value. Accepts either a plain number -// // or an Easing.xxx function (which has a .type numeric property). -// static easingtype_t moduleAnimationReadEasing( -// const jerry_value_t val -// ) { -// if(jerry_value_is_number(val)) { -// return (easingtype_t)(uint32_t)jerry_value_as_number(val); -// } -// if(jerry_value_is_object(val) || jerry_value_is_function(val)) { -// jerry_value_t key = jerry_string_sz("type"); -// jerry_value_t typeVal = jerry_object_get(val, key); -// jerry_value_free(key); -// easingtype_t result = EASING_LINEAR; -// if(jerry_value_is_number(typeVal)) { -// result = (easingtype_t)(uint32_t)jerry_value_as_number(typeVal); -// } -// jerry_value_free(typeVal); -// return result; -// } -// return EASING_LINEAR; -// } +static inline animation_t *moduleAnimationGet( + const jerry_call_info_t *callInfo +) { + return (animation_t *)moduleAnimationGetWrapper(callInfo); +} -// // Fires onReach callbacks for any keyframes crossed between prevTime and -// // newTime. Returns an exception value on error, or jerry_undefined(). -// static jerry_value_t moduleAnimationFireKeyframes( -// const jerry_value_t thisVal, -// float_t prevTime, -// float_t newTime -// ) { -// jerry_value_t kfKey = jerry_string_sz("_keyframes"); -// jerry_value_t kfArr = jerry_object_get(thisVal, kfKey); -// jerry_value_free(kfKey); +static easingtype_t moduleAnimationReadEasing(const jerry_value_t val) { + if(jerry_value_is_number(val)) { + return (easingtype_t)(uint32_t)jerry_value_as_number(val); + } + if(jerry_value_is_object(val) || jerry_value_is_function(val)) { + jerry_value_t key = jerry_string_sz("type"); + jerry_value_t typeVal = jerry_object_get(val, key); + jerry_value_free(key); + easingtype_t result = EASING_LINEAR; + if(jerry_value_is_number(typeVal)) { + result = (easingtype_t)(uint32_t)jerry_value_as_number(typeVal); + } + jerry_value_free(typeVal); + return result; + } + return EASING_LINEAR; +} -// if(!jerry_value_is_array(kfArr)) { -// jerry_value_free(kfArr); -// return jerry_undefined(); -// } +static void jsAnimationPropertyFree(void *ptr, jerry_object_native_info_t *info) { + (void)info; + memoryFree(ptr); +} -// jerry_length_t len = jerry_array_length(kfArr); -// for(jerry_length_t i = 0; i < len; i++) { -// jerry_value_t kfObj = jerry_object_get_index(kfArr, i); +static void jsAnimationOnStartBridge(void *params, void *user) { + (void)params; + jsAnimation_t *janim = (jsAnimation_t *)user; + if(!janim->onStartCallback) return; + jerry_value_t ret = jerry_call(janim->onStartCallback, jerry_undefined(), NULL, 0); + jerry_value_free(ret); +} -// jerry_value_t tKey = jerry_string_sz("time"); -// jerry_value_t tVal = jerry_object_get(kfObj, tKey); -// jerry_value_free(tKey); -// float_t kfTime = (float_t)jerry_value_as_number(tVal); -// jerry_value_free(tVal); +static void jsAnimationOnCompleteBridge(void *params, void *user) { + (void)params; + jsAnimation_t *janim = (jsAnimation_t *)user; + if(!janim->onCompleteCallback) return; + jerry_value_t ret = jerry_call(janim->onCompleteCallback, jerry_undefined(), NULL, 0); + jerry_value_free(ret); +} -// if(prevTime < kfTime && newTime >= kfTime) { -// jerry_value_t cbKey = jerry_string_sz("onReach"); -// jerry_value_t cb = jerry_object_get(kfObj, cbKey); -// jerry_value_free(cbKey); +static void jsAnimationFree(void *ptr, jerry_object_native_info_t *info) { + (void)info; + jsAnimation_t *janim = (jsAnimation_t *)ptr; + if(janim->onStartCallback) { + eventUnsubscribe(&janim->anim.onStart, jsAnimationOnStartBridge, janim); + jerry_value_free(janim->onStartCallback); + } + if(janim->onCompleteCallback) { + eventUnsubscribe( + &janim->anim.onComplete, jsAnimationOnCompleteBridge, janim + ); + jerry_value_free(janim->onCompleteCallback); + } + memoryFree(ptr); +} -// if(jerry_value_is_function(cb)) { -// jerry_value_t r = jerry_call(cb, thisVal, NULL, 0); -// jerry_value_free(cb); -// if(jerry_value_is_exception(r)) { -// jerry_value_free(kfObj); -// jerry_value_free(kfArr); -// return r; -// } -// jerry_value_free(r); -// } else { -// jerry_value_free(cb); -// } -// } +// Fires onReach callbacks for keyframes crossed between prevTime and newTime. +// Reads _keyframes from propObj; calls each callback with propObj as this. +static jerry_value_t moduleAnimationFireKeyframes( + const jerry_value_t propObj, + float_t prevTime, + float_t newTime +) { + jerry_value_t kfKey = jerry_string_sz("_keyframes"); + jerry_value_t kfArr = jerry_object_get(propObj, kfKey); + jerry_value_free(kfKey); -// jerry_value_free(kfObj); -// } + if(!jerry_value_is_array(kfArr)) { + jerry_value_free(kfArr); + return jerry_undefined(); + } -// jerry_value_free(kfArr); -// return jerry_undefined(); -// } + jerry_length_t len = jerry_array_length(kfArr); + for(jerry_length_t i = 0; i < len; i++) { + jerry_value_t kfObj = jerry_object_get_index(kfArr, i); -// // Fires onComplete once. Returns an exception on error or jerry_undefined(). -// static jerry_value_t moduleAnimationFireComplete( -// const jerry_value_t thisVal -// ) { -// jerry_value_t firedKey = jerry_string_sz("_completeFired"); -// jerry_value_t firedVal = jerry_object_get(thisVal, firedKey); -// bool_t alreadyFired = jerry_value_is_true(firedVal); -// jerry_value_free(firedVal); + jerry_value_t tKey = jerry_string_sz("time"); + jerry_value_t tVal = jerry_object_get(kfObj, tKey); + jerry_value_free(tKey); + float_t kfTime = (float_t)jerry_value_as_number(tVal); + jerry_value_free(tVal); -// if(alreadyFired) { -// jerry_value_free(firedKey); -// return jerry_undefined(); -// } + if(prevTime < kfTime && newTime >= kfTime) { + jerry_value_t cbKey = jerry_string_sz("onReach"); + jerry_value_t cb = jerry_object_get(kfObj, cbKey); + jerry_value_free(cbKey); -// jerry_value_t trueVal = jerry_boolean(true); -// jerry_object_set(thisVal, firedKey, trueVal); -// jerry_value_free(firedKey); -// jerry_value_free(trueVal); + if(jerry_value_is_function(cb)) { + jerry_value_t r = jerry_call(cb, propObj, NULL, 0); + jerry_value_free(cb); + if(jerry_value_is_exception(r)) { + jerry_value_free(kfObj); + jerry_value_free(kfArr); + return r; + } + jerry_value_free(r); + } else { + jerry_value_free(cb); + } + } -// jerry_value_t cbKey = jerry_string_sz("onComplete"); -// jerry_value_t cb = jerry_object_get(thisVal, cbKey); -// jerry_value_free(cbKey); + jerry_value_free(kfObj); + } -// if(jerry_value_is_function(cb)) { -// jerry_value_t r = jerry_call(cb, thisVal, NULL, 0); -// jerry_value_free(cb); -// if(jerry_value_is_exception(r)) return r; -// jerry_value_free(r); -// } else { -// jerry_value_free(cb); -// } + jerry_value_free(kfArr); + return jerry_undefined(); +} -// return jerry_undefined(); -// } +moduleBaseFunction(moduleAnimationPropGetValue) { + jsAnimationProperty_t *jsprop = (jsAnimationProperty_t *)scriptProtoGetValue( + &MODULE_ANIMATION_PROP_PROTO, callInfo->this_value + ); + if(!jsprop) return moduleBaseThrow("Invalid AnimationProperty instance"); + return jerry_number((double)jsprop->value); +} -// moduleBaseFunction(moduleAnimationConstructor) { -// jsAnimation_t *janim = (jsAnimation_t *)memoryAllocate(sizeof(jsAnimation_t)); -// animationInit(&janim->anim); -// janim->value = 0.0f; +moduleBaseFunction(moduleAnimationConstructor) { + jsAnimation_t *janim = (jsAnimation_t *)memoryAllocate(sizeof(jsAnimation_t)); + animationInit(&janim->anim); + janim->onStartCallback = 0; + janim->onCompleteCallback = 0; -// if(argc > 0 && jerry_value_is_boolean(args[argc - 1])) { -// if(jerry_value_is_true(args[argc - 1])) { -// janim->anim.flags |= ANIMATION_FLAG_LOOP_ENABLED; -// } -// } + if(argc > 0 && jerry_value_is_boolean(args[argc - 1])) { + if(jerry_value_is_true(args[argc - 1])) { + janim->anim.flags |= ANIMATION_FLAG_LOOP_ENABLED; + } + } -// animationproperty_t *prop = animationAddProperty(&janim->anim, &janim->value); + jerry_object_set_native_ptr(callInfo->this_value, &MODULE_ANIMATION_PROTO.info, janim); -// if(argc > 0 && jerry_value_is_array(args[0])) { -// jerry_length_t len = jerry_array_length(args[0]); -// for(jerry_length_t i = 0; i < len; i++) { -// jerry_value_t kf = jerry_object_get_index(args[0], i); + jerry_value_t propsArr = jerry_array(0); -// jerry_value_t tKey = jerry_string_sz("time"); -// jerry_value_t vKey = jerry_string_sz("value"); -// jerry_value_t eKey = jerry_string_sz("easing"); + if(argc > 0 && jerry_value_is_array(args[0])) { + jerry_length_t numProps = jerry_array_length(args[0]); + for(jerry_length_t p = 0; p < numProps; p++) { + jerry_value_t kfArr = jerry_object_get_index(args[0], p); + if(!jerry_value_is_array(kfArr)) { + jerry_value_free(kfArr); + continue; + } -// float_t t = (float_t)jerry_value_as_number( -// jerry_object_get(kf, tKey) -// ); -// float_t v = (float_t)jerry_value_as_number( -// jerry_object_get(kf, vKey) -// ); -// jerry_value_t eVal = jerry_object_get(kf, eKey); -// easingtype_t e = moduleAnimationReadEasing(eVal); + jsAnimationProperty_t *jsprop = (jsAnimationProperty_t *)memoryAllocate( + sizeof(jsAnimationProperty_t) + ); + jsprop->value = 0.0f; -// jerry_value_free(tKey); -// jerry_value_free(vKey); -// jerry_value_free(eKey); -// jerry_value_free(eVal); -// jerry_value_free(kf); + animationproperty_t *prop = animationAddProperty(&janim->anim, &jsprop->value); -// animationPropertyAddKeyframe(&janim->anim, prop, t, v, e); -// } + jerry_length_t kfLen = jerry_array_length(kfArr); + for(jerry_length_t i = 0; i < kfLen; i++) { + jerry_value_t kf = jerry_object_get_index(kfArr, i); -// // Store the JS keyframes array for onReach callback detection. -// jerry_value_t kfKey = jerry_string_sz("_keyframes"); -// jerry_object_set(callInfo->this_value, kfKey, args[0]); -// jerry_value_free(kfKey); -// } + jerry_value_t tKey = jerry_string_sz("time"); + jerry_value_t tVal = jerry_object_get(kf, tKey); + jerry_value_free(tKey); + float_t t = (float_t)jerry_value_as_number(tVal); + jerry_value_free(tVal); -// jerry_value_t firedKey = jerry_string_sz("_completeFired"); -// jerry_value_t falseVal = jerry_boolean(false); -// jerry_object_set(callInfo->this_value, firedKey, falseVal); -// jerry_value_free(firedKey); -// jerry_value_free(falseVal); + jerry_value_t vKey = jerry_string_sz("value"); + jerry_value_t vVal = jerry_object_get(kf, vKey); + jerry_value_free(vKey); + float_t v = (float_t)jerry_value_as_number(vVal); + jerry_value_free(vVal); -// jerry_object_set_native_ptr( -// callInfo->this_value, &MODULE_ANIMATION_PROTO.info, janim -// ); -// return jerry_undefined(); -// } + jerry_value_t eKey = jerry_string_sz("easing"); + jerry_value_t eVal = jerry_object_get(kf, eKey); + jerry_value_free(eKey); + easingtype_t e = moduleAnimationReadEasing(eVal); + jerry_value_free(eVal); -// moduleBaseFunction(moduleAnimationUpdate) { -// jsAnimation_t *janim = moduleAnimationGetWrapper(callInfo); -// if(!janim) return moduleBaseThrow("Invalid Animation instance"); -// if(argc < 1 || !jerry_value_is_number(args[0])) { -// return moduleBaseThrow("update() expects a number delta"); -// } + jerry_value_free(kf); + animationPropertyAddKeyframe(prop, t, v, e); + } -// float_t prevTime = janim->anim.time; -// float_t delta = (float_t)jerry_value_as_number(args[0]); -// animationUpdate(&janim->anim, delta); + // Create the JS AnimationProperty object manually so &jsprop->value stays + // stable as the C animation target (scriptProtoCreateValue would memcpy). + jerry_value_t propObj = jerry_object(); + jerry_object_set_native_ptr(propObj, &MODULE_ANIMATION_PROP_PROTO.info, jsprop); + jerry_object_set_proto(propObj, MODULE_ANIMATION_PROP_PROTO.prototype); -// jerry_value_t kfResult = moduleAnimationFireKeyframes( -// callInfo->this_value, prevTime, janim->anim.time -// ); -// if(jerry_value_is_exception(kfResult)) return kfResult; -// jerry_value_free(kfResult); + jerry_value_t kfStoreKey = jerry_string_sz("_keyframes"); + jerry_object_set(propObj, kfStoreKey, kfArr); + jerry_value_free(kfStoreKey); + jerry_value_free(kfArr); -// if(animationIsFinished(&janim->anim)) { -// jerry_value_t cResult = moduleAnimationFireComplete( -// callInfo->this_value -// ); -// if(jerry_value_is_exception(cResult)) return cResult; -// jerry_value_free(cResult); -// } + jerry_object_set_index(propsArr, (uint32_t)p, propObj); + jerry_value_free(propObj); + } + } -// return jerry_number((double)janim->value); -// } + jerry_value_t propsKey = jerry_string_sz("properties"); + jerry_object_set(callInfo->this_value, propsKey, propsArr); + jerry_value_free(propsKey); + jerry_value_free(propsArr); -// moduleBaseFunction(moduleAnimationGetValue) { -// jsAnimation_t *janim = moduleAnimationGetWrapper(callInfo); -// if(!janim) return moduleBaseThrow("Invalid Animation instance"); -// return jerry_number((double)janim->value); -// } + return jerry_undefined(); +} -// moduleBaseFunction(moduleAnimationReset) { -// animation_t *anim = moduleAnimationGet(callInfo); -// if(!anim) return moduleBaseThrow("Invalid Animation instance"); -// animationReset(anim); +moduleBaseFunction(moduleAnimationUpdate) { + jsAnimation_t *janim = moduleAnimationGetWrapper(callInfo); + if(!janim) return moduleBaseThrow("Invalid Animation instance"); + if(argc < 1 || !jerry_value_is_number(args[0])) { + return moduleBaseThrow("update() expects a number delta"); + } -// jerry_value_t key = jerry_string_sz("_completeFired"); -// jerry_value_t falseVal = jerry_boolean(false); -// jerry_object_set(callInfo->this_value, key, falseVal); -// jerry_value_free(key); -// jerry_value_free(falseVal); + float_t prevTime = janim->anim.time; + animationUpdate(&janim->anim, (float_t)jerry_value_as_number(args[0])); -// return jerry_undefined(); -// } + jerry_value_t propsKey = jerry_string_sz("properties"); + jerry_value_t propsArr = jerry_object_get(callInfo->this_value, propsKey); + jerry_value_free(propsKey); + if(jerry_value_is_array(propsArr)) { + jerry_length_t len = jerry_array_length(propsArr); + for(jerry_length_t p = 0; p < len; p++) { + jerry_value_t propObj = jerry_object_get_index(propsArr, p); + jerry_value_t result = moduleAnimationFireKeyframes( + propObj, prevTime, janim->anim.time + ); + jerry_value_free(propObj); + if(jerry_value_is_exception(result)) { + jerry_value_free(propsArr); + return result; + } + jerry_value_free(result); + } + } + jerry_value_free(propsArr); -// moduleBaseFunction(moduleAnimationGetComplete) { -// animation_t *anim = moduleAnimationGet(callInfo); -// return anim ? jerry_boolean(animationIsFinished(anim)) : jerry_boolean(false); -// } + return jerry_undefined(); +} -// moduleBaseFunction(moduleAnimationGetLoop) { -// animation_t *anim = moduleAnimationGet(callInfo); -// return anim ? jerry_boolean(animationIsLooping(anim)) : jerry_boolean(false); -// } +moduleBaseFunction(moduleAnimationReset) { + animation_t *anim = moduleAnimationGet(callInfo); + if(!anim) return moduleBaseThrow("Invalid Animation instance"); + animationReset(anim); + return jerry_undefined(); +} -// moduleBaseFunction(moduleAnimationSetLoop) { -// animation_t *anim = moduleAnimationGet(callInfo); -// if(!anim) return moduleBaseThrow("Invalid Animation instance"); -// if(argc < 1) return moduleBaseThrow("Expected boolean"); -// if(jerry_value_is_true(args[0])) { -// anim->flags |= ANIMATION_FLAG_LOOP_ENABLED; -// } else { -// anim->flags &= ~ANIMATION_FLAG_LOOP_ENABLED; -// } -// return jerry_undefined(); -// } +moduleBaseFunction(moduleAnimationGetComplete) { + animation_t *anim = moduleAnimationGet(callInfo); + return anim ? jerry_boolean(animationIsFinished(anim)) : jerry_boolean(false); +} -// moduleBaseFunction(moduleAnimationGetTime) { -// animation_t *anim = moduleAnimationGet(callInfo); -// return anim ? jerry_number((double)anim->time) : jerry_number(0.0); -// } +moduleBaseFunction(moduleAnimationGetLoop) { + animation_t *anim = moduleAnimationGet(callInfo); + return anim ? jerry_boolean(animationIsLooping(anim)) : jerry_boolean(false); +} -// moduleBaseFunction(moduleAnimationGetDuration) { -// animation_t *anim = moduleAnimationGet(callInfo); -// return anim ? jerry_number((double)anim->duration) : jerry_number(0.0); -// } +moduleBaseFunction(moduleAnimationSetLoop) { + animation_t *anim = moduleAnimationGet(callInfo); + if(!anim) return moduleBaseThrow("Invalid Animation instance"); + if(argc < 1) return moduleBaseThrow("Expected boolean"); + if(jerry_value_is_true(args[0])) { + anim->flags |= ANIMATION_FLAG_LOOP_ENABLED; + } else { + anim->flags &= ~ANIMATION_FLAG_LOOP_ENABLED; + } + return jerry_undefined(); +} + +moduleBaseFunction(moduleAnimationGetTime) { + animation_t *anim = moduleAnimationGet(callInfo); + return anim ? jerry_number((double)anim->time) : jerry_number(0.0); +} + +moduleBaseFunction(moduleAnimationGetDuration) { + animation_t *anim = moduleAnimationGet(callInfo); + return anim + ? jerry_number((double)animationGetDuration(anim)) + : jerry_number(0.0); +} + +moduleBaseFunction(moduleAnimationGetOnStart) { + jsAnimation_t *janim = moduleAnimationGetWrapper(callInfo); + if(!janim || !janim->onStartCallback) return jerry_null(); + return jerry_value_copy(janim->onStartCallback); +} + +moduleBaseFunction(moduleAnimationSetOnStart) { + jsAnimation_t *janim = moduleAnimationGetWrapper(callInfo); + if(!janim) return moduleBaseThrow("Invalid Animation instance"); + + if(janim->onStartCallback) { + eventUnsubscribe(&janim->anim.onStart, jsAnimationOnStartBridge, janim); + jerry_value_free(janim->onStartCallback); + janim->onStartCallback = 0; + } + if(argc >= 1 && jerry_value_is_function(args[0])) { + janim->onStartCallback = jerry_value_copy(args[0]); + eventSubscribe(&janim->anim.onStart, jsAnimationOnStartBridge, janim); + } + return jerry_undefined(); +} + +moduleBaseFunction(moduleAnimationGetOnComplete) { + jsAnimation_t *janim = moduleAnimationGetWrapper(callInfo); + if(!janim || !janim->onCompleteCallback) return jerry_null(); + return jerry_value_copy(janim->onCompleteCallback); +} + +moduleBaseFunction(moduleAnimationSetOnComplete) { + jsAnimation_t *janim = moduleAnimationGetWrapper(callInfo); + if(!janim) return moduleBaseThrow("Invalid Animation instance"); + + if(janim->onCompleteCallback) { + eventUnsubscribe( + &janim->anim.onComplete, jsAnimationOnCompleteBridge, janim + ); + jerry_value_free(janim->onCompleteCallback); + janim->onCompleteCallback = 0; + } + if(argc >= 1 && jerry_value_is_function(args[0])) { + janim->onCompleteCallback = jerry_value_copy(args[0]); + eventSubscribe(&janim->anim.onComplete, jsAnimationOnCompleteBridge, janim); + } + return jerry_undefined(); +} static void moduleAnimation(void) { - // scriptProtoInit( - // &MODULE_ANIMATION_PROTO, - // "Animation", - // sizeof(jsAnimation_t), - // moduleAnimationConstructor - // ); + scriptProtoInit( + &MODULE_ANIMATION_PROP_PROTO, NULL, sizeof(jsAnimationProperty_t), NULL + ); + MODULE_ANIMATION_PROP_PROTO.info.free_cb = jsAnimationPropertyFree; + scriptProtoDefineProp( + &MODULE_ANIMATION_PROP_PROTO, "value", + moduleAnimationPropGetValue, NULL + ); - // scriptProtoDefineFunc( - // &MODULE_ANIMATION_PROTO, "update", moduleAnimationUpdate - // ); - // scriptProtoDefineFunc( - // &MODULE_ANIMATION_PROTO, "getValue", moduleAnimationGetValue - // ); - // scriptProtoDefineFunc( - // &MODULE_ANIMATION_PROTO, "reset", moduleAnimationReset - // ); - // scriptProtoDefineProp( - // &MODULE_ANIMATION_PROTO, "complete", - // moduleAnimationGetComplete, NULL - // ); - // scriptProtoDefineProp( - // &MODULE_ANIMATION_PROTO, "loop", - // moduleAnimationGetLoop, moduleAnimationSetLoop - // ); - // scriptProtoDefineProp( - // &MODULE_ANIMATION_PROTO, "time", - // moduleAnimationGetTime, NULL - // ); - // scriptProtoDefineProp( - // &MODULE_ANIMATION_PROTO, "duration", - // moduleAnimationGetDuration, NULL - // ); + scriptProtoInit( + &MODULE_ANIMATION_PROTO, + "Animation", + sizeof(jsAnimation_t), + moduleAnimationConstructor + ); + MODULE_ANIMATION_PROTO.info.free_cb = jsAnimationFree; + + scriptProtoDefineFunc( + &MODULE_ANIMATION_PROTO, "update", moduleAnimationUpdate + ); + scriptProtoDefineFunc( + &MODULE_ANIMATION_PROTO, "reset", moduleAnimationReset + ); + scriptProtoDefineProp( + &MODULE_ANIMATION_PROTO, "complete", + moduleAnimationGetComplete, NULL + ); + scriptProtoDefineProp( + &MODULE_ANIMATION_PROTO, "loop", + moduleAnimationGetLoop, moduleAnimationSetLoop + ); + scriptProtoDefineProp( + &MODULE_ANIMATION_PROTO, "time", + moduleAnimationGetTime, NULL + ); + scriptProtoDefineProp( + &MODULE_ANIMATION_PROTO, "duration", + moduleAnimationGetDuration, NULL + ); + scriptProtoDefineProp( + &MODULE_ANIMATION_PROTO, "onStart", + moduleAnimationGetOnStart, moduleAnimationSetOnStart + ); + scriptProtoDefineProp( + &MODULE_ANIMATION_PROTO, "onComplete", + moduleAnimationGetOnComplete, moduleAnimationSetOnComplete + ); } diff --git a/src/dusk/script/module/event/moduleEvent.h b/src/dusk/script/module/event/moduleEvent.h new file mode 100644 index 00000000..82a38af7 --- /dev/null +++ b/src/dusk/script/module/event/moduleEvent.h @@ -0,0 +1,69 @@ +// 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 "event/event.h" +#include "assert/assert.h" + +#define MODULE_EVENT_POOL_MAX 32 + +typedef struct { + jerry_value_t callback; // owned copy; slot is free when event == NULL + event_t *event; +} jsEventSub_t; + +static jsEventSub_t MODULE_EVENT_POOL[MODULE_EVENT_POOL_MAX]; + +static void moduleEventBridge(void *params, void *user) { + (void)params; + jsEventSub_t *sub = (jsEventSub_t *)user; + jerry_value_t ret = jerry_call(sub->callback, jerry_undefined(), NULL, 0); + jerry_value_free(ret); +} + +static bool_t moduleEventCallbackEquals( + const jerry_value_t a, + const jerry_value_t b +) { + jerry_value_t result = jerry_binary_op(JERRY_BIN_OP_STRICT_EQUAL, a, b); + bool_t eq = jerry_value_is_true(result); + jerry_value_free(result); + return eq; +} + +static void moduleEventJsSubscribe(event_t *event, jerry_value_t callback) { + assertNotNull(event, "event must not be NULL"); + + for(uint8_t i = 0; i < MODULE_EVENT_POOL_MAX; i++) { + if(MODULE_EVENT_POOL[i].event != event) continue; + if(moduleEventCallbackEquals(MODULE_EVENT_POOL[i].callback, callback)) return; + } + + for(uint8_t i = 0; i < MODULE_EVENT_POOL_MAX; i++) { + if(MODULE_EVENT_POOL[i].event != NULL) continue; + MODULE_EVENT_POOL[i].callback = jerry_value_copy(callback); + MODULE_EVENT_POOL[i].event = event; + eventSubscribe(event, moduleEventBridge, &MODULE_EVENT_POOL[i]); + return; + } + + assertUnreachable("MODULE_EVENT_POOL_MAX exceeded"); +} + +static void moduleEventJsUnsubscribe(event_t *event, jerry_value_t callback) { + assertNotNull(event, "event must not be NULL"); + + for(uint8_t i = 0; i < MODULE_EVENT_POOL_MAX; i++) { + if(MODULE_EVENT_POOL[i].event != event) continue; + if(!moduleEventCallbackEquals(MODULE_EVENT_POOL[i].callback, callback)) continue; + + eventUnsubscribe(event, moduleEventBridge, &MODULE_EVENT_POOL[i]); + jerry_value_free(MODULE_EVENT_POOL[i].callback); + MODULE_EVENT_POOL[i].callback = 0; + MODULE_EVENT_POOL[i].event = NULL; + return; + } +} diff --git a/src/dusk/script/module/input/moduleinput.h b/src/dusk/script/module/input/moduleinput.h index cb9a28a5..39242d98 100644 --- a/src/dusk/script/module/input/moduleinput.h +++ b/src/dusk/script/module/input/moduleinput.h @@ -9,6 +9,7 @@ #include "script/module/modulebase.h" #include "script/scriptproto.h" #include "script/module/math/modulevec2.h" +#include "script/module/event/moduleEvent.h" #include "input/input.h" static scriptproto_t MODULE_INPUT_PROTO; @@ -130,6 +131,54 @@ moduleBaseFunction(moduleInputAxis2D) { return moduleVec2Push(result); } +moduleBaseFunction(moduleInputOnPressed) { + if(argc < 2) return moduleBaseThrow("Input.onPressed: expected (action, fn)"); + moduleBaseRequireNumber(0); + moduleBaseRequireFunction(1); + const inputaction_t action = (inputaction_t)jerry_value_as_number(args[0]); + if(action <= INPUT_ACTION_NULL || action >= INPUT_ACTION_COUNT) { + return moduleBaseThrow("Input.onPressed: invalid action"); + } + moduleEventJsSubscribe(&INPUT.actions[action].onPressed, args[1]); + return jerry_undefined(); +} + +moduleBaseFunction(moduleInputOffPressed) { + if(argc < 2) return moduleBaseThrow("Input.offPressed: expected (action, fn)"); + moduleBaseRequireNumber(0); + moduleBaseRequireFunction(1); + const inputaction_t action = (inputaction_t)jerry_value_as_number(args[0]); + if(action <= INPUT_ACTION_NULL || action >= INPUT_ACTION_COUNT) { + return moduleBaseThrow("Input.offPressed: invalid action"); + } + moduleEventJsUnsubscribe(&INPUT.actions[action].onPressed, args[1]); + return jerry_undefined(); +} + +moduleBaseFunction(moduleInputOnReleased) { + if(argc < 2) return moduleBaseThrow("Input.onReleased: expected (action, fn)"); + moduleBaseRequireNumber(0); + moduleBaseRequireFunction(1); + const inputaction_t action = (inputaction_t)jerry_value_as_number(args[0]); + if(action <= INPUT_ACTION_NULL || action >= INPUT_ACTION_COUNT) { + return moduleBaseThrow("Input.onReleased: invalid action"); + } + moduleEventJsSubscribe(&INPUT.actions[action].onReleased, args[1]); + return jerry_undefined(); +} + +moduleBaseFunction(moduleInputOffReleased) { + if(argc < 2) return moduleBaseThrow("Input.offReleased: expected (action, fn)"); + moduleBaseRequireNumber(0); + moduleBaseRequireFunction(1); + const inputaction_t action = (inputaction_t)jerry_value_as_number(args[0]); + if(action <= INPUT_ACTION_NULL || action >= INPUT_ACTION_COUNT) { + return moduleBaseThrow("Input.offReleased: invalid action"); + } + moduleEventJsUnsubscribe(&INPUT.actions[action].onReleased, args[1]); + return jerry_undefined(); +} + static void moduleInput(void) { moduleBaseEval(INPUT_ACTION_SCRIPT); @@ -174,4 +223,16 @@ static void moduleInput(void) { scriptProtoDefineStaticFunc( &MODULE_INPUT_PROTO, "axis2D", moduleInputAxis2D ); + scriptProtoDefineStaticFunc( + &MODULE_INPUT_PROTO, "onPressed", moduleInputOnPressed + ); + scriptProtoDefineStaticFunc( + &MODULE_INPUT_PROTO, "offPressed", moduleInputOffPressed + ); + scriptProtoDefineStaticFunc( + &MODULE_INPUT_PROTO, "onReleased", moduleInputOnReleased + ); + scriptProtoDefineStaticFunc( + &MODULE_INPUT_PROTO, "offReleased", moduleInputOffReleased + ); } \ No newline at end of file diff --git a/src/dusk/script/module/module.h b/src/dusk/script/module/module.h index a1929593..0a03d7db 100644 --- a/src/dusk/script/module/module.h +++ b/src/dusk/script/module/module.h @@ -25,7 +25,9 @@ #include "script/module/engine/moduleengine.h" #include "script/module/item/moduleitem.h" #include "script/module/story/modulestory.h" +#include "script/module/event/moduleEvent.h" #include "script/module/ui/moduletextbox.h" +#include "script/module/ui/modulefullbox.h" static void moduleRegister(void) { moduleInclude(); @@ -48,4 +50,5 @@ static void moduleRegister(void) { moduleItem(); moduleStory(); moduleTextbox(); + moduleFullbox(); } diff --git a/src/dusk/script/module/ui/modulefullbox.h b/src/dusk/script/module/ui/modulefullbox.h new file mode 100644 index 00000000..29574378 --- /dev/null +++ b/src/dusk/script/module/ui/modulefullbox.h @@ -0,0 +1,146 @@ +// Copyright (c) 2026 Dominic Masters +// +// This software is released under the MIT License. +// https://opensource.org/licenses/MIT + +#pragma once +#include "script/module/modulebase.h" +#include "script/scriptproto.h" +#include "script/module/display/modulecolor.h" +#include "script/module/event/moduleEvent.h" +#include "ui/uifullbox.h" +#include "animation/easing.h" + +static scriptproto_t MODULE_FULLBOX_UNDER_PROTO; +static scriptproto_t MODULE_FULLBOX_OVER_PROTO; + +// Stored to allow getter to return the current callback without pool search. +static jerry_value_t MODULE_FULLBOX_UNDER_CALLBACK = 0; +static jerry_value_t MODULE_FULLBOX_OVER_CALLBACK = 0; + +static easingtype_t moduleFullboxReadEasing(jerry_value_t val) { + if(jerry_value_is_number(val)) { + return (easingtype_t)(int32_t)jerry_value_as_number(val); + } + jerry_value_t typeKey = jerry_string_sz("type"); + jerry_value_t typeVal = jerry_object_get(val, typeKey); + jerry_value_free(typeKey); + easingtype_t type = EASING_LINEAR; + if(jerry_value_is_number(typeVal)) { + type = (easingtype_t)(int32_t)jerry_value_as_number(typeVal); + } + jerry_value_free(typeVal); + return type; +} + +moduleBaseFunction(moduleFullboxUnderTransition) { + if(argc < 4) { + return moduleBaseThrow("FullboxUnder.transition: expected 4 arguments"); + } + + color_t *from = (color_t*)scriptProtoGetValue(&MODULE_COLOR_PROTO, args[0]); + if(!from) return moduleBaseThrow("FullboxUnder.transition: arg 0 must be a Color"); + + color_t *to = (color_t*)scriptProtoGetValue(&MODULE_COLOR_PROTO, args[1]); + if(!to) return moduleBaseThrow("FullboxUnder.transition: arg 1 must be a Color"); + + if(!jerry_value_is_number(args[2])) { + return moduleBaseThrow("FullboxUnder.transition: arg 2 must be a number"); + } + + uiFullboxTransition( + &UI_FULLBOX_UNDER, + *from, *to, + (float_t)jerry_value_as_number(args[2]), + moduleFullboxReadEasing(args[3]) + ); + return jerry_undefined(); +} + +moduleBaseFunction(moduleFullboxOverTransition) { + if(argc < 4) { + return moduleBaseThrow("FullboxOver.transition: expected 4 arguments"); + } + + color_t *from = (color_t*)scriptProtoGetValue(&MODULE_COLOR_PROTO, args[0]); + if(!from) return moduleBaseThrow("FullboxOver.transition: arg 0 must be a Color"); + + color_t *to = (color_t*)scriptProtoGetValue(&MODULE_COLOR_PROTO, args[1]); + if(!to) return moduleBaseThrow("FullboxOver.transition: arg 1 must be a Color"); + + if(!jerry_value_is_number(args[2])) { + return moduleBaseThrow("FullboxOver.transition: arg 2 must be a number"); + } + + uiFullboxTransition( + &UI_FULLBOX_OVER, + *from, *to, + (float_t)jerry_value_as_number(args[2]), + moduleFullboxReadEasing(args[3]) + ); + return jerry_undefined(); +} + +moduleBaseFunction(moduleFullboxUnderGetOnTransitionEnd) { + (void)callInfo; (void)args; (void)argc; + if(!MODULE_FULLBOX_UNDER_CALLBACK) return jerry_null(); + return jerry_value_copy(MODULE_FULLBOX_UNDER_CALLBACK); +} + +moduleBaseFunction(moduleFullboxUnderSetOnTransitionEnd) { + (void)callInfo; + if(MODULE_FULLBOX_UNDER_CALLBACK) { + moduleEventJsUnsubscribe( + &UI_FULLBOX_UNDER.onTransitionEnd, MODULE_FULLBOX_UNDER_CALLBACK + ); + jerry_value_free(MODULE_FULLBOX_UNDER_CALLBACK); + MODULE_FULLBOX_UNDER_CALLBACK = 0; + } + if(argc >= 1 && jerry_value_is_function(args[0])) { + MODULE_FULLBOX_UNDER_CALLBACK = jerry_value_copy(args[0]); + moduleEventJsSubscribe(&UI_FULLBOX_UNDER.onTransitionEnd, args[0]); + } + return jerry_undefined(); +} + +moduleBaseFunction(moduleFullboxOverGetOnTransitionEnd) { + (void)callInfo; (void)args; (void)argc; + if(!MODULE_FULLBOX_OVER_CALLBACK) return jerry_null(); + return jerry_value_copy(MODULE_FULLBOX_OVER_CALLBACK); +} + +moduleBaseFunction(moduleFullboxOverSetOnTransitionEnd) { + (void)callInfo; + if(MODULE_FULLBOX_OVER_CALLBACK) { + moduleEventJsUnsubscribe( + &UI_FULLBOX_OVER.onTransitionEnd, MODULE_FULLBOX_OVER_CALLBACK + ); + jerry_value_free(MODULE_FULLBOX_OVER_CALLBACK); + MODULE_FULLBOX_OVER_CALLBACK = 0; + } + if(argc >= 1 && jerry_value_is_function(args[0])) { + MODULE_FULLBOX_OVER_CALLBACK = jerry_value_copy(args[0]); + moduleEventJsSubscribe(&UI_FULLBOX_OVER.onTransitionEnd, args[0]); + } + return jerry_undefined(); +} + +static void moduleFullbox(void) { + scriptProtoInit(&MODULE_FULLBOX_UNDER_PROTO, "FullboxUnder", sizeof(uint8_t), NULL); + scriptProtoDefineStaticFunc( + &MODULE_FULLBOX_UNDER_PROTO, "transition", moduleFullboxUnderTransition + ); + scriptProtoDefineStaticProp( + &MODULE_FULLBOX_UNDER_PROTO, "onTransitionEnd", + moduleFullboxUnderGetOnTransitionEnd, moduleFullboxUnderSetOnTransitionEnd + ); + + scriptProtoInit(&MODULE_FULLBOX_OVER_PROTO, "FullboxOver", sizeof(uint8_t), NULL); + scriptProtoDefineStaticFunc( + &MODULE_FULLBOX_OVER_PROTO, "transition", moduleFullboxOverTransition + ); + scriptProtoDefineStaticProp( + &MODULE_FULLBOX_OVER_PROTO, "onTransitionEnd", + moduleFullboxOverGetOnTransitionEnd, moduleFullboxOverSetOnTransitionEnd + ); +} diff --git a/src/dusk/script/module/ui/moduletextbox.h b/src/dusk/script/module/ui/moduletextbox.h index 697c8eaa..946a753e 100644 --- a/src/dusk/script/module/ui/moduletextbox.h +++ b/src/dusk/script/module/ui/moduletextbox.h @@ -6,8 +6,12 @@ #pragma once #include "script/module/modulebase.h" #include "script/scriptproto.h" +#include "script/module/event/moduleEvent.h" #include "ui/uitextbox.h" +static jerry_value_t MODULE_TEXTBOX_PAGE_COMPLETE_CALLBACK = 0; +static jerry_value_t MODULE_TEXTBOX_LAST_PAGE_CALLBACK = 0; + static scriptproto_t MODULE_TEXTBOX_PROTO; moduleBaseFunction(moduleTextboxSetText) { @@ -77,6 +81,50 @@ moduleBaseFunction(moduleTextboxGetPageCount) { return jerry_number((double)UI_TEXTBOX.pageCount); } +moduleBaseFunction(moduleTextboxGetOnPageComplete) { + (void)callInfo; (void)args; (void)argc; + if(!MODULE_TEXTBOX_PAGE_COMPLETE_CALLBACK) return jerry_null(); + return jerry_value_copy(MODULE_TEXTBOX_PAGE_COMPLETE_CALLBACK); +} + +moduleBaseFunction(moduleTextboxSetOnPageComplete) { + (void)callInfo; + if(MODULE_TEXTBOX_PAGE_COMPLETE_CALLBACK) { + moduleEventJsUnsubscribe( + &UI_TEXTBOX.onPageComplete, MODULE_TEXTBOX_PAGE_COMPLETE_CALLBACK + ); + jerry_value_free(MODULE_TEXTBOX_PAGE_COMPLETE_CALLBACK); + MODULE_TEXTBOX_PAGE_COMPLETE_CALLBACK = 0; + } + if(argc >= 1 && jerry_value_is_function(args[0])) { + MODULE_TEXTBOX_PAGE_COMPLETE_CALLBACK = jerry_value_copy(args[0]); + moduleEventJsSubscribe(&UI_TEXTBOX.onPageComplete, args[0]); + } + return jerry_undefined(); +} + +moduleBaseFunction(moduleTextboxGetOnLastPage) { + (void)callInfo; (void)args; (void)argc; + if(!MODULE_TEXTBOX_LAST_PAGE_CALLBACK) return jerry_null(); + return jerry_value_copy(MODULE_TEXTBOX_LAST_PAGE_CALLBACK); +} + +moduleBaseFunction(moduleTextboxSetOnLastPage) { + (void)callInfo; + if(MODULE_TEXTBOX_LAST_PAGE_CALLBACK) { + moduleEventJsUnsubscribe( + &UI_TEXTBOX.onLastPage, MODULE_TEXTBOX_LAST_PAGE_CALLBACK + ); + jerry_value_free(MODULE_TEXTBOX_LAST_PAGE_CALLBACK); + MODULE_TEXTBOX_LAST_PAGE_CALLBACK = 0; + } + if(argc >= 1 && jerry_value_is_function(args[0])) { + MODULE_TEXTBOX_LAST_PAGE_CALLBACK = jerry_value_copy(args[0]); + moduleEventJsSubscribe(&UI_TEXTBOX.onLastPage, args[0]); + } + return jerry_undefined(); +} + static void moduleTextbox(void) { scriptProtoInit( &MODULE_TEXTBOX_PROTO, "Textbox", sizeof(uint8_t), NULL @@ -119,4 +167,12 @@ static void moduleTextbox(void) { &MODULE_TEXTBOX_PROTO, "pageCount", moduleTextboxGetPageCount, NULL ); + scriptProtoDefineStaticProp( + &MODULE_TEXTBOX_PROTO, "onPageComplete", + moduleTextboxGetOnPageComplete, moduleTextboxSetOnPageComplete + ); + scriptProtoDefineStaticProp( + &MODULE_TEXTBOX_PROTO, "onLastPage", + moduleTextboxGetOnLastPage, moduleTextboxSetOnLastPage + ); } diff --git a/src/dusk/ui/CMakeLists.txt b/src/dusk/ui/CMakeLists.txt index 9ef01edc..e61731ee 100644 --- a/src/dusk/ui/CMakeLists.txt +++ b/src/dusk/ui/CMakeLists.txt @@ -10,5 +10,6 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME} uifps.c uielement.c uiframe.c + uifullbox.c uitextbox.c ) \ No newline at end of file diff --git a/src/dusk/ui/ui.c b/src/dusk/ui/ui.c index a81777d9..0c078327 100644 --- a/src/dusk/ui/ui.c +++ b/src/dusk/ui/ui.c @@ -11,6 +11,8 @@ #include "display/spritebatch/spritebatch.h" #include "display/screen/screen.h" #include "ui/uielement.h" +#include "ui/uifullbox.h" +#include "time/time.h" #include "log/log.h" ui_t UI; @@ -28,6 +30,8 @@ errorret_t uiInit(void) { } void uiUpdate(void) { + uiFullboxUpdate(&UI_FULLBOX_UNDER, TIME.delta); + uiFullboxUpdate(&UI_FULLBOX_OVER, TIME.delta); } errorret_t uiRender(void) { diff --git a/src/dusk/ui/uielement.c b/src/dusk/ui/uielement.c index 34c17027..698f072a 100644 --- a/src/dusk/ui/uielement.c +++ b/src/dusk/ui/uielement.c @@ -12,12 +12,20 @@ #include "ui/uifps.h" #include "engine/engine.h" #include "ui/uitextbox.h" +#include "ui/uifullbox.h" uielement_t UI_ELEMENTS[] = { + // Fullbox under: above scene, below system UI. + { .type = UI_ELEMENT_TYPE_NATIVE, .native = { .draw = uiFullboxUnderDraw } }, + + { .type = UI_ELEMENT_TYPE_SCRIPT, .script = { .script = "ui/test.js" } }, + { .type = UI_ELEMENT_TYPE_NATIVE, .native = { .draw = consoleDraw } }, { .type = UI_ELEMENT_TYPE_NATIVE, .native = { .draw = uiFPSDraw } }, { .type = UI_ELEMENT_TYPE_NATIVE, .native = { .draw = uiTextboxDraw } }, - { .type = UI_ELEMENT_TYPE_SCRIPT, .script = { .script = "ui/test.js" } }, + + // Fullbox over: above absolutely everything. + { .type = UI_ELEMENT_TYPE_NATIVE, .native = { .draw = uiFullboxOverDraw } }, { .type = UI_ELEMENT_TYPE_NULL }, }; diff --git a/src/dusk/ui/uifullbox.c b/src/dusk/ui/uifullbox.c new file mode 100644 index 00000000..1108e07e --- /dev/null +++ b/src/dusk/ui/uifullbox.c @@ -0,0 +1,94 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "uifullbox.h" +#include "assert/assert.h" +#include "util/memory.h" +#include "display/screen/screen.h" +#include "display/texture/texture.h" +#include "display/spritebatch/spritebatch.h" +#include "display/shader/shaderunlit.h" + +uifullbox_t UI_FULLBOX_UNDER; +uifullbox_t UI_FULLBOX_OVER; + +void uiFullboxInit(uifullbox_t *fullbox) { + assertNotNull(fullbox, "fullbox must not be NULL"); + memoryZero(fullbox, sizeof(uifullbox_t)); + eventInit(&fullbox->onTransitionEnd); +} + +void uiFullboxUpdate(uifullbox_t *fullbox, float_t delta) { + assertNotNull(fullbox, "fullbox must not be NULL"); + if(fullbox->duration <= 0.0f || fullbox->time >= fullbox->duration) return; + + fullbox->time += delta; + if(fullbox->time >= fullbox->duration) { + fullbox->time = fullbox->duration; + eventInvoke(&fullbox->onTransitionEnd, fullbox); + } +} + +static color_t uiFullboxGetColor(const uifullbox_t *fullbox) { + if(fullbox->duration <= 0.0f || fullbox->time >= fullbox->duration) { + return fullbox->toColor; + } + float_t t = easingApply(fullbox->easing, fullbox->time / fullbox->duration); + return color4b( + (uint8_t)((float_t)fullbox->fromColor.r + ((float_t)fullbox->toColor.r - (float_t)fullbox->fromColor.r) * t), + (uint8_t)((float_t)fullbox->fromColor.g + ((float_t)fullbox->toColor.g - (float_t)fullbox->fromColor.g) * t), + (uint8_t)((float_t)fullbox->fromColor.b + ((float_t)fullbox->toColor.b - (float_t)fullbox->fromColor.b) * t), + (uint8_t)((float_t)fullbox->fromColor.a + ((float_t)fullbox->toColor.a - (float_t)fullbox->fromColor.a) * t) + ); +} + +errorret_t uiFullboxDraw(uifullbox_t *fullbox) { + assertNotNull(fullbox, "fullbox must not be NULL"); + + color_t color = uiFullboxGetColor(fullbox); + if(color.a == 0) errorOk(); + + errorChain(shaderSetTexture(&SHADER_UNLIT, SHADER_UNLIT_TEXTURE, &TEXTURE_WHITE)); + #if MESH_ENABLE_COLOR + #else + errorChain(shaderSetColor(&SHADER_UNLIT, SHADER_UNLIT_COLOR, color)); + #endif + + errorChain(spriteBatchPush( + 0.0f, 0.0f, + (float_t)SCREEN.width, (float_t)SCREEN.height, + #if MESH_ENABLE_COLOR + color, + #endif + 0.0f, 0.0f, 1.0f, 1.0f + )); + errorChain(spriteBatchFlush()); + errorOk(); +} + +void uiFullboxTransition( + uifullbox_t *fullbox, + color_t from, + color_t to, + float_t duration, + easingtype_t easing +) { + assertNotNull(fullbox, "fullbox must not be NULL"); + fullbox->fromColor = from; + fullbox->toColor = to; + fullbox->duration = duration; + fullbox->time = 0.0f; + fullbox->easing = easing; +} + +errorret_t uiFullboxUnderDraw(void) { + return uiFullboxDraw(&UI_FULLBOX_UNDER); +} + +errorret_t uiFullboxOverDraw(void) { + return uiFullboxDraw(&UI_FULLBOX_OVER); +} diff --git a/src/dusk/ui/uifullbox.h b/src/dusk/ui/uifullbox.h new file mode 100644 index 00000000..e5e3d476 --- /dev/null +++ b/src/dusk/ui/uifullbox.h @@ -0,0 +1,75 @@ +/** + * 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 "display/color.h" +#include "animation/easing.h" +#include "event/event.h" + +typedef struct { + color_t fromColor; + color_t toColor; + float_t duration; + float_t time; + easingtype_t easing; + event_t onTransitionEnd; +} uifullbox_t; + +extern uifullbox_t UI_FULLBOX_UNDER; +extern uifullbox_t UI_FULLBOX_OVER; + +/** + * Initializes a fullbox, zeroing all fields. + * + * @param fullbox The fullbox to initialize. + */ +void uiFullboxInit(uifullbox_t *fullbox); + +/** + * Advances the fullbox transition. Fires onTransitionEnd once when the + * transition completes. Safe to call when no transition is running. + * + * @param fullbox The fullbox to update. + * @param delta Seconds elapsed since last update. + */ +void uiFullboxUpdate(uifullbox_t *fullbox, float_t delta); + +/** + * Renders the fullbox. Skipped entirely when the current alpha is zero. + * + * @param fullbox The fullbox to draw. + * @return Any error that occurs. + */ +errorret_t uiFullboxDraw(uifullbox_t *fullbox); + +/** + * Begins a color transition. + * + * @param fullbox The fullbox to transition. + * @param from Start color. + * @param to End color. + * @param duration Transition duration in seconds. + * @param easing Easing function to apply. + */ +void uiFullboxTransition( + uifullbox_t *fullbox, + color_t from, + color_t to, + float_t duration, + easingtype_t easing +); + +/** + * Draw function for UI_FULLBOX_UNDER (registered in UI_ELEMENTS). + */ +errorret_t uiFullboxUnderDraw(void); + +/** + * Draw function for UI_FULLBOX_OVER (registered in UI_ELEMENTS). + */ +errorret_t uiFullboxOverDraw(void); diff --git a/src/dusk/ui/uitextbox.c b/src/dusk/ui/uitextbox.c index fd7da50d..a7e88b85 100644 --- a/src/dusk/ui/uitextbox.c +++ b/src/dusk/ui/uitextbox.c @@ -9,6 +9,7 @@ #include "util/string.h" #include "time/time.h" #include "input/input.h" +#include "event/event.h" #include "display/screen/screen.h" #include "display/texture/texture.h" #include "display/spritebatch/spritebatch.h" @@ -44,6 +45,9 @@ errorret_t uiTextboxInit(void) { UI_TEXTBOX.frame.tileset.uv[1] = 1.0f / 3.0f; UI_TEXTBOX.frame.texture = &TEXTURE_WHITE; + eventInit(&UI_TEXTBOX.onPageComplete); + eventInit(&UI_TEXTBOX.onLastPage); + errorOk(); } @@ -153,7 +157,9 @@ errorret_t uiTextboxUpdate(void) { if(TIME.dynamicUpdate) errorOk(); #endif - if(!uiTextboxPageIsComplete()) { + bool_t wasComplete = uiTextboxPageIsComplete(); + + if(!wasComplete) { UI_TEXTBOX.scroll += UI_TEXTBOX_SCROLL_CHARS_PER_TICK; } @@ -165,6 +171,13 @@ errorret_t uiTextboxUpdate(void) { } } + if(!wasComplete && uiTextboxPageIsComplete()) { + eventInvoke(&UI_TEXTBOX.onPageComplete, &UI_TEXTBOX); + if(!uiTextboxHasNextPage()) { + eventInvoke(&UI_TEXTBOX.onLastPage, &UI_TEXTBOX); + } + } + errorOk(); } diff --git a/src/dusk/ui/uitextbox.h b/src/dusk/ui/uitextbox.h index 9317a685..033e8415 100644 --- a/src/dusk/ui/uitextbox.h +++ b/src/dusk/ui/uitextbox.h @@ -9,6 +9,7 @@ #include "uiframe.h" #include "display/text/text.h" #include "input/inputaction.h" +#include "event/event.h" #define UI_TEXTBOX_TEXT_MAX 1024 #define UI_TEXTBOX_LINES_MAX 64 @@ -40,6 +41,9 @@ typedef struct { int32_t currentPage; int32_t scroll; inputaction_t advanceAction; + + event_t onPageComplete; + event_t onLastPage; } uitextbox_t; extern uitextbox_t UI_TEXTBOX;