From 6da02b25faa86db9feff4674aa8aba0e0ffc51e1 Mon Sep 17 00:00:00 2001 From: Dominic Masters Date: Tue, 5 May 2026 22:10:47 -0500 Subject: [PATCH] Testing cutscenes --- assets/cutscenes/MoveCubeCutscene.js | 34 +++ assets/scenes/cube.js | 14 +- src/dusk/CMakeLists.txt | 1 + src/dusk/animation/animation.h | 63 ++++++ src/dusk/animation/easing.c | 106 +++++++++ src/dusk/animation/easing.h | 49 +++++ src/dusk/animation/keyframe.h | 15 ++ src/dusk/cutscene/CMakeLists.txt | 9 + src/dusk/cutscene/cutscene.c | 180 ++++++++++++++++ src/dusk/cutscene/cutscene.h | 105 +++++++++ src/dusk/engine/engine.c | 4 + src/dusk/scene/scene.c | 2 + .../script/module/cutscene/modulecutscene.h | 204 ++++++++++++++++++ src/dusk/script/module/module.h | 2 + 14 files changed, 786 insertions(+), 2 deletions(-) create mode 100644 assets/cutscenes/MoveCubeCutscene.js create mode 100644 src/dusk/animation/animation.h create mode 100644 src/dusk/animation/easing.c create mode 100644 src/dusk/animation/easing.h create mode 100644 src/dusk/animation/keyframe.h create mode 100644 src/dusk/cutscene/CMakeLists.txt create mode 100644 src/dusk/cutscene/cutscene.c create mode 100644 src/dusk/cutscene/cutscene.h create mode 100644 src/dusk/script/module/cutscene/modulecutscene.h diff --git a/assets/cutscenes/MoveCubeCutscene.js b/assets/cutscenes/MoveCubeCutscene.js new file mode 100644 index 00000000..71c1f00d --- /dev/null +++ b/assets/cutscenes/MoveCubeCutscene.js @@ -0,0 +1,34 @@ +var PHASE_LEFT = 0; +var PHASE_RIGHT = 1; + +var SPEED = 3.0; +var DURATION = 2.0; + +function MoveCubeCutscene(params) { + Cutscene.call(this); + this.cube = params.cube; + this.phase = PHASE_LEFT; + this.timer = 0.0; +} + +MoveCubeCutscene.prototype = Object.create(Cutscene.prototype); +MoveCubeCutscene.prototype.constructor = MoveCubeCutscene; + +MoveCubeCutscene.prototype.update = function() { + this.timer += TIME.delta; + + if(this.phase === PHASE_LEFT) { + this.cube.position.position.x -= SPEED * TIME.delta; + if(this.timer >= DURATION) { + this.phase = PHASE_RIGHT; + this.timer = 0.0; + } + } else if(this.phase === PHASE_RIGHT) { + this.cube.position.position.x += SPEED * TIME.delta; + if(this.timer >= DURATION) { + Cutscene.finish(); + } + } +}; + +module = MoveCubeCutscene; diff --git a/assets/scenes/cube.js b/assets/scenes/cube.js index e8dc7b0f..ff3a02ea 100644 --- a/assets/scenes/cube.js +++ b/assets/scenes/cube.js @@ -1,4 +1,5 @@ var CubeEntity = include('entities/CubeEntity.js'); +var MoveCubeCutscene = include('cutscenes/MoveCubeCutscene.js'); function CubeScene() { this.cam = new Entity(); @@ -13,17 +14,26 @@ function CubeScene() { this.spriteEnt = new Entity(); this.spriteEnt.add(POSITION); this.spriteEnt.position.position = new Vec3(16, 16, 0); - // this.spriteEnt.sprite.setTexture('ui/minogram.png'); + + this.inputEnabled = false; + + var scene = this; + Cutscene.play(new MoveCubeCutscene({ cube: this.cube })).then(function() { + scene.inputEnabled = true; + }); } CubeScene.prototype = Object.create(Scene.prototype); CubeScene.prototype.constructor = CubeScene; CubeScene.prototype.update = function() { - this.cube.update(); + if(this.inputEnabled) { + this.cube.update(); + } }; CubeScene.prototype.dispose = function() { + Cutscene.stop(); this.cam.dispose(); this.cube.dispose(); this.spriteEnt.dispose(); diff --git a/src/dusk/CMakeLists.txt b/src/dusk/CMakeLists.txt index 11682e65..69440679 100644 --- a/src/dusk/CMakeLists.txt +++ b/src/dusk/CMakeLists.txt @@ -56,6 +56,7 @@ target_sources(${DUSK_BINARY_TARGET_NAME} # Subdirs add_subdirectory(assert) add_subdirectory(asset) +add_subdirectory(cutscene) add_subdirectory(item) add_subdirectory(story) add_subdirectory(console) diff --git a/src/dusk/animation/animation.h b/src/dusk/animation/animation.h new file mode 100644 index 00000000..74319c11 --- /dev/null +++ b/src/dusk/animation/animation.h @@ -0,0 +1,63 @@ +// Copyright (c) 2026 Dominic Masters +// +// This software is released under the MIT License. +// https://opensource.org/licenses/MIT + +#pragma once +#include "keyframe.h" +#include "error/error.h" + +typedef struct { + keyframe_t keyframes[ANIMATION_KEYFRAME_COUNT_MAX]; + uint8_t keyframeCount; + float_t time; + float_t duration; + bool_t loop; + bool_t complete; +} animation_t; + +/** + * Zeroes an animation struct. + * + * @param anim The animation to initialize. + */ +void animationInit(animation_t *anim); + +/** + * Appends a keyframe. Keyframes must be added in ascending time order. + * + * @param anim The animation to modify. + * @param time Time in seconds for this keyframe. + * @param value Value at this keyframe. + * @param easing Easing applied to the segment after this keyframe. + */ +void animationAddKeyframe( + animation_t *anim, + float_t time, + float_t value, + easingtype_t easing +); + +/** + * Advances the animation by delta seconds and returns the current value. + * + * @param anim The animation to update. + * @param delta Seconds elapsed since last update. + * @return Interpolated value at the new time. + */ +float_t animationUpdate(animation_t *anim, float_t delta); + +/** + * Returns the interpolated value at the current time without advancing. + * + * @param anim The animation to read. + * @return Interpolated value. + */ +float_t animationGetValue(const animation_t *anim); + +/** + * Resets the animation to the beginning. + * + * @param anim The animation to reset. + */ +void animationReset(animation_t *anim); diff --git a/src/dusk/animation/easing.c b/src/dusk/animation/easing.c new file mode 100644 index 00000000..bf1fed6c --- /dev/null +++ b/src/dusk/animation/easing.c @@ -0,0 +1,106 @@ +// Copyright (c) 2026 Dominic Masters +// +// This software is released under the MIT License. +// https://opensource.org/licenses/MIT + +#include "easing.h" +#include "assert/assert.h" +#include + +#define EASING_PI 3.14159265358979323846f +#define EASING_C1 1.70158f +#define EASING_C2 (EASING_C1 * 1.525f) +#define EASING_C3 (EASING_C1 + 1.0f) + +float_t easingLinear(float_t t) { + return t; +} + +float_t easingInSine(float_t t) { + return 1.0f - cosf(t * EASING_PI * 0.5f); +} + +float_t easingOutSine(float_t t) { + return sinf(t * EASING_PI * 0.5f); +} + +float_t easingInOutSine(float_t t) { + return -(cosf(EASING_PI * t) - 1.0f) * 0.5f; +} + +float_t easingInQuad(float_t t) { + return t * t; +} + +float_t easingOutQuad(float_t t) { + float_t u = 1.0f - t; + return 1.0f - u * u; +} + +float_t easingInOutQuad(float_t t) { + if(t < 0.5f) return 2.0f * t * t; + float_t u = -2.0f * t + 2.0f; + return 1.0f - u * u * 0.5f; +} + +float_t easingInCubic(float_t t) { + return t * t * t; +} + +float_t easingOutCubic(float_t t) { + float_t u = 1.0f - t; + return 1.0f - u * u * u; +} + +float_t easingInOutCubic(float_t t) { + if(t < 0.5f) return 4.0f * t * t * t; + float_t u = -2.0f * t + 2.0f; + return 1.0f - u * u * u * 0.5f; +} + +float_t easingInQuart(float_t t) { + return t * t * t * t; +} + +float_t easingOutQuart(float_t t) { + float_t u = 1.0f - t; + return 1.0f - u * u * u * u; +} + +float_t easingInOutQuart(float_t t) { + if(t < 0.5f) return 8.0f * t * t * t * t; + float_t u = -2.0f * t + 2.0f; + return 1.0f - u * u * u * u * 0.5f; +} + +float_t easingInBack(float_t t) { + return EASING_C3 * t * t * t - EASING_C1 * t * t; +} + +float_t easingOutBack(float_t t) { + float_t u = t - 1.0f; + return 1.0f + EASING_C3 * u * u * u + EASING_C1 * u * u; +} + +float_t easingInOutBack(float_t t) { + if(t < 0.5f) { + float_t u = 2.0f * t; + return u * u * ((EASING_C2 + 1.0f) * u - EASING_C2) * 0.5f; + } + float_t u = 2.0f * t - 2.0f; + return (u * u * ((EASING_C2 + 1.0f) * u + EASING_C2) + 2.0f) * 0.5f; +} + +const easingfn_t EASING_FUNCTIONS[EASING_COUNT] = { + easingLinear, + easingInSine, easingOutSine, easingInOutSine, + easingInQuad, easingOutQuad, easingInOutQuad, + easingInCubic, easingOutCubic, easingInOutCubic, + easingInQuart, easingOutQuart, easingInOutQuart, + easingInBack, easingOutBack, easingInOutBack, +}; + +float_t easingApply(easingtype_t type, float_t t) { + assertTrue(type < EASING_COUNT, "Invalid easing type"); + return EASING_FUNCTIONS[type](t); +} diff --git a/src/dusk/animation/easing.h b/src/dusk/animation/easing.h new file mode 100644 index 00000000..600372c7 --- /dev/null +++ b/src/dusk/animation/easing.h @@ -0,0 +1,49 @@ +// Copyright (c) 2026 Dominic Masters +// +// This software is released under the MIT License. +// https://opensource.org/licenses/MIT + +#pragma once +#include "dusk.h" + +#define EASING_LINEAR 0 +#define EASING_IN_SINE 1 +#define EASING_OUT_SINE 2 +#define EASING_IN_OUT_SINE 3 +#define EASING_IN_QUAD 4 +#define EASING_OUT_QUAD 5 +#define EASING_IN_OUT_QUAD 6 +#define EASING_IN_CUBIC 7 +#define EASING_OUT_CUBIC 8 +#define EASING_IN_OUT_CUBIC 9 +#define EASING_IN_QUART 10 +#define EASING_OUT_QUART 11 +#define EASING_IN_OUT_QUART 12 +#define EASING_IN_BACK 13 +#define EASING_OUT_BACK 14 +#define EASING_IN_OUT_BACK 15 +#define EASING_COUNT 16 + +typedef uint8_t easingtype_t; +typedef float_t (*easingfn_t)(float_t t); + +extern const easingfn_t EASING_FUNCTIONS[EASING_COUNT]; + +float_t easingApply(easingtype_t type, float_t t); + +float_t easingLinear(float_t t); +float_t easingInSine(float_t t); +float_t easingOutSine(float_t t); +float_t easingInOutSine(float_t t); +float_t easingInQuad(float_t t); +float_t easingOutQuad(float_t t); +float_t easingInOutQuad(float_t t); +float_t easingInCubic(float_t t); +float_t easingOutCubic(float_t t); +float_t easingInOutCubic(float_t t); +float_t easingInQuart(float_t t); +float_t easingOutQuart(float_t t); +float_t easingInOutQuart(float_t t); +float_t easingInBack(float_t t); +float_t easingOutBack(float_t t); +float_t easingInOutBack(float_t t); diff --git a/src/dusk/animation/keyframe.h b/src/dusk/animation/keyframe.h new file mode 100644 index 00000000..de45cfdb --- /dev/null +++ b/src/dusk/animation/keyframe.h @@ -0,0 +1,15 @@ +// Copyright (c) 2026 Dominic Masters +// +// This software is released under the MIT License. +// https://opensource.org/licenses/MIT + +#pragma once +#include "easing.h" + +#define ANIMATION_KEYFRAME_COUNT_MAX 16 + +typedef struct { + float_t time; + float_t value; + easingtype_t easing; +} keyframe_t; diff --git a/src/dusk/cutscene/CMakeLists.txt b/src/dusk/cutscene/CMakeLists.txt new file mode 100644 index 00000000..cf650948 --- /dev/null +++ b/src/dusk/cutscene/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 + cutscene.c +) diff --git a/src/dusk/cutscene/cutscene.c b/src/dusk/cutscene/cutscene.c new file mode 100644 index 00000000..6466dbc9 --- /dev/null +++ b/src/dusk/cutscene/cutscene.c @@ -0,0 +1,180 @@ +// Copyright (c) 2026 Dominic Masters +// +// This software is released under the MIT License. +// https://opensource.org/licenses/MIT + +#include "cutscene.h" +#include "assert/assert.h" +#include "util/memory.h" +#include "script/module/modulebase.h" +#include "script/module/cutscene/modulecutscene.h" +#include "console/console.h" + +cutscene_t CUTSCENE; + +static errorret_t cutsceneCallMethod(const char_t *method) { + if(CUTSCENE.scriptRef == CUTSCENE_SCRIPT_REF_NONE) errorOk(); + + jerry_value_t key = jerry_string_sz(method); + jerry_value_t fn = jerry_object_get(CUTSCENE.scriptRef, key); + jerry_value_free(key); + + if(!jerry_value_is_function(fn)) { + jerry_value_free(fn); + errorOk(); + } + + jerry_value_t result = jerry_call(fn, CUTSCENE.scriptRef, NULL, 0); + jerry_value_free(fn); + + if(jerry_value_is_exception(result)) { + cutsceneevent_t *event = &CUTSCENE.events[CUTSCENE.eventCurrent]; + char_t errMsg[512]; + moduleBaseExceptionMessage(result, errMsg, sizeof(errMsg)); + jerry_value_free(result); + errorThrow( + "Cutscene event '%s' %s failed: %s", + event->script.script, method, errMsg + ); + } + + jerry_value_free(result); + errorOk(); +} + +static errorret_t cutsceneEventStart(void) { + cutsceneevent_t *event = &CUTSCENE.events[CUTSCENE.eventCurrent]; + + if(event->type == CUTSCENE_EVENT_TYPE_NATIVE) { + if(event->native.onStart) errorChain(event->native.onStart()); + } else if(event->type == CUTSCENE_EVENT_TYPE_SCRIPT) { + jerry_value_t eventClass = CUTSCENE_SCRIPT_REF_NONE; + errorChain(scriptManagerExecFile(event->script.script, &eventClass)); + + if(!jerry_value_is_function(eventClass)) { + if(eventClass != CUTSCENE_SCRIPT_REF_NONE) jerry_value_free(eventClass); + errorThrow( + "Cutscene event '%s' must export a constructor function", + event->script.script + ); + } + + jerry_value_t eventObj = jerry_construct(eventClass, NULL, 0); + jerry_value_free(eventClass); + + if(jerry_value_is_exception(eventObj)) { + char_t errMsg[512]; + moduleBaseExceptionMessage(eventObj, errMsg, sizeof(errMsg)); + jerry_value_free(eventObj); + errorThrow( + "Cutscene event '%s' constructor threw: %s", + event->script.script, errMsg + ); + } + + CUTSCENE.scriptRef = eventObj; + errorChain(cutsceneCallMethod("onStart")); + } + + errorOk(); +} + +static errorret_t cutsceneEventEnd(void) { + cutsceneevent_t *event = &CUTSCENE.events[CUTSCENE.eventCurrent]; + + if(event->type == CUTSCENE_EVENT_TYPE_NATIVE) { + if(event->native.onEnd) errorChain(event->native.onEnd()); + } else if(event->type == CUTSCENE_EVENT_TYPE_SCRIPT) { + errorret_t err = cutsceneCallMethod("onEnd"); + if(CUTSCENE.scriptRef != CUTSCENE_SCRIPT_REF_NONE) { + jerry_value_free(CUTSCENE.scriptRef); + CUTSCENE.scriptRef = CUTSCENE_SCRIPT_REF_NONE; + } + errorChain(err); + } + + errorOk(); +} + +errorret_t cutsceneInit(void) { + memoryZero(&CUTSCENE, sizeof(cutscene_t)); + CUTSCENE.scriptRef = CUTSCENE_SCRIPT_REF_NONE; + CUTSCENE.activeRef = CUTSCENE_SCRIPT_REF_NONE; + consolePrint("Cutscene init"); + errorOk(); +} + +errorret_t cutsceneUpdate(void) { + errorChain(moduleCutsceneUpdate()); + + if(!CUTSCENE.active) errorOk(); + + cutsceneevent_t *event = &CUTSCENE.events[CUTSCENE.eventCurrent]; + + if(event->type == CUTSCENE_EVENT_TYPE_NATIVE) { + if(event->native.onUpdate) errorChain(event->native.onUpdate()); + } else if(event->type == CUTSCENE_EVENT_TYPE_SCRIPT) { + errorChain(cutsceneCallMethod("onUpdate")); + } + + errorOk(); +} + +errorret_t cutscenePlay( + const cutsceneevent_t *events, + const uint8_t eventCount +) { + assertNotNull(events, "Events cannot be null"); + assertTrue(eventCount > 0, "Event count must be greater than zero"); + assertTrue( + eventCount <= CUTSCENE_EVENT_COUNT_MAX, + "Event count exceeds CUTSCENE_EVENT_COUNT_MAX" + ); + + if(CUTSCENE.active) { + errorChain(cutsceneStop()); + } + + memoryCopy(CUTSCENE.events, events, sizeof(cutsceneevent_t) * eventCount); + CUTSCENE.eventCount = eventCount; + CUTSCENE.eventCurrent = 0; + CUTSCENE.active = true; + + errorChain(cutsceneEventStart()); + consolePrint("Cutscene play"); + errorOk(); +} + +errorret_t cutsceneAdvance(void) { + if(!CUTSCENE.active) errorOk(); + + errorChain(cutsceneEventEnd()); + CUTSCENE.eventCurrent++; + + if(CUTSCENE.eventCurrent >= CUTSCENE.eventCount) { + CUTSCENE.active = false; + errorOk(); + } + + errorChain(cutsceneEventStart()); + consolePrint("Cutscene advance"); + errorOk(); +} + +errorret_t cutsceneStop(void) { + if(!CUTSCENE.active) errorOk(); + + errorChain(cutsceneEventEnd()); + CUTSCENE.active = false; + consolePrint("Cutscene stop"); + errorOk(); +} + +errorret_t cutsceneDispose(void) { + errorChain(cutsceneStop()); + errorOk(); +} + +bool_t cutsceneIsActive(void) { + return CUTSCENE.active; +} \ No newline at end of file diff --git a/src/dusk/cutscene/cutscene.h b/src/dusk/cutscene/cutscene.h new file mode 100644 index 00000000..a4dd489d --- /dev/null +++ b/src/dusk/cutscene/cutscene.h @@ -0,0 +1,105 @@ +// Copyright (c) 2026 Dominic Masters +// +// This software is released under the MIT License. +// https://opensource.org/licenses/MIT + +#pragma once +#include "script/scriptmanager.h" +#include "error/error.h" + +#define CUTSCENE_EVENT_COUNT_MAX 16 +#define CUTSCENE_SCRIPT_REF_NONE ((jerry_value_t)0) + +typedef enum { + CUTSCENE_EVENT_TYPE_NULL, + CUTSCENE_EVENT_TYPE_NATIVE, + CUTSCENE_EVENT_TYPE_SCRIPT, + CUTSCENE_EVENT_TYPE_COUNT +} cutsceneeventtype_t; + +typedef struct { + cutsceneeventtype_t type; + + union { + struct { + errorret_t (*onStart)(void); + errorret_t (*onEnd)(void); + errorret_t (*onUpdate)(void); + } native; + + struct { + const char_t *script; + } script; + }; +} cutsceneevent_t; + +typedef struct { + cutsceneevent_t events[CUTSCENE_EVENT_COUNT_MAX]; + uint8_t eventCount; + uint8_t eventCurrent; + bool_t active; + jerry_value_t scriptRef; + jerry_value_t activeRef; +} cutscene_t; + +extern cutscene_t CUTSCENE; + +/** + * Initializes the cutscene manager. + * + * @return Any error state that happened. + */ +errorret_t cutsceneInit(void); + +/** + * Ticks the active cutscene event, calling its onUpdate callback. + * Does nothing when no cutscene is playing. + * + * @return Any error state that happened. + */ +errorret_t cutsceneUpdate(void); + +/** + * Copies the given event array and begins playing from the first + * event. If a cutscene is already playing it is stopped first. + * + * @param events Array of events to copy. + * @param eventCount Number of events. Must be > 0 and + * <= CUTSCENE_EVENT_COUNT_MAX. + * @return Any error state that happened. + */ +errorret_t cutscenePlay( + const cutsceneevent_t *events, + const uint8_t eventCount +); + +/** + * Ends the current event and starts the next one. + * Marks the cutscene as inactive after the last event ends. + * Does nothing when no cutscene is playing. + * + * @return Any error state that happened. + */ +errorret_t cutsceneAdvance(void); + +/** + * Ends the current event and stops the cutscene immediately. + * Does nothing when no cutscene is playing. + * + * @return Any error state that happened. + */ +errorret_t cutsceneStop(void); + +/** + * Disposes of the cutscene manager, stopping any active cutscene. + * + * @return Any error state that happened. + */ +errorret_t cutsceneDispose(void); + +/** + * Returns whether a cutscene is currently playing. + * + * @return true if a cutscene is active. + */ +bool_t cutsceneIsActive(void); diff --git a/src/dusk/engine/engine.c b/src/dusk/engine/engine.c index 03ad998c..c8013b11 100644 --- a/src/dusk/engine/engine.c +++ b/src/dusk/engine/engine.c @@ -12,6 +12,7 @@ #include "locale/localemanager.h" #include "display/display.h" #include "scene/scene.h" +#include "cutscene/cutscene.h" #include "asset/asset.h" #include "ui/ui.h" #include "script/scriptmanager.h" @@ -52,6 +53,7 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) { errorChain(scriptManagerInit()); errorChain(displayInit()); errorChain(uiInit()); + errorChain(cutsceneInit()); errorChain(sceneInit()); entityManagerInit(); backpackInit(); @@ -79,6 +81,7 @@ errorret_t engineUpdate(void) { // Scene update occurs last because only after rendering would we want to do // scene switching, refer to sceneSet() for information. + errorChain(cutsceneUpdate()); errorChain(sceneUpdate()); errorOk(); @@ -89,6 +92,7 @@ void engineExit(void) { } errorret_t engineDispose(void) { + cutsceneDispose(); sceneDispose(); errorChain(networkDispose()); entityManagerDispose(); diff --git a/src/dusk/scene/scene.c b/src/dusk/scene/scene.c index ffa39938..eae281e3 100644 --- a/src/dusk/scene/scene.c +++ b/src/dusk/scene/scene.c @@ -16,6 +16,7 @@ #include "util/string.h" #include "script/scriptmanager.h" #include "script/module/scene/modulescene.h" +#include "script/module/cutscene/modulecutscene.h" #include "ui/ui.h" scene_t SCENE; @@ -175,6 +176,7 @@ errorret_t sceneSetImmediate(const char_t *scene) { SCENE.sceneActive = false; } + moduleCutsceneReset(); moduleSceneReset(); stringCopy( diff --git a/src/dusk/script/module/cutscene/modulecutscene.h b/src/dusk/script/module/cutscene/modulecutscene.h new file mode 100644 index 00000000..62342d89 --- /dev/null +++ b/src/dusk/script/module/cutscene/modulecutscene.h @@ -0,0 +1,204 @@ +// Copyright (c) 2026 Dominic Masters +// +// This software is released under the MIT License. +// https://opensource.org/licenses/MIT + +#pragma once +#include "script/module/modulebase.h" +#include "script/scriptproto.h" +#include "cutscene/cutscene.h" +#include "error/error.h" + +static scriptproto_t MODULE_CUTSCENE_PROTO; + +/** + * Disposes the active JS cutscene without firing the then callback. + * Safe to call when no cutscene is active. + */ +static void moduleCutsceneReset(void) { + if(CUTSCENE.activeRef == CUTSCENE_SCRIPT_REF_NONE) return; + + jerry_value_t key = jerry_string_sz("dispose"); + jerry_value_t fn = jerry_object_get(CUTSCENE.activeRef, key); + jerry_value_free(key); + + if(jerry_value_is_function(fn)) { + jerry_value_t result = jerry_call(fn, CUTSCENE.activeRef, NULL, 0); + jerry_value_free(fn); + jerry_value_free(result); + } else { + jerry_value_free(fn); + } + + jerry_value_free(CUTSCENE.activeRef); + CUTSCENE.activeRef = CUTSCENE_SCRIPT_REF_NONE; +} + +/** + * Ticks the active JS cutscene. Called from cutsceneUpdate(). + * + * @return Any error state that happened. + */ +static errorret_t moduleCutsceneUpdate(void) { + if(CUTSCENE.activeRef == CUTSCENE_SCRIPT_REF_NONE) errorOk(); + + jerry_value_t key = jerry_string_sz("update"); + jerry_value_t fn = jerry_object_get(CUTSCENE.activeRef, key); + jerry_value_free(key); + + if(!jerry_value_is_function(fn)) { + jerry_value_free(fn); + errorOk(); + } + + jerry_value_t result = jerry_call(fn, CUTSCENE.activeRef, NULL, 0); + jerry_value_free(fn); + + if(jerry_value_is_exception(result)) { + char_t errMsg[512]; + moduleBaseExceptionMessage(result, errMsg, sizeof(errMsg)); + jerry_value_free(result); + errorThrow("Cutscene update failed: %s", errMsg); + } + + jerry_value_free(result); + errorOk(); +} + +moduleBaseFunction(moduleCutsceneDefaultConstructor) { + return jerry_undefined(); +} + +moduleBaseFunction(moduleCutsceneDefaultUpdate) { + return jerry_undefined(); +} + +moduleBaseFunction(moduleCutsceneDefaultDispose) { + return jerry_undefined(); +} + +/** + * cutscene.then(callback) + * + * Sets the completion callback fired when Cutscene.finish() is called. + * Only one callback may be set; calling then() again replaces it. + * Returns the instance for optional chaining. + */ +moduleBaseFunction(moduleCutsceneThen) { + if(argc < 1 || !jerry_value_is_function(args[0])) { + return moduleBaseThrow("then() expects a function argument"); + } + + jerry_value_t key = jerry_string_sz("_onFinished"); + jerry_object_set(callInfo->this_value, key, args[0]); + jerry_value_free(key); + + return jerry_value_copy(callInfo->this_value); +} + +/** + * Cutscene.play(instance) + * + * Registers a cutscene instance with the engine and begins calling its + * update() each tick. Stops any currently active cutscene first. + * Returns the instance to allow chaining .then() directly. + */ +moduleBaseFunction(moduleCutscenePlay) { + if(argc < 1 || !jerry_value_is_object(args[0])) { + return moduleBaseThrow("play() expects a cutscene instance"); + } + + if(CUTSCENE.activeRef != CUTSCENE_SCRIPT_REF_NONE) { + moduleCutsceneReset(); + } + + CUTSCENE.activeRef = jerry_value_copy(args[0]); + return jerry_value_copy(args[0]); +} + +/** + * Cutscene.finish() + * + * Marks the active cutscene as complete: calls dispose(), clears the + * active ref, then fires the then() callback if one was set. + * Intended to be called from within the cutscene's own update(). + */ +moduleBaseFunction(moduleCutsceneFinish) { + if(CUTSCENE.activeRef == CUTSCENE_SCRIPT_REF_NONE) { + return jerry_undefined(); + } + + jerry_value_t cbKey = jerry_string_sz("_onFinished"); + jerry_value_t callback = jerry_object_get(CUTSCENE.activeRef, cbKey); + jerry_value_free(cbKey); + + jerry_value_t dispKey = jerry_string_sz("dispose"); + jerry_value_t dispFn = jerry_object_get(CUTSCENE.activeRef, dispKey); + jerry_value_free(dispKey); + + if(jerry_value_is_function(dispFn)) { + jerry_value_t r = jerry_call(dispFn, CUTSCENE.activeRef, NULL, 0); + jerry_value_free(dispFn); + if(jerry_value_is_exception(r)) { + jerry_value_free(callback); + return r; + } + jerry_value_free(r); + } else { + jerry_value_free(dispFn); + } + + jerry_value_free(CUTSCENE.activeRef); + CUTSCENE.activeRef = CUTSCENE_SCRIPT_REF_NONE; + + if(jerry_value_is_function(callback)) { + jerry_value_t r = jerry_call(callback, jerry_undefined(), NULL, 0); + jerry_value_free(callback); + if(jerry_value_is_exception(r)) return r; + jerry_value_free(r); + } else { + jerry_value_free(callback); + } + + return jerry_undefined(); +} + +/** + * Cutscene.stop() + * + * Immediately disposes the active cutscene without firing the then + * callback. No-op when no cutscene is active. + */ +moduleBaseFunction(moduleCutsceneStop) { + moduleCutsceneReset(); + return jerry_undefined(); +} + +static void moduleCutscene(void) { + scriptProtoInit( + &MODULE_CUTSCENE_PROTO, + "Cutscene", + sizeof(uint8_t), + moduleCutsceneDefaultConstructor + ); + + scriptProtoDefineFunc( + &MODULE_CUTSCENE_PROTO, "update", moduleCutsceneDefaultUpdate + ); + scriptProtoDefineFunc( + &MODULE_CUTSCENE_PROTO, "dispose", moduleCutsceneDefaultDispose + ); + scriptProtoDefineFunc( + &MODULE_CUTSCENE_PROTO, "then", moduleCutsceneThen + ); + + scriptProtoDefineStaticFunc( + &MODULE_CUTSCENE_PROTO, "play", moduleCutscenePlay + ); + scriptProtoDefineStaticFunc( + &MODULE_CUTSCENE_PROTO, "finish", moduleCutsceneFinish + ); + scriptProtoDefineStaticFunc( + &MODULE_CUTSCENE_PROTO, "stop", moduleCutsceneStop + ); +} diff --git a/src/dusk/script/module/module.h b/src/dusk/script/module/module.h index 660e0487..aed09d4d 100644 --- a/src/dusk/script/module/module.h +++ b/src/dusk/script/module/module.h @@ -18,6 +18,7 @@ #include "script/module/display/modulespritebatch.h" #include "script/module/display/moduletext.h" #include "script/module/scene/modulescene.h" +#include "script/module/cutscene/modulecutscene.h" #include "script/module/console/moduleconsole.h" #include "script/module/engine/moduleengine.h" #include "script/module/item/moduleitem.h" @@ -36,6 +37,7 @@ static void moduleRegister(void) { moduleSpriteBatch(); moduleText(); moduleScene(); + moduleCutscene(); moduleConsole(); moduleEngine(); moduleItem();