Restored cutscene code

This commit is contained in:
2026-07-23 09:15:47 -05:00
parent 0852b8463b
commit 0667a44e14
55 changed files with 3066 additions and 24 deletions
+16
View File
@@ -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": [
{
+66 -16
View File
@@ -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 {
-1
View File
@@ -15,5 +15,4 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
ui.c
uielement.c
# uitextbox.c
)
+1
View File
@@ -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)
+11
View File
@@ -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)
+127
View File
@@ -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 } \
}
+24
View File
@@ -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 \
))
+102
View File
@@ -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();
}
+149
View File
@@ -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);
+14
View File
@@ -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)
@@ -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
)
@@ -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;
}
@@ -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
);
@@ -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;
}
@@ -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
);
@@ -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;
}
@@ -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
);
@@ -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;
}
@@ -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
);
+88
View File
@@ -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;
}
+134
View File
@@ -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
);
@@ -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
)
@@ -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;
}
@@ -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
);
@@ -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();
}
@@ -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
);
@@ -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;
}
@@ -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
);
@@ -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;
}
@@ -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
);
@@ -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;
}
@@ -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
);
@@ -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;
}
@@ -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
);
@@ -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);
}
@@ -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
);
@@ -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
)
@@ -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();
}
@@ -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
);
@@ -6,4 +6,5 @@
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
entityplayer.c
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();
}
@@ -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
);
@@ -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,
@@ -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.
+3
View File
@@ -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)
@@ -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();
}
@@ -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.
*
+3 -3
View File
@@ -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
+2
View File
@@ -7,3 +7,5 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
uitestlabel.c
)
add_subdirectory(textbox)
+10
View File
@@ -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
)
+242
View File
@@ -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;
}
+138
View File
@@ -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);
+121
View File
@@ -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;
}
+94
View File
@@ -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);
+10
View File
@@ -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
)