Restored cutscene code
This commit is contained in:
@@ -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
@@ -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 {
|
||||
|
||||
@@ -15,5 +15,4 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
PUBLIC
|
||||
ui.c
|
||||
uielement.c
|
||||
# uitextbox.c
|
||||
)
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
@@ -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 } \
|
||||
}
|
||||
@@ -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 \
|
||||
))
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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
|
||||
);
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -7,3 +7,5 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
PUBLIC
|
||||
uitestlabel.c
|
||||
)
|
||||
|
||||
add_subdirectory(textbox)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user