diff --git a/assets/scenes/test.json b/assets/scenes/test.json index f1b2899e..fe6641ef 100644 --- a/assets/scenes/test.json +++ b/assets/scenes/test.json @@ -20,6 +20,22 @@ } ] }, + { + "name": "npc", + "components": [ + { "type": "POSITION", "x": -3, "y": 1, "z": 0 }, + { + "type": "RENDERABLE", + "renderType": "SHADER_MATERIAL", + "priority": 0, + "shaderType": "UNLIT", + "color": { "r": 80, "g": 255, "b": 120, "a": 255 }, + "displayState": { "cull": false, "depthTest": true, "blend": false } + }, + { "type": "PHYSICS", "bodyType": "STATIC" }, + { "type": "INTERACTABLE", "kind": "ITEM_PICKUP", "pickupId": 1 } + ] + }, { "components": [ { diff --git a/src/dusk/entity/component.h b/src/dusk/entity/component.h index c93b285c..b56769e4 100644 --- a/src/dusk/entity/component.h +++ b/src/dusk/entity/component.h @@ -23,25 +23,75 @@ typedef union { #undef X } componentdata_t; +/** + * Callback signature for a component's init/dispose hooks. + * + * @param mgr The entity manager that owns the entity. + * @param entityId The entity ID. + * @param componentId The component ID. + */ +typedef void (*componentcallback_t)( + entitymanager_t *mgr, + const entityid_t entityId, + const componentid_t componentId +); + +/** + * Callback signature for a component's render hook. + * + * @param mgr The entity manager that owns the entity. + * @param entityId The entity ID. + * @param componentId The component ID. + * @return Error state. + */ +typedef errorret_t (*componentcallbackerror_t)( + entitymanager_t *mgr, + const entityid_t entityId, + const componentid_t componentId +); + +/** + * Callback signature for a component's serialize hook. + * + * @param mgr The entity manager that owns the entity. + * @param entityId The entity ID. + * @param componentId The component ID. + * @param doc The mutable JSON document to allocate values from. + * @param json The JSON object to write the component's fields into. + * @return Error state. + */ +typedef errorret_t (*componentserializecallback_t)( + entitymanager_t *mgr, + const entityid_t entityId, + const componentid_t componentId, + yyjson_mut_doc *doc, + yyjson_mut_val *json +); + +/** + * Callback signature for a component's deserialize hook. + * + * @param mgr The entity manager that owns the entity. + * @param entityId The entity ID. + * @param componentId The component ID. + * @param json The JSON object to read the component's fields from. + * @return Error state. + */ +typedef errorret_t (*componentdeserializecallback_t)( + entitymanager_t *mgr, + const entityid_t entityId, + const componentid_t componentId, + yyjson_val *json +); + typedef struct { const char_t *enumName; const char_t *name; - void (*init)(entitymanager_t *, const entityid_t, const componentid_t); - void (*dispose)(entitymanager_t *, const entityid_t, const componentid_t); - errorret_t (*render)(entitymanager_t *, const entityid_t, const componentid_t); - errorret_t (*serialize)( - entitymanager_t *, - const entityid_t, - const componentid_t, - yyjson_mut_doc *, - yyjson_mut_val * - ); - errorret_t (*deserialize)( - entitymanager_t *, - const entityid_t, - const componentid_t, - yyjson_val * - ); + componentcallback_t init; + componentcallback_t dispose; + componentcallbackerror_t render; + componentserializecallback_t serialize; + componentdeserializecallback_t deserialize; } componentdefinition_t; typedef enum { diff --git a/src/dusk/ui/CMakeLists.txt b/src/dusk/ui/CMakeLists.txt index d83ddcfc..c7b76cd4 100644 --- a/src/dusk/ui/CMakeLists.txt +++ b/src/dusk/ui/CMakeLists.txt @@ -15,5 +15,4 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME} PUBLIC ui.c uielement.c - # uitextbox.c ) \ No newline at end of file diff --git a/src/duskrpg/CMakeLists.txt b/src/duskrpg/CMakeLists.txt index b74ba477..0fe748d0 100644 --- a/src/duskrpg/CMakeLists.txt +++ b/src/duskrpg/CMakeLists.txt @@ -9,6 +9,7 @@ target_include_directories(${DUSK_LIBRARY_TARGET_NAME} ${CMAKE_CURRENT_LIST_DIR} ) +add_subdirectory(cutscene) add_subdirectory(entity) add_subdirectory(game) add_subdirectory(input) diff --git a/src/duskrpg/cutscene/CMakeLists.txt b/src/duskrpg/cutscene/CMakeLists.txt new file mode 100644 index 00000000..f454d77e --- /dev/null +++ b/src/duskrpg/cutscene/CMakeLists.txt @@ -0,0 +1,11 @@ +# 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 + cutscenesystem.c +) + +add_subdirectory(item) diff --git a/src/duskrpg/cutscene/cutscene.h b/src/duskrpg/cutscene/cutscene.h new file mode 100644 index 00000000..1eb24838 --- /dev/null +++ b/src/duskrpg/cutscene/cutscene.h @@ -0,0 +1,127 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "cutscene/cutscenepause.h" +#include "cutscene/item/cutsceneitem.h" + +typedef struct cutscene_s { + const cutsceneitem_t *items; + uint8_t itemCount; + cutscenepause_t pause; + + /** Bytes of CUTSCENE_SYSTEM.userData this cutscene's callbacks use. */ + size_t dataSize; +} cutscene_t; + +/** + * Declares a static cutscene_t named NAME with the given items. + * + * @param NAME Identifier for the resulting cutscene_t. + * @param SIZE Bytes of scratch user data this cutscene's callbacks use. + * @param PAUSE_TYPE A cutscenepause_t value applied while running. + * @param ... cutsceneitem_t initializers, see the CUTSCENE_* item macros. + */ +#define CUTSCENE(NAME, SIZE, PAUSE_TYPE, ...) \ + static const cutsceneitem_t NAME##_ITEMS[] = { __VA_ARGS__ }; \ + static const cutscene_t NAME = { \ + NAME##_ITEMS, \ + sizeof(NAME##_ITEMS) / sizeof(cutsceneitem_t), \ + (PAUSE_TYPE), \ + (SIZE) \ + } + +#define CUTSCENE_TEXT(TEXT) \ + { .type = CUTSCENE_ITEM_TYPE_TEXT, .text = { TEXT } } + +#define CUTSCENE_CALLBACK(CALLBACK) \ + { .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = (CALLBACK) } + +#define CUTSCENE_WAIT(SECONDS) \ + { .type = CUTSCENE_ITEM_TYPE_WAIT, .wait = (SECONDS) } + +#define CUTSCENE_CUTSCENE(CUTSCENE_PTR) \ + { .type = CUTSCENE_ITEM_TYPE_CUTSCENE, .cutscene = (CUTSCENE_PTR) } + +#define CUTSCENE_SET_PAUSE(PAUSE) \ + { .type = CUTSCENE_ITEM_TYPE_SET_PAUSE, .setPause = (PAUSE) } + +/** + * @param ITEMS Identifier of a previously-declared cutsceneitem_t array; + * its element count is taken via sizeof(ITEMS), so it must be a real + * array, not a pointer. + */ +#define CUTSCENE_CONCURRENT(ITEMS) \ + { \ + .type = CUTSCENE_ITEM_TYPE_CONCURRENT, \ + .concurrent = { (ITEMS), sizeof(ITEMS) / sizeof(cutsceneitem_t) } \ + } + +#define CUTSCENE_ENTITY_TELEPORT(ENTITY_INDEX, X, Y, Z) \ + { \ + .type = CUTSCENE_ITEM_TYPE_ENTITY_TELEPORT, \ + .entityTeleport = { (ENTITY_INDEX), { (X), (Y), (Z) } } \ + } + +#define CUTSCENE_ENTITY_WALK_TO(ENTITY_INDEX, X, Y, Z, SPEED) \ + { \ + .type = CUTSCENE_ITEM_TYPE_ENTITY_WALK_TO, \ + .entityWalkTo = { \ + (ENTITY_INDEX), (const vec3[]){ { (X), (Y), (Z) } }, 1, (SPEED) \ + } \ + } + +/** + * @param POSITIONS Identifier of a previously-declared vec3 array of + * waypoints; its element count is taken via sizeof(POSITIONS), so it + * must be a real array, not a pointer. + */ +#define CUTSCENE_ENTITY_WALK_PATH(ENTITY_INDEX, POSITIONS, SPEED) \ + { \ + .type = CUTSCENE_ITEM_TYPE_ENTITY_WALK_TO, \ + .entityWalkTo = { \ + (ENTITY_INDEX), (POSITIONS), sizeof(POSITIONS) / sizeof(vec3), (SPEED) \ + } \ + } + +#define CUTSCENE_ENTITY_WALK_TO_ENTITY( \ + ENTITY_INDEX, TARGET_ENTITY_INDEX, OFFSET_X, OFFSET_Z, SPEED \ +) \ + { \ + .type = CUTSCENE_ITEM_TYPE_ENTITY_WALK_TO_ENTITY, \ + .entityWalkToEntity = { \ + (ENTITY_INDEX), (TARGET_ENTITY_INDEX), (OFFSET_X), (OFFSET_Z), (SPEED) \ + } \ + } + +#define CUTSCENE_ENTITY_TURN(ENTITY_INDEX, YAW) \ + { \ + .type = CUTSCENE_ITEM_TYPE_ENTITY_TURN, \ + .entityTurn = { (ENTITY_INDEX), (YAW) } \ + } + +#define CUTSCENE_ENTITY_ADD(PREFAB_NAME, X, Y, Z) \ + { \ + .type = CUTSCENE_ITEM_TYPE_ENTITY_ADD, \ + .entityAdd = { (PREFAB_NAME), { (X), (Y), (Z) } } \ + } + +#define CUTSCENE_ENTITY_REMOVE(ENTITY_INDEX) \ + { \ + .type = CUTSCENE_ITEM_TYPE_ENTITY_REMOVE, \ + .entityRemove = { (ENTITY_INDEX) } \ + } + +/** + * @param MESSAGE Shown in UI_TEXTBOX_MAIN; "" uses + * ENTITY_INTERACTABLE_ITEM_PICKUP_MESSAGE_DEFAULT. + */ +#define CUTSCENE_ENTITY_ITEM_PICKUP(ENTITY_INDEX, MESSAGE) \ + { \ + .type = CUTSCENE_ITEM_TYPE_ENTITY_ITEM_PICKUP, \ + .entityItemPickup = { (ENTITY_INDEX), MESSAGE } \ + } diff --git a/src/duskrpg/cutscene/cutscenepause.h b/src/duskrpg/cutscene/cutscenepause.h new file mode 100644 index 00000000..222d28c0 --- /dev/null +++ b/src/duskrpg/cutscene/cutscenepause.h @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "dusk.h" + +typedef uint8_t cutscenepause_t; + +#define CUTSCENE_PAUSE_NONE ((cutscenepause_t)0) +#define CUTSCENE_PAUSE_NPC ((cutscenepause_t)(1 << 0)) +#define CUTSCENE_PAUSE_PLAYER ((cutscenepause_t)(1 << 1)) +#define CUTSCENE_PAUSE_WORLD ((cutscenepause_t)(1 << 2)) + +#define CUTSCENE_PAUSE_DEFAULT ((cutscenepause_t)( \ + CUTSCENE_PAUSE_NPC | CUTSCENE_PAUSE_PLAYER \ +)) + +#define CUTSCENE_PAUSE_ALL ((cutscenepause_t)( \ + CUTSCENE_PAUSE_NPC | CUTSCENE_PAUSE_PLAYER | CUTSCENE_PAUSE_WORLD \ +)) diff --git a/src/duskrpg/cutscene/cutscenesystem.c b/src/duskrpg/cutscene/cutscenesystem.c new file mode 100644 index 00000000..be5c9d71 --- /dev/null +++ b/src/duskrpg/cutscene/cutscenesystem.c @@ -0,0 +1,102 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "cutscene/cutscenesystem.h" +#include "scene/scene.h" +#include "util/memory.h" +#include "assert/assert.h" + +cutscenesystem_t CUTSCENE_SYSTEM; + +errorret_t cutsceneSystemInit(void) { + memoryZero(&CUTSCENE_SYSTEM, sizeof(cutscenesystem_t)); + CUTSCENE_SYSTEM.entityInteract = ENTITY_ID_INVALID; + CUTSCENE_SYSTEM.entityInteracted = ENTITY_ID_INVALID; + CUTSCENE_SYSTEM.entityLastCreated = ENTITY_ID_INVALID; + CUTSCENE_SYSTEM.entityLastRef = ENTITY_ID_INVALID; + errorOk(); +} + +void cutsceneSystemStartCutscene(const cutscene_t *cutscene) { + cutsceneSystemStartCutsceneWith( + cutscene, ENTITY_ID_INVALID, ENTITY_ID_INVALID + ); +} + +void cutsceneSystemStartCutsceneWith( + const cutscene_t *cutscene, + const entityid_t interact, + const entityid_t interacted +) { + assertNotNull(cutscene, "cutscene must not be NULL"); + assertTrue( + cutscene->dataSize <= CUTSCENE_SYSTEM_SIZE_MAX, + "Cutscene dataSize exceeds CUTSCENE_SYSTEM_SIZE_MAX" + ); + + CUTSCENE_SYSTEM.mgr = sceneGetEntities(sceneGetActive()); + CUTSCENE_SYSTEM.scene = cutscene; + CUTSCENE_SYSTEM.currentItem = 0; + CUTSCENE_SYSTEM.pause = cutscene->pause; + CUTSCENE_SYSTEM.entityInteract = interact; + CUTSCENE_SYSTEM.entityInteracted = interacted; + CUTSCENE_SYSTEM.entityLastCreated = ENTITY_ID_INVALID; + CUTSCENE_SYSTEM.entityLastRef = ENTITY_ID_INVALID; + memoryZero(&CUTSCENE_SYSTEM.data, sizeof(cutsceneitemdata_t)); + if(cutscene->dataSize > 0) { + memoryZero(CUTSCENE_SYSTEM.userData, cutscene->dataSize); + } + + if(cutscene->itemCount > 0) { + cutsceneItemStart(&cutscene->items[0], &CUTSCENE_SYSTEM.data); + } +} + +entityid_t cutsceneSystemGetEntity(const uint8_t entityIndex) { + switch(entityIndex) { + case CUTSCENE_ENTITY_INTERACT: return CUTSCENE_SYSTEM.entityInteract; + case CUTSCENE_ENTITY_INTERACTED: return CUTSCENE_SYSTEM.entityInteracted; + case CUTSCENE_ENTITY_LAST_CREATED: + return CUTSCENE_SYSTEM.entityLastCreated; + case CUTSCENE_ENTITY_LAST_REF: return CUTSCENE_SYSTEM.entityLastRef; + default: return (entityid_t)entityIndex; + } +} + +void cutsceneSystemNext(void) { + CUTSCENE_SYSTEM.currentItem++; + memoryZero(&CUTSCENE_SYSTEM.data, sizeof(cutsceneitemdata_t)); + + if(CUTSCENE_SYSTEM.currentItem >= CUTSCENE_SYSTEM.scene->itemCount) { + CUTSCENE_SYSTEM.scene = NULL; + CUTSCENE_SYSTEM.pause = CUTSCENE_PAUSE_NONE; + return; + } + + cutsceneItemStart(cutsceneSystemGetCurrentItem(), &CUTSCENE_SYSTEM.data); +} + +errorret_t cutsceneSystemUpdate(void) { + if(CUTSCENE_SYSTEM.scene == NULL) errorOk(); + + if(cutsceneItemUpdate( + cutsceneSystemGetCurrentItem(), &CUTSCENE_SYSTEM.data + )) { + cutsceneSystemNext(); + } + + errorOk(); +} + +const cutsceneitem_t *cutsceneSystemGetCurrentItem(void) { + if(CUTSCENE_SYSTEM.scene == NULL) return NULL; + return &CUTSCENE_SYSTEM.scene->items[CUTSCENE_SYSTEM.currentItem]; +} + +errorret_t cutsceneSystemDispose(void) { + errorOk(); +} diff --git a/src/duskrpg/cutscene/cutscenesystem.h b/src/duskrpg/cutscene/cutscenesystem.h new file mode 100644 index 00000000..2eca95d3 --- /dev/null +++ b/src/duskrpg/cutscene/cutscenesystem.h @@ -0,0 +1,149 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "entity/entitybase.h" +#include "cutscene/cutscene.h" +#include "error/error.h" + +/** Resolves to CUTSCENE_SYSTEM.entityInteract; see + * cutsceneSystemGetEntity(). */ +#define CUTSCENE_ENTITY_INTERACT ((entityid_t)0xFE) +/** Resolves to CUTSCENE_SYSTEM.entityInteracted; see + * cutsceneSystemGetEntity(). */ +#define CUTSCENE_ENTITY_INTERACTED ((entityid_t)0xFD) +/** Resolves to CUTSCENE_SYSTEM.entityLastCreated; see + * cutsceneSystemGetEntity(). */ +#define CUTSCENE_ENTITY_LAST_CREATED ((entityid_t)0xFC) +/** Resolves to CUTSCENE_SYSTEM.entityLastRef; see + * cutsceneSystemGetEntity(). */ +#define CUTSCENE_ENTITY_LAST_REF ((entityid_t)0xFB) + +/** Size in bytes of CUTSCENE_SYSTEM.userData. */ +#define CUTSCENE_SYSTEM_SIZE_MAX 8192 + +typedef struct { + /** The entity manager the running cutscene's entity items operate on; + * resolved from the active scene when a cutscene starts. */ + entitymanager_t *mgr; + + /** The currently running cutscene, or NULL if none is running. */ + const cutscene_t *scene; + + /** Index of the currently running item within scene->items. */ + uint8_t currentItem; + + /** Pause flags applied by the running cutscene; see cutscenepause_t. */ + cutscenepause_t pause; + + /** Entity that initiated the current cutscene, if started via + * cutsceneSystemStartCutsceneWith(); see CUTSCENE_ENTITY_INTERACT. */ + entityid_t entityInteract; + + /** Entity that was interacted with to start the current cutscene, if + * started via cutsceneSystemStartCutsceneWith(); see + * CUTSCENE_ENTITY_INTERACTED. */ + entityid_t entityInteracted; + + /** Entity most recently spawned by a CUTSCENE_ENTITY_ADD item; see + * CUTSCENE_ENTITY_LAST_CREATED. */ + entityid_t entityLastCreated; + + /** Entity most recently referenced by CUTSCENE_ENTITY_LAST_CREATED/ + * CUTSCENE_ENTITY_LAST_REF; see CUTSCENE_ENTITY_LAST_REF. */ + entityid_t entityLastRef; + + /** Runtime data for the currently running item. */ + cutsceneitemdata_t data; + + /** Scratch storage for a running cutscene's own state (see + * cutscene_t.dataSize) and for CUTSCENE_CALLBACK's user pointer. */ + uint8_t userData[CUTSCENE_SYSTEM_SIZE_MAX]; +} cutscenesystem_t; + +extern cutscenesystem_t CUTSCENE_SYSTEM; + +/** + * Initializes CUTSCENE_SYSTEM. No cutscene is running until + * cutsceneSystemStartCutscene()/cutsceneSystemStartCutsceneWith() is + * called. + * + * @return Error state. + */ +errorret_t cutsceneSystemInit(void); + +/** + * Starts a cutscene. Equivalent to cutsceneSystemStartCutsceneWith() with + * both entity IDs set to ENTITY_ID_INVALID. + * + * @param cutscene The cutscene to start. + */ +void cutsceneSystemStartCutscene(const cutscene_t *cutscene); + +/** + * Starts a cutscene, recording which entities (if any) initiated it via + * an interaction, so its items can reference them through + * CUTSCENE_ENTITY_INTERACT/CUTSCENE_ENTITY_INTERACTED. Resolves + * CUTSCENE_SYSTEM.mgr from the active scene (see sceneGetActive()). + * + * If a cutscene is already running, this replaces it outright -- there + * is no call/return stack, so starting a cutscene from within another + * (see CUTSCENE_CUTSCENE()) is a one-way jump; the outer cutscene's + * remaining items are abandoned, not resumed afterwards. + * + * @param cutscene The cutscene to start. + * @param interact The entity that initiated the interaction, or + * ENTITY_ID_INVALID. + * @param interacted The entity that was interacted with, or + * ENTITY_ID_INVALID. + */ +void cutsceneSystemStartCutsceneWith( + const cutscene_t *cutscene, + const entityid_t interact, + const entityid_t interacted +); + +/** + * Resolves an item's entityIndex field into a real entity ID: the + * CUTSCENE_ENTITY_* sentinels resolve to their corresponding + * CUTSCENE_SYSTEM field, any other value passes through unchanged as a + * literal entity ID within CUTSCENE_SYSTEM.mgr. + * + * @param entityIndex The entityIndex field from a cutscene item. + * @return The resolved entity ID. + */ +entityid_t cutsceneSystemGetEntity(const uint8_t entityIndex); + +/** + * Advances to the next item in the running cutscene (starting it), or + * ends the cutscene if none remain. Called by cutsceneSystemUpdate() once + * the current item completes. + */ +void cutsceneSystemNext(void); + +/** + * Updates the running cutscene: ticks the current item, advancing to the + * next one (see cutsceneSystemNext()) once it completes. No-op if no + * cutscene is running. + * + * @return Error state. + */ +errorret_t cutsceneSystemUpdate(void); + +/** + * Gets the currently running item. + * + * @return The current item, or NULL if no cutscene is running. + */ +const cutsceneitem_t *cutsceneSystemGetCurrentItem(void); + +/** + * Disposes CUTSCENE_SYSTEM. + * + * @return Error state. + */ +errorret_t cutsceneSystemDispose(void); diff --git a/src/duskrpg/cutscene/item/CMakeLists.txt b/src/duskrpg/cutscene/item/CMakeLists.txt new file mode 100644 index 00000000..6d149b59 --- /dev/null +++ b/src/duskrpg/cutscene/item/CMakeLists.txt @@ -0,0 +1,14 @@ +# 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 + cutscenecallback.c + cutsceneitem.c +) + +add_subdirectory(control) +add_subdirectory(entity) +add_subdirectory(ui) diff --git a/src/duskrpg/cutscene/item/control/CMakeLists.txt b/src/duskrpg/cutscene/item/control/CMakeLists.txt new file mode 100644 index 00000000..a3859c01 --- /dev/null +++ b/src/duskrpg/cutscene/item/control/CMakeLists.txt @@ -0,0 +1,11 @@ +# 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 + cutsceneconcurrent.c + cutscenesetpause.c + cutscenewait.c +) diff --git a/src/duskrpg/cutscene/item/control/cutsceneconcurrent.c b/src/duskrpg/cutscene/item/control/cutsceneconcurrent.c new file mode 100644 index 00000000..f2e75bb6 --- /dev/null +++ b/src/duskrpg/cutscene/item/control/cutsceneconcurrent.c @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "cutscene/item/cutsceneitem.h" +#include "assert/assert.h" + +void cutsceneConcurrentStart( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +) { + assertTrue( + item->concurrent.count <= CUTSCENE_CONCURRENT_MAX, + "Too many items in CUTSCENE_CONCURRENT" + ); + for(uint8_t i = 0; i < item->concurrent.count; i++) { + assertTrue( + item->concurrent.items[i].type != CUTSCENE_ITEM_TYPE_CONCURRENT, + "Concurrent items cannot be nested" + ); + cutsceneItemStart( + &item->concurrent.items[i], + (cutsceneitemdata_t *)&data->concurrent.childData[i] + ); + } +} + +bool_t cutsceneConcurrentUpdate( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +) { + for(uint8_t i = 0; i < item->concurrent.count; i++) { + if(data->concurrent.doneMask & (1u << i)) continue; + if(cutsceneItemUpdate( + &item->concurrent.items[i], + (cutsceneitemdata_t *)&data->concurrent.childData[i] + )) { + data->concurrent.doneMask |= (uint8_t)(1u << i); + } + } + uint8_t allDone = (uint8_t)((1u << item->concurrent.count) - 1u); + return data->concurrent.doneMask == allDone; +} diff --git a/src/duskrpg/cutscene/item/control/cutsceneconcurrent.h b/src/duskrpg/cutscene/item/control/cutsceneconcurrent.h new file mode 100644 index 00000000..ccbaef6f --- /dev/null +++ b/src/duskrpg/cutscene/item/control/cutsceneconcurrent.h @@ -0,0 +1,62 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "cutscenewait.h" +#include "cutscene/item/entity/cutsceneentitywalkto.h" + +typedef struct cutsceneitem_s cutsceneitem_t; +typedef union cutsceneitemdata_u cutsceneitemdata_t; + +/** Maximum number of items that may run inside a CUTSCENE_CONCURRENT. */ +#define CUTSCENE_CONCURRENT_MAX 8 + +/** + * Static (const) data for a concurrent cutscene item. + */ +typedef struct { + const cutsceneitem_t *items; + uint8_t count; +} cutsceneconcurrent_t; + +/** + * Runtime data for one non-concurrent child item. + * Concurrent items cannot be nested. + */ +typedef union { + cutscenewaitdata_t wait; + cutsceneentitywalktodata_t entityWalkTo; +} cutsceneconcurrentchilddata_t; + +/** Runtime data for a running concurrent item. */ +typedef struct { + cutsceneconcurrentchilddata_t childData[CUTSCENE_CONCURRENT_MAX]; + uint8_t doneMask; +} cutsceneconcurrentdata_t; + +/** + * Starts a concurrent item (starts all child items simultaneously). + * + * @param item The cutscene item. + * @param data Runtime data storage. + */ +void cutsceneConcurrentStart( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +); + +/** + * Updates a concurrent item (ticks all unfinished children). + * + * @param item The cutscene item. + * @param data Runtime data storage. + * @returns true once every child has completed. + */ +bool_t cutsceneConcurrentUpdate( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +); diff --git a/src/duskrpg/cutscene/item/control/cutscenesetpause.c b/src/duskrpg/cutscene/item/control/cutscenesetpause.c new file mode 100644 index 00000000..a43831d2 --- /dev/null +++ b/src/duskrpg/cutscene/item/control/cutscenesetpause.c @@ -0,0 +1,23 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "cutscene/item/cutsceneitem.h" +#include "cutscene/cutscenesystem.h" + +void cutsceneSetPauseStart( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +) { + CUTSCENE_SYSTEM.pause = item->setPause; +} + +bool_t cutsceneSetPauseUpdate( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +) { + return true; +} diff --git a/src/duskrpg/cutscene/item/control/cutscenesetpause.h b/src/duskrpg/cutscene/item/control/cutscenesetpause.h new file mode 100644 index 00000000..9ef1121c --- /dev/null +++ b/src/duskrpg/cutscene/item/control/cutscenesetpause.h @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "cutscene/cutscenepause.h" + +typedef struct cutsceneitem_s cutsceneitem_t; +typedef union cutsceneitemdata_u cutsceneitemdata_t; + +/** + * Starts a set-pause item (applies the new pause flags immediately). + * + * @param item The cutscene item. + * @param data Runtime data storage. + */ +void cutsceneSetPauseStart( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +); + +/** + * Updates a set-pause item (always completes immediately). + * + * @param item The cutscene item. + * @param data Runtime data storage. + * @returns true always. + */ +bool_t cutsceneSetPauseUpdate( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +); diff --git a/src/duskrpg/cutscene/item/control/cutscenewait.c b/src/duskrpg/cutscene/item/control/cutscenewait.c new file mode 100644 index 00000000..0a1d1184 --- /dev/null +++ b/src/duskrpg/cutscene/item/control/cutscenewait.c @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "cutscene/item/cutsceneitem.h" +#include "time/time.h" + +void cutsceneWaitStart( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +) { + data->wait = item->wait; +} + +bool_t cutsceneWaitUpdate( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +) { + data->wait -= TIME.delta; + return data->wait <= 0; +} diff --git a/src/duskrpg/cutscene/item/control/cutscenewait.h b/src/duskrpg/cutscene/item/control/cutscenewait.h new file mode 100644 index 00000000..d070fdac --- /dev/null +++ b/src/duskrpg/cutscene/item/control/cutscenewait.h @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "dusk.h" + +typedef struct cutsceneitem_s cutsceneitem_t; +typedef union cutsceneitemdata_u cutsceneitemdata_t; + +typedef float_t cutscenewait_t; +typedef float_t cutscenewaitdata_t; + +/** + * Starts a wait item (stores the duration in data). + * + * @param item The cutscene item. + * @param data Runtime data storage. + */ +void cutsceneWaitStart( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +); + +/** + * Updates a wait item. + * + * @param item The cutscene item. + * @param data Runtime data storage. + * @returns true when the wait has elapsed. + */ +bool_t cutsceneWaitUpdate( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +); diff --git a/src/duskrpg/cutscene/item/cutscenecallback.c b/src/duskrpg/cutscene/item/cutscenecallback.c new file mode 100644 index 00000000..201288c0 --- /dev/null +++ b/src/duskrpg/cutscene/item/cutscenecallback.c @@ -0,0 +1,23 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "cutsceneitem.h" +#include "cutscene/cutscenesystem.h" + +void cutsceneCallbackStart( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +) { + if(item->callback != NULL) item->callback(CUTSCENE_SYSTEM.userData); +} + +bool_t cutsceneCallbackUpdate( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +) { + return true; +} diff --git a/src/duskrpg/cutscene/item/cutscenecallback.h b/src/duskrpg/cutscene/item/cutscenecallback.h new file mode 100644 index 00000000..fc12277c --- /dev/null +++ b/src/duskrpg/cutscene/item/cutscenecallback.h @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "dusk.h" + +typedef struct cutsceneitem_s cutsceneitem_t; +typedef union cutsceneitemdata_u cutsceneitemdata_t; + +/** @param user The cutscene's own userData scratch buffer; see cutscene_t. */ +typedef void (*cutscenecallback_t)(void *user); + +/** + * Starts a callback item (invokes the callback immediately). + * + * @param item The cutscene item. + * @param data Runtime data storage. + */ +void cutsceneCallbackStart( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +); + +/** + * Updates a callback item (always completes immediately). + * + * @param item The cutscene item. + * @param data Runtime data storage. + * @returns true always. + */ +bool_t cutsceneCallbackUpdate( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +); diff --git a/src/duskrpg/cutscene/item/cutsceneitem.c b/src/duskrpg/cutscene/item/cutsceneitem.c new file mode 100644 index 00000000..345bec3e --- /dev/null +++ b/src/duskrpg/cutscene/item/cutsceneitem.c @@ -0,0 +1,88 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "cutscene/item/cutsceneitem.h" +#include "cutscene/cutscenesystem.h" +#include "assert/assert.h" + +const cutsceneitemcallbacks_t +CUTSCENE_ITEM_CALLBACKS[CUTSCENE_ITEM_TYPE_COUNT] = { + [CUTSCENE_ITEM_TYPE_NULL] = { NULL, NULL }, + [CUTSCENE_ITEM_TYPE_TEXT] = { + cutsceneTextStart, cutsceneTextUpdate + }, + [CUTSCENE_ITEM_TYPE_CALLBACK] = { + cutsceneCallbackStart, cutsceneCallbackUpdate + }, + [CUTSCENE_ITEM_TYPE_WAIT] = { + cutsceneWaitStart, cutsceneWaitUpdate + }, + [CUTSCENE_ITEM_TYPE_CUTSCENE] = { + cutsceneCutsceneStart, cutsceneCutsceneUpdate + }, + [CUTSCENE_ITEM_TYPE_SET_PAUSE] = { + cutsceneSetPauseStart, cutsceneSetPauseUpdate + }, + [CUTSCENE_ITEM_TYPE_CONCURRENT] = { + cutsceneConcurrentStart, cutsceneConcurrentUpdate + }, + [CUTSCENE_ITEM_TYPE_ENTITY_TELEPORT] = { + cutsceneEntityTeleportStart, cutsceneEntityTeleportUpdate + }, + [CUTSCENE_ITEM_TYPE_ENTITY_WALK_TO] = { + cutsceneEntityWalkToStart, cutsceneEntityWalkToUpdate + }, + [CUTSCENE_ITEM_TYPE_ENTITY_WALK_TO_ENTITY] = { + cutsceneEntityWalkToEntityStart, cutsceneEntityWalkToEntityUpdate + }, + [CUTSCENE_ITEM_TYPE_ENTITY_TURN] = { + cutsceneEntityTurnStart, cutsceneEntityTurnUpdate + }, + [CUTSCENE_ITEM_TYPE_ENTITY_ADD] = { + cutsceneEntityAddStart, cutsceneEntityAddUpdate + }, + [CUTSCENE_ITEM_TYPE_ENTITY_REMOVE] = { + cutsceneEntityRemoveStart, cutsceneEntityRemoveUpdate + }, + [CUTSCENE_ITEM_TYPE_ENTITY_ITEM_PICKUP] = { + cutsceneEntityItemPickupStart, cutsceneEntityItemPickupUpdate + } +}; + +void cutsceneItemStart( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +) { + assertTrue(item->type < CUTSCENE_ITEM_TYPE_COUNT, "Invalid cutscene item"); + cutsceneitemstartcallback_t start = CUTSCENE_ITEM_CALLBACKS[item->type].start; + if(start != NULL) start(item, data); +} + +bool_t cutsceneItemUpdate( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +) { + assertTrue(item->type < CUTSCENE_ITEM_TYPE_COUNT, "Invalid cutscene item"); + cutsceneitemupdatecallback_t update = + CUTSCENE_ITEM_CALLBACKS[item->type].update; + if(update == NULL) return true; + return update(item, data); +} + +void cutsceneCutsceneStart( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +) { + cutsceneSystemStartCutscene(item->cutscene); +} + +bool_t cutsceneCutsceneUpdate( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +) { + return true; +} diff --git a/src/duskrpg/cutscene/item/cutsceneitem.h b/src/duskrpg/cutscene/item/cutsceneitem.h new file mode 100644 index 00000000..5e987e96 --- /dev/null +++ b/src/duskrpg/cutscene/item/cutsceneitem.h @@ -0,0 +1,134 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "dusk.h" +#include "cutscene/item/cutscenecallback.h" +#include "cutscene/item/control/cutscenewait.h" +#include "cutscene/item/control/cutscenesetpause.h" +#include "cutscene/item/control/cutsceneconcurrent.h" +#include "cutscene/item/entity/cutsceneentityteleport.h" +#include "cutscene/item/entity/cutsceneentitywalkto.h" +#include "cutscene/item/entity/cutsceneentitywalktoentity.h" +#include "cutscene/item/entity/cutsceneentityturn.h" +#include "cutscene/item/entity/cutsceneentityadd.h" +#include "cutscene/item/entity/cutsceneentityremove.h" +#include "cutscene/item/entity/cutsceneentityitempickup.h" +#include "cutscene/item/ui/cutscenetext.h" + +typedef struct cutscene_s cutscene_t; + +typedef enum { + CUTSCENE_ITEM_TYPE_NULL, + CUTSCENE_ITEM_TYPE_TEXT, + CUTSCENE_ITEM_TYPE_CALLBACK, + CUTSCENE_ITEM_TYPE_WAIT, + CUTSCENE_ITEM_TYPE_CUTSCENE, + CUTSCENE_ITEM_TYPE_SET_PAUSE, + CUTSCENE_ITEM_TYPE_CONCURRENT, + CUTSCENE_ITEM_TYPE_ENTITY_TELEPORT, + CUTSCENE_ITEM_TYPE_ENTITY_WALK_TO, + CUTSCENE_ITEM_TYPE_ENTITY_WALK_TO_ENTITY, + CUTSCENE_ITEM_TYPE_ENTITY_TURN, + CUTSCENE_ITEM_TYPE_ENTITY_ADD, + CUTSCENE_ITEM_TYPE_ENTITY_REMOVE, + CUTSCENE_ITEM_TYPE_ENTITY_ITEM_PICKUP, + CUTSCENE_ITEM_TYPE_COUNT +} cutsceneitemtype_t; + +typedef struct cutsceneitem_s { + cutsceneitemtype_t type; + union { + cutscenetext_t text; + cutscenecallback_t callback; + cutscenewait_t wait; + const cutscene_t *cutscene; + cutscenepause_t setPause; + cutsceneconcurrent_t concurrent; + cutsceneentityteleport_t entityTeleport; + cutsceneentitywalkto_t entityWalkTo; + cutsceneentitywalktoentity_t entityWalkToEntity; + cutsceneentityturn_t entityTurn; + cutsceneentityadd_t entityAdd; + cutsceneentityremove_t entityRemove; + cutsceneentityitempickup_t entityItemPickup; + }; +} cutsceneitem_t; + +typedef union cutsceneitemdata_u { + cutscenewaitdata_t wait; + cutsceneentitywalktodata_t entityWalkTo; + cutsceneconcurrentdata_t concurrent; +} cutsceneitemdata_t; + +typedef void (*cutsceneitemstartcallback_t)( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +); + +typedef bool_t (*cutsceneitemupdatecallback_t)( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +); + +typedef struct { + cutsceneitemstartcallback_t start; + cutsceneitemupdatecallback_t update; +} cutsceneitemcallbacks_t; + +extern const cutsceneitemcallbacks_t + CUTSCENE_ITEM_CALLBACKS[CUTSCENE_ITEM_TYPE_COUNT]; + +/** + * Starts a cutscene item by dispatching to its type's start callback. + * + * @param item The cutscene item. + * @param data Runtime data storage for the item. + */ +void cutsceneItemStart( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +); + +/** + * Updates a cutscene item by dispatching to its type's update callback. + * + * @param item The cutscene item. + * @param data Runtime data storage for the item. + * @returns true once the item has completed. + */ +bool_t cutsceneItemUpdate( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +); + +/** + * Starts a nested-cutscene item -- hands control of CUTSCENE_SYSTEM over to + * item->cutscene immediately (see cutsceneSystemStartCutscene()). This is a + * one-way jump, not a call/return: the outer cutscene's remaining items are + * abandoned, matching CUTSCENE_SYSTEM's single active-scene design. + * + * @param item The cutscene item. + * @param data Runtime data storage (unused). + */ +void cutsceneCutsceneStart( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +); + +/** + * Updates a nested-cutscene item (always completes immediately -- control + * has already moved to the nested scene by the time this would run). + * + * @param item The cutscene item. + * @param data Runtime data storage (unused). + * @returns true always. + */ +bool_t cutsceneCutsceneUpdate( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +); diff --git a/src/duskrpg/cutscene/item/entity/CMakeLists.txt b/src/duskrpg/cutscene/item/entity/CMakeLists.txt new file mode 100644 index 00000000..6ee66018 --- /dev/null +++ b/src/duskrpg/cutscene/item/entity/CMakeLists.txt @@ -0,0 +1,15 @@ +# 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 + cutsceneentityadd.c + cutsceneentityitempickup.c + cutsceneentityremove.c + cutsceneentityteleport.c + cutsceneentityturn.c + cutsceneentitywalkto.c + cutsceneentitywalktoentity.c +) diff --git a/src/duskrpg/cutscene/item/entity/cutsceneentityadd.c b/src/duskrpg/cutscene/item/entity/cutsceneentityadd.c new file mode 100644 index 00000000..7a3916bf --- /dev/null +++ b/src/duskrpg/cutscene/item/entity/cutsceneentityadd.c @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "cutscene/item/cutsceneitem.h" +#include "cutscene/cutscenesystem.h" +#include "entity/entitymanager.h" +#include "entity/entityprefab.h" +#include "entity/component/display/entityposition.h" + +void cutsceneEntityAddStart( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +) { + entityid_t entityId = entityManagerAdd(CUTSCENE_SYSTEM.mgr); + + errorret_t ret = entityPrefabResolveAndApply( + CUTSCENE_SYSTEM.mgr, entityId, item->entityAdd.prefabName + ); + if(errorIsNotOk(ret)) errorCatch(ret); + + componentid_t posComp = entityGetComponent( + CUTSCENE_SYSTEM.mgr, entityId, COMPONENT_TYPE_POSITION + ); + if(posComp != COMPONENT_ID_INVALID) { + vec3 position = { + item->entityAdd.position[0], + item->entityAdd.position[1], + item->entityAdd.position[2] + }; + entityPositionSetLocalPosition( + CUTSCENE_SYSTEM.mgr, entityId, posComp, position + ); + } + + CUTSCENE_SYSTEM.entityLastCreated = entityId; + CUTSCENE_SYSTEM.entityLastRef = entityId; +} + +bool_t cutsceneEntityAddUpdate( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +) { + return true; +} diff --git a/src/duskrpg/cutscene/item/entity/cutsceneentityadd.h b/src/duskrpg/cutscene/item/entity/cutsceneentityadd.h new file mode 100644 index 00000000..af3190c6 --- /dev/null +++ b/src/duskrpg/cutscene/item/entity/cutsceneentityadd.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 "dusk.h" + +typedef struct cutsceneitem_s cutsceneitem_t; +typedef union cutsceneitemdata_u cutsceneitemdata_t; + +typedef struct { + /** Name of the entity prefab to apply; see entityPrefabResolveAndApply(). */ + const char_t *prefabName; + vec3 position; +} cutsceneentityadd_t; + +/** + * Starts an entity add item: spawns a new entity, applies prefabName to + * it (see entityPrefabResolveAndApply()), and positions it if it ends up + * with a POSITION component. Sets CUTSCENE_SYSTEM.entityLastCreated (and + * entityLastRef) to the new entity. If prefabName fails to resolve, the + * error is logged (see errorCatch()) and the entity is left as-is -- + * still created, just without whatever the prefab would have added. + * + * @param item The cutscene item. + * @param data Runtime data storage. + */ +void cutsceneEntityAddStart( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +); + +/** + * Updates an entity add item (always completes immediately). + * + * @param item The cutscene item. + * @param data Runtime data storage. + * @returns true always. + */ +bool_t cutsceneEntityAddUpdate( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +); diff --git a/src/duskrpg/cutscene/item/entity/cutsceneentityitempickup.c b/src/duskrpg/cutscene/item/entity/cutsceneentityitempickup.c new file mode 100644 index 00000000..aa75c5a0 --- /dev/null +++ b/src/duskrpg/cutscene/item/entity/cutsceneentityitempickup.c @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "cutscene/item/cutsceneitem.h" +#include "cutscene/cutscenesystem.h" +#include "entity/entitymanager.h" +#include "entity/component/overworld/entityinteractable.h" +#include "ui/textbox/uitextboxmain.h" + +void cutsceneEntityItemPickupStart( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +) { + entityid_t entityId = cutsceneSystemGetEntity( + item->entityItemPickup.entityIndex + ); + const char_t *message = item->entityItemPickup.message[0] != '\0' + ? item->entityItemPickup.message + : ENTITY_INTERACTABLE_ITEM_PICKUP_MESSAGE_DEFAULT; + uiTextboxMainSetText(message); + entityDisposeDeep(CUTSCENE_SYSTEM.mgr, entityId); +} + +bool_t cutsceneEntityItemPickupUpdate( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +) { + return !uiTextboxMainIsActive(); +} diff --git a/src/duskrpg/cutscene/item/entity/cutsceneentityitempickup.h b/src/duskrpg/cutscene/item/entity/cutsceneentityitempickup.h new file mode 100644 index 00000000..81a65a83 --- /dev/null +++ b/src/duskrpg/cutscene/item/entity/cutsceneentityitempickup.h @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "dusk.h" + +typedef struct cutsceneitem_s cutsceneitem_t; +typedef union cutsceneitemdata_u cutsceneitemdata_t; + +#define CUTSCENE_ENTITY_ITEM_PICKUP_MESSAGE_MAX 128 + +typedef struct { + uint8_t entityIndex; + + /** Shown in UI_TEXTBOX_MAIN; empty means use + * ENTITY_INTERACTABLE_ITEM_PICKUP_MESSAGE_DEFAULT. */ + char_t message[CUTSCENE_ENTITY_ITEM_PICKUP_MESSAGE_MAX]; +} cutsceneentityitempickup_t; + +/** + * Starts an entity item-pickup item: shows message (or the default + * pickup message if empty) in UI_TEXTBOX_MAIN and removes the entity + * immediately (see entityDisposeDeep()), mirroring + * ENTITY_INTERACTABLE_TYPE_ITEM_PICKUP's own interaction behavior for + * use within an authored cutscene. + * + * @param item The cutscene item. + * @param data Runtime data storage. + */ +void cutsceneEntityItemPickupStart( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +); + +/** + * Updates an entity item-pickup item -- waits until UI_TEXTBOX_MAIN is + * dismissed. + * + * @param item The cutscene item. + * @param data Runtime data storage. + * @returns true once the textbox is no longer active. + */ +bool_t cutsceneEntityItemPickupUpdate( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +); diff --git a/src/duskrpg/cutscene/item/entity/cutsceneentityremove.c b/src/duskrpg/cutscene/item/entity/cutsceneentityremove.c new file mode 100644 index 00000000..18f2a58d --- /dev/null +++ b/src/duskrpg/cutscene/item/entity/cutsceneentityremove.c @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "cutscene/item/cutsceneitem.h" +#include "cutscene/cutscenesystem.h" +#include "entity/entitymanager.h" + +void cutsceneEntityRemoveStart( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +) { + entityid_t entityId = cutsceneSystemGetEntity( + item->entityRemove.entityIndex + ); + entityDisposeDeep(CUTSCENE_SYSTEM.mgr, entityId); +} + +bool_t cutsceneEntityRemoveUpdate( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +) { + return true; +} diff --git a/src/duskrpg/cutscene/item/entity/cutsceneentityremove.h b/src/duskrpg/cutscene/item/entity/cutsceneentityremove.h new file mode 100644 index 00000000..f99079da --- /dev/null +++ b/src/duskrpg/cutscene/item/entity/cutsceneentityremove.h @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "dusk.h" + +typedef struct cutsceneitem_s cutsceneitem_t; +typedef union cutsceneitemdata_u cutsceneitemdata_t; + +typedef struct { + uint8_t entityIndex; +} cutsceneentityremove_t; + +/** + * Starts an entity remove item (removes the entity from the world + * immediately, via entityDisposeDeep()). + * + * @param item The cutscene item. + * @param data Runtime data storage. + */ +void cutsceneEntityRemoveStart( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +); + +/** + * Updates an entity remove item (always completes immediately). + * + * @param item The cutscene item. + * @param data Runtime data storage. + * @returns true always. + */ +bool_t cutsceneEntityRemoveUpdate( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +); diff --git a/src/duskrpg/cutscene/item/entity/cutsceneentityteleport.c b/src/duskrpg/cutscene/item/entity/cutsceneentityteleport.c new file mode 100644 index 00000000..48152cc0 --- /dev/null +++ b/src/duskrpg/cutscene/item/entity/cutsceneentityteleport.c @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "cutscene/item/cutsceneitem.h" +#include "cutscene/cutscenesystem.h" +#include "entity/entitymanager.h" +#include "entity/component/display/entityposition.h" + +void cutsceneEntityTeleportStart( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +) { + entityid_t entityId = cutsceneSystemGetEntity( + item->entityTeleport.entityIndex + ); + componentid_t posComp = entityGetComponent( + CUTSCENE_SYSTEM.mgr, entityId, COMPONENT_TYPE_POSITION + ); + if(posComp == COMPONENT_ID_INVALID) return; + + vec3 target = { + item->entityTeleport.target[0], + item->entityTeleport.target[1], + item->entityTeleport.target[2] + }; + entityPositionSetLocalPosition( + CUTSCENE_SYSTEM.mgr, entityId, posComp, target + ); +} + +bool_t cutsceneEntityTeleportUpdate( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +) { + return true; +} diff --git a/src/duskrpg/cutscene/item/entity/cutsceneentityteleport.h b/src/duskrpg/cutscene/item/entity/cutsceneentityteleport.h new file mode 100644 index 00000000..bf441648 --- /dev/null +++ b/src/duskrpg/cutscene/item/entity/cutsceneentityteleport.h @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "dusk.h" + +typedef struct cutsceneitem_s cutsceneitem_t; +typedef union cutsceneitemdata_u cutsceneitemdata_t; + +typedef struct { + uint8_t entityIndex; + vec3 target; +} cutsceneentityteleport_t; + +/** + * Starts an entity teleport item (teleports the entity immediately, via + * entityPositionSetLocalPosition()). No-op if the entity has no POSITION + * component. + * + * @param item The cutscene item. + * @param data Runtime data storage. + */ +void cutsceneEntityTeleportStart( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +); + +/** + * Updates an entity teleport item (always completes immediately). + * + * @param item The cutscene item. + * @param data Runtime data storage. + * @returns true always. + */ +bool_t cutsceneEntityTeleportUpdate( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +); diff --git a/src/duskrpg/cutscene/item/entity/cutsceneentityturn.c b/src/duskrpg/cutscene/item/entity/cutsceneentityturn.c new file mode 100644 index 00000000..1915e4cf --- /dev/null +++ b/src/duskrpg/cutscene/item/entity/cutsceneentityturn.c @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "cutscene/item/cutsceneitem.h" +#include "cutscene/cutscenesystem.h" +#include "entity/entitymanager.h" +#include "entity/component/display/entityposition.h" + +void cutsceneEntityTurnStart( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +) { + entityid_t entityId = cutsceneSystemGetEntity(item->entityTurn.entityIndex); + componentid_t posComp = entityGetComponent( + CUTSCENE_SYSTEM.mgr, entityId, COMPONENT_TYPE_POSITION + ); + if(posComp == COMPONENT_ID_INVALID) return; + + vec3 rotation; + entityPositionGetLocalRotation( + CUTSCENE_SYSTEM.mgr, entityId, posComp, rotation + ); + rotation[1] = item->entityTurn.yaw; + entityPositionSetLocalRotation( + CUTSCENE_SYSTEM.mgr, entityId, posComp, rotation + ); +} + +bool_t cutsceneEntityTurnUpdate( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +) { + return true; +} diff --git a/src/duskrpg/cutscene/item/entity/cutsceneentityturn.h b/src/duskrpg/cutscene/item/entity/cutsceneentityturn.h new file mode 100644 index 00000000..bce3524f --- /dev/null +++ b/src/duskrpg/cutscene/item/entity/cutsceneentityturn.h @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "dusk.h" + +typedef struct cutsceneitem_s cutsceneitem_t; +typedef union cutsceneitemdata_u cutsceneitemdata_t; + +typedef struct { + uint8_t entityIndex; + + /** Target yaw, in radians. See entityPositionSetLocalRotation(). */ + float_t yaw; +} cutsceneentityturn_t; + +/** + * Starts an entity turn item: sets the entity's local yaw immediately. + * No-op if the entity has no POSITION component. Unlike the original + * tile-based version, there's no per-entity action/animation state to + * wait on here, so this snaps instantly rather than deferring to Update. + * + * @param item The cutscene item. + * @param data Runtime data storage. + */ +void cutsceneEntityTurnStart( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +); + +/** + * Updates an entity turn item (always completes immediately). + * + * @param item The cutscene item. + * @param data Runtime data storage. + * @returns true always. + */ +bool_t cutsceneEntityTurnUpdate( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +); diff --git a/src/duskrpg/cutscene/item/entity/cutsceneentitywalkto.c b/src/duskrpg/cutscene/item/entity/cutsceneentitywalkto.c new file mode 100644 index 00000000..049305d8 --- /dev/null +++ b/src/duskrpg/cutscene/item/entity/cutsceneentitywalkto.c @@ -0,0 +1,94 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "cutscene/item/cutsceneitem.h" +#include "cutscene/cutscenesystem.h" +#include "entity/entitymanager.h" +#include "entity/component/display/entityposition.h" +#include "time/time.h" +#include "util/math.h" + +void cutsceneEntityWalkToStart( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +) { + data->entityWalkTo.currentIndex = 0; +} + +bool_t cutsceneEntityWalkToUpdate( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +) { + uint8_t i = data->entityWalkTo.currentIndex; + entityid_t entityId = cutsceneSystemGetEntity( + item->entityWalkTo.entityIndex + ); + float_t speed = item->entityWalkTo.speed > 0.0f + ? item->entityWalkTo.speed : CUTSCENE_ENTITY_WALK_SPEED_DEFAULT; + + vec3 target = { + item->entityWalkTo.positions[i][0], + item->entityWalkTo.positions[i][1], + item->entityWalkTo.positions[i][2] + }; + if(!cutsceneEntityStepToward(CUTSCENE_SYSTEM.mgr, entityId, target, speed)) + return false; + + i++; + if(i < item->entityWalkTo.count) { + data->entityWalkTo.currentIndex = i; + return false; + } + return true; +} + +bool_t cutsceneEntityStepToward( + entitymanager_t *mgr, + const entityid_t entityId, + vec3 target, + const float_t speed +) { + componentid_t posComp = entityGetComponent( + mgr, entityId, COMPONENT_TYPE_POSITION + ); + if(posComp == COMPONENT_ID_INVALID) return true; + + vec3 pos; + entityPositionGetLocalPosition(mgr, entityId, posComp, pos); + + vec3 delta = { + target[0] - pos[0], target[1] - pos[1], target[2] - pos[2] + }; + float_t distSq = delta[0] * delta[0] + delta[1] * delta[1] + + delta[2] * delta[2]; + if(distSq <= CUTSCENE_ENTITY_WALK_ARRIVE_THRESHOLD_SQ) { + entityPositionSetLocalPosition(mgr, entityId, posComp, target); + return true; + } + + float_t dist = sqrtf(distSq); + float_t step = speed * TIME.delta; + float_t t = step >= dist ? 1.0f : step / dist; + + vec3 newPos = { + pos[0] + delta[0] * t, + pos[1] + delta[1] * t, + pos[2] + delta[2] * t + }; + entityPositionSetLocalPosition(mgr, entityId, posComp, newPos); + + // Face the direction of travel: entityPlayerUpdate's convention is + // front = +local Z, so yaw = atan2(dx, dz) on the XZ ground plane. + if(mathAbs(delta[0]) > 0.0001f || mathAbs(delta[2]) > 0.0001f) { + vec3 rotation; + entityPositionGetLocalRotation(mgr, entityId, posComp, rotation); + rotation[1] = atan2f(delta[0], delta[2]); + entityPositionSetLocalRotation(mgr, entityId, posComp, rotation); + } + + return false; +} diff --git a/src/duskrpg/cutscene/item/entity/cutsceneentitywalkto.h b/src/duskrpg/cutscene/item/entity/cutsceneentitywalkto.h new file mode 100644 index 00000000..96910bcf --- /dev/null +++ b/src/duskrpg/cutscene/item/entity/cutsceneentitywalkto.h @@ -0,0 +1,73 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "entity/entitybase.h" + +typedef struct cutsceneitem_s cutsceneitem_t; +typedef union cutsceneitemdata_u cutsceneitemdata_t; + +/** Default speed for CUTSCENE_ENTITY_WALK_TO/_PATH, in units/second. */ +#define CUTSCENE_ENTITY_WALK_SPEED_DEFAULT 4.0f + +/** Below this squared distance, an entity is considered to have arrived. */ +#define CUTSCENE_ENTITY_WALK_ARRIVE_THRESHOLD_SQ 0.0001f + +typedef struct { + uint8_t entityIndex; + const vec3 *positions; + uint8_t count; + float_t speed; +} cutsceneentitywalkto_t; + +typedef struct { + uint8_t currentIndex; +} cutsceneentitywalktodata_t; + +/** + * Starts an entity walk-to item (resets the waypoint index). + * + * @param item The cutscene item. + * @param data Runtime data storage. + */ +void cutsceneEntityWalkToStart( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +); + +/** + * Updates an entity walk-to item (steps the entity toward the next + * waypoint). + * + * @param item The cutscene item. + * @param data Runtime data storage. + * @returns true once all waypoints have been reached. + */ +bool_t cutsceneEntityWalkToUpdate( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +); + +/** + * Steps an entity's local position directly toward target at speed + * units/second (straight-line, no pathfinding/obstacle avoidance), and + * faces it in the direction of travel. No-op (returns true immediately) + * if the entity has no POSITION component. + * + * @param mgr The entity manager that owns the entity. + * @param entityId The entity to move. + * @param target The local-space destination. + * @param speed Movement speed, in units/second. + * @returns true once the entity is within + * CUTSCENE_ENTITY_WALK_ARRIVE_THRESHOLD_SQ of target. + */ +bool_t cutsceneEntityStepToward( + entitymanager_t *mgr, + const entityid_t entityId, + vec3 target, + const float_t speed +); diff --git a/src/duskrpg/cutscene/item/entity/cutsceneentitywalktoentity.c b/src/duskrpg/cutscene/item/entity/cutsceneentitywalktoentity.c new file mode 100644 index 00000000..a7eaadd3 --- /dev/null +++ b/src/duskrpg/cutscene/item/entity/cutsceneentitywalktoentity.c @@ -0,0 +1,51 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "cutscene/item/cutsceneitem.h" +#include "cutscene/cutscenesystem.h" +#include "cutscene/item/entity/cutsceneentitywalkto.h" +#include "entity/entitymanager.h" +#include "entity/component/display/entityposition.h" + +void cutsceneEntityWalkToEntityStart( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +) { +} + +bool_t cutsceneEntityWalkToEntityUpdate( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +) { + entityid_t entityId = cutsceneSystemGetEntity( + item->entityWalkToEntity.entityIndex + ); + entityid_t targetId = cutsceneSystemGetEntity( + item->entityWalkToEntity.targetEntityIndex + ); + + componentid_t targetPosComp = entityGetComponent( + CUTSCENE_SYSTEM.mgr, targetId, COMPONENT_TYPE_POSITION + ); + if(targetPosComp == COMPONENT_ID_INVALID) return true; + + vec3 targetPos; + entityPositionGetWorldPosition( + CUTSCENE_SYSTEM.mgr, targetId, targetPosComp, targetPos + ); + + vec3 dest = { + targetPos[0] + item->entityWalkToEntity.offsetX, + targetPos[1], + targetPos[2] + item->entityWalkToEntity.offsetZ + }; + + float_t speed = item->entityWalkToEntity.speed > 0.0f + ? item->entityWalkToEntity.speed : CUTSCENE_ENTITY_WALK_SPEED_DEFAULT; + + return cutsceneEntityStepToward(CUTSCENE_SYSTEM.mgr, entityId, dest, speed); +} diff --git a/src/duskrpg/cutscene/item/entity/cutsceneentitywalktoentity.h b/src/duskrpg/cutscene/item/entity/cutsceneentitywalktoentity.h new file mode 100644 index 00000000..d68a84ce --- /dev/null +++ b/src/duskrpg/cutscene/item/entity/cutsceneentitywalktoentity.h @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "dusk.h" + +typedef struct cutsceneitem_s cutsceneitem_t; +typedef union cutsceneitemdata_u cutsceneitemdata_t; + +typedef struct { + uint8_t entityIndex; + uint8_t targetEntityIndex; + + /** Offset on the XZ ground plane from the target's current position. */ + float_t offsetX; + float_t offsetZ; + + float_t speed; +} cutsceneentitywalktoentity_t; + +/** + * Starts an entity walk-to-entity item. No setup is needed -- the + * destination is recomputed from the target's live position every + * Update. + * + * @param item The cutscene item. + * @param data Runtime data storage. + */ +void cutsceneEntityWalkToEntityStart( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +); + +/** + * Updates an entity walk-to-entity item: re-reads the target entity's + * current world position each frame, applies the XZ offset, and steps + * the entity toward it (see cutsceneEntityStepToward()). Unlike the + * original tile-based version there's no terrain to resolve a Y from -- + * the destination Y is taken directly from the target's own position. + * + * @param item The cutscene item. + * @param data Runtime data storage. + * @returns true once the entity has reached the target's side. + */ +bool_t cutsceneEntityWalkToEntityUpdate( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +); diff --git a/src/duskrpg/cutscene/item/ui/CMakeLists.txt b/src/duskrpg/cutscene/item/ui/CMakeLists.txt new file mode 100644 index 00000000..d69c13a2 --- /dev/null +++ b/src/duskrpg/cutscene/item/ui/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 + cutscenetext.c +) diff --git a/src/duskrpg/cutscene/item/ui/cutscenetext.c b/src/duskrpg/cutscene/item/ui/cutscenetext.c new file mode 100644 index 00000000..219ecbe6 --- /dev/null +++ b/src/duskrpg/cutscene/item/ui/cutscenetext.c @@ -0,0 +1,23 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "cutscene/item/cutsceneitem.h" +#include "ui/textbox/uitextboxmain.h" + +void cutsceneTextStart( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +) { + uiTextboxMainSetText(item->text.text); +} + +bool_t cutsceneTextUpdate( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +) { + return !uiTextboxMainIsActive(); +} diff --git a/src/duskrpg/cutscene/item/ui/cutscenetext.h b/src/duskrpg/cutscene/item/ui/cutscenetext.h new file mode 100644 index 00000000..b90334eb --- /dev/null +++ b/src/duskrpg/cutscene/item/ui/cutscenetext.h @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "dusk.h" + +typedef struct cutsceneitem_s cutsceneitem_t; +typedef union cutsceneitemdata_u cutsceneitemdata_t; + +#define CUTSCENE_TEXT_MAX_CHARS 256 + +typedef struct { + char_t text[CUTSCENE_TEXT_MAX_CHARS]; +} cutscenetext_t; + +/** + * Starts a text item -- shows the given text in UI_TEXTBOX_MAIN. + * + * @param item The cutscene item. + * @param data Runtime data storage. + */ +void cutsceneTextStart( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +); + +/** + * Updates a text item -- waits until UI_TEXTBOX_MAIN is dismissed. + * + * @param item The cutscene item. + * @param data Runtime data storage. + * @returns true once the textbox is no longer active. + */ +bool_t cutsceneTextUpdate( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +); diff --git a/src/duskrpg/entity/component/overworld/CMakeLists.txt b/src/duskrpg/entity/component/overworld/CMakeLists.txt index 5b926b93..edc8f61b 100644 --- a/src/duskrpg/entity/component/overworld/CMakeLists.txt +++ b/src/duskrpg/entity/component/overworld/CMakeLists.txt @@ -6,4 +6,5 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME} PUBLIC entityplayer.c + entityinteractable.c ) diff --git a/src/duskrpg/entity/component/overworld/entityinteractable.c b/src/duskrpg/entity/component/overworld/entityinteractable.c new file mode 100644 index 00000000..188b084b --- /dev/null +++ b/src/duskrpg/entity/component/overworld/entityinteractable.c @@ -0,0 +1,246 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "entityinteractable.h" +#include "entity/entitymanager.h" +#include "console/console.h" +#include "cutscene/cutscenesystem.h" +#include "ui/textbox/uitextboxmain.h" +#include "util/memory.h" +#include "util/string.h" +#include "assert/assert.h" + +void entityInteractableInit( + entitymanager_t *mgr, + const entityid_t entityId, + const componentid_t componentId +) { + entityinteractable_t *interactable = entityInteractableGet( + mgr, entityId, componentId + ); + memoryZero(interactable, sizeof(entityinteractable_t)); + interactable->enabled = true; +} + +entityinteractable_t *entityInteractableGet( + entitymanager_t *mgr, + const entityid_t entityId, + const componentid_t componentId +) { + return componentGetData( + mgr, entityId, componentId, COMPONENT_TYPE_INTERACTABLE + ); +} + +void entityInteractableSetEnabled( + entitymanager_t *mgr, + const entityid_t entityId, + const componentid_t componentId, + const bool_t enabled +) { + entityinteractable_t *interactable = entityInteractableGet( + mgr, entityId, componentId + ); + interactable->enabled = enabled; +} + +bool_t entityInteractableIsEnabled( + entitymanager_t *mgr, + const entityid_t entityId, + const componentid_t componentId +) { + entityinteractable_t *interactable = entityInteractableGet( + mgr, entityId, componentId + ); + return interactable->enabled; +} + +void entityInteractableSetItemPickup( + entitymanager_t *mgr, + const entityid_t entityId, + const componentid_t componentId, + const itempickupid_t pickupId, + const char_t *message +) { + entityinteractable_t *interactable = entityInteractableGet( + mgr, entityId, componentId + ); + memoryZero(&interactable->data, sizeof(entityinteractabledata_t)); + interactable->type = ENTITY_INTERACTABLE_TYPE_ITEM_PICKUP; + interactable->data.itemPickup.pickupId = pickupId; + stringCopy( + interactable->data.itemPickup.message, message ? message : "", + sizeof(interactable->data.itemPickup.message) + ); +} + +void entityInteractableSetCutscene( + entitymanager_t *mgr, + const entityid_t entityId, + const componentid_t componentId, + const cutscene_t *cutscene +) { + entityinteractable_t *interactable = entityInteractableGet( + mgr, entityId, componentId + ); + memoryZero(&interactable->data, sizeof(entityinteractabledata_t)); + interactable->type = ENTITY_INTERACTABLE_TYPE_CUTSCENE; + interactable->data.cutscene.cutscene = cutscene; +} + +void entityInteractableSetFunction( + entitymanager_t *mgr, + const entityid_t entityId, + const componentid_t componentId, + const entityinteractablefunctioncallback_t callback, + void *user +) { + entityinteractable_t *interactable = entityInteractableGet( + mgr, entityId, componentId + ); + memoryZero(&interactable->data, sizeof(entityinteractabledata_t)); + interactable->type = ENTITY_INTERACTABLE_TYPE_FUNCTION; + interactable->data.function.callback = callback; + interactable->data.function.user = user; +} + +errorret_t entityInteractableTryInteract( + entitymanager_t *mgr, + const entityid_t entityId, + const componentid_t componentId, + const entityid_t interactorEntityId +) { + entityinteractable_t *interactable = entityInteractableGet( + mgr, entityId, componentId + ); + if(!interactable->enabled) errorOk(); + + switch(interactable->type) { + case ENTITY_INTERACTABLE_TYPE_ITEM_PICKUP: { + consolePrint( + "Entity %d picked up pickup %u from entity %d ('%s')", + interactorEntityId, interactable->data.itemPickup.pickupId, + entityId, entityGetName(mgr, entityId) + ); + const char_t *message = interactable->data.itemPickup.message[0] != '\0' + ? interactable->data.itemPickup.message + : ENTITY_INTERACTABLE_ITEM_PICKUP_MESSAGE_DEFAULT; + uiTextboxMainSetText(message); + entityDisposeDeep(mgr, entityId); + break; + } + + case ENTITY_INTERACTABLE_TYPE_CUTSCENE: + if(interactable->data.cutscene.cutscene) { + cutsceneSystemStartCutsceneWith( + interactable->data.cutscene.cutscene, interactorEntityId, entityId + ); + } + break; + + case ENTITY_INTERACTABLE_TYPE_FUNCTION: + if(interactable->data.function.callback) { + interactable->data.function.callback( + mgr, interactorEntityId, entityId, interactable->data.function.user + ); + } + break; + + default: + assertUnreachable("Unknown interactable type"); + } + + errorOk(); +} + +errorret_t entityInteractableSerialize( + entitymanager_t *mgr, + const entityid_t entityId, + const componentid_t componentId, + yyjson_mut_doc *doc, + yyjson_mut_val *json +) { + entityinteractable_t *interactable = entityInteractableGet( + mgr, entityId, componentId + ); + yyjson_mut_obj_add_bool(doc, json, "enabled", interactable->enabled); + + const char_t *typeName; + switch(interactable->type) { + case ENTITY_INTERACTABLE_TYPE_ITEM_PICKUP: + typeName = "ITEM_PICKUP"; + break; + case ENTITY_INTERACTABLE_TYPE_CUTSCENE: + typeName = "CUTSCENE"; + break; + case ENTITY_INTERACTABLE_TYPE_FUNCTION: + typeName = "FUNCTION"; + break; + default: + assertUnreachable("Unknown interactable type"); + } + // "kind", not "type" -- the JSON object's own "type" key is already the + // outer component-type discriminator ("INTERACTABLE"); this is this + // interactable's own sub-type. + yyjson_mut_obj_add_str(doc, json, "kind", typeName); + + if(interactable->type == ENTITY_INTERACTABLE_TYPE_ITEM_PICKUP) { + yyjson_mut_obj_add_uint( + doc, json, "pickupId", interactable->data.itemPickup.pickupId + ); + if(interactable->data.itemPickup.message[0] != '\0') { + yyjson_mut_obj_add_str( + doc, json, "message", interactable->data.itemPickup.message + ); + } + } + + errorOk(); +} + +errorret_t entityInteractableDeserialize( + entitymanager_t *mgr, + const entityid_t entityId, + const componentid_t componentId, + yyjson_val *json +) { + yyjson_val *v; + if((v = yyjson_obj_get(json, "enabled"))) { + entityInteractableSetEnabled( + mgr, entityId, componentId, yyjson_get_bool(v) + ); + } + + if((v = yyjson_obj_get(json, "kind"))) { + const char_t *typeName = yyjson_get_str(v); + if(stringEquals(typeName, "ITEM_PICKUP")) { + yyjson_val *pickupIdVal = yyjson_obj_get(json, "pickupId"); + itempickupid_t pickupId = pickupIdVal + ? (itempickupid_t)yyjson_get_uint(pickupIdVal) : 0; + yyjson_val *messageVal = yyjson_obj_get(json, "message"); + const char_t *message = messageVal ? yyjson_get_str(messageVal) : NULL; + entityInteractableSetItemPickup( + mgr, entityId, componentId, pickupId, message + ); + } else if(stringEquals(typeName, "CUTSCENE")) { + errorThrow( + "Cannot deserialize a CUTSCENE interactable -- its cutscene_t " + "pointer is runtime-only and must be set via " + "entityInteractableSetCutscene()" + ); + } else if(stringEquals(typeName, "FUNCTION")) { + errorThrow( + "Cannot deserialize a FUNCTION interactable -- its callback is " + "runtime-only and must be set via entityInteractableSetFunction()" + ); + } else { + errorThrow("Unknown interactable type '%s'", typeName); + } + } + + errorOk(); +} diff --git a/src/duskrpg/entity/component/overworld/entityinteractable.h b/src/duskrpg/entity/component/overworld/entityinteractable.h new file mode 100644 index 00000000..c212edc7 --- /dev/null +++ b/src/duskrpg/entity/component/overworld/entityinteractable.h @@ -0,0 +1,275 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "entity/entitybase.h" +#include "error/error.h" +#include "yyjson.h" + +typedef struct cutscene_s cutscene_t; + +/** Persistently tracked pickup identifier; see + * entityInteractableSetItemPickup(). */ +typedef uint32_t itempickupid_t; + +/** Not persistently tracked -- always collectible, once per in-game day. */ +#define ITEM_PICKUP_ID_ONCE_PER_DAY 0xFFFFFFFF +/** Not persistently tracked -- always collectible, once per map load. */ +#define ITEM_PICKUP_ID_ONCE_PER_MAP 0xFFFFFFFE + +/** Maximum length of entityinteractableitempickup_t.message. */ +#define ENTITY_INTERACTABLE_ITEM_PICKUP_MESSAGE_MAX 128 + +/** Shown in UI_TEXTBOX_MAIN when an ITEM_PICKUP's own message is empty. */ +#define ENTITY_INTERACTABLE_ITEM_PICKUP_MESSAGE_DEFAULT \ + "You picked up an item!" + +typedef enum { + ENTITY_INTERACTABLE_TYPE_ITEM_PICKUP, + ENTITY_INTERACTABLE_TYPE_CUTSCENE, + ENTITY_INTERACTABLE_TYPE_FUNCTION, + + ENTITY_INTERACTABLE_TYPE_COUNT +} entityinteractabletype_t; + +typedef struct { + /** + * Identifies this pickup for persistent do-not-respawn tracking, or one + * of the ITEM_PICKUP_ID_ONCE_PER_* sentinels for a pickup that's always + * collectible again (on the given cadence) instead of being tracked. + * The actual persistence lookup is not implemented yet -- for now every + * ITEM_PICKUP always succeeds and removes itself regardless of this + * value (see entityInteractableTryInteract()). + */ + itempickupid_t pickupId; + + /** Shown in UI_TEXTBOX_MAIN on a successful interaction; empty means + * use ENTITY_INTERACTABLE_ITEM_PICKUP_MESSAGE_DEFAULT. */ + char_t message[ENTITY_INTERACTABLE_ITEM_PICKUP_MESSAGE_MAX]; +} entityinteractableitempickup_t; + +/** + * Callback fired by an ENTITY_INTERACTABLE_TYPE_FUNCTION interactable. + * + * @param mgr The entity manager that owns both entities. + * @param interactorEntityId The entity ID that initiated the interaction. + * @param entityId The interactable entity's ID. + * @param user The user pointer passed to entityInteractableSetFunction(). + */ +typedef void (*entityinteractablefunctioncallback_t)( + entitymanager_t *mgr, + const entityid_t interactorEntityId, + const entityid_t entityId, + void *user +); + +typedef struct { + entityinteractablefunctioncallback_t callback; + void *user; +} entityinteractablefunction_t; + +typedef struct { + const cutscene_t *cutscene; +} entityinteractablecutscene_t; + +typedef union { + entityinteractableitempickup_t itemPickup; + entityinteractablefunction_t function; + entityinteractablecutscene_t cutscene; +} entityinteractabledata_t; + +typedef struct { + /** False makes entityInteractableTryInteract() a no-op, e.g. an + * already-opened chest or a used-up switch. */ + bool_t enabled; + + entityinteractabletype_t type; + entityinteractabledata_t data; +} entityinteractable_t; + +/** + * Initializes the interactable component: enabled, type + * ENTITY_INTERACTABLE_TYPE_ITEM_PICKUP with pickupId 0 (call one of the + * entityInteractableSet* functions to configure it properly). + * + * @param mgr The entity manager that owns the entity. + * @param entityId The entity ID. + * @param componentId The component ID. + */ +void entityInteractableInit( + entitymanager_t *mgr, + const entityid_t entityId, + const componentid_t componentId +); + +/** + * Gets the underlying interactable structure (temporarily) for the given + * entity. Prefer the dedicated getters/setters where possible. + * + * @param mgr The entity manager that owns the entity. + * @param entityId The entity ID. + * @param componentId The component ID. + * @return The interactable component data for the given entity and + * component ID. + */ +entityinteractable_t *entityInteractableGet( + entitymanager_t *mgr, + const entityid_t entityId, + const componentid_t componentId +); + +/** + * Sets whether this entity can currently be interacted with. + * + * @param mgr The entity manager that owns the entity. + * @param entityId The entity ID. + * @param componentId The component ID. + * @param enabled True if interactable, false to disable. + */ +void entityInteractableSetEnabled( + entitymanager_t *mgr, + const entityid_t entityId, + const componentid_t componentId, + const bool_t enabled +); + +/** + * Checks whether this entity can currently be interacted with. + * + * @param mgr The entity manager that owns the entity. + * @param entityId The entity ID. + * @param componentId The component ID. + * @return True if interactable. + */ +bool_t entityInteractableIsEnabled( + entitymanager_t *mgr, + const entityid_t entityId, + const componentid_t componentId +); + +/** + * Configures this entity as an ENTITY_INTERACTABLE_TYPE_ITEM_PICKUP: a + * successful interaction shows message (or the default pickup message if + * NULL/empty) in UI_TEXTBOX_MAIN and removes the entity (see + * entityDisposeDeep()). Does not yet grant an item to the interactor's + * inventory -- that's a planned follow-up once an item-type field is + * added here. + * + * @param mgr The entity manager that owns the entity. + * @param entityId The entity ID. + * @param componentId The component ID. + * @param pickupId See entityinteractableitempickup_t.pickupId. + * @param message See entityinteractableitempickup_t.message; NULL is + * treated the same as empty. + */ +void entityInteractableSetItemPickup( + entitymanager_t *mgr, + const entityid_t entityId, + const componentid_t componentId, + const itempickupid_t pickupId, + const char_t *message +); + +/** + * Configures this entity as an ENTITY_INTERACTABLE_TYPE_CUTSCENE: a + * successful interaction starts cutscene via + * cutsceneSystemStartCutsceneWith(), passing the interactor and this + * entity through as CUTSCENE_ENTITY_INTERACT/CUTSCENE_ENTITY_INTERACTED. + * + * @param mgr The entity manager that owns the entity. + * @param entityId The entity ID. + * @param componentId The component ID. + * @param cutscene The cutscene to start on interaction. + */ +void entityInteractableSetCutscene( + entitymanager_t *mgr, + const entityid_t entityId, + const componentid_t componentId, + const cutscene_t *cutscene +); + +/** + * Configures this entity as an ENTITY_INTERACTABLE_TYPE_FUNCTION: a + * successful interaction invokes callback. + * + * @param mgr The entity manager that owns the entity. + * @param entityId The entity ID. + * @param componentId The component ID. + * @param callback The function to invoke on interaction. + * @param user Arbitrary pointer forwarded to callback unchanged. + */ +void entityInteractableSetFunction( + entitymanager_t *mgr, + const entityid_t entityId, + const componentid_t componentId, + const entityinteractablefunctioncallback_t callback, + void *user +); + +/** + * Attempts an interaction with this entity, e.g. from a player pressing + * the accept bind while overlapping it. No-op if not enabled. Behavior + * otherwise depends on type: + * - ITEM_PICKUP: shows its message in UI_TEXTBOX_MAIN and removes this + * entity (entityDisposeDeep()). + * - CUTSCENE: starts the configured cutscene. + * - FUNCTION: invokes the configured callback. + * + * @param mgr The entity manager that owns the entity. + * @param entityId The entity ID. + * @param componentId The component ID. + * @param interactorEntityId The entity ID that initiated the interaction. + * @return Error state. + */ +errorret_t entityInteractableTryInteract( + entitymanager_t *mgr, + const entityid_t entityId, + const componentid_t componentId, + const entityid_t interactorEntityId +); + +/** + * Serializes the interactable's "enabled", "kind" + * ("ITEM_PICKUP"/"CUTSCENE"/"FUNCTION" -- named "kind" rather than "type" + * since the JSON object's "type" key is already the outer + * component-type discriminator, "INTERACTABLE"), and (for ITEM_PICKUP + * only) "pickupId" and (if non-empty) "message" into the given JSON + * object. FUNCTION callbacks are runtime-only (a C function pointer) and + * are not serialized. + * + * @param mgr The entity manager that owns the entity. + * @param entityId The entity ID. + * @param componentId The component ID. + * @param doc The mutable JSON document to allocate values from. + * @param json The JSON object to write the component's fields into. + * @return Error state. + */ +errorret_t entityInteractableSerialize( + entitymanager_t *mgr, + const entityid_t entityId, + const componentid_t componentId, + yyjson_mut_doc *doc, + yyjson_mut_val *json +); + +/** + * Reads "enabled", "kind", "pickupId", and "message" (see + * entityInteractableSerialize()) from the given JSON object and applies + * whichever are present, leaving the rest at their current values. + * + * @param mgr The entity manager that owns the entity. + * @param entityId The entity ID. + * @param componentId The component ID. + * @param json The JSON object to read the component's fields from. + * @return Error state. + */ +errorret_t entityInteractableDeserialize( + entitymanager_t *mgr, + const entityid_t entityId, + const componentid_t componentId, + yyjson_val *json +); diff --git a/src/duskrpg/entity/component/overworld/entityplayer.c b/src/duskrpg/entity/component/overworld/entityplayer.c index ce2d6baa..adf785f9 100644 --- a/src/duskrpg/entity/component/overworld/entityplayer.c +++ b/src/duskrpg/entity/component/overworld/entityplayer.c @@ -10,6 +10,9 @@ #include "entity/component/physics/entityphysics.h" #include "entity/component/display/entitycamera.h" #include "entity/component/display/entityposition.h" +#include "entity/component/trigger/entitytrigger.h" +#include "entity/component/overworld/entityinteractable.h" +#include "ui/textbox/uitextboxmain.h" #include "input/input.h" #include "time/time.h" #include "util/memory.h" @@ -36,6 +39,7 @@ void entityPlayerInit( player->moveSpeed = ENTITY_PLAYER_MOVE_SPEED_DEFAULT; player->jumpImpulse = ENTITY_PLAYER_JUMP_IMPULSE_DEFAULT; player->turnSpeed = ENTITY_PLAYER_TURN_SPEED_DEFAULT; + player->canInteract = true; entityUpdateAdd(mgr, entityId, entityPlayerUpdate, componentId, NULL); } @@ -48,18 +52,44 @@ entityplayer_t *entityPlayerGet( return componentGetData(mgr, entityId, componentId, COMPONENT_TYPE_PLAYER); } +void entityPlayerSetCanInteract( + entitymanager_t *mgr, + const entityid_t entityId, + const componentid_t componentId, + const bool_t canInteract +) { + entityplayer_t *player = entityPlayerGet(mgr, entityId, componentId); + player->canInteract = canInteract; +} + void entityPlayerUpdate( entitymanager_t *mgr, const entityid_t entityId, const componentid_t componentId, void *user ) { + entityplayer_t *player = entityPlayerGet(mgr, entityId, componentId); + bool_t textboxOpen = uiTextboxMainIsActive(); + + if(player->canInteract && !textboxOpen && inputPressed(INPUT_BIND_ACCEPT)) { + entityPlayerTryInteract(mgr, entityId); + } + componentid_t physComp = entityGetComponent( mgr, entityId, COMPONENT_TYPE_PHYSICS ); if(physComp == COMPONENT_ID_INVALID) return; - entityplayer_t *player = entityPlayerGet(mgr, entityId, componentId); + // Freeze horizontal movement while UI_TEXTBOX_MAIN has control -- leave + // vertical velocity (gravity/jumping) alone. + if(textboxOpen) { + vec3 velocity; + entityPhysicsGetVelocity(mgr, entityId, physComp, velocity); + velocity[0] = 0.0f; + velocity[2] = 0.0f; + entityPhysicsSetVelocity(mgr, entityId, physComp, velocity); + return; + } vec2 moveInput; inputAxis2D( @@ -126,6 +156,32 @@ void entityPlayerUpdate( entityPositionSetLocalRotation(mgr, entityId, posComp, rotation); } +void entityPlayerTryInteract( + entitymanager_t *mgr, + const entityid_t entityId +) { + componentid_t trigComp = entityGetComponent( + mgr, entityId, COMPONENT_TYPE_TRIGGER + ); + if(trigComp == COMPONENT_ID_INVALID) return; + + uint8_t occupantCount = + entityTriggerGetOccupantCount(mgr, entityId, trigComp); + for(uint8_t i = 0; i < occupantCount; i++) { + entityid_t occupant = entityTriggerGetOccupantEntityId( + mgr, entityId, trigComp, i + ); + componentid_t interactableComp = entityGetComponent( + mgr, occupant, COMPONENT_TYPE_INTERACTABLE + ); + if(interactableComp == COMPONENT_ID_INVALID) continue; + if(!entityInteractableIsEnabled(mgr, occupant, interactableComp)) continue; + + entityInteractableTryInteract(mgr, occupant, interactableComp, entityId); + return; + } +} + errorret_t entityPlayerSerialize( entitymanager_t *mgr, const entityid_t entityId, diff --git a/src/duskrpg/entity/component/overworld/entityplayer.h b/src/duskrpg/entity/component/overworld/entityplayer.h index fa0b12dd..6e106b63 100644 --- a/src/duskrpg/entity/component/overworld/entityplayer.h +++ b/src/duskrpg/entity/component/overworld/entityplayer.h @@ -16,11 +16,15 @@ typedef struct { /** Yaw turn rate, in radians/second, when facing the movement direction. */ float_t turnSpeed; + + /** Runtime gate on entityPlayerUpdate()'s interaction check, e.g. set + * false while a dialogue box or cutscene has control. */ + bool_t canInteract; } entityplayer_t; /** * Initializes the player component: sets default move speed, jump - * impulse, and turn speed. + * impulse, and turn speed. canInteract defaults to true. * * @param mgr The entity manager that owns the entity. * @param entityId The entity ID. @@ -48,6 +52,22 @@ entityplayer_t *entityPlayerGet( const componentid_t componentId ); +/** + * Sets whether the player's interaction check runs on future updates. + * + * @param mgr The entity manager that owns the entity. + * @param entityId The entity ID. + * @param componentId The component ID. + * @param canInteract False to suppress interaction attempts, e.g. while + * a dialogue box or cutscene has control. + */ +void entityPlayerSetCanInteract( + entitymanager_t *mgr, + const entityid_t entityId, + const componentid_t componentId, + const bool_t canInteract +); + /** * Per-tick update for the player component: reads the movement input axes * and drives the entity's physics velocity, relative to the current @@ -61,6 +81,19 @@ entityplayer_t *entityPlayerGet( * movement input -- releasing input leaves the entity facing whichever * way it last moved, rather than snapping to a default orientation. * + * While UI_TEXTBOX_MAIN is active (see uiTextboxMainIsActive()), both + * horizontal movement and interaction attempts are suppressed: horizontal + * velocity is forced to zero every tick (vertical velocity is left alone, + * so gravity/jumping still apply) and the INPUT_BIND_ACCEPT check below + * is skipped so the same press doesn't also advance/dismiss the textbox. + * + * Otherwise, if canInteract and INPUT_BIND_ACCEPT was just pressed, + * checks the entity's own TRIGGER component (if any) for the first + * overlapping entity that has an INTERACTABLE component and attempts an + * interaction with it (see entityInteractableTryInteract()). No-op if + * the entity has no TRIGGER component or nothing interactable currently + * overlaps it. + * * Registered automatically as an update callback by entityPlayerInit. * * @param mgr The entity manager that owns the entity. @@ -75,6 +108,21 @@ void entityPlayerUpdate( void *user ); +/** + * Internal. Checks the entity's own TRIGGER component (if any) for the + * first overlapping entity with an enabled INTERACTABLE component and + * attempts an interaction with it (see entityInteractableTryInteract()). + * No-op if the entity has no TRIGGER component or nothing eligible + * currently overlaps it. + * + * @param mgr The entity manager that owns the entity. + * @param entityId The entity ID attempting the interaction. + */ +void entityPlayerTryInteract( + entitymanager_t *mgr, + const entityid_t entityId +); + /** * Serializes the player's "moveSpeed", "jumpImpulse", and "turnSpeed" * into the given JSON object. diff --git a/src/duskrpg/entity/gamecomponentlist.h b/src/duskrpg/entity/gamecomponentlist.h index 92fe2b4e..64d7a2c6 100644 --- a/src/duskrpg/entity/gamecomponentlist.h +++ b/src/duskrpg/entity/gamecomponentlist.h @@ -8,6 +8,7 @@ // Game-specific component types, appended after the engine's inbuilt // components in entity/componentlist.h. #include "entity/component/overworld/entityplayer.h" +#include "entity/component/overworld/entityinteractable.h" // Name (Uppercase) // Structure @@ -20,3 +21,5 @@ X(PLAYER, entityplayer_t, player, entityPlayerInit, NULL, NULL, entityPlayerSerialize, entityPlayerDeserialize) +X(INTERACTABLE, entityinteractable_t, interactable, entityInteractableInit, + NULL, NULL, entityInteractableSerialize, entityInteractableDeserialize) diff --git a/src/duskrpg/entity/prefab/entityprefabplayer.c b/src/duskrpg/entity/prefab/entityprefabplayer.c index e83323f3..7a198743 100644 --- a/src/duskrpg/entity/prefab/entityprefabplayer.c +++ b/src/duskrpg/entity/prefab/entityprefabplayer.c @@ -10,6 +10,7 @@ #include "entity/component/display/entityposition.h" #include "entity/component/display/entityrenderable.h" #include "entity/component/physics/entityphysics.h" +#include "entity/component/trigger/entitytrigger.h" #include "display/color.h" errorret_t entityPrefabPlayerApply( @@ -37,5 +38,17 @@ errorret_t entityPrefabPlayerApply( entityPhysicsSetBodyType(mgr, entityId, physComp, PHYSICS_BODY_DYNAMIC); entityPhysicsSetCollideMask(mgr, entityId, physComp, 0x3); + // Interact box: a little larger than the player's own body, so nearby + // interactable entities overlap it without needing to touch the player + // exactly (see entityPlayerTryInteract()). + componentid_t interactComp = entityAddComponent( + mgr, entityId, COMPONENT_TYPE_TRIGGER + ); + physicsshape_t interactShape = { + .type = PHYSICS_SHAPE_CUBE, + .data.cube.halfExtents = { 1.0f, 1.0f, 1.0f } + }; + entityTriggerSetShape(mgr, entityId, interactComp, interactShape); + errorOk(); } diff --git a/src/duskrpg/entity/prefab/entityprefabplayer.h b/src/duskrpg/entity/prefab/entityprefabplayer.h index 9199a55f..070e0356 100644 --- a/src/duskrpg/entity/prefab/entityprefabplayer.h +++ b/src/duskrpg/entity/prefab/entityprefabplayer.h @@ -11,8 +11,9 @@ /** * Applies the PLAYER prefab: a POSITION 5 units up, a red - * SHADER_MATERIAL RENDERABLE, a PLAYER component, and a DYNAMIC PHYSICS - * body tagged with collideMask 0x3 (world + player layers). C-coded + * SHADER_MATERIAL RENDERABLE, a PLAYER component, a DYNAMIC PHYSICS body + * tagged with collideMask 0x3 (world + player layers), and a 2x2x2 cube + * TRIGGER (the interact box checked by entityPlayerTryInteract()). C-coded * equivalent of the hand-authored "player" entity in * assets/scenes/test.json. * diff --git a/src/duskrpg/item/CMakeLists.txt b/src/duskrpg/item/CMakeLists.txt index c23c10f1..faa81581 100644 --- a/src/duskrpg/item/CMakeLists.txt +++ b/src/duskrpg/item/CMakeLists.txt @@ -4,9 +4,9 @@ # https://opensource.org/licenses/MIT # Sources -# itemgive.c/h are not built yet -- they depend on the RPG textbox UI -# (ui/rpg/textbox/uitextboxmain.h), which hasn't been restored. Add -# itemgive.c back here once that's back. +# itemgive.c/h are not built yet -- they depend on the RPG textbox UI, +# now restored at ui/textbox/uitextboxmain.h. Add itemgive.c back here +# once itemgive.c/h themselves are restored. target_sources(${DUSK_LIBRARY_TARGET_NAME} PUBLIC item.c diff --git a/src/duskrpg/ui/CMakeLists.txt b/src/duskrpg/ui/CMakeLists.txt index df6679d7..f0282dc1 100644 --- a/src/duskrpg/ui/CMakeLists.txt +++ b/src/duskrpg/ui/CMakeLists.txt @@ -7,3 +7,5 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME} PUBLIC uitestlabel.c ) + +add_subdirectory(textbox) diff --git a/src/duskrpg/ui/textbox/CMakeLists.txt b/src/duskrpg/ui/textbox/CMakeLists.txt new file mode 100644 index 00000000..e248e4fe --- /dev/null +++ b/src/duskrpg/ui/textbox/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 + uitextbox.c + uitextboxmain.c +) diff --git a/src/duskrpg/ui/textbox/uitextbox.c b/src/duskrpg/ui/textbox/uitextbox.c new file mode 100644 index 00000000..69ab0f92 --- /dev/null +++ b/src/duskrpg/ui/textbox/uitextbox.c @@ -0,0 +1,242 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "uitextbox.h" +#include "assert/assert.h" +#include "util/memory.h" +#include "util/string.h" +#include "time/time.h" +#include "display/text/text.h" +#include "display/color.h" +#include "display/spritebatch/spritebatch.h" +#include "display/shader/shaderunlit.h" +#include "ui/frame/uiframe.h" + +void uiTextboxInit( + uitextbox_t *box, + char_t *text, + const uint32_t maxLength, + uitextboxline_t *lines, + const uint32_t linesMax +) { + assertNotNull(box, "Textbox cannot be NULL"); + assertNotNull(text, "Text buffer cannot be NULL"); + assertTrue(maxLength >= 1, "maxLength must be at least 1"); + assertNotNull(lines, "Lines buffer cannot be NULL"); + assertTrue(linesMax >= 1, "linesMax must be at least 1"); + memoryZero(box, sizeof(uitextbox_t)); + box->text = text; + box->maxLength = maxLength; + box->lines = lines; + box->linesMax = linesMax; +} + +void uiTextboxSetText(uitextbox_t *box, const char_t *text) { + assertNotNull(box, "Textbox cannot be NULL"); + assertNotNull(text, "Text cannot be NULL"); + stringCopy(box->text, text, box->maxLength); + box->currentPage = 0; + box->scroll = 0; + box->layoutWidth = 0.0f; + box->layoutHeight = 0.0f; +} + +void uiTextboxBuildLayout( + uitextbox_t *box, + const float_t width, + const float_t height +) { + assertNotNull(box, "Textbox cannot be NULL"); + + box->layoutWidth = width; + box->layoutHeight = height; + box->lineCount = 0; + box->pageCount = 1; + + float_t fontW = (float_t)FONT_DEFAULT.tileset->tileWidth; + float_t fontH = (float_t)FONT_DEFAULT.tileset->tileHeight; + + if(fontW <= 0.0f || fontH <= 0.0f) return; + + box->charsPerLine = (int32_t)(width / fontW); + box->linesPerPage = (int32_t)(height / (fontH + UI_TEXTBOX_LINE_SPACING)); + if(box->linesPerPage > UI_TEXTBOX_LINES_PER_PAGE_MAX) { + box->linesPerPage = UI_TEXTBOX_LINES_PER_PAGE_MAX; + } + + if(box->charsPerLine <= 0 || box->linesPerPage <= 0) return; + if(box->text[0] == '\0') return; + + char_t *src = box->text; + int32_t i = 0; + + while(src[i] != '\0' && box->lineCount < (int32_t)box->linesMax) { + if(src[i] == '\t') { + i++; + int32_t rem = box->lineCount % box->linesPerPage; + int32_t pad = rem > 0 ? box->linesPerPage - rem : 0; + while(pad > 0 && box->lineCount < (int32_t)box->linesMax) { + box->lines[box->lineCount].start = i; + box->lines[box->lineCount].count = 0; + box->lineCount++; + pad--; + } + continue; + } + + int32_t lineStart = i; + int32_t lineWidth = 0; + + while(src[i] != '\0') { + char_t c = src[i]; + + if(c == '\n') { i++; break; } + if(c == '\t') break; + + if(c == ' ') { + int32_t wordLen = 0; + int32_t j = i + 1; + while( + src[j] != ' ' && src[j] != '\n' && + src[j] != '\t' && src[j] != '\0' + ) { + wordLen++; + j++; + } + + if(lineWidth > 0 && lineWidth + 1 + wordLen > box->charsPerLine) { + i++; + break; + } + + lineWidth++; + i++; + } else { + if(lineWidth >= box->charsPerLine) break; + lineWidth++; + i++; + } + } + + box->lines[box->lineCount].start = lineStart; + box->lines[box->lineCount].count = lineWidth; + box->lineCount++; + } + + if(box->lineCount == 0) { + box->pageCount = 1; + } else { + box->pageCount = + (box->lineCount + box->linesPerPage - 1) / box->linesPerPage; + } +} + +errorret_t uiTextboxUpdate(uitextbox_t *box) { + assertNotNull(box, "Textbox cannot be NULL"); + + #ifdef DUSK_TIME_DYNAMIC + if(TIME.dynamicUpdate) errorOk(); + #endif + + if(!uiTextboxPageIsComplete(box)) { + box->scroll += UI_TEXTBOX_SCROLL_CHARS_PER_TICK; + } + + errorOk(); +} + +errorret_t uiTextboxDraw( + uitextbox_t *box, + const float_t x, + const float_t y, + const float_t width, + const float_t height +) { + assertNotNull(box, "Textbox cannot be NULL"); + + float_t startX = (float_t)UI_FRAME_START_X; + float_t startY = (float_t)UI_FRAME_START_Y; + float_t contentX = x + startX; + float_t contentY = y + startY; + float_t contentW = width - 2.0f * startX; + float_t contentH = height - 2.0f * startY; + + if(contentW != box->layoutWidth || contentH != box->layoutHeight) { + uiTextboxBuildLayout(box, contentW, contentH); + } + + errorChain(uiFrameDraw(x, y, width, height)); + + if(box->lineCount == 0 || box->text[0] == '\0') errorOk(); + + float_t fontW = (float_t)FONT_DEFAULT.tileset->tileWidth; + float_t fontH = (float_t)FONT_DEFAULT.tileset->tileHeight; + + shadermaterial_t material = { + .unlit = { + .color = COLOR_WHITE, + .texture = FONT_DEFAULT.texture + } + }; + + int32_t pageFirst = box->currentPage * box->linesPerPage; + int32_t pageLast = pageFirst + box->linesPerPage; + if(pageLast > box->lineCount) pageLast = box->lineCount; + + int32_t charsLeft = box->scroll; + + for(int32_t li = pageFirst; li < pageLast && charsLeft > 0; li++) { + uitextboxline_t *line = &box->lines[li]; + int32_t visible = line->count < charsLeft ? line->count : charsLeft; + float_t lineY = contentY + + (float_t)(li - pageFirst) * (fontH + UI_TEXTBOX_LINE_SPACING); + + for(int32_t ci = 0; ci < visible; ci++) { + char_t c = box->text[line->start + ci]; + if(c == ' ') continue; + spritebatchsprite_t sprite = textGetSprite( + (vec2){ contentX + (float_t)ci * fontW, lineY }, + c, + &FONT_DEFAULT + ); + errorChain(spriteBatchBuffer(&sprite, 1, &SHADER_UNLIT, material)); + } + + charsLeft -= visible; + } + + errorOk(); +} + +int32_t uiTextboxGetPageCharCount(const uitextbox_t *box) { + assertNotNull(box, "Textbox cannot be NULL"); + int32_t first = box->currentPage * box->linesPerPage; + int32_t last = first + box->linesPerPage; + if(last > box->lineCount) last = box->lineCount; + int32_t total = 0; + for(int32_t i = first; i < last; i++) { + total += box->lines[i].count; + } + return total; +} + +bool_t uiTextboxPageIsComplete(const uitextbox_t *box) { + assertNotNull(box, "Textbox cannot be NULL"); + return box->scroll >= uiTextboxGetPageCharCount(box); +} + +bool_t uiTextboxHasNextPage(const uitextbox_t *box) { + assertNotNull(box, "Textbox cannot be NULL"); + return box->currentPage + 1 < box->pageCount; +} + +void uiTextboxNextPage(uitextbox_t *box) { + assertNotNull(box, "Textbox cannot be NULL"); + if(!uiTextboxHasNextPage(box)) return; + box->currentPage++; + box->scroll = 0; +} diff --git a/src/duskrpg/ui/textbox/uitextbox.h b/src/duskrpg/ui/textbox/uitextbox.h new file mode 100644 index 00000000..047e7e1f --- /dev/null +++ b/src/duskrpg/ui/textbox/uitextbox.h @@ -0,0 +1,138 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "error/error.h" + +#define UI_TEXTBOX_LINES_PER_PAGE_MAX 4 +#define UI_TEXTBOX_SCROLL_CHARS_PER_TICK 1 +#define UI_TEXTBOX_LINE_SPACING 0.0f + +typedef struct { + int32_t start; + int32_t count; +} uitextboxline_t; + +typedef struct { + char_t *text; + uint32_t maxLength; + + uitextboxline_t *lines; + uint32_t linesMax; + int32_t lineCount; + int32_t charsPerLine; + int32_t linesPerPage; + int32_t pageCount; + + // last dimensions used for layout; rebuild triggers when these change + float_t layoutWidth; + float_t layoutHeight; + + int32_t currentPage; + int32_t scroll; +} uitextbox_t; + +/** + * Initializes a textbox, zeroing all state and binding it to caller-owned + * text and line storage. + * + * @param box The textbox to initialize. + * @param text Caller-owned buffer the textbox copies its text into. + * @param maxLength Capacity of text, in characters. + * @param lines Caller-owned buffer the textbox lays lines out into. + * @param linesMax Capacity of lines, in entries. + */ +void uiTextboxInit( + uitextbox_t *box, + char_t *text, + const uint32_t maxLength, + uitextboxline_t *lines, + const uint32_t linesMax +); + +/** + * Copies text into the textbox and marks layout as dirty. + * Resets currentPage and scroll to 0. + * + * @param box The textbox to update. + * @param text Null-terminated source string. + */ +void uiTextboxSetText(uitextbox_t *box, const char_t *text); + +/** + * Rebuilds word-wrap and page layout for the given draw dimensions. + * Called automatically by uiTextboxDraw when width or height changes. + * + * @param box The textbox to rebuild. + * @param width Available content width in pixels. + * @param height Available content height in pixels. + */ +void uiTextboxBuildLayout( + uitextbox_t *box, + const float_t width, + const float_t height +); + +/** + * Advances the typewriter scroll by UI_TEXTBOX_SCROLL_CHARS_PER_TICK. + * Skipped on dynamic ticks. + * + * @param box The textbox to update. + * @returns Any error that occurs. + */ +errorret_t uiTextboxUpdate(uitextbox_t *box); + +/** + * Draws the textbox frame and visible text. Rebuilds layout automatically + * if width or height differs from the last draw call. + * + * @param box The textbox to draw. + * @param x Screen x position. + * @param y Screen y position. + * @param width Draw width in pixels. + * @param height Draw height in pixels. + * @returns Any error that occurs. + */ +errorret_t uiTextboxDraw( + uitextbox_t *box, + const float_t x, + const float_t y, + const float_t width, + const float_t height +); + +/** + * Returns the total visible char count for the current page. + * + * @param box The textbox to query. + * @returns Total chars on the current page. + */ +int32_t uiTextboxGetPageCharCount(const uitextbox_t *box); + +/** + * Returns true when scroll has fully revealed the current page. + * + * @param box The textbox to query. + * @returns True if the current page is fully visible. + */ +bool_t uiTextboxPageIsComplete(const uitextbox_t *box); + +/** + * Returns true when there is at least one more page after the current one. + * + * @param box The textbox to query. + * @returns True if a next page exists. + */ +bool_t uiTextboxHasNextPage(const uitextbox_t *box); + +/** + * Advances to the next page and resets scroll to 0. + * Has no effect if already on the last page. + * + * @param box The textbox to advance. + */ +void uiTextboxNextPage(uitextbox_t *box); diff --git a/src/duskrpg/ui/textbox/uitextboxmain.c b/src/duskrpg/ui/textbox/uitextboxmain.c new file mode 100644 index 00000000..f81cce8a --- /dev/null +++ b/src/duskrpg/ui/textbox/uitextboxmain.c @@ -0,0 +1,121 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "uitextboxmain.h" +#include "ui/focus/uifocus.h" +#include "display/screen/screen.h" +#include "display/text/text.h" +#include "display/color.h" +#include "display/spritebatch/spritebatch.h" +#include "display/shader/shaderunlit.h" +#include "ui/frame/uiframe.h" + +uitextboxmain_t UI_TEXTBOX_MAIN; +static uifocusitem_t *focusItem = NULL; + +errorret_t uiTextboxMainInit(void) { + uiTextboxInit( + &UI_TEXTBOX_MAIN.box, + UI_TEXTBOX_MAIN.text, UI_TEXTBOX_MAIN_TEXT_MAX, + UI_TEXTBOX_MAIN.lines, UI_TEXTBOX_MAIN_LINES_MAX + ); + errorOk(); +} + +void uiTextboxMainSetText(const char_t *text) { + uiTextboxSetText(&UI_TEXTBOX_MAIN.box, text); + if(focusItem != NULL) return; + focusItem = uiFocusPush( + 1, 1, + uiTextboxMainFocusSelected, + NULL, + uiTextboxMainFocusClosed, + NULL, + NULL + ); +} + +errorret_t uiTextboxMainUpdate(void) { + if(focusItem == NULL) errorOk(); + return uiTextboxUpdate(&UI_TEXTBOX_MAIN.box); +} + +errorret_t uiTextboxMainDraw(void) { + if(focusItem == NULL) errorOk(); + float_t fontH = (float_t)FONT_DEFAULT.tileset->tileHeight; + float_t h = (float_t)UI_TEXTBOX_MAIN_LINES * fontH + + (float_t)(UI_TEXTBOX_MAIN_LINES - 1) * UI_TEXTBOX_LINE_SPACING + + 2.0f * (float_t)UI_FRAME_START_Y; + float_t w = (float_t)SCREEN.scanWidth; + float_t x = (float_t)SCREEN.scanX; + float_t y = (float_t)(SCREEN.scanY + SCREEN.scanHeight) - h; + errorChain(uiTextboxDraw(&UI_TEXTBOX_MAIN.box, x, y, w, h)); + + if(!uiTextboxPageIsComplete(&UI_TEXTBOX_MAIN.box)) errorOk(); + + float_t fontW = (float_t)FONT_DEFAULT.tileset->tileWidth; + float_t contentX = x + (float_t)UI_FRAME_START_X; + float_t contentY = y + (float_t)UI_FRAME_START_Y; + float_t contentW = w - 2.0f * (float_t)UI_FRAME_START_X; + float_t contentH = h - 2.0f * (float_t)UI_FRAME_START_Y; + + shadermaterial_t material = { + .unlit = { + .color = COLOR_WHITE, + .texture = FONT_DEFAULT.texture + } + }; + + spritebatchsprite_t caret = textGetSprite( + (vec2){ + contentX + contentW - fontW, + contentY + contentH - fontH + }, + 'v', + &FONT_DEFAULT + ); + errorChain(spriteBatchBuffer(&caret, 1, &SHADER_UNLIT, material)); + + errorOk(); +} + +bool_t uiTextboxMainPageIsComplete(void) { + return uiTextboxPageIsComplete(&UI_TEXTBOX_MAIN.box); +} + +bool_t uiTextboxMainHasNextPage(void) { + return uiTextboxHasNextPage(&UI_TEXTBOX_MAIN.box); +} + +void uiTextboxMainNextPage(void) { + uiTextboxNextPage(&UI_TEXTBOX_MAIN.box); +} + +bool_t uiTextboxMainIsActive(void) { + return focusItem != NULL; +} + +bool_t uiTextboxMainFocusSelected(const uifocusitem_t *item) { + if(!uiTextboxPageIsComplete(&UI_TEXTBOX_MAIN.box)) { + UI_TEXTBOX_MAIN.box.scroll = + uiTextboxGetPageCharCount(&UI_TEXTBOX_MAIN.box); + return true; + } + + if(uiTextboxHasNextPage(&UI_TEXTBOX_MAIN.box)) { + uiTextboxNextPage(&UI_TEXTBOX_MAIN.box); + return true; + } + + uiFocusPopItem(focusItem); + return true; +} + +bool_t uiTextboxMainFocusClosed(const uifocusitem_t *item) { + focusItem = NULL; + return true; +} diff --git a/src/duskrpg/ui/textbox/uitextboxmain.h b/src/duskrpg/ui/textbox/uitextboxmain.h new file mode 100644 index 00000000..d8e65d71 --- /dev/null +++ b/src/duskrpg/ui/textbox/uitextboxmain.h @@ -0,0 +1,94 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "uitextbox.h" +#include "ui/focus/uifocusitem.h" + +#define UI_TEXTBOX_MAIN_LINES 4 +#define UI_TEXTBOX_MAIN_TEXT_MAX 1024 +#define UI_TEXTBOX_MAIN_LINES_MAX 64 + +typedef struct { + uitextbox_t box; + char_t text[UI_TEXTBOX_MAIN_TEXT_MAX]; + uitextboxline_t lines[UI_TEXTBOX_MAIN_LINES_MAX]; +} uitextboxmain_t; + +extern uitextboxmain_t UI_TEXTBOX_MAIN; + +/** + * Initializes UI_TEXTBOX_MAIN. + * + * @returns Any error that occurs. + */ +errorret_t uiTextboxMainInit(void); + +/** + * Copies text into UI_TEXTBOX_MAIN and resets page and scroll. + * + * @param text Null-terminated source string. + */ +void uiTextboxMainSetText(const char_t *text); + +/** + * Advances the typewriter scroll for UI_TEXTBOX_MAIN. + * + * @returns Any error that occurs. + */ +errorret_t uiTextboxMainUpdate(void); + +/** + * Draws UI_TEXTBOX_MAIN full-width at the bottom of the screen. + * Position and size are derived from SCREEN each call. + * + * @returns Any error that occurs. + */ +errorret_t uiTextboxMainDraw(void); + +/** + * Returns true when the current page is fully scrolled in. + * + * @returns True if the current page is complete. + */ +bool_t uiTextboxMainPageIsComplete(void); + +/** + * Returns true when at least one more page follows the current one. + * + * @returns True if a next page exists. + */ +bool_t uiTextboxMainHasNextPage(void); + +/** + * Advances UI_TEXTBOX_MAIN to the next page and resets scroll. + * Has no effect if already on the last page. + */ +void uiTextboxMainNextPage(void); + +/** + * Returns true when UI_TEXTBOX_MAIN has focus (is visible and active). + * + * @returns True if the textbox is currently active. + */ +bool_t uiTextboxMainIsActive(void); + +/** + * Internal focus callback - skip scroll or advance page or dismiss. + * + * @param item The active focus item. + * @returns True. + */ +bool_t uiTextboxMainFocusSelected(const uifocusitem_t *item); + +/** + * Internal focus callback - clears the focus item pointer on dismiss. + * + * @param item The focus item being closed. + * @returns True. + */ +bool_t uiTextboxMainFocusClosed(const uifocusitem_t *item); diff --git a/src/duskrpg/ui/uielementgame.h b/src/duskrpg/ui/uielementgame.h index 9ec517e7..7ceff4ff 100644 --- a/src/duskrpg/ui/uielementgame.h +++ b/src/duskrpg/ui/uielementgame.h @@ -20,5 +20,15 @@ // src/dusk/ui/uielement.h, or use any int32_t value of your own. #include "ui/uitestlabel.h" +#include "ui/textbox/uitextboxmain.h" +#include "cutscene/cutscenesystem.h" X(uiTestLabelInit, NULL, uiTestLabelDraw, NULL, UI_ELEMENT_ORDER_DEBUG) +X( + uiTextboxMainInit, uiTextboxMainUpdate, uiTextboxMainDraw, NULL, + UI_ELEMENT_ORDER_DEFAULT +) +X( + cutsceneSystemInit, cutsceneSystemUpdate, NULL, cutsceneSystemDispose, + UI_ELEMENT_ORDER_DEFAULT +)