Compare commits
51
Commits
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"moves": {
|
||||
"fire": {
|
||||
// Target flags
|
||||
"enemies": true,
|
||||
"allies": true,
|
||||
"multiple": false,
|
||||
"self": true,
|
||||
"default": "enemy",
|
||||
"target": "SINGLE_ENEMY"
|
||||
},
|
||||
|
||||
"slash": {
|
||||
"enemies": true,
|
||||
"allies": true,
|
||||
"self": false,
|
||||
"default": "enemy",
|
||||
"target": "SINGLE_ENEMY"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"type": "MODAL",
|
||||
"title": "Fire",
|
||||
"message": "A burst of flame scorches the target."
|
||||
},
|
||||
{
|
||||
"type": "WAIT",
|
||||
"seconds": 2
|
||||
},
|
||||
{
|
||||
"type": "MODAL_CLOSE"
|
||||
},
|
||||
{
|
||||
"type": "BATTLE_FIGHTER_DAMAGE",
|
||||
"fighter": "target",
|
||||
"amount": 12
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"type": "MODAL",
|
||||
"title": "Slash",
|
||||
"message": "A quick blade strike cuts across the target."
|
||||
},
|
||||
{
|
||||
"type": "WAIT",
|
||||
"seconds": 2
|
||||
},
|
||||
{
|
||||
"type": "MODAL_CLOSE"
|
||||
},
|
||||
{
|
||||
"type": "BATTLE_FIGHTER_DAMAGE",
|
||||
"fighter": "target",
|
||||
"amount": 8
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"type": "BATTLE_START",
|
||||
"enemies": [
|
||||
{
|
||||
"attack": 8, "defense": 4, "magic": 0, "speed": 5, "luck": 2,
|
||||
"healthMax": 30, "mpMax": 0,
|
||||
"abilities": ["fire"]
|
||||
},
|
||||
{
|
||||
"attack": 6, "defense": 3, "magic": 0, "speed": 7, "luck": 2,
|
||||
"healthMax": 20, "mpMax": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -62,7 +62,7 @@
|
||||
},
|
||||
{
|
||||
"type": "CUTSCENE",
|
||||
"name": "main_menu"
|
||||
"name": "cutscenes/main_menu.jsonc"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
// Placeholder for the new-game intro flow - just shows a modal for now.
|
||||
"items": [
|
||||
{
|
||||
"type": "MODAL",
|
||||
"title": "New Game",
|
||||
"message": "This is a placeholder for the new game intro.",
|
||||
"blocking": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"items": {
|
||||
"potion": { "type": "MEDICINE" },
|
||||
"potato": { "type": "FOOD" },
|
||||
"apple": { "type": "FOOD" }
|
||||
}
|
||||
}
|
||||
+20
-1
@@ -73,6 +73,12 @@ msgstr "Select Save"
|
||||
msgid "ui.select_save.empty"
|
||||
msgstr "Empty Slot"
|
||||
|
||||
msgid "ui.select_save.corrupt"
|
||||
msgstr "Corrupted Save"
|
||||
|
||||
msgid "ui.select_save.corrupt_warning"
|
||||
msgstr "This save file is corrupted and could not be loaded. Please contact support if this issue persists."
|
||||
|
||||
msgid "ui.select_save.slot_format"
|
||||
msgstr "%s Lv.%d %s"
|
||||
|
||||
@@ -260,6 +266,10 @@ msgstr "loading"
|
||||
msgid "ui.autosave.saving"
|
||||
msgstr "SAVING"
|
||||
|
||||
|
||||
#
|
||||
# Items
|
||||
#
|
||||
msgid "item.potion.name"
|
||||
msgstr "Potion"
|
||||
|
||||
@@ -267,4 +277,13 @@ msgid "item.potato.name"
|
||||
msgstr "Potato"
|
||||
|
||||
msgid "item.apple.name"
|
||||
msgstr "Apple"
|
||||
msgstr "Apple"
|
||||
|
||||
#
|
||||
# Moves
|
||||
#
|
||||
msgid "move.fire.name"
|
||||
msgstr "Fire"
|
||||
|
||||
msgid "move.slash.name"
|
||||
msgstr "Slash"
|
||||
@@ -1 +1 @@
|
||||
{"tiles":[[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],null,null,null,null,null,[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],null,null,null,null,null,[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],null,null,null,null,null,[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[6,0],[2,0],[2,0],[2,0],[2,0],[2,0],[2,0],[7,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[3,0],[1,1],[1,1],[1,1],[1,1],[1,1],[1,1],[5,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[3,0],[1,1],[1,1],[1,1],[1,1],[1,1],[1,1],[5,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[3,0],[1,1],[1,1],[1,1],[1,1],[1,1],[1,1],[5,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[8,0],[4,0],[4,0],[4,0],[12,0],[1,1],[1,1],[5,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[3,0],[1,1],[1,1],[5,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[3,0],[1,1],[1,1],[5,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[8,0],[4,0],[4,0],[9,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0]],"meshes":[{"file":"house_5_3.dmf","pos":[1,1,0]}],"entities":[{"type":"global","globalId":3,"pos":[8,8,1]},{"type":"item","itemId":1,"quantity":1,"pos":[12,2,0]}],"areas":[{"min":[11,3,0],"max":[16,9,10],"callbackId":1,"notify":3,"trigger":6}]}
|
||||
{"tiles":[[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],null,null,null,null,null,[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],null,null,null,null,null,[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],null,null,null,null,null,[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[6,0],[2,0],[2,0],[2,0],[2,0],[2,0],[2,0],[7,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[3,0],[1,1],[1,1],[1,1],[1,1],[1,1],[1,1],[5,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[3,0],[1,1],[1,1],[1,1],[1,1],[1,1],[1,1],[5,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[3,0],[1,1],[1,1],[1,1],[1,1],[1,1],[1,1],[5,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[8,0],[4,0],[4,0],[4,0],[12,0],[1,1],[1,1],[5,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[3,0],[1,1],[1,1],[5,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[3,0],[1,1],[1,1],[5,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[8,0],[4,0],[4,0],[9,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0]],"meshes":[{"file":"house_5_3.dmf","pos":[1,1,0]}],"entities":[{"type":"global","globalId":3,"pos":[8,8,1]},{"type":"global","globalId":4,"pos":[12,2,0]}],"areas":[{"min":[11,3,0],"max":[16,9,10],"callbackId":1,"notify":3,"trigger":6}]}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"members": {
|
||||
"hero": {
|
||||
"attack": 10,
|
||||
"defense": 5,
|
||||
"magic": 0,
|
||||
"speed": 10,
|
||||
"luck": 0,
|
||||
"healthMax": 30,
|
||||
"mpMax": 10
|
||||
},
|
||||
"companion": {
|
||||
"attack": 8,
|
||||
"defense": 6,
|
||||
"magic": 4,
|
||||
"speed": 8,
|
||||
"luck": 1,
|
||||
"healthMax": 25,
|
||||
"mpMax": 15
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"flags": {
|
||||
"test": {
|
||||
"initial": true
|
||||
},
|
||||
"test2": {
|
||||
"initial": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,7 @@ target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
|
||||
)
|
||||
|
||||
set(DUSK_BACKTRACE ON CACHE BOOL "Enable backtrace support for assert failures.")
|
||||
set(DUSK_TRACK_MEMORY ON CACHE BOOL "Track memoryAllocate() call sites to help find memory leaks.")
|
||||
|
||||
# Define platform-specific macros.
|
||||
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
|
||||
|
||||
@@ -50,6 +50,12 @@ if(DUSK_BACKTRACE)
|
||||
)
|
||||
endif()
|
||||
|
||||
if(DUSK_TRACK_MEMORY)
|
||||
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
|
||||
DUSK_TRACK_MEMORY
|
||||
)
|
||||
endif()
|
||||
|
||||
# Includes
|
||||
target_include_directories(${DUSK_LIBRARY_TARGET_NAME}
|
||||
PUBLIC
|
||||
|
||||
@@ -12,13 +12,16 @@
|
||||
// "DSK2" - distinct from a plain zip's "PK\x03\x04" so a stray plain zip
|
||||
// never gets misread as a valid dusk.dsk.
|
||||
#define ASSET_DSK_MAGIC_SIZE 4
|
||||
#define ASSET_DSK_MAGIC "DSK2"
|
||||
#define ASSET_DSK_MAGIC "DSK"
|
||||
#define ASSET_DSK_VERSION 2
|
||||
|
||||
// magic(4) + version(4) + size(4) + checksum(4), all little-endian
|
||||
// regardless of host - see assetDskParseHeader. The zip archive itself
|
||||
// starts immediately after, at ASSET_DSK_HEADER_SIZE.
|
||||
#define ASSET_DSK_HEADER_SIZE 16
|
||||
// Magic + version + size + checksum
|
||||
#define ASSET_DSK_HEADER_SIZE ( \
|
||||
ASSET_DSK_MAGIC_SIZE + \
|
||||
sizeof(uint32_t) + \
|
||||
sizeof(uint32_t) + \
|
||||
sizeof(uint32_t) \
|
||||
)
|
||||
|
||||
/**
|
||||
* Parsed dusk.dsk (DSK2 format) header. dusk.dsk is a single uncompressed
|
||||
|
||||
@@ -17,6 +17,5 @@ add_subdirectory(locale)
|
||||
add_subdirectory(json)
|
||||
add_subdirectory(chunk)
|
||||
add_subdirectory(dmf)
|
||||
add_subdirectory(cutscene)
|
||||
add_subdirectory(wav)
|
||||
add_subdirectory(mp3)
|
||||
@@ -52,12 +52,6 @@ assetloadercallbacks_t ASSET_LOADER_CALLBACKS[ASSET_LOADER_TYPE_COUNT] = {
|
||||
.dispose = assetChunkDispose
|
||||
},
|
||||
|
||||
[ASSET_LOADER_TYPE_CUTSCENE] = {
|
||||
.loadSync = assetCutsceneLoaderSync,
|
||||
.loadAsync = assetCutsceneLoaderAsync,
|
||||
.dispose = assetCutsceneDispose
|
||||
},
|
||||
|
||||
[ASSET_LOADER_TYPE_WAV] = {
|
||||
.loadSync = assetWavLoaderSync,
|
||||
.loadAsync = assetWavLoaderAsync,
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
#include "asset/loader/locale/assetlocaleloader.h"
|
||||
#include "asset/loader/json/assetjsonloader.h"
|
||||
#include "asset/loader/chunk/assetchunkloader.h"
|
||||
#include "asset/loader/cutscene/assetcutsceneloader.h"
|
||||
#include "asset/loader/wav/assetwavloader.h"
|
||||
#include "asset/loader/mp3/assetmp3loader.h"
|
||||
|
||||
@@ -27,7 +26,6 @@ typedef enum {
|
||||
ASSET_LOADER_TYPE_LOCALE,
|
||||
ASSET_LOADER_TYPE_JSON,
|
||||
ASSET_LOADER_TYPE_CHUNK,
|
||||
ASSET_LOADER_TYPE_CUTSCENE,
|
||||
ASSET_LOADER_TYPE_WAV,
|
||||
ASSET_LOADER_TYPE_MP3,
|
||||
|
||||
@@ -42,7 +40,6 @@ typedef union {
|
||||
assetlocaleloaderloading_t locale;
|
||||
assetjsonloaderloading_t json;
|
||||
assetchunkloaderloading_t chunk;
|
||||
assetcutsceneloaderloading_t cutscene;
|
||||
assetwavloaderloading_t wav;
|
||||
assetmp3loaderloading_t mp3;
|
||||
} assetloaderloading_t;
|
||||
@@ -55,7 +52,6 @@ typedef union {
|
||||
assetlocaleoutput_t locale;
|
||||
assetjsonoutput_t json;
|
||||
assetchunkoutput_t chunk;
|
||||
assetcutsceneoutput_t cutscene;
|
||||
assetwavoutput_t wav;
|
||||
assetmp3output_t mp3;
|
||||
} assetloaderoutput_t;
|
||||
@@ -66,7 +62,6 @@ typedef union {
|
||||
assetlocaleloaderinput_t locale;
|
||||
assetjsonloaderinput_t json;
|
||||
assetchunkloaderinput_t chunk;
|
||||
assetcutsceneloaderinput_t cutscene;
|
||||
assetwavloaderinput_t wav;
|
||||
assetmp3loaderinput_t mp3;
|
||||
} assetloaderinput_t;
|
||||
|
||||
@@ -1,225 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "assetcutsceneloader.h"
|
||||
#include "assert/assert.h"
|
||||
#include "util/memory.h"
|
||||
#include "util/string.h"
|
||||
#include "asset/loader/assetloading.h"
|
||||
#include "asset/loader/assetentry.h"
|
||||
#include "asset/loader/assetloader.h"
|
||||
#include "asset/asset.h"
|
||||
#include "rpg/cutscene/item/json/cutscenejsonpauseflags.h"
|
||||
|
||||
errorret_t assetCutsceneParseItem(
|
||||
assetloading_t *loading,
|
||||
yyjson_val *itemObj,
|
||||
cutsceneitem_t *item,
|
||||
uint8_t *pool,
|
||||
size_t *poolOffset,
|
||||
const uint8_t index
|
||||
) {
|
||||
if(!yyjson_is_obj(itemObj)) {
|
||||
assetLoaderErrorThrow(
|
||||
loading,
|
||||
"Cutscene item %u is not an object",
|
||||
(uint32_t)index
|
||||
);
|
||||
}
|
||||
|
||||
const char_t *typeName = yyjson_get_str(yyjson_obj_get(itemObj, "type"));
|
||||
if(!cutsceneItemLookupType(typeName, &item->type)) {
|
||||
assetLoaderErrorThrow(
|
||||
loading,
|
||||
"Cutscene item %u: missing/unsupported item type %s",
|
||||
(uint32_t)index,
|
||||
typeName
|
||||
);
|
||||
}
|
||||
|
||||
cutsceneitemloadcallback_t *load = CUTSCENE_ITEM_CALLBACKS[item->type].load;
|
||||
if(load == NULL) {
|
||||
assetLoaderErrorThrow(
|
||||
loading, "Cutscene item %u: unhandled item type %u", (uint32_t)index,
|
||||
(uint32_t)item->type
|
||||
);
|
||||
}
|
||||
|
||||
errorret_t result = load(itemObj, item, pool, poolOffset);
|
||||
if(errorIsNotOk(result)) {
|
||||
loading->entry->state = ASSET_ENTRY_STATE_ERROR;
|
||||
errorChain(result);
|
||||
}
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
size_t assetCutsceneComputePoolSize(yyjson_val *itemsArr) {
|
||||
size_t size = 0;
|
||||
yyjson_arr_iter iter = yyjson_arr_iter_with(itemsArr);
|
||||
yyjson_val *itemVal;
|
||||
while((itemVal = yyjson_arr_iter_next(&iter)) != NULL) {
|
||||
const char_t *typeName = yyjson_get_str(yyjson_obj_get(itemVal, "type"));
|
||||
cutsceneitemtype_t type;
|
||||
if(!cutsceneItemLookupType(typeName, &type)) continue;
|
||||
|
||||
cutsceneitempoolsizecallback_t *poolSize =
|
||||
CUTSCENE_ITEM_CALLBACKS[type].poolSize;
|
||||
if(poolSize == NULL) continue;
|
||||
|
||||
size = cutsceneJsonPoolAlign(size) + poolSize(itemVal);
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
void assetCutsceneFreeParsed(assetcutsceneoutput_t *out) {
|
||||
if(out->items != NULL) {
|
||||
memoryFree(out->items);
|
||||
out->items = NULL;
|
||||
}
|
||||
if(out->pool != NULL) {
|
||||
memoryFree(out->pool);
|
||||
out->pool = NULL;
|
||||
}
|
||||
if(out->doc != NULL) {
|
||||
yyjson_doc_free(out->doc);
|
||||
out->doc = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
errorret_t assetCutsceneLoaderAsync(assetloading_t *loading) {
|
||||
assertNotNull(loading, "Loading cannot be NULL");
|
||||
assertNotMainThread("Should be called from an async thread.");
|
||||
|
||||
if(
|
||||
loading->loading.cutscene.state != ASSET_CUTSCENE_LOADING_STATE_READ_FILE
|
||||
) {
|
||||
errorOk();
|
||||
}
|
||||
|
||||
assertNull(loading->loading.cutscene.buffer, "Buffer already defined?");
|
||||
|
||||
assetfile_t *file = &loading->loading.cutscene.file;
|
||||
assetLoaderErrorChain(loading,
|
||||
assetFileInit(file, loading->entry->name, NULL, NULL)
|
||||
);
|
||||
|
||||
if(file->size > ASSET_CUTSCENE_FILE_SIZE_MAX) {
|
||||
assetLoaderErrorThrow(
|
||||
loading, "Cutscene JSON exceeds maximum allowed size"
|
||||
);
|
||||
}
|
||||
|
||||
size_t fileSize = (size_t)file->size;
|
||||
uint8_t *buffer = memoryAllocate(fileSize);
|
||||
assetLoaderErrorChain(loading, assetFileOpen(file));
|
||||
assetLoaderErrorChain(loading, assetFileRead(file, buffer, fileSize));
|
||||
assertTrue(
|
||||
file->lastRead == file->size,
|
||||
"Failed to read entire cutscene file."
|
||||
);
|
||||
assetLoaderErrorChain(loading, assetFileClose(file));
|
||||
assetLoaderErrorChain(loading, assetFileDispose(file));
|
||||
|
||||
loading->loading.cutscene.buffer = buffer;
|
||||
loading->loading.cutscene.size = fileSize;
|
||||
loading->loading.cutscene.state = ASSET_CUTSCENE_LOADING_STATE_PARSE;
|
||||
loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t assetCutsceneLoaderSync(assetloading_t *loading) {
|
||||
assertNotNull(loading, "Loading cannot be NULL");
|
||||
assertTrue(loading->type == ASSET_LOADER_TYPE_CUTSCENE, "Invalid type.");
|
||||
assertIsMainThread("Must be called from the main thread.");
|
||||
|
||||
if(loading->loading.cutscene.state == ASSET_CUTSCENE_LOADING_STATE_INITIAL) {
|
||||
loading->loading.cutscene.state = ASSET_CUTSCENE_LOADING_STATE_READ_FILE;
|
||||
loading->entry->state = ASSET_ENTRY_STATE_PENDING_ASYNC;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
assetcutsceneoutput_t *out = &loading->entry->data.cutscene;
|
||||
uint8_t *buffer = loading->loading.cutscene.buffer;
|
||||
assertNotNull(buffer, "Cutscene data should have been loaded by now.");
|
||||
size_t bufferSize = loading->loading.cutscene.size;
|
||||
|
||||
yyjson_doc *doc = yyjson_read(
|
||||
(char *)buffer, bufferSize,
|
||||
YYJSON_READ_ALLOW_COMMENTS | YYJSON_READ_ALLOW_TRAILING_COMMAS
|
||||
);
|
||||
memoryFree(buffer);
|
||||
loading->loading.cutscene.buffer = NULL;
|
||||
|
||||
if(doc == NULL) {
|
||||
assetLoaderErrorThrow(loading, "Failed to parse cutscene JSON");
|
||||
}
|
||||
out->doc = doc;
|
||||
|
||||
yyjson_val *root = yyjson_doc_get_root(doc);
|
||||
yyjson_val *itemsArr = yyjson_obj_get(root, "items");
|
||||
if(!yyjson_is_arr(itemsArr)) {
|
||||
assetCutsceneFreeParsed(out);
|
||||
assetLoaderErrorThrow(loading, "Cutscene JSON is missing an 'items' array");
|
||||
}
|
||||
|
||||
size_t itemCount = yyjson_arr_size(itemsArr);
|
||||
if(itemCount > UINT8_MAX) {
|
||||
assetCutsceneFreeParsed(out);
|
||||
assetLoaderErrorThrow(loading, "Cutscene has more than 255 items");
|
||||
}
|
||||
|
||||
cutscenepause_t pause = CUTSCENE_PAUSE_DEFAULT;
|
||||
yyjson_val *pauseArr = yyjson_obj_get(root, "pause");
|
||||
if(yyjson_is_arr(pauseArr)) {
|
||||
if(!cutsceneJsonParsePauseFlags(pauseArr, &pause)) {
|
||||
assetCutsceneFreeParsed(out);
|
||||
assetLoaderErrorThrow(
|
||||
loading, "Cutscene JSON has an invalid 'pause' flag"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
out->items = memoryAllocate(itemCount * sizeof(cutsceneitem_t));
|
||||
memoryZero(out->items, itemCount * sizeof(cutsceneitem_t));
|
||||
|
||||
size_t poolSize = assetCutsceneComputePoolSize(itemsArr);
|
||||
out->pool = poolSize > 0 ? memoryAllocate(poolSize) : NULL;
|
||||
|
||||
size_t poolOffset = 0;
|
||||
yyjson_arr_iter itemIter = yyjson_arr_iter_with(itemsArr);
|
||||
yyjson_val *itemVal;
|
||||
uint8_t index = 0;
|
||||
while((itemVal = yyjson_arr_iter_next(&itemIter)) != NULL) {
|
||||
errorret_t itemResult = assetCutsceneParseItem(
|
||||
loading, itemVal, &out->items[index], out->pool, &poolOffset, index
|
||||
);
|
||||
if(errorIsNotOk(itemResult)) {
|
||||
assetCutsceneFreeParsed(out);
|
||||
errorChain(itemResult);
|
||||
}
|
||||
index++;
|
||||
}
|
||||
|
||||
out->cutscene.items = out->items;
|
||||
out->cutscene.itemCount = (uint8_t)itemCount;
|
||||
out->cutscene.pause = pause;
|
||||
out->cutscene.dataSize = 0;
|
||||
|
||||
loading->entry->state = ASSET_ENTRY_STATE_LOADED;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t assetCutsceneDispose(assetentry_t *entry) {
|
||||
assertNotNull(entry, "Entry cannot be NULL");
|
||||
assertTrue(entry->type == ASSET_LOADER_TYPE_CUTSCENE, "Invalid type.");
|
||||
assertIsMainThread("Must be called from the main thread.");
|
||||
|
||||
assetCutsceneFreeParsed(&entry->data.cutscene);
|
||||
|
||||
errorOk();
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "asset/assetfile.h"
|
||||
#include "rpg/cutscene/cutscene.h"
|
||||
#include "rpg/cutscene/item/cutsceneitem.h"
|
||||
#include "rpg/cutscene/item/json/cutscenejsonpool.h"
|
||||
#include "yyjson.h"
|
||||
|
||||
#define ASSET_CUTSCENE_FILE_SIZE_MAX (1024 * 64)
|
||||
|
||||
typedef struct assetloading_s assetloading_t;
|
||||
typedef struct assetentry_s assetentry_t;
|
||||
|
||||
typedef struct {
|
||||
void *nothing;
|
||||
} assetcutsceneloaderinput_t;
|
||||
|
||||
typedef enum {
|
||||
ASSET_CUTSCENE_LOADING_STATE_INITIAL,
|
||||
ASSET_CUTSCENE_LOADING_STATE_READ_FILE,
|
||||
ASSET_CUTSCENE_LOADING_STATE_PARSE
|
||||
} assetcutsceneloadingstate_t;
|
||||
|
||||
typedef struct {
|
||||
assetfile_t file;
|
||||
assetcutsceneloadingstate_t state;
|
||||
uint8_t *buffer;
|
||||
size_t size;
|
||||
} assetcutsceneloaderloading_t;
|
||||
|
||||
typedef struct {
|
||||
cutscene_t cutscene;
|
||||
cutsceneitem_t *items;
|
||||
uint8_t *pool;
|
||||
yyjson_doc *doc;
|
||||
} assetcutsceneoutput_t;
|
||||
|
||||
/**
|
||||
* Parses one cutscene item object into item, writing any pool-backed
|
||||
* array data (entityWalkTo positions, mapAreaWait areaIds) into pool at
|
||||
* *poolOffset and advancing it. Every field/enum name is validated as
|
||||
* real (untrusted, authored) file content - failures are real errors, not
|
||||
* asserts. Dispatches to the matching type's Load callback (see
|
||||
* CUTSCENE_ITEM_CALLBACKS in rpg/cutscene/item/cutsceneitem.h) - the
|
||||
* per-type parsing logic itself lives there now, colocated with that
|
||||
* type's runtime behavior.
|
||||
*
|
||||
* @param loading Loading information for the asset being loaded.
|
||||
* @param itemObj The item's JSON object.
|
||||
* @param item Destination item, already zeroed by the caller.
|
||||
* @param pool Base pointer of the entry's pool allocation (may be NULL if
|
||||
* assetCutsceneComputePoolSize returned 0).
|
||||
* @param poolOffset In/out cursor into pool.
|
||||
* @param index Index of this item within the cutscene, for error messages.
|
||||
* @return Error code indicating success or failure of the parse.
|
||||
*/
|
||||
errorret_t assetCutsceneParseItem(
|
||||
assetloading_t *loading,
|
||||
yyjson_val *itemObj,
|
||||
cutsceneitem_t *item,
|
||||
uint8_t *pool,
|
||||
size_t *poolOffset,
|
||||
const uint8_t index
|
||||
);
|
||||
|
||||
/**
|
||||
* Frees whatever of out->items/out->pool/out->doc is currently non-NULL
|
||||
* and clears them - shared by both the normal disposer and every parse
|
||||
* failure path's cleanup.
|
||||
*
|
||||
* @param out The output to free.
|
||||
*/
|
||||
void assetCutsceneFreeParsed(assetcutsceneoutput_t *out);
|
||||
|
||||
/**
|
||||
* Asynchronous loader for cutscene assets. Reads the raw JSONC file bytes
|
||||
* into the loading buffer so the sync phase can parse without blocking the
|
||||
* main thread on I/O.
|
||||
*
|
||||
* @param loading Loading information for the asset being loaded.
|
||||
* @return Error code indicating success or failure of the load operation.
|
||||
*/
|
||||
errorret_t assetCutsceneLoaderAsync(assetloading_t *loading);
|
||||
|
||||
/**
|
||||
* Synchronous loader for cutscene assets. Parses the JSONC previously read
|
||||
* by the async phase into a heap-allocated cutsceneitem_t array + pool.
|
||||
*
|
||||
* @param loading Loading information for the asset being loaded.
|
||||
* @return Error code indicating success or failure of the load operation.
|
||||
*/
|
||||
errorret_t assetCutsceneLoaderSync(assetloading_t *loading);
|
||||
|
||||
/**
|
||||
* Disposer for cutscene assets.
|
||||
*
|
||||
* @param entry Asset entry containing the cutscene data to dispose.
|
||||
* @return Error code indicating success or failure of the dispose operation.
|
||||
*/
|
||||
errorret_t assetCutsceneDispose(assetentry_t *entry);
|
||||
@@ -12,11 +12,10 @@
|
||||
|
||||
// Forward-declared rather than including asset/asset.h - this header only
|
||||
// ever needs a pointer to it, and pulling in the full asset module here
|
||||
// creates a real include cycle for anything that reaches this header via
|
||||
// asset/loader/cutscene/assetcutsceneloader.h (asset.h -> ... ->
|
||||
// assetloader.h -> assetcutsceneloader.h -> cutscene.h -> cutsceneitem.h
|
||||
// -> the audio cutscene items -> this header -> asset/asset.h again,
|
||||
// mid-parse). audiomixerchannel.c itself still includes the full header.
|
||||
// risks an include cycle for anything that reaches this header via a
|
||||
// cutscene item (cutscene.h -> cutsceneitem.h -> the audio cutscene items
|
||||
// -> this header -> asset/asset.h, mid-parse). audiomixerchannel.c itself
|
||||
// still includes the full header.
|
||||
typedef struct assetentry_s assetentry_t;
|
||||
|
||||
typedef struct audiostream_s audiostream_t;
|
||||
|
||||
@@ -47,8 +47,8 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) {
|
||||
errorChain(localeManagerInit());
|
||||
errorChain(displayInit());
|
||||
errorChain(audioInit());
|
||||
errorChain(uiInit());
|
||||
errorChain(rpgInit());
|
||||
errorChain(uiInit());
|
||||
#ifdef DUSK_NETWORK
|
||||
errorChain(networkInit());
|
||||
#endif
|
||||
@@ -108,5 +108,7 @@ errorret_t engineDispose(void) {
|
||||
errorChain(assetDispose());
|
||||
perfDispose();
|
||||
|
||||
memoryCheckUnfreed();
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
@@ -7,9 +7,9 @@
|
||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
PUBLIC
|
||||
battle.c
|
||||
battlefighter.c
|
||||
party.c
|
||||
)
|
||||
|
||||
# Subdirs
|
||||
add_subdirectory(testbattle)
|
||||
add_subdirectory(fighter)
|
||||
add_subdirectory(move)
|
||||
add_subdirectory(state)
|
||||
|
||||
+42
-291
@@ -9,320 +9,71 @@
|
||||
#include "assert/assert.h"
|
||||
#include "util/memory.h"
|
||||
#include "rpg/cutscene/cutscenesystem.h"
|
||||
#include "scene/scene.h"
|
||||
|
||||
battle_t BATTLE;
|
||||
|
||||
void battleInit(void) {
|
||||
memoryZero(&BATTLE, sizeof(battle_t));
|
||||
for(uint8_t i = 0; i < BATTLE_FIGHTER_COUNT_MAX; i++) {
|
||||
BATTLE.fighters[i].id = i;
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t battleGetAvailableFighter(void) {
|
||||
for(uint8_t i = 0; i < BATTLE_FIGHTER_COUNT_MAX; i++) {
|
||||
if(BATTLE.fighters[i].status == BATTLE_FIGHTER_STATUS_NULL) return i;
|
||||
}
|
||||
|
||||
return 0xFF;
|
||||
}
|
||||
|
||||
battlefighter_t *battleAddFighter(
|
||||
const battlefighterteam_t team,
|
||||
const battlefightercontroller_t controller,
|
||||
const battlefighterstats_t stats,
|
||||
const uint16_t healthMax,
|
||||
const uint16_t mpMax
|
||||
) {
|
||||
const uint8_t index = battleGetAvailableFighter();
|
||||
if(index == 0xFF) return NULL;
|
||||
|
||||
battlefighter_t *fighter = &BATTLE.fighters[index];
|
||||
battleFighterInit(fighter, team, controller, stats, healthMax, mpMax);
|
||||
return fighter;
|
||||
}
|
||||
|
||||
void battleStart(
|
||||
const battleencountertype_t encounterType,
|
||||
const bool_t fleeAvailable
|
||||
battlefighter_t *fighters[BATTLE_FIGHTER_POS_COUNT]
|
||||
) {
|
||||
assertTrue(encounterType < BATTLE_ENCOUNTER_COUNT, "Invalid encounter type");
|
||||
assertNotNull(fighters, "Fighters cannot be NULL");
|
||||
|
||||
BATTLE.encounterType = encounterType;
|
||||
BATTLE.fleeAvailable = fleeAvailable;
|
||||
BATTLE.result = BATTLE_RESULT_NONE;
|
||||
BATTLE.round = 1;
|
||||
|
||||
battleSetState(BATTLE_STATE_OPENING);
|
||||
}
|
||||
|
||||
void battleDispose(void) {
|
||||
battleInit();
|
||||
}
|
||||
|
||||
battlefighter_t *battleGetCurrentFighter(void) {
|
||||
if(BATTLE.state != BATTLE_STATE_PLAYER_SELECTION) return NULL;
|
||||
if(BATTLE.selectionIndex >= BATTLE.executionCount) return NULL;
|
||||
return &BATTLE.fighters[BATTLE.executionOrder[BATTLE.selectionIndex]];
|
||||
}
|
||||
|
||||
uint8_t battleGetAliveCount(const battlefighterteam_t team) {
|
||||
uint8_t count = 0;
|
||||
for(uint8_t i = 0; i < BATTLE_FIGHTER_COUNT_MAX; i++) {
|
||||
if(BATTLE.fighters[i].team != team) continue;
|
||||
if(!battleFighterIsAlive(&BATTLE.fighters[i])) continue;
|
||||
count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
void battleResolveAttack(
|
||||
battlefighter_t *attacker,
|
||||
battlefighter_t *defender
|
||||
) {
|
||||
assertNotNull(attacker, "Attacker cannot be NULL");
|
||||
assertNotNull(defender, "Defender cannot be NULL");
|
||||
|
||||
const int32_t rawDamage =
|
||||
(int32_t)attacker->stats.attack - (int32_t)defender->stats.defense;
|
||||
const uint16_t damage = rawDamage > 0 ? (uint16_t)rawDamage : 1;
|
||||
|
||||
defender->health = damage >= defender->health ? 0 : defender->health - damage;
|
||||
if(defender->health == 0) defender->status = BATTLE_FIGHTER_STATUS_DEAD;
|
||||
}
|
||||
|
||||
battleresult_t battleCheckResult(void) {
|
||||
if(BATTLE.result != BATTLE_RESULT_NONE) return BATTLE.result;
|
||||
|
||||
if(battleGetAliveCount(BATTLE_FIGHTER_TEAM_ALLY) == 0) {
|
||||
battleSetResult(BATTLE_RESULT_LOSS);
|
||||
} else if(battleGetAliveCount(BATTLE_FIGHTER_TEAM_ENEMY) == 0) {
|
||||
battleSetResult(BATTLE_RESULT_WIN);
|
||||
}
|
||||
|
||||
return BATTLE.result;
|
||||
}
|
||||
|
||||
void battleQueueAction(
|
||||
const uint8_t fighterIndex,
|
||||
const battleactiontype_t type,
|
||||
const uint8_t targetIndex
|
||||
) {
|
||||
battleaction_t *action = &BATTLE.actions[fighterIndex];
|
||||
action->type = type;
|
||||
action->targetIndex = targetIndex;
|
||||
|
||||
if(BATTLE.onActionDecided != NULL) {
|
||||
BATTLE.onActionDecided(&BATTLE.fighters[fighterIndex], action);
|
||||
}
|
||||
}
|
||||
|
||||
void battlePlayerAttack(const uint8_t targetIndex) {
|
||||
battlefighter_t *fighter = battleGetCurrentFighter();
|
||||
if(fighter == NULL) return;
|
||||
if(targetIndex >= BATTLE_FIGHTER_COUNT_MAX) return;
|
||||
if(!battleFighterIsAlive(&BATTLE.fighters[targetIndex])) return;
|
||||
|
||||
battleQueueAction(fighter->id, BATTLE_ACTION_ATTACK, targetIndex);
|
||||
BATTLE.selectionIndex++;
|
||||
battleAdvanceSelection();
|
||||
}
|
||||
|
||||
void battlePlayerFlee(void) {
|
||||
battlefighter_t *fighter = battleGetCurrentFighter();
|
||||
if(fighter == NULL) return;
|
||||
if(!BATTLE.fleeAvailable) return;
|
||||
|
||||
battleSetResult(BATTLE_RESULT_FLED);
|
||||
}
|
||||
|
||||
void battleUpdate(void) {
|
||||
if(BATTLE.state == BATTLE_STATE_NONE) return;
|
||||
if(BATTLE.state == BATTLE_STATE_ENDED) return;
|
||||
if(CUTSCENE_SYSTEM.pause & CUTSCENE_PAUSE_BATTLE) return;
|
||||
|
||||
switch(BATTLE.state) {
|
||||
case BATTLE_STATE_OPENING:
|
||||
battleSetState(BATTLE_STATE_PRE_ROUND);
|
||||
break;
|
||||
|
||||
case BATTLE_STATE_PRE_ROUND:
|
||||
battleUpdatePreRound();
|
||||
break;
|
||||
|
||||
case BATTLE_STATE_AI_SELECTION:
|
||||
battleUpdateAiSelection();
|
||||
break;
|
||||
|
||||
case BATTLE_STATE_MOVES_EXECUTING:
|
||||
battleUpdateMovesExecuting();
|
||||
break;
|
||||
|
||||
case BATTLE_STATE_POST_ROUND:
|
||||
battleUpdatePostRound();
|
||||
break;
|
||||
|
||||
default:
|
||||
// BATTLE_STATE_PLAYER_SELECTION: waits on battlePlayerAttack/Flee.
|
||||
// BATTLE_STATE_NONE/ENDED: handled above.
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void battleBuildExecutionOrder(const bool_t applyEncounterBias) {
|
||||
BATTLE.executionCount = 0;
|
||||
for(uint8_t i = 0; i < BATTLE_FIGHTER_COUNT_MAX; i++) {
|
||||
if(!battleFighterIsAlive(&BATTLE.fighters[i])) continue;
|
||||
BATTLE.executionOrder[BATTLE.executionCount++] = i;
|
||||
}
|
||||
|
||||
// Insertion sort by speed descending -- fine for BATTLE_FIGHTER_COUNT_MAX.
|
||||
for(uint8_t i = 1; i < BATTLE.executionCount; i++) {
|
||||
const uint8_t key = BATTLE.executionOrder[i];
|
||||
const uint16_t keySpeed = BATTLE.fighters[key].stats.speed;
|
||||
|
||||
int8_t j = (int8_t)i - 1;
|
||||
while(
|
||||
j >= 0 &&
|
||||
BATTLE.fighters[BATTLE.executionOrder[j]].stats.speed < keySpeed
|
||||
) {
|
||||
BATTLE.executionOrder[j + 1] = BATTLE.executionOrder[j];
|
||||
j--;
|
||||
#if DUSK_ASSERTIONS
|
||||
// Must be at least two non null fighter present, may need to ensure they
|
||||
// are from seperate teams also.
|
||||
uint8_t nonNullCount = 0;
|
||||
for(uint8_t i = 0; i < BATTLE_FIGHTER_POS_COUNT; i++) {
|
||||
if(fighters[i] != NULL) nonNullCount++;
|
||||
}
|
||||
BATTLE.executionOrder[j + 1] = key;
|
||||
}
|
||||
assertTrue(nonNullCount >= 2, "Must have at least two fighters");
|
||||
#endif
|
||||
|
||||
if(!applyEncounterBias) return;
|
||||
|
||||
if(BATTLE.encounterType == BATTLE_ENCOUNTER_PLAYER_ADVANTAGE) {
|
||||
battleMoveTeamFirst(BATTLE_FIGHTER_TEAM_ALLY);
|
||||
} else if(BATTLE.encounterType == BATTLE_ENCOUNTER_BACK_ATTACK) {
|
||||
battleMoveTeamFirst(BATTLE_FIGHTER_TEAM_ENEMY);
|
||||
}
|
||||
}
|
||||
|
||||
void battleMoveTeamFirst(const battlefighterteam_t team) {
|
||||
uint8_t sorted[BATTLE_FIGHTER_COUNT_MAX];
|
||||
uint8_t count = 0;
|
||||
|
||||
for(uint8_t i = 0; i < BATTLE.executionCount; i++) {
|
||||
if(BATTLE.fighters[BATTLE.executionOrder[i]].team != team) continue;
|
||||
sorted[count++] = BATTLE.executionOrder[i];
|
||||
}
|
||||
for(uint8_t i = 0; i < BATTLE.executionCount; i++) {
|
||||
if(BATTLE.fighters[BATTLE.executionOrder[i]].team == team) continue;
|
||||
sorted[count++] = BATTLE.executionOrder[i];
|
||||
}
|
||||
|
||||
// TODO: I may not need to zero entire battle.
|
||||
memoryZero(&BATTLE, sizeof(battle_t));
|
||||
memoryCopy(
|
||||
BATTLE.executionOrder, sorted, sizeof(uint8_t) * BATTLE.executionCount
|
||||
BATTLE.fighters,
|
||||
fighters,
|
||||
sizeof(battlefighter_t *) * BATTLE_FIGHTER_POS_COUNT
|
||||
);
|
||||
}
|
||||
|
||||
battlefighter_t *battleAIChooseTarget(const battlefighter_t *fighter) {
|
||||
const battlefighterteam_t enemyTeam =
|
||||
fighter->team == BATTLE_FIGHTER_TEAM_ALLY ?
|
||||
BATTLE_FIGHTER_TEAM_ENEMY : BATTLE_FIGHTER_TEAM_ALLY;
|
||||
// Enter the opening state.
|
||||
battleSetState(BATTLE_STATE_OPENING);
|
||||
|
||||
battlefighter_t *weakest = NULL;
|
||||
for(uint8_t i = 0; i < BATTLE_FIGHTER_COUNT_MAX; i++) {
|
||||
battlefighter_t *candidate = &BATTLE.fighters[i];
|
||||
if(candidate->team != enemyTeam) continue;
|
||||
if(!battleFighterIsAlive(candidate)) continue;
|
||||
if(weakest == NULL || candidate->health < weakest->health) {
|
||||
weakest = candidate;
|
||||
}
|
||||
}
|
||||
|
||||
return weakest;
|
||||
// Change scene
|
||||
sceneSet(SCENE_TYPE_BATTLE);
|
||||
}
|
||||
|
||||
void battleSetState(const battlestate_t next) {
|
||||
const battlestate_t previous = BATTLE.state;
|
||||
assertTrue(next < BATTLE_STATE_COUNT, "Invalid battle state");
|
||||
assertNotNull(BATTLE_STATE_FUNCTIONS[next].init, "Missing state init function");
|
||||
assertNotNull(BATTLE_STATE_FUNCTIONS[next].update, "Missing state update function");
|
||||
assertNotNull(BATTLE_STATE_FUNCTIONS[next].dispose, "Missing state dispose function");
|
||||
|
||||
if(BATTLE.state != BATTLE_STATE_NULL) {
|
||||
BATTLE_STATE_FUNCTIONS[BATTLE.state].dispose();
|
||||
}
|
||||
BATTLE.state = next;
|
||||
if(BATTLE.onStateChanged != NULL) BATTLE.onStateChanged(previous, next);
|
||||
BATTLE_STATE_FUNCTIONS[BATTLE.state].init();
|
||||
}
|
||||
|
||||
void battleSetResult(const battleresult_t result) {
|
||||
BATTLE.result = result;
|
||||
battleSetState(BATTLE_STATE_ENDED);
|
||||
void battleUpdate(void) {
|
||||
if(BATTLE.state == BATTLE_STATE_NULL) return;
|
||||
assertTrue(BATTLE.state < BATTLE_STATE_COUNT, "Invalid battle state");
|
||||
assertNotNull(BATTLE_STATE_FUNCTIONS[BATTLE.state].update, "Missing state update function");
|
||||
|
||||
BATTLE_STATE_FUNCTIONS[BATTLE.state].update();
|
||||
}
|
||||
|
||||
bool_t battleFighterNeedsDecision(
|
||||
const uint8_t fighterIndex,
|
||||
const battlefightercontroller_t controller
|
||||
) {
|
||||
return battleFighterIsAlive(&BATTLE.fighters[fighterIndex])
|
||||
&& BATTLE.fighters[fighterIndex].controller == controller
|
||||
&& BATTLE.actions[fighterIndex].type == BATTLE_ACTION_NONE;
|
||||
}
|
||||
void battleDispose(void) {
|
||||
if(BATTLE.state == BATTLE_STATE_NULL) return;
|
||||
assertTrue(BATTLE.state < BATTLE_STATE_COUNT, "Invalid battle state");
|
||||
assertNotNull(BATTLE_STATE_FUNCTIONS[BATTLE.state].dispose, "Missing state dispose function");
|
||||
|
||||
void battleAdvanceSelection(void) {
|
||||
while(BATTLE.selectionIndex < BATTLE.executionCount) {
|
||||
const uint8_t fighterIndex = BATTLE.executionOrder[BATTLE.selectionIndex];
|
||||
if(
|
||||
battleFighterNeedsDecision(fighterIndex, BATTLE_FIGHTER_CONTROLLER_PLAYER)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
BATTLE.selectionIndex++;
|
||||
}
|
||||
|
||||
battleSetState(BATTLE_STATE_AI_SELECTION);
|
||||
}
|
||||
|
||||
void battleUpdatePreRound(void) {
|
||||
// No need to clear BATTLE.actions here: every living fighter's action is
|
||||
// unconditionally reset to BATTLE_ACTION_NONE as it's processed in
|
||||
// battleUpdateMovesExecuting, and round 1 starts pre-zeroed by
|
||||
// battleInit(). Clearing it here would also wipe out any action a
|
||||
// cutscene force-queued while parked at BATTLE_STATE_PRE_ROUND.
|
||||
battleBuildExecutionOrder(BATTLE.round == 1);
|
||||
|
||||
BATTLE.selectionIndex = 0;
|
||||
battleSetState(BATTLE_STATE_PLAYER_SELECTION);
|
||||
battleAdvanceSelection();
|
||||
}
|
||||
|
||||
void battleUpdateAiSelection(void) {
|
||||
for(uint8_t i = 0; i < BATTLE.executionCount; i++) {
|
||||
const uint8_t fighterIndex = BATTLE.executionOrder[i];
|
||||
if(
|
||||
!battleFighterNeedsDecision(fighterIndex, BATTLE_FIGHTER_CONTROLLER_AI)
|
||||
) continue;
|
||||
|
||||
battlefighter_t *target =
|
||||
battleAIChooseTarget(&BATTLE.fighters[fighterIndex]);
|
||||
if(target == NULL) continue;
|
||||
|
||||
battleQueueAction(fighterIndex, BATTLE_ACTION_ATTACK, target->id);
|
||||
}
|
||||
|
||||
BATTLE.executionIndex = 0;
|
||||
battleSetState(BATTLE_STATE_MOVES_EXECUTING);
|
||||
}
|
||||
|
||||
void battleUpdateMovesExecuting(void) {
|
||||
if(BATTLE.executionIndex >= BATTLE.executionCount) {
|
||||
battleSetState(BATTLE_STATE_POST_ROUND);
|
||||
return;
|
||||
}
|
||||
|
||||
const uint8_t fighterIndex = BATTLE.executionOrder[BATTLE.executionIndex++];
|
||||
battlefighter_t *fighter = &BATTLE.fighters[fighterIndex];
|
||||
if(!battleFighterIsAlive(fighter)) return;
|
||||
|
||||
battleaction_t *action = &BATTLE.actions[fighterIndex];
|
||||
if(action->type == BATTLE_ACTION_ATTACK) {
|
||||
battlefighter_t *target = &BATTLE.fighters[action->targetIndex];
|
||||
if(battleFighterIsAlive(target)) battleResolveAttack(fighter, target);
|
||||
}
|
||||
action->type = BATTLE_ACTION_NONE;
|
||||
|
||||
battleCheckResult();
|
||||
}
|
||||
|
||||
void battleUpdatePostRound(void) {
|
||||
BATTLE.round++;
|
||||
battleSetState(BATTLE_STATE_PRE_ROUND);
|
||||
}
|
||||
BATTLE_STATE_FUNCTIONS[BATTLE.state].dispose();
|
||||
memoryZero(&BATTLE, sizeof(battle_t));
|
||||
}
|
||||
+26
-264
@@ -6,89 +6,38 @@
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "battlefighter.h"
|
||||
|
||||
#define BATTLE_FIGHTER_COUNT_MAX 8
|
||||
|
||||
typedef enum {
|
||||
BATTLE_ENCOUNTER_REGULAR,
|
||||
BATTLE_ENCOUNTER_PLAYER_ADVANTAGE,
|
||||
BATTLE_ENCOUNTER_BACK_ATTACK,
|
||||
|
||||
BATTLE_ENCOUNTER_COUNT
|
||||
} battleencountertype_t;
|
||||
#include "rpg/battle/fighter/battlefighter.h"
|
||||
#include "rpg/battle/move/battlemove.h"
|
||||
#include "rpg/battle/move/battlemoveaction.h"
|
||||
#include "rpg/battle/fighter/battlefighterpos.h"
|
||||
#include "rpg/battle/state/battlestate.h"
|
||||
|
||||
typedef enum {
|
||||
BATTLE_RESULT_NONE,
|
||||
BATTLE_RESULT_WIN,
|
||||
BATTLE_RESULT_LOSS,
|
||||
BATTLE_RESULT_FLED,
|
||||
|
||||
BATTLE_RESULT_COUNT
|
||||
} battleresult_t;
|
||||
|
||||
// Where BATTLE currently is within a round. A cutscene can pause progression
|
||||
// (CUTSCENE_PAUSE_BATTLE) and use CUTSCENE_BATTLE_WAIT_STATE to synchronize
|
||||
// with any of these, or CUTSCENE_BATTLE_FORCE_ACTION to decide a fighter's
|
||||
// action ahead of PLAYER_SELECTION/AI_SELECTION reaching them.
|
||||
typedef enum {
|
||||
BATTLE_STATE_NONE, // Battle inactive.
|
||||
|
||||
BATTLE_STATE_OPENING, // Entered once by battleStart().
|
||||
BATTLE_STATE_PRE_ROUND, // Rebuilds execution order, clears the action queue.
|
||||
BATTLE_STATE_PLAYER_SELECTION, // Waits on battlePlayerAttack/Flee.
|
||||
BATTLE_STATE_AI_SELECTION, // Auto-queues every undecided AI fighter.
|
||||
BATTLE_STATE_MOVES_EXECUTING, // Resolves one queued action per update.
|
||||
BATTLE_STATE_POST_ROUND, // Round wrap-up; loops back to PRE_ROUND.
|
||||
|
||||
BATTLE_STATE_ENDED, // Terminal for WIN/LOSS/FLED alike -- see BATTLE.result.
|
||||
|
||||
BATTLE_STATE_COUNT
|
||||
} battlestate_t;
|
||||
|
||||
typedef enum {
|
||||
BATTLE_ACTION_NONE, // No action decided yet for this fighter this round.
|
||||
BATTLE_ACTION_ATTACK,
|
||||
|
||||
BATTLE_ACTION_COUNT
|
||||
} battleactiontype_t;
|
||||
|
||||
typedef struct {
|
||||
battleactiontype_t type;
|
||||
uint8_t targetIndex; // Meaningful for BATTLE_ACTION_ATTACK.
|
||||
} battleaction_t;
|
||||
battlefighter_t *fighters[BATTLE_FIGHTER_POS_COUNT];
|
||||
|
||||
typedef void (*battlestatechangedcallback_t)(
|
||||
const battlestate_t previous,
|
||||
const battlestate_t next
|
||||
);
|
||||
// Order of who gets to act.
|
||||
uint8_t fighterOrder[BATTLE_FIGHTER_POS_COUNT];
|
||||
uint8_t fighterOrderCount;
|
||||
|
||||
typedef void (*battleactiondecidedcallback_t)(
|
||||
const battlefighter_t *fighter,
|
||||
const battleaction_t *action
|
||||
);
|
||||
// Actions to be executed.
|
||||
battlemoveaction_t actionsPending[BATTLE_FIGHTER_POS_COUNT];
|
||||
uint8_t actionsPendingCount;
|
||||
|
||||
typedef struct {
|
||||
battlestate_t state;
|
||||
battlefighter_t fighters[BATTLE_FIGHTER_COUNT_MAX];
|
||||
battleaction_t actions[BATTLE_FIGHTER_COUNT_MAX];
|
||||
battlestatedata_t stateData;
|
||||
|
||||
battleencountertype_t encounterType;
|
||||
bool_t fleeAvailable;
|
||||
// Set once a side has been wiped out (see cutsceneBattlePostMoveStart)
|
||||
// or the player has fled (see battleStateSelectionPlayerFlee). Stays
|
||||
// BATTLE_RESULT_NONE for the rest of the battle otherwise.
|
||||
battleresult_t result;
|
||||
|
||||
// Fighter indices (into fighters[]), sorted for the current round.
|
||||
uint8_t executionOrder[BATTLE_FIGHTER_COUNT_MAX];
|
||||
uint8_t executionCount;
|
||||
uint8_t executionIndex;
|
||||
uint16_t round;
|
||||
|
||||
// Cursor into executionOrder used by BATTLE_STATE_PLAYER_SELECTION to find
|
||||
// the next player-controlled fighter that still needs a decision.
|
||||
uint8_t selectionIndex;
|
||||
|
||||
battlestatechangedcallback_t onStateChanged;
|
||||
battleactiondecidedcallback_t onActionDecided;
|
||||
} battle_t;
|
||||
|
||||
extern battle_t BATTLE;
|
||||
@@ -99,215 +48,28 @@ extern battle_t BATTLE;
|
||||
void battleInit(void);
|
||||
|
||||
/**
|
||||
* Gets an available (unused) fighter slot index.
|
||||
*
|
||||
* @return The index of an available fighter slot, or 0xFF if none are
|
||||
* available.
|
||||
*/
|
||||
uint8_t battleGetAvailableFighter(void);
|
||||
|
||||
/**
|
||||
* Adds a fighter to the battle in the next available slot.
|
||||
*
|
||||
* @param team The team the fighter belongs to.
|
||||
* @param controller Who makes decisions for the fighter.
|
||||
* @param stats The fighter's base combat stats.
|
||||
* @param healthMax The fighter's maximum health.
|
||||
* @param mpMax The fighter's maximum mp.
|
||||
* @return Pointer to the newly added fighter, or NULL if the battle is
|
||||
* already full.
|
||||
*/
|
||||
battlefighter_t *battleAddFighter(
|
||||
const battlefighterteam_t team,
|
||||
const battlefightercontroller_t controller,
|
||||
const battlefighterstats_t stats,
|
||||
const uint16_t healthMax,
|
||||
const uint16_t mpMax
|
||||
);
|
||||
|
||||
/**
|
||||
* Starts the battle: enters BATTLE_STATE_OPENING and marks the battle
|
||||
* active. Call once every fighter has been added via battleAddFighter.
|
||||
*
|
||||
* @param encounterType Determines the opening round's execution order.
|
||||
* @param fleeAvailable Whether the party may attempt to flee this battle.
|
||||
* Starts a battle.
|
||||
*
|
||||
* @param fighters Fighters in the battle.
|
||||
*/
|
||||
void battleStart(
|
||||
const battleencountertype_t encounterType,
|
||||
const bool_t fleeAvailable
|
||||
battlefighter_t *fighters[BATTLE_FIGHTER_POS_COUNT]
|
||||
);
|
||||
|
||||
/**
|
||||
* Disposes of the battle, clearing all fighters and marking it inactive.
|
||||
*/
|
||||
void battleDispose(void);
|
||||
|
||||
/**
|
||||
* Returns the fighter currently awaiting a player decision.
|
||||
*
|
||||
* @return Pointer to the fighter awaiting a decision, or NULL if the battle
|
||||
* isn't in BATTLE_STATE_PLAYER_SELECTION or every player-controlled
|
||||
* fighter has already decided.
|
||||
*/
|
||||
battlefighter_t *battleGetCurrentFighter(void);
|
||||
|
||||
/**
|
||||
* Returns the number of living fighters on a team.
|
||||
*
|
||||
* @param team The team to count.
|
||||
* @return Count of living fighters on that team.
|
||||
*/
|
||||
uint8_t battleGetAliveCount(const battlefighterteam_t team);
|
||||
|
||||
/**
|
||||
* Resolves a physical attack from attacker onto defender: damage is the
|
||||
* attacker's attack stat minus the defender's defense stat (minimum 1),
|
||||
* subtracted from the defender's health. The defender is marked dead
|
||||
* once health reaches 0.
|
||||
*
|
||||
* @param attacker The attacking fighter.
|
||||
* @param defender The defending fighter.
|
||||
*/
|
||||
void battleResolveAttack(
|
||||
battlefighter_t *attacker,
|
||||
battlefighter_t *defender
|
||||
);
|
||||
|
||||
/**
|
||||
* Checks whether the battle has been won or lost, transitioning to
|
||||
* BATTLE_STATE_ENDED and updating BATTLE.result if so. Does nothing if a
|
||||
* result has already been set (e.g. by a successful flee).
|
||||
*
|
||||
* @return The battle's current result.
|
||||
*/
|
||||
battleresult_t battleCheckResult(void);
|
||||
|
||||
/**
|
||||
* Queues an action for a fighter to perform once BATTLE_STATE_MOVES_EXECUTING
|
||||
* reaches them this round, overwriting any action already queued for that
|
||||
* fighter. Fires BATTLE.onActionDecided.
|
||||
*
|
||||
* @param fighterIndex Index into BATTLE.fighters of the deciding fighter.
|
||||
* @param type The type of action to perform.
|
||||
* @param targetIndex Index into BATTLE.fighters of the target, meaningful
|
||||
* for BATTLE_ACTION_ATTACK.
|
||||
*/
|
||||
void battleQueueAction(
|
||||
const uint8_t fighterIndex,
|
||||
const battleactiontype_t type,
|
||||
const uint8_t targetIndex
|
||||
);
|
||||
|
||||
/**
|
||||
* Submits the currently-selecting fighter's attack against a target, if the
|
||||
* battle is in BATTLE_STATE_PLAYER_SELECTION and awaiting a decision.
|
||||
* Queues the action and advances the selection cursor.
|
||||
*
|
||||
* @param targetIndex Index into BATTLE.fighters of the target.
|
||||
*/
|
||||
void battlePlayerAttack(const uint8_t targetIndex);
|
||||
|
||||
/**
|
||||
* Submits a flee attempt for the currently-selecting fighter, if the battle
|
||||
* is in BATTLE_STATE_PLAYER_SELECTION and fleeing is available for this
|
||||
* battle. Always succeeds, ending the battle with BATTLE_RESULT_FLED.
|
||||
*/
|
||||
void battlePlayerFlee(void);
|
||||
|
||||
/**
|
||||
* Updates the battle simulation for one frame, dispatching on BATTLE.state.
|
||||
* No-op if the battle isn't active, has already ended, or
|
||||
* CUTSCENE_PAUSE_BATTLE is set.
|
||||
*/
|
||||
void battleUpdate(void);
|
||||
|
||||
/**
|
||||
* Rebuilds BATTLE.executionOrder/executionCount from every currently living
|
||||
* fighter, sorted by speed descending.
|
||||
*
|
||||
* @param applyEncounterBias If true, reorders the freshly speed-sorted
|
||||
* queue so BATTLE.encounterType's favoured team goes first (used only
|
||||
* for the opening round).
|
||||
*/
|
||||
void battleBuildExecutionOrder(const bool_t applyEncounterBias);
|
||||
|
||||
/**
|
||||
* Stably partitions BATTLE.executionOrder so every fighter on the given team
|
||||
* comes first, preserving each side's relative (speed-sorted) order.
|
||||
*
|
||||
* @param team The team to move to the front of the execution order.
|
||||
*/
|
||||
void battleMoveTeamFirst(const battlefighterteam_t team);
|
||||
|
||||
/**
|
||||
* Picks an AI target for fighter: the lowest-health living fighter on
|
||||
* the opposing team.
|
||||
*
|
||||
* @param fighter The AI-controlled fighter choosing a target.
|
||||
* @return The chosen target, or NULL if the opposing team has no
|
||||
* living fighters.
|
||||
*/
|
||||
battlefighter_t *battleAIChooseTarget(const battlefighter_t *fighter);
|
||||
|
||||
/**
|
||||
* Sets BATTLE.state and fires BATTLE.onStateChanged with the previous and
|
||||
* new state.
|
||||
* Transitions the battle to a new state: disposes the current state (if
|
||||
* any) and initializes next.
|
||||
*
|
||||
* @param next The state to transition to.
|
||||
*/
|
||||
void battleSetState(const battlestate_t next);
|
||||
|
||||
/**
|
||||
* Sets BATTLE.result and transitions to BATTLE_STATE_ENDED.
|
||||
*
|
||||
* @param result The result to end the battle with.
|
||||
* Updates the battle system.
|
||||
*/
|
||||
void battleSetResult(const battleresult_t result);
|
||||
void battleUpdate(void);
|
||||
|
||||
/**
|
||||
* Checks whether a fighter is a live, undecided candidate for the given
|
||||
* controller -- i.e. whether PLAYER_SELECTION or AI_SELECTION should still
|
||||
* be deciding an action for it this round.
|
||||
*
|
||||
* @param fighterIndex Index into BATTLE.fighters to check.
|
||||
* @param controller The controller PLAYER_SELECTION/AI_SELECTION is
|
||||
* currently deciding for.
|
||||
* @return True if the fighter is alive, matches controller, and has no
|
||||
* action queued yet.
|
||||
* Disposes of the battle, clearing all fighters and marking it inactive.
|
||||
*/
|
||||
bool_t battleFighterNeedsDecision(
|
||||
const uint8_t fighterIndex,
|
||||
const battlefightercontroller_t controller
|
||||
);
|
||||
|
||||
/**
|
||||
* Advances BATTLE.selectionIndex to the next player-controlled fighter that
|
||||
* still needs a decision, or transitions to BATTLE_STATE_AI_SELECTION once
|
||||
* none remain.
|
||||
*/
|
||||
void battleAdvanceSelection(void);
|
||||
|
||||
/**
|
||||
* Handles BATTLE_STATE_PRE_ROUND: rebuilds the execution order and moves on
|
||||
* to BATTLE_STATE_PLAYER_SELECTION, positioning the selection cursor.
|
||||
*/
|
||||
void battleUpdatePreRound(void);
|
||||
|
||||
/**
|
||||
* Handles BATTLE_STATE_AI_SELECTION: queues an attack for every undecided
|
||||
* AI-controlled fighter, then moves on to BATTLE_STATE_MOVES_EXECUTING.
|
||||
*/
|
||||
void battleUpdateAiSelection(void);
|
||||
|
||||
/**
|
||||
* Handles BATTLE_STATE_MOVES_EXECUTING: resolves one queued action from
|
||||
* BATTLE.executionOrder per call, or transitions to BATTLE_STATE_POST_ROUND
|
||||
* once every fighter this round has been processed.
|
||||
*/
|
||||
void battleUpdateMovesExecuting(void);
|
||||
|
||||
/**
|
||||
* Handles BATTLE_STATE_POST_ROUND: advances BATTLE.round and transitions
|
||||
* back to BATTLE_STATE_PRE_ROUND.
|
||||
*/
|
||||
void battleUpdatePostRound(void);
|
||||
void battleDispose(void);
|
||||
@@ -1,43 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "battlefighter.h"
|
||||
#include "assert/assert.h"
|
||||
#include "util/memory.h"
|
||||
|
||||
void battleFighterInit(
|
||||
battlefighter_t *fighter,
|
||||
const battlefighterteam_t team,
|
||||
const battlefightercontroller_t controller,
|
||||
const battlefighterstats_t stats,
|
||||
const uint16_t healthMax,
|
||||
const uint16_t mpMax
|
||||
) {
|
||||
assertNotNull(fighter, "Fighter pointer cannot be NULL");
|
||||
assertTrue(team < BATTLE_FIGHTER_TEAM_COUNT, "Invalid fighter team");
|
||||
assertTrue(
|
||||
controller < BATTLE_FIGHTER_CONTROLLER_COUNT,
|
||||
"Invalid fighter controller"
|
||||
);
|
||||
|
||||
const uint8_t id = fighter->id;
|
||||
memoryZero(fighter, sizeof(battlefighter_t));
|
||||
fighter->id = id;
|
||||
fighter->status = BATTLE_FIGHTER_STATUS_NORMAL;
|
||||
fighter->team = team;
|
||||
fighter->controller = controller;
|
||||
fighter->stats = stats;
|
||||
fighter->healthMax = healthMax;
|
||||
fighter->health = healthMax;
|
||||
fighter->mpMax = mpMax;
|
||||
fighter->mp = mpMax;
|
||||
}
|
||||
|
||||
bool_t battleFighterIsAlive(const battlefighter_t *fighter) {
|
||||
assertNotNull(fighter, "Fighter pointer cannot be NULL");
|
||||
return fighter->status == BATTLE_FIGHTER_STATUS_NORMAL;
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "error/error.h"
|
||||
|
||||
// An empty status means the slot in BATTLE.fighters is unused.
|
||||
typedef enum {
|
||||
BATTLE_FIGHTER_STATUS_NULL,
|
||||
|
||||
BATTLE_FIGHTER_STATUS_NORMAL,
|
||||
BATTLE_FIGHTER_STATUS_DEAD,
|
||||
|
||||
BATTLE_FIGHTER_STATUS_COUNT
|
||||
} battlefighterstatus_t;
|
||||
|
||||
typedef enum {
|
||||
BATTLE_FIGHTER_TEAM_ALLY,
|
||||
BATTLE_FIGHTER_TEAM_ENEMY,
|
||||
|
||||
BATTLE_FIGHTER_TEAM_COUNT
|
||||
} battlefighterteam_t;
|
||||
|
||||
typedef enum {
|
||||
BATTLE_FIGHTER_CONTROLLER_PLAYER,
|
||||
BATTLE_FIGHTER_CONTROLLER_AI,
|
||||
|
||||
BATTLE_FIGHTER_CONTROLLER_COUNT
|
||||
} battlefightercontroller_t;
|
||||
|
||||
// Base combat stats, kept separate from the resource pools (health/mp) on
|
||||
// battlefighter_t so that equipment/buffs can later modify them without
|
||||
// touching current health/mp state.
|
||||
typedef struct {
|
||||
uint16_t attack;
|
||||
uint16_t defense;
|
||||
uint16_t magic;
|
||||
uint16_t speed;
|
||||
uint16_t luck;
|
||||
} battlefighterstats_t;
|
||||
|
||||
typedef struct {
|
||||
uint8_t id;
|
||||
battlefighterstatus_t status;
|
||||
battlefighterteam_t team;
|
||||
battlefightercontroller_t controller;
|
||||
|
||||
uint16_t health;
|
||||
uint16_t healthMax;
|
||||
uint16_t mp;
|
||||
uint16_t mpMax;
|
||||
|
||||
battlefighterstats_t stats;
|
||||
} battlefighter_t;
|
||||
|
||||
/**
|
||||
* Initializes a battle fighter in place, filling health/mp to their maximum
|
||||
* values and setting its status to normal.
|
||||
*
|
||||
* @param fighter Pointer to the fighter to initialize.
|
||||
* @param team The team the fighter belongs to.
|
||||
* @param controller Who makes decisions for the fighter.
|
||||
* @param stats The fighter's base combat stats.
|
||||
* @param healthMax The fighter's maximum health.
|
||||
* @param mpMax The fighter's maximum mp.
|
||||
*/
|
||||
void battleFighterInit(
|
||||
battlefighter_t *fighter,
|
||||
const battlefighterteam_t team,
|
||||
const battlefightercontroller_t controller,
|
||||
const battlefighterstats_t stats,
|
||||
const uint16_t healthMax,
|
||||
const uint16_t mpMax
|
||||
);
|
||||
|
||||
/**
|
||||
* Returns true if the fighter is in a state where it can still act (i.e.
|
||||
* is not dead).
|
||||
*
|
||||
* @param fighter Pointer to the fighter to check.
|
||||
* @returns True if the fighter can act.
|
||||
*/
|
||||
bool_t battleFighterIsAlive(const battlefighter_t *fighter);
|
||||
+2
-1
@@ -3,7 +3,8 @@
|
||||
# This software is released under the MIT License.
|
||||
# https://opensource.org/licenses/MIT
|
||||
|
||||
# Sources
|
||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
PUBLIC
|
||||
assetcutsceneloader.c
|
||||
battlefighter.c
|
||||
)
|
||||
@@ -0,0 +1,230 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "battlefighter.h"
|
||||
#include "assert/assert.h"
|
||||
#include "util/memory.h"
|
||||
|
||||
errorret_t battleFighterParseStatField(
|
||||
yyjson_val *obj,
|
||||
const char_t *key,
|
||||
uint16_t *out
|
||||
) {
|
||||
yyjson_val *val = yyjson_obj_get(obj, key);
|
||||
if(!yyjson_is_int(val)) {
|
||||
errorThrow("Fighter: missing/invalid '%s'", key);
|
||||
}
|
||||
|
||||
const int64_t num = yyjson_get_sint(val);
|
||||
if(num < 0 || num > UINT16_MAX) {
|
||||
errorThrow("Fighter: '%s' must be between 0 and %u", key, UINT16_MAX);
|
||||
}
|
||||
|
||||
*out = (uint16_t)num;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t battleFighterParseOptionalIntField(
|
||||
yyjson_val *obj,
|
||||
const char_t *key,
|
||||
const int32_t defaultValue,
|
||||
int32_t *out
|
||||
) {
|
||||
yyjson_val *val = yyjson_obj_get(obj, key);
|
||||
if(val == NULL) {
|
||||
*out = defaultValue;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
if(!yyjson_is_int(val)) errorThrow("Fighter: invalid '%s'", key);
|
||||
*out = (int32_t)yyjson_get_sint(val);
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t battleFighterParse(battlefighter_t *fighter, yyjson_val *obj) {
|
||||
assertNotNull(fighter, "Fighter cannot be NULL");
|
||||
assertNotNull(obj, "JSON object cannot be NULL");
|
||||
|
||||
int32_t teamNum;
|
||||
errorChain(battleFighterParseOptionalIntField(
|
||||
obj, "team", BATTLE_FIGHTER_TEAM_ALLY, &teamNum
|
||||
));
|
||||
if(teamNum < 0 || teamNum >= BATTLE_FIGHTER_TEAM_COUNT) {
|
||||
errorThrow("Fighter: invalid 'team'");
|
||||
}
|
||||
|
||||
int32_t controllerNum;
|
||||
errorChain(battleFighterParseOptionalIntField(
|
||||
obj, "controller", BATTLE_FIGHTER_CONTROLLER_PLAYER, &controllerNum
|
||||
));
|
||||
if(controllerNum < 0 || controllerNum >= BATTLE_FIGHTER_CONTROLLER_COUNT) {
|
||||
errorThrow("Fighter: invalid 'controller'");
|
||||
}
|
||||
|
||||
battlefighterstats_t stats;
|
||||
errorChain(battleFighterParseStatField(obj, "attack", &stats.attack));
|
||||
errorChain(battleFighterParseStatField(obj, "defense", &stats.defense));
|
||||
errorChain(battleFighterParseStatField(obj, "magic", &stats.magic));
|
||||
errorChain(battleFighterParseStatField(obj, "speed", &stats.speed));
|
||||
errorChain(battleFighterParseStatField(obj, "luck", &stats.luck));
|
||||
errorChain(battleFighterParseStatField(obj, "healthMax", &stats.healthMax));
|
||||
errorChain(battleFighterParseStatField(obj, "mpMax", &stats.mpMax));
|
||||
|
||||
battleFighterInit(
|
||||
fighter, (battlefighterteam_t)teamNum,
|
||||
(battlefightercontroller_t)controllerNum, stats
|
||||
);
|
||||
|
||||
int32_t statusNum;
|
||||
errorChain(battleFighterParseOptionalIntField(
|
||||
obj, "status", BATTLE_FIGHTER_STATUS_NORMAL, &statusNum
|
||||
));
|
||||
if(statusNum < 0 || statusNum >= BATTLE_FIGHTER_STATUS_COUNT) {
|
||||
errorThrow("Fighter: invalid 'status'");
|
||||
}
|
||||
fighter->status = (battlefighterstatus_t)statusNum;
|
||||
|
||||
int32_t health;
|
||||
errorChain(
|
||||
battleFighterParseOptionalIntField(obj, "health", stats.healthMax, &health)
|
||||
);
|
||||
if(health < 0 || health > stats.healthMax) {
|
||||
errorThrow("Fighter: 'health' out of range");
|
||||
}
|
||||
fighter->stats.health = (uint16_t)health;
|
||||
|
||||
int32_t mp;
|
||||
errorChain(battleFighterParseOptionalIntField(obj, "mp", stats.mpMax, &mp));
|
||||
if(mp < 0 || mp > stats.mpMax) {
|
||||
errorThrow("Fighter: 'mp' out of range");
|
||||
}
|
||||
fighter->stats.mp = (uint16_t)mp;
|
||||
|
||||
yyjson_val *abilities = yyjson_obj_get(obj, "abilities");
|
||||
if(abilities != NULL) {
|
||||
if(
|
||||
!yyjson_is_arr(abilities) ||
|
||||
yyjson_arr_size(abilities) > BATTLE_FIGHTER_ABILITY_COUNT_MAX
|
||||
) {
|
||||
errorThrow(
|
||||
"Fighter: 'abilities' must have 0-%u entries",
|
||||
BATTLE_FIGHTER_ABILITY_COUNT_MAX
|
||||
);
|
||||
}
|
||||
|
||||
yyjson_arr_iter iter = yyjson_arr_iter_with(abilities);
|
||||
yyjson_val *abilityVal;
|
||||
while((abilityVal = yyjson_arr_iter_next(&iter)) != NULL) {
|
||||
const char_t *name = yyjson_get_str(abilityVal);
|
||||
battlemoveid_t ability =
|
||||
name == NULL ? BATTLE_MOVE_ID_NULL : battleMoveGetIdByName(name);
|
||||
if(ability == BATTLE_MOVE_ID_NULL) {
|
||||
errorThrow("Fighter: invalid entry in 'abilities'");
|
||||
}
|
||||
battleFighterAbilityAdd(fighter, ability);
|
||||
}
|
||||
}
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
yyjson_mut_val *battleFighterSerialize(
|
||||
const battlefighter_t *fighter,
|
||||
yyjson_mut_doc *doc
|
||||
) {
|
||||
assertNotNull(fighter, "Fighter cannot be NULL");
|
||||
assertNotNull(doc, "Doc cannot be NULL");
|
||||
|
||||
yyjson_mut_val *obj = yyjson_mut_obj(doc);
|
||||
|
||||
yyjson_mut_obj_add_uint(doc, obj, "team", fighter->team);
|
||||
yyjson_mut_obj_add_uint(doc, obj, "controller", fighter->controller);
|
||||
yyjson_mut_obj_add_uint(doc, obj, "status", fighter->status);
|
||||
|
||||
yyjson_mut_obj_add_uint(doc, obj, "attack", fighter->stats.attack);
|
||||
yyjson_mut_obj_add_uint(doc, obj, "defense", fighter->stats.defense);
|
||||
yyjson_mut_obj_add_uint(doc, obj, "magic", fighter->stats.magic);
|
||||
yyjson_mut_obj_add_uint(doc, obj, "speed", fighter->stats.speed);
|
||||
yyjson_mut_obj_add_uint(doc, obj, "luck", fighter->stats.luck);
|
||||
yyjson_mut_obj_add_uint(doc, obj, "healthMax", fighter->stats.healthMax);
|
||||
yyjson_mut_obj_add_uint(doc, obj, "mpMax", fighter->stats.mpMax);
|
||||
yyjson_mut_obj_add_uint(doc, obj, "health", fighter->stats.health);
|
||||
yyjson_mut_obj_add_uint(doc, obj, "mp", fighter->stats.mp);
|
||||
|
||||
yyjson_mut_val *abilities = yyjson_mut_arr(doc);
|
||||
for(uint8_t i = 0; i < fighter->abilityCount; i++) {
|
||||
yyjson_mut_arr_add_str(doc, abilities, BATTLE_MOVES[fighter->abilities[i]].name);
|
||||
}
|
||||
yyjson_mut_obj_add_val(doc, obj, "abilities", abilities);
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
void battleFighterInit(
|
||||
battlefighter_t *fighter,
|
||||
const battlefighterteam_t team,
|
||||
const battlefightercontroller_t controller,
|
||||
const battlefighterstats_t stats
|
||||
) {
|
||||
assertNotNull(fighter, "Fighter pointer cannot be NULL");
|
||||
assertTrue(team < BATTLE_FIGHTER_TEAM_COUNT, "Invalid fighter team");
|
||||
assertTrue(
|
||||
controller < BATTLE_FIGHTER_CONTROLLER_COUNT,
|
||||
"Invalid fighter controller"
|
||||
);
|
||||
|
||||
const uint8_t id = fighter->id;
|
||||
memoryZero(fighter, sizeof(battlefighter_t));
|
||||
fighter->id = id;
|
||||
fighter->status = BATTLE_FIGHTER_STATUS_NORMAL;
|
||||
fighter->team = team;
|
||||
fighter->controller = controller;
|
||||
fighter->stats = stats;
|
||||
fighter->stats.health = stats.healthMax;
|
||||
fighter->stats.mp = stats.mpMax;
|
||||
}
|
||||
|
||||
bool_t battleFighterIsAlive(const battlefighter_t *fighter) {
|
||||
assertNotNull(fighter, "Fighter pointer cannot be NULL");
|
||||
return fighter->status == BATTLE_FIGHTER_STATUS_NORMAL;
|
||||
}
|
||||
|
||||
void battleFighterAbilityAdd(
|
||||
battlefighter_t *fighter,
|
||||
const battlemoveid_t ability
|
||||
) {
|
||||
assertNotNull(fighter, "Fighter pointer cannot be NULL");
|
||||
assertTrue(
|
||||
fighter->abilityCount < BATTLE_FIGHTER_ABILITY_COUNT_MAX,
|
||||
"Fighter already has the maximum number of abilities"
|
||||
);
|
||||
|
||||
fighter->abilities[fighter->abilityCount++] = ability;
|
||||
}
|
||||
|
||||
void battleFighterDamage(
|
||||
battlefighter_t *fighter,
|
||||
const uint16_t amount
|
||||
) {
|
||||
assertNotNull(fighter, "Fighter pointer cannot be NULL");
|
||||
|
||||
fighter->stats.health =
|
||||
amount >= fighter->stats.health ? 0 : fighter->stats.health - amount;
|
||||
if(fighter->stats.health == 0) fighter->status = BATTLE_FIGHTER_STATUS_DEAD;
|
||||
}
|
||||
|
||||
void battleFighterHeal(
|
||||
battlefighter_t *fighter,
|
||||
const uint16_t amount
|
||||
) {
|
||||
assertNotNull(fighter, "Fighter pointer cannot be NULL");
|
||||
if(!battleFighterIsAlive(fighter)) return;
|
||||
|
||||
const uint32_t healed = (uint32_t)fighter->stats.health + amount;
|
||||
fighter->stats.health = healed > fighter->stats.healthMax
|
||||
? fighter->stats.healthMax : (uint16_t)healed;
|
||||
}
|
||||
@@ -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 "error/error.h"
|
||||
#include "yyjson.h"
|
||||
#include "rpg/battle/move/battlemove.h"
|
||||
#include "rpg/battle/fighter/battlefighterstatus.h"
|
||||
#include "rpg/battle/fighter/battlefighterteam.h"
|
||||
#include "rpg/battle/fighter/battlefightercontroller.h"
|
||||
#include "rpg/battle/fighter/battlefighterstats.h"
|
||||
#include "rpg/battle/fighter/battlefighterpos.h"
|
||||
|
||||
#define BATTLE_FIGHTER_ABILITY_COUNT_MAX 8
|
||||
|
||||
typedef struct battlefighter_s {
|
||||
uint8_t id;
|
||||
battlefighterstatus_t status;
|
||||
battlefighterteam_t team;
|
||||
battlefightercontroller_t controller;
|
||||
|
||||
|
||||
battlefighterstats_t stats;
|
||||
|
||||
battlemoveid_t abilities[BATTLE_FIGHTER_ABILITY_COUNT_MAX];
|
||||
battlemoveid_t abilityCount;
|
||||
} battlefighter_t;
|
||||
|
||||
/**
|
||||
* Initializes a battle fighter in place, filling health/mp to the maximum
|
||||
* values carried on stats and setting its status to normal.
|
||||
*
|
||||
* @param fighter Pointer to the fighter to initialize.
|
||||
* @param team The team the fighter belongs to.
|
||||
* @param controller Who makes decisions for the fighter.
|
||||
* @param stats The fighter's base combat stats, including healthMax/mpMax.
|
||||
*/
|
||||
void battleFighterInit(
|
||||
battlefighter_t *fighter,
|
||||
const battlefighterteam_t team,
|
||||
const battlefightercontroller_t controller,
|
||||
const battlefighterstats_t stats
|
||||
);
|
||||
|
||||
/**
|
||||
* Generic battlefighter_t parser, shared by every place that builds a
|
||||
* fighter from JSON (party.jsonc roster members, BATTLE_START enemy
|
||||
* definitions, save-file party members) - see battleFighterSerialize for
|
||||
* the inverse. Reads 'team'/'controller'/'status' as ints (each optional,
|
||||
* defaulting to ALLY/PLAYER/NORMAL respectively - a caller that needs a
|
||||
* different team/controller, e.g. BATTLE_START's enemies, sets those
|
||||
* fields on fighter after this returns), 'attack'/'defense'/'magic'/
|
||||
* 'speed'/'luck'/'healthMax'/'mpMax' as required ints in [0, UINT16_MAX],
|
||||
* 'health'/'mp' as optional ints defaulting to healthMax/mpMax (a fresh
|
||||
* fighter), and an optional 'abilities' array of move name strings. Does
|
||||
* not touch fighter->id - set it (or leave it) before calling, same as
|
||||
* battleFighterInit.
|
||||
*
|
||||
* @param fighter Pointer to the fighter to fill in.
|
||||
* @param obj The JSON object to read fields from.
|
||||
* @return Error if a required field is missing/invalid, or an abilities
|
||||
* entry doesn't name a real move.
|
||||
*/
|
||||
errorret_t battleFighterParse(battlefighter_t *fighter, yyjson_val *obj);
|
||||
|
||||
/**
|
||||
* Inverse of battleFighterParse: builds a new JSON object (not yet
|
||||
* attached anywhere - the caller attaches it, e.g. via
|
||||
* yyjson_mut_arr_add_val/yyjson_mut_obj_add_val) with every field
|
||||
* battleFighterParse reads except id (see its doc comment for why).
|
||||
*
|
||||
* @param fighter The fighter to serialize.
|
||||
* @param doc The mutable document to allocate the object in.
|
||||
* @return The newly built object.
|
||||
*/
|
||||
yyjson_mut_val *battleFighterSerialize(
|
||||
const battlefighter_t *fighter,
|
||||
yyjson_mut_doc *doc
|
||||
);
|
||||
|
||||
/**
|
||||
* Returns true if the fighter is in a state where it can still act (i.e.
|
||||
* is not dead).
|
||||
*
|
||||
* @param fighter Pointer to the fighter to check.
|
||||
* @returns True if the fighter can act.
|
||||
*/
|
||||
bool_t battleFighterIsAlive(const battlefighter_t *fighter);
|
||||
|
||||
/**
|
||||
* Appends an ability to the fighter's ability list.
|
||||
*
|
||||
* @param fighter Pointer to the fighter to add the ability to.
|
||||
* @param ability The ability to add.
|
||||
*/
|
||||
void battleFighterAbilityAdd(
|
||||
battlefighter_t *fighter,
|
||||
const battlemoveid_t ability
|
||||
);
|
||||
|
||||
/**
|
||||
* Subtracts amount from the fighter's health (floored at 0), marking it
|
||||
* dead once health reaches 0.
|
||||
*
|
||||
* @param fighter Pointer to the fighter to damage.
|
||||
* @param amount The amount of damage to apply.
|
||||
*/
|
||||
void battleFighterDamage(
|
||||
battlefighter_t *fighter,
|
||||
const uint16_t amount
|
||||
);
|
||||
|
||||
/**
|
||||
* Adds amount to the fighter's health (capped at healthMax). No-op if the
|
||||
* fighter isn't alive -- healing can't revive a dead fighter.
|
||||
*
|
||||
* @param fighter Pointer to the fighter to heal.
|
||||
* @param amount The amount to heal.
|
||||
*/
|
||||
void battleFighterHeal(
|
||||
battlefighter_t *fighter,
|
||||
const uint16_t amount
|
||||
);
|
||||
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "error/error.h"
|
||||
|
||||
typedef enum {
|
||||
BATTLE_FIGHTER_CONTROLLER_NONE,
|
||||
|
||||
BATTLE_FIGHTER_CONTROLLER_PLAYER,
|
||||
BATTLE_FIGHTER_CONTROLLER_AI,
|
||||
|
||||
BATTLE_FIGHTER_CONTROLLER_COUNT
|
||||
} battlefightercontroller_t;
|
||||
@@ -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 "dusk.h"
|
||||
|
||||
typedef uint8_t battlefighterpos_t;
|
||||
|
||||
#define BATTLE_FIGHTER_POS(x) ((battlefighterpos_t)(x))
|
||||
|
||||
// Left (Enemy side)
|
||||
#define BATTLE_FIGHTER_POS_0 BATTLE_FIGHTER_POS(0)
|
||||
#define BATTLE_FIGHTER_POS_1 BATTLE_FIGHTER_POS(1)
|
||||
#define BATTLE_FIGHTER_POS_2 BATTLE_FIGHTER_POS(2)
|
||||
#define BATTLE_FIGHTER_POS_3 BATTLE_FIGHTER_POS(3)
|
||||
|
||||
// Right (Ally side)
|
||||
#define BATTLE_FIGHTER_POS_4 BATTLE_FIGHTER_POS(4)
|
||||
#define BATTLE_FIGHTER_POS_5 BATTLE_FIGHTER_POS(5)
|
||||
#define BATTLE_FIGHTER_POS_6 BATTLE_FIGHTER_POS(6)
|
||||
#define BATTLE_FIGHTER_POS_7 BATTLE_FIGHTER_POS(7)
|
||||
|
||||
#define BATTLE_FIGHTER_POS_COUNT (BATTLE_FIGHTER_POS_7 + 1)
|
||||
|
||||
// Sentinels resolved by battleStateExecutingInit against the currently
|
||||
// resolving action, for cutscene items authored inside a move's own
|
||||
// cutscene (e.g. cutscenes/battle/moves/slash.jsonc) that need to refer
|
||||
// to "whoever is being hit"/"whoever is attacking" without knowing their
|
||||
// concrete BATTLE.fighters index ahead of time.
|
||||
#define BATTLE_FIGHTER_POS_TARGET BATTLE_FIGHTER_POS(0xFE)
|
||||
#define BATTLE_FIGHTER_POS_USER BATTLE_FIGHTER_POS(0xFD)
|
||||
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "dusk.h"
|
||||
#include "rpg/battle/fighter/battlefighterstats.h"
|
||||
|
||||
#define BATTLE_FIGHTER_SPECIES_NAME_MAX 16
|
||||
#define BATTLE_FIGHTER_SPECIES_COUNT_MAX 250
|
||||
|
||||
typedef uint8_t battlespeciesid_t;
|
||||
|
||||
#define BATTLE_SPECIES_ID_NULL ((battlespeciesid_t)0)
|
||||
|
||||
typedef struct {
|
||||
char_t name[BATTLE_FIGHTER_SPECIES_NAME_MAX];
|
||||
battlefighterstats_t baseStats;
|
||||
} battlefighterspecies_t;
|
||||
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* 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 {
|
||||
uint16_t health;
|
||||
uint16_t mp;
|
||||
|
||||
uint16_t healthMax;
|
||||
uint16_t mpMax;
|
||||
uint16_t attack;
|
||||
uint16_t defense;
|
||||
uint16_t magic;
|
||||
uint16_t speed;
|
||||
uint16_t luck;
|
||||
} battlefighterstats_t;
|
||||
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "dusk.h"
|
||||
|
||||
typedef enum {
|
||||
BATTLE_FIGHTER_STATUS_NULL,
|
||||
|
||||
BATTLE_FIGHTER_STATUS_NORMAL,
|
||||
BATTLE_FIGHTER_STATUS_DEAD,
|
||||
|
||||
// Poisoned, burned, confused, etc.
|
||||
|
||||
BATTLE_FIGHTER_STATUS_COUNT
|
||||
} battlefighterstatus_t;
|
||||
+7
-2
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
@@ -8,4 +8,9 @@
|
||||
#pragma once
|
||||
#include "dusk.h"
|
||||
|
||||
typedef uint8_t storyflagvalue_t;
|
||||
typedef enum {
|
||||
BATTLE_FIGHTER_TEAM_ALLY,
|
||||
BATTLE_FIGHTER_TEAM_ENEMY,
|
||||
|
||||
BATTLE_FIGHTER_TEAM_COUNT
|
||||
} battlefighterteam_t;
|
||||
+1
-1
@@ -6,5 +6,5 @@
|
||||
# Sources
|
||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
PUBLIC
|
||||
testbattle.c
|
||||
battlemove.c
|
||||
)
|
||||
@@ -0,0 +1,139 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "battlemove.h"
|
||||
#include "asset/asset.h"
|
||||
#include "asset/loader/assetloader.h"
|
||||
#include "assert/assert.h"
|
||||
#include "util/memory.h"
|
||||
#include "util/string.h"
|
||||
#include <string.h>
|
||||
|
||||
battleability_t BATTLE_MOVES[BATTLE_MOVE_COUNT_MAX];
|
||||
uint8_t BATTLE_MOVE_COUNT;
|
||||
|
||||
errorret_t battleMoveInit(void) {
|
||||
memoryZero(BATTLE_MOVES, sizeof(BATTLE_MOVES));
|
||||
BATTLE_MOVE_COUNT = BATTLE_MOVE_ID_NULL + 1;// Reserve NULL.
|
||||
|
||||
// Get moves JSON.
|
||||
assetentry_t *entry = assetLock(
|
||||
"battle/moves.jsonc", ASSET_LOADER_TYPE_JSON, NULL
|
||||
);
|
||||
errorret_t ret = assetRequireLoaded(entry);
|
||||
if(errorIsNotOk(ret)) {
|
||||
assetUnlockEntry(entry);
|
||||
errorChain(ret);
|
||||
}
|
||||
|
||||
yyjson_val *root = yyjson_doc_get_root(entry->data.json);
|
||||
yyjson_val *moves = yyjson_obj_get(root, "moves");
|
||||
yyjson_obj_iter iter = yyjson_obj_iter_with(moves);
|
||||
yyjson_val *key;
|
||||
while((key = yyjson_obj_iter_next(&iter))) {
|
||||
if(BATTLE_MOVE_COUNT >= BATTLE_MOVE_COUNT_MAX) {
|
||||
assetUnlockEntry(entry);
|
||||
errorThrow(
|
||||
"Battle move: too many moves defined (max %u)",
|
||||
(uint32_t)BATTLE_MOVE_COUNT_MAX
|
||||
);
|
||||
}
|
||||
|
||||
battlemoveid_t id = BATTLE_MOVE_COUNT++;
|
||||
battleability_t *move = &BATTLE_MOVES[id];
|
||||
|
||||
// Name
|
||||
if(!yyjson_is_str(key)) {
|
||||
assetUnlockEntry(entry);
|
||||
errorThrow("Battle move: keys in 'moves' must be strings");
|
||||
}
|
||||
const char_t *name = yyjson_get_str(key);
|
||||
stringCopy(move->name, name, BATTLE_MOVE_NAME_MAX);
|
||||
|
||||
if(strlen(name) >= BATTLE_MOVE_NAME_MAX) {
|
||||
assetUnlockEntry(entry);
|
||||
errorThrow(
|
||||
"Battle move '%s': name must be less than %u characters",
|
||||
name, (uint32_t)BATTLE_MOVE_NAME_MAX
|
||||
);
|
||||
}
|
||||
|
||||
// Must me object
|
||||
yyjson_val *val = yyjson_obj_iter_get_val(key);
|
||||
if(!yyjson_is_obj(val)) {
|
||||
assetUnlockEntry(entry);
|
||||
errorThrow("Battle move '%s': value must be an object", name);
|
||||
}
|
||||
|
||||
// Valid targets
|
||||
yyjson_val *enemies = yyjson_obj_get(val, "enemies");
|
||||
if(enemies != NULL && !yyjson_is_bool(enemies)) {
|
||||
assetUnlockEntry(entry);
|
||||
errorThrow("Battle move '%s': 'enemies' must be a boolean", name);
|
||||
}
|
||||
yyjson_val *allies = yyjson_obj_get(val, "allies");
|
||||
if(allies != NULL && !yyjson_is_bool(allies)) {
|
||||
assetUnlockEntry(entry);
|
||||
errorThrow("Battle move '%s': 'allies' must be a boolean", name);
|
||||
}
|
||||
yyjson_val *multiple = yyjson_obj_get(val, "multiple");
|
||||
if(multiple != NULL && !yyjson_is_bool(multiple)) {
|
||||
assetUnlockEntry(entry);
|
||||
errorThrow("Battle move '%s': 'multiple' must be a boolean", name);
|
||||
}
|
||||
yyjson_val *self = yyjson_obj_get(val, "self");
|
||||
if(self != NULL && !yyjson_is_bool(self)) {
|
||||
assetUnlockEntry(entry);
|
||||
errorThrow("Battle move '%s': 'self' must be a boolean", name);
|
||||
}
|
||||
|
||||
battletarget_t target = 0;
|
||||
if(yyjson_get_bool(enemies)) target |= BATTLE_TARGET_ENEMIES;
|
||||
if(yyjson_get_bool(allies)) target |= BATTLE_TARGET_ALLIES;
|
||||
if(yyjson_get_bool(multiple)) target |= BATTLE_TARGET_MULTIPLE;
|
||||
if(yyjson_get_bool(self)) target |= BATTLE_TARGET_SELF;
|
||||
move->target = target;
|
||||
if(move->target == 0) {
|
||||
assetUnlockEntry(entry);
|
||||
errorThrow("Battle move '%s': missing target type", name);
|
||||
}
|
||||
|
||||
// Default target (optional)
|
||||
yyjson_val *defaultVal = yyjson_obj_get(val, "default");
|
||||
if(defaultVal != NULL && !yyjson_is_str(defaultVal)) {
|
||||
assetUnlockEntry(entry);
|
||||
errorThrow("Battle move '%s': 'default' must be a string", name);
|
||||
}
|
||||
|
||||
const char_t *defaultTarget = yyjson_get_str(defaultVal);
|
||||
if(defaultTarget == NULL) {
|
||||
move->targetDefault = 0;
|
||||
} else if(stringCompareInsensitive(defaultTarget, "enemy") == 0) {
|
||||
move->targetDefault = BATTLE_TARGET_ENEMIES;
|
||||
} else if(stringCompareInsensitive(defaultTarget, "ally") == 0) {
|
||||
move->targetDefault = BATTLE_TARGET_ALLIES;
|
||||
} else if(stringCompareInsensitive(defaultTarget, "self") == 0) {
|
||||
move->targetDefault = BATTLE_TARGET_SELF;
|
||||
} else {
|
||||
assetUnlockEntry(entry);
|
||||
errorThrow("Battle move %s has invalid default target.", name);
|
||||
}
|
||||
}
|
||||
|
||||
assetUnlockEntry(entry);
|
||||
errorOk();
|
||||
}
|
||||
|
||||
battlemoveid_t battleMoveGetIdByName(const char_t *name) {
|
||||
assertNotNull(name, "name must not be NULL");
|
||||
|
||||
for(battlemoveid_t id = BATTLE_MOVE_ID_NULL + 1; id < BATTLE_MOVE_COUNT; id++) {
|
||||
if(stringEquals(BATTLE_MOVES[id].name, name)) return id;
|
||||
}
|
||||
|
||||
return BATTLE_MOVE_ID_NULL;
|
||||
}
|
||||
@@ -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 "asset/asset.h"
|
||||
#include "battletarget.h"
|
||||
|
||||
typedef uint8_t battlemoveid_t;
|
||||
|
||||
#define BATTLE_MOVE_NAME_MAX 16
|
||||
#define BATTLE_MOVE_COUNT_MAX 250
|
||||
|
||||
#define BATTLE_MOVE_ID_NULL ((battlemoveid_t)0)
|
||||
|
||||
extern uint8_t BATTLE_MOVE_COUNT;
|
||||
|
||||
typedef struct {
|
||||
char_t name[BATTLE_MOVE_NAME_MAX];
|
||||
battletarget_t target;
|
||||
battletarget_t targetDefault;
|
||||
} battleability_t;
|
||||
|
||||
extern battleability_t BATTLE_MOVES[BATTLE_MOVE_COUNT_MAX];
|
||||
|
||||
/**
|
||||
* Initializes the battle move subsystem, loading assets/battle/moves.jsonc.
|
||||
*
|
||||
* @return Any error that occurs.
|
||||
*/
|
||||
errorret_t battleMoveInit(void);
|
||||
|
||||
/**
|
||||
* Finds a move's id by name (exact match).
|
||||
*
|
||||
* @param name The move name to search for.
|
||||
* @return The matching move's id, or BATTLE_MOVE_ID_NULL if not found.
|
||||
*/
|
||||
battlemoveid_t battleMoveGetIdByName(const char_t *name);
|
||||
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "rpg/battle/move/battlemove.h"
|
||||
#include "rpg/battle/fighter/battlefighterpos.h"
|
||||
|
||||
typedef struct battlefighter_s battlefighter_t;
|
||||
|
||||
typedef struct {
|
||||
battlemoveid_t move;
|
||||
battlefighterpos_t targets[BATTLE_FIGHTER_POS_COUNT];
|
||||
battlefighter_t *user;
|
||||
} battlemoveaction_t;
|
||||
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* 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 battletarget_t;
|
||||
|
||||
#define BATTLE_TARGET_ENEMIES (1 << 0)
|
||||
#define BATTLE_TARGET_ALLIES (1 << 1)
|
||||
#define BATTLE_TARGET_MULTIPLE (1 << 2)
|
||||
#define BATTLE_TARGET_SELF (1 << 3)
|
||||
+147
-30
@@ -8,56 +8,134 @@
|
||||
#include "party.h"
|
||||
#include "assert/assert.h"
|
||||
#include "util/memory.h"
|
||||
#include "util/string.h"
|
||||
#include "save/save.h"
|
||||
#include "asset/asset.h"
|
||||
#include "asset/loader/assetloader.h"
|
||||
|
||||
party_t PARTY;
|
||||
errorret_t partyInit(party_t *party) {
|
||||
assertNotNull(party, "Party cannot be NULL");
|
||||
|
||||
void partyInit(void) {
|
||||
memoryZero(&PARTY, sizeof(party_t));
|
||||
for(uint8_t i = 0; i < PARTY_MEMBER_COUNT_MAX; i++) {
|
||||
PARTY.members[i].id = i;
|
||||
}
|
||||
memoryZero(party, sizeof(party_t));
|
||||
for(uint8_t i = 0; i < PARTY_ACTIVE_SIZE_MAX; i++) {
|
||||
PARTY.order[i] = PARTY_ORDER_EMPTY;
|
||||
party->order[i] = PARTY_ORDER_EMPTY;
|
||||
}
|
||||
|
||||
assetentry_t *entry = assetLock("party.jsonc", ASSET_LOADER_TYPE_JSON, NULL);
|
||||
errorret_t result = assetRequireLoaded(entry);
|
||||
if(errorIsNotOk(result)) {
|
||||
assetUnlockEntry(entry);
|
||||
errorChain(result);
|
||||
}
|
||||
|
||||
yyjson_val *root = yyjson_doc_get_root(entry->data.json);
|
||||
yyjson_val *members = yyjson_obj_get(root, "members");
|
||||
if(!yyjson_is_obj(members)) {
|
||||
assetUnlockEntry(entry);
|
||||
errorThrow("party.jsonc: missing/invalid 'members' object");
|
||||
}
|
||||
|
||||
const size_t memberCount = yyjson_obj_size(members);
|
||||
if(memberCount > PARTY_MEMBER_COUNT_MAX) {
|
||||
assetUnlockEntry(entry);
|
||||
errorThrow(
|
||||
"party.jsonc: defines more than PARTY_MEMBER_COUNT_MAX (%u) members",
|
||||
PARTY_MEMBER_COUNT_MAX
|
||||
);
|
||||
}
|
||||
|
||||
uint8_t idx = 0;
|
||||
yyjson_val *key;
|
||||
yyjson_val *memberObj;
|
||||
yyjson_obj_iter iter = yyjson_obj_iter_with(members);
|
||||
while((key = yyjson_obj_iter_next(&iter))) {
|
||||
memberObj = yyjson_obj_iter_get_val(key);
|
||||
|
||||
if(yyjson_get_len(key) >= PARTY_KEY_LENGTH_MAX) {
|
||||
assetUnlockEntry(entry);
|
||||
errorThrow(
|
||||
"party.jsonc: member key '%s' exceeds PARTY_KEY_LENGTH_MAX (%u)",
|
||||
yyjson_get_str(key), PARTY_KEY_LENGTH_MAX
|
||||
);
|
||||
}
|
||||
|
||||
stringCopy(party->keys[idx], yyjson_get_str(key), PARTY_KEY_LENGTH_MAX - 1);
|
||||
|
||||
battlefighter_t *member = &party->members[idx];
|
||||
member->id = idx;
|
||||
errorret_t parseResult = battleFighterParse(member, memberObj);
|
||||
if(errorIsNotOk(parseResult)) {
|
||||
assetUnlockEntry(entry);
|
||||
errorChain(parseResult);
|
||||
}
|
||||
|
||||
idx++;
|
||||
}
|
||||
|
||||
assetUnlockEntry(entry);
|
||||
|
||||
// TEST: only the first defined member starts active, in the first party
|
||||
// position - replace once there's a real starting-party/character-
|
||||
// creation flow. Deliberately separate from the roster seeding above:
|
||||
// order (not a member's presence in the roster) is what determines who's
|
||||
// currently in the party.
|
||||
if(memberCount > 0) party->order[0] = 0;
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
uint8_t partyGetAvailableMember(void) {
|
||||
uint8_t partyGetMemberIndexByKey(const char_t *key) {
|
||||
assertNotNull(key, "Key cannot be NULL");
|
||||
|
||||
const party_t *party = &SAVE.slot.party;
|
||||
for(uint8_t i = 0; i < PARTY_MEMBER_COUNT_MAX; i++) {
|
||||
if(PARTY.members[i].status == BATTLE_FIGHTER_STATUS_NULL) return i;
|
||||
if(stringEquals(party->keys[i], key)) return i;
|
||||
}
|
||||
|
||||
return 0xFF;
|
||||
return PARTY_MEMBER_NOT_FOUND;
|
||||
}
|
||||
|
||||
battlefighter_t *partyAddMember(
|
||||
const battlefighterstats_t stats,
|
||||
const uint16_t healthMax,
|
||||
const uint16_t mpMax
|
||||
) {
|
||||
const uint8_t index = partyGetAvailableMember();
|
||||
if(index == 0xFF) return NULL;
|
||||
const char_t *partyGetMemberKey(const uint8_t memberIndex) {
|
||||
assertTrue(memberIndex < PARTY_MEMBER_COUNT_MAX, "Invalid party member index");
|
||||
return SAVE.slot.party.keys[memberIndex];
|
||||
}
|
||||
|
||||
battlefighter_t *member = &PARTY.members[index];
|
||||
battleFighterInit(
|
||||
member, BATTLE_FIGHTER_TEAM_ALLY, BATTLE_FIGHTER_CONTROLLER_PLAYER,
|
||||
stats, healthMax, mpMax
|
||||
bool_t partyIsMemberActive(const uint8_t memberIndex) {
|
||||
assertTrue(memberIndex < PARTY_MEMBER_COUNT_MAX, "Invalid party member index");
|
||||
|
||||
const party_t *party = &SAVE.slot.party;
|
||||
for(uint8_t i = 0; i < PARTY_ACTIVE_SIZE_MAX; i++) {
|
||||
if(party->order[i] == memberIndex) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void partyAddMember(const uint8_t memberIndex) {
|
||||
assertTrue(memberIndex < PARTY_MEMBER_COUNT_MAX, "Invalid party member index");
|
||||
|
||||
party_t *party = &SAVE.slot.party;
|
||||
assertTrue(
|
||||
party->members[memberIndex].status != BATTLE_FIGHTER_STATUS_NULL,
|
||||
"Member does not exist in the roster"
|
||||
);
|
||||
|
||||
for(uint8_t i = 0; i < PARTY_ACTIVE_SIZE_MAX; i++) {
|
||||
if(PARTY.order[i] != PARTY_ORDER_EMPTY) continue;
|
||||
PARTY.order[i] = index;
|
||||
break;
|
||||
}
|
||||
if(partyIsMemberActive(memberIndex)) return;
|
||||
|
||||
return member;
|
||||
for(uint8_t i = 0; i < PARTY_ACTIVE_SIZE_MAX; i++) {
|
||||
if(party->order[i] != PARTY_ORDER_EMPTY) continue;
|
||||
party->order[i] = memberIndex;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
battlefighter_t *partyGetOrderMember(const uint8_t slot) {
|
||||
assertTrue(slot < PARTY_ACTIVE_SIZE_MAX, "Invalid party order slot");
|
||||
|
||||
const uint8_t index = PARTY.order[slot];
|
||||
party_t *party = &SAVE.slot.party;
|
||||
const uint8_t index = party->order[slot];
|
||||
if(index == PARTY_ORDER_EMPTY) return NULL;
|
||||
return &PARTY.members[index];
|
||||
return &party->members[index];
|
||||
}
|
||||
|
||||
void partySetOrder(const uint8_t slot, const uint8_t memberIndex) {
|
||||
@@ -67,5 +145,44 @@ void partySetOrder(const uint8_t slot, const uint8_t memberIndex) {
|
||||
"Invalid party member index"
|
||||
);
|
||||
|
||||
PARTY.order[slot] = memberIndex;
|
||||
SAVE.slot.party.order[slot] = memberIndex;
|
||||
}
|
||||
|
||||
void partyRemoveMember(const uint8_t memberIndex) {
|
||||
assertTrue(memberIndex < PARTY_MEMBER_COUNT_MAX, "Invalid party member index");
|
||||
|
||||
party_t *party = &SAVE.slot.party;
|
||||
|
||||
uint8_t removedSlot = PARTY_ORDER_EMPTY;
|
||||
for(uint8_t i = 0; i < PARTY_ACTIVE_SIZE_MAX; i++) {
|
||||
if(party->order[i] != memberIndex) continue;
|
||||
removedSlot = i;
|
||||
break;
|
||||
}
|
||||
if(removedSlot == PARTY_ORDER_EMPTY) return;
|
||||
|
||||
for(uint8_t i = removedSlot; i < PARTY_ACTIVE_SIZE_MAX - 1; i++) {
|
||||
party->order[i] = party->order[i + 1];
|
||||
}
|
||||
party->order[PARTY_ACTIVE_SIZE_MAX - 1] = PARTY_ORDER_EMPTY;
|
||||
}
|
||||
|
||||
void partyHealMember(const uint8_t memberIndex) {
|
||||
assertTrue(memberIndex < PARTY_MEMBER_COUNT_MAX, "Invalid party member index");
|
||||
|
||||
battlefighter_t *member = &SAVE.slot.party.members[memberIndex];
|
||||
if(member->status == BATTLE_FIGHTER_STATUS_NULL) return;
|
||||
|
||||
member->status = BATTLE_FIGHTER_STATUS_NORMAL;
|
||||
member->stats.health = member->stats.healthMax;
|
||||
member->stats.mp = member->stats.mpMax;
|
||||
}
|
||||
|
||||
void partyHealAll(void) {
|
||||
const party_t *party = &SAVE.slot.party;
|
||||
|
||||
for(uint8_t i = 0; i < PARTY_MEMBER_COUNT_MAX; i++) {
|
||||
if(party->members[i].status == BATTLE_FIGHTER_STATUS_NULL) continue;
|
||||
partyHealMember(i);
|
||||
}
|
||||
}
|
||||
|
||||
+87
-34
@@ -6,55 +6,80 @@
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "battlefighter.h"
|
||||
#include "rpg/battle/fighter/battlefighter.h"
|
||||
#include "yyjson.h"
|
||||
|
||||
#define PARTY_MEMBER_COUNT_MAX 4
|
||||
#define PARTY_ACTIVE_SIZE_MAX 3
|
||||
#define PARTY_MEMBER_COUNT_MAX 10
|
||||
#define PARTY_ACTIVE_SIZE_MAX 4
|
||||
#define PARTY_KEY_LENGTH_MAX 32
|
||||
#define PARTY_ORDER_EMPTY 0xFF
|
||||
#define PARTY_MEMBER_NOT_FOUND 0xFF
|
||||
|
||||
typedef struct {
|
||||
battlefighter_t members[PARTY_MEMBER_COUNT_MAX];
|
||||
|
||||
// Maps an active battle slot to the roster member filling it, or
|
||||
// PARTY_ORDER_EMPTY if the slot is unfilled. Only the first
|
||||
// PARTY_ACTIVE_SIZE_MAX of the PARTY_MEMBER_COUNT_MAX roster members
|
||||
// can be in the active lineup at once.
|
||||
char_t keys[PARTY_MEMBER_COUNT_MAX][PARTY_KEY_LENGTH_MAX];
|
||||
uint8_t order[PARTY_ACTIVE_SIZE_MAX];
|
||||
} party_t;
|
||||
|
||||
extern party_t PARTY;
|
||||
|
||||
/**
|
||||
* Initializes the party system with an empty roster and order.
|
||||
*/
|
||||
void partyInit(void);
|
||||
|
||||
/**
|
||||
* Gets an available (unused) party member slot index.
|
||||
* Initializes party with an empty order, then seeds every roster slot
|
||||
* directly from party.jsonc's "members" object (each key/value pair fills
|
||||
* the next members[]/keys[] slot in the object's iteration order, key
|
||||
* copied into keys[] and value parsed via battleFighterParse) - unlike
|
||||
* partyAddMember, this never touches order itself except for one
|
||||
* placeholder default: if party.jsonc defines at least one member,
|
||||
* order[0] is set to 0 (the first member starts active in the first party
|
||||
* position). Replace this default once there's a real starting-party/
|
||||
* character-creation flow.
|
||||
*
|
||||
* @return The index of an available slot, or 0xFF if the party is full.
|
||||
* @param party The party to initialize.
|
||||
* @return Error if party.jsonc fails to load/parse, defines more members
|
||||
* than PARTY_MEMBER_COUNT_MAX has room for, or a key exceeds
|
||||
* PARTY_KEY_LENGTH_MAX.
|
||||
*/
|
||||
uint8_t partyGetAvailableMember(void);
|
||||
errorret_t partyInit(party_t *party);
|
||||
|
||||
/**
|
||||
* Adds a member to the party roster in the next available slot. Party
|
||||
* members are always allies controlled by the player. If there is a
|
||||
* free active order slot, the new member is placed into it.
|
||||
* Looks up a roster member's index by its party.jsonc key (see
|
||||
* party_t.keys).
|
||||
*
|
||||
* @param stats The member's base combat stats.
|
||||
* @param healthMax The member's maximum health.
|
||||
* @param mpMax The member's maximum mp.
|
||||
* @return Pointer to the newly added party member, or NULL if the party is
|
||||
* already full.
|
||||
* @param key The member's key to search for.
|
||||
* @return The matching roster index, or PARTY_MEMBER_NOT_FOUND if no
|
||||
* member has that key.
|
||||
*/
|
||||
battlefighter_t *partyAddMember(
|
||||
const battlefighterstats_t stats,
|
||||
const uint16_t healthMax,
|
||||
const uint16_t mpMax
|
||||
);
|
||||
uint8_t partyGetMemberIndexByKey(const char_t *key);
|
||||
|
||||
/**
|
||||
* Gets the roster member currently occupying an active order slot.
|
||||
* Gets a roster member's party.jsonc key by index (see party_t.keys).
|
||||
*
|
||||
* @param memberIndex The roster member index to query.
|
||||
* @return The member's key - "" if memberIndex is past however many
|
||||
* members party.jsonc actually defines.
|
||||
*/
|
||||
const char_t *partyGetMemberKey(const uint8_t memberIndex);
|
||||
|
||||
/**
|
||||
* Checks whether memberIndex currently occupies a slot in the current save
|
||||
* slot's party order (i.e. is actively in the party right now).
|
||||
*
|
||||
* @param memberIndex The roster member index to check.
|
||||
* @return True if memberIndex appears anywhere in order.
|
||||
*/
|
||||
bool_t partyIsMemberActive(const uint8_t memberIndex);
|
||||
|
||||
/**
|
||||
* Adds an existing roster member (see partyInit - every roster slot
|
||||
* always exists once initialized) to the current save slot's active party
|
||||
* order, in the first free order slot. No-op if memberIndex is already
|
||||
* active, or if there is no free order slot.
|
||||
*
|
||||
* @param memberIndex The roster member index to activate.
|
||||
*/
|
||||
void partyAddMember(const uint8_t memberIndex);
|
||||
|
||||
/**
|
||||
* Gets the roster member currently occupying an active order slot in the
|
||||
* current save slot's party.
|
||||
*
|
||||
* @param slot The active order slot to query.
|
||||
* @return Pointer to the member in that slot, or NULL if the slot is
|
||||
@@ -63,11 +88,39 @@ battlefighter_t *partyAddMember(
|
||||
battlefighter_t *partyGetOrderMember(const uint8_t slot);
|
||||
|
||||
/**
|
||||
* Assigns a roster member to an active order slot, replacing whatever
|
||||
* was there. Use PARTY_ORDER_EMPTY to clear a slot.
|
||||
* Assigns a roster member to an active order slot in the current save
|
||||
* slot's party, replacing whatever was there. Use PARTY_ORDER_EMPTY to
|
||||
* clear a slot.
|
||||
*
|
||||
* @param slot The active order slot to assign.
|
||||
* @param memberIndex The roster member index to place there, or
|
||||
* PARTY_ORDER_EMPTY to clear the slot.
|
||||
*/
|
||||
void partySetOrder(const uint8_t slot, const uint8_t memberIndex);
|
||||
|
||||
/**
|
||||
* Removes memberIndex from the current save slot's active party order -
|
||||
* the roster member itself is untouched (it still exists, per partyInit;
|
||||
* it's simply no longer active) - shifting every later order entry down
|
||||
* one slot to close the gap, so order stays packed with no holes between
|
||||
* active members. No-op if memberIndex isn't currently active.
|
||||
*
|
||||
* @param memberIndex The roster member index to deactivate.
|
||||
*/
|
||||
void partyRemoveMember(const uint8_t memberIndex);
|
||||
|
||||
/**
|
||||
* Fully restores a roster member's health/mp to their max and clears a
|
||||
* dead status back to normal - unlike battleFighterHeal, this can revive
|
||||
* a dead member (there's no combat context here to keep them down). No-op
|
||||
* if the slot is empty.
|
||||
*
|
||||
* @param memberIndex The roster member index to heal.
|
||||
*/
|
||||
void partyHealMember(const uint8_t memberIndex);
|
||||
|
||||
/**
|
||||
* Calls partyHealMember for every occupied slot in the current save
|
||||
* slot's party roster.
|
||||
*/
|
||||
void partyHealAll(void);
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
# Copyright (c) 2026 Dominic Masters
|
||||
#
|
||||
# This software is released under the MIT License.
|
||||
# https://opensource.org/licenses/MIT
|
||||
|
||||
# Sources
|
||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
PUBLIC
|
||||
battlestate.c
|
||||
battlestateopening.c
|
||||
battlestatepreround.c
|
||||
battlestateselection.c
|
||||
battlestateexecuting.c
|
||||
battlestatepostround.c
|
||||
battlestatelost.c
|
||||
battlestatewon.c
|
||||
battlestatefled.c
|
||||
battlestateending.c
|
||||
)
|
||||
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "battlestate.h"
|
||||
|
||||
battlestatefunctions_t BATTLE_STATE_FUNCTIONS[BATTLE_STATE_COUNT] = {
|
||||
[BATTLE_STATE_NULL] = { 0 },
|
||||
|
||||
[BATTLE_STATE_OPENING] = {
|
||||
.init = battleStateOpeningInit,
|
||||
.update = battleStateOpeningUpdate,
|
||||
.dispose = battleStateOpeningDispose
|
||||
},
|
||||
|
||||
[BATTLE_STATE_PRE_ROUND] = {
|
||||
.init = battleStatePreRoundInit,
|
||||
.update = battleStatePreRoundUpdate,
|
||||
.dispose = battleStatePreRoundDispose
|
||||
},
|
||||
|
||||
[BATTLE_STATE_SELECTION] = {
|
||||
.init = battleStateSelectionInit,
|
||||
.update = battleStateSelectionUpdate,
|
||||
.dispose = battleStateSelectionDispose
|
||||
},
|
||||
|
||||
[BATTLE_STATE_EXECUTING] = {
|
||||
.init = battleStateExecutingInit,
|
||||
.update = battleStateExecutingUpdate,
|
||||
.dispose = battleStateExecutingDispose
|
||||
},
|
||||
|
||||
[BATTLE_STATE_POST_ROUND] = {
|
||||
.init = battleStatePostRoundInit,
|
||||
.update = battleStatePostRoundUpdate,
|
||||
.dispose = battleStatePostRoundDispose
|
||||
},
|
||||
|
||||
[BATTLE_STATE_LOST] = {
|
||||
.init = battleStateLostInit,
|
||||
.update = battleStateLostUpdate,
|
||||
.dispose = battleStateLostDispose
|
||||
},
|
||||
|
||||
[BATTLE_STATE_WON] = {
|
||||
.init = battleStateWonInit,
|
||||
.update = battleStateWonUpdate,
|
||||
.dispose = battleStateWonDispose
|
||||
},
|
||||
|
||||
[BATTLE_STATE_FLED] = {
|
||||
.init = battleStateFledInit,
|
||||
.update = battleStateFledUpdate,
|
||||
.dispose = battleStateFledDispose
|
||||
},
|
||||
|
||||
[BATTLE_STATE_ENDING] = {
|
||||
.init = battleStateEndingInit,
|
||||
.update = battleStateEndingUpdate,
|
||||
.dispose = battleStateEndingDispose
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "battlestateopening.h"
|
||||
#include "battlestatepreround.h"
|
||||
#include "battlestateselection.h"
|
||||
#include "battlestateexecuting.h"
|
||||
#include "battlestatepostround.h"
|
||||
#include "battlestatelost.h"
|
||||
#include "battlestatewon.h"
|
||||
#include "battlestatefled.h"
|
||||
#include "battlestateending.h"
|
||||
|
||||
typedef enum {
|
||||
// No active battle.
|
||||
BATTLE_STATE_NULL,
|
||||
|
||||
// Battle hasn't actually started yet, equiv to trainer intro in Pokemon.
|
||||
BATTLE_STATE_OPENING,
|
||||
// Pre round, anything that happens before AI or Player can act.
|
||||
BATTLE_STATE_PRE_ROUND,
|
||||
// Player is deciding what to do, waiting for their input
|
||||
BATTLE_STATE_SELECTION,
|
||||
// Currently executing the moves.
|
||||
BATTLE_STATE_EXECUTING,
|
||||
// Post round, anything that happens after AI or Player has acted, loops back
|
||||
// after this point
|
||||
BATTLE_STATE_POST_ROUND,
|
||||
// Battle has ended with a player loss
|
||||
BATTLE_STATE_LOST,
|
||||
// Battle has ended with a player win
|
||||
BATTLE_STATE_WON,
|
||||
// Battle has ended with a player fleeing
|
||||
BATTLE_STATE_FLED,
|
||||
// Battle state is done, this is fade out or whatever.
|
||||
BATTLE_STATE_ENDING,
|
||||
|
||||
BATTLE_STATE_COUNT
|
||||
} battlestate_t;
|
||||
|
||||
typedef union {
|
||||
battlestateopening_t opening;
|
||||
battlestatepreround_t preRound;
|
||||
battlestateselection_t selection;
|
||||
battlestateexecuting_t executing;
|
||||
battlestatepostround_t postRound;
|
||||
battlestatelost_t lost;
|
||||
battlestatewon_t won;
|
||||
battlestatefled_t fled;
|
||||
battlestateending_t ending;
|
||||
} battlestatedata_t;
|
||||
|
||||
typedef struct {
|
||||
void (*init)();
|
||||
void (*update)();
|
||||
void (*dispose)();
|
||||
} battlestatefunctions_t;
|
||||
|
||||
extern battlestatefunctions_t BATTLE_STATE_FUNCTIONS[BATTLE_STATE_COUNT];
|
||||
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "battlestateending.h"
|
||||
#include "rpg/battle/battle.h"
|
||||
#include "scene/scene.h"
|
||||
|
||||
void battleStateEndingInit(void) {
|
||||
// Tear down the battle and hand control back to the overworld. The
|
||||
// overworld scene keeps no state of its own in scenedata_t (player/map
|
||||
// state lives in the separate global entity/map systems, untouched by
|
||||
// switching to/from SCENE_TYPE_BATTLE), so there's nothing else to
|
||||
// restore here.
|
||||
battleDispose();
|
||||
sceneSet(SCENE_TYPE_OVERWORLD);
|
||||
}
|
||||
|
||||
void battleStateEndingUpdate(void) {
|
||||
|
||||
}
|
||||
|
||||
void battleStateEndingDispose(void) {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* 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 {
|
||||
|
||||
} battlestateending_t;
|
||||
|
||||
/**
|
||||
* Called when the battle enters BATTLE_STATE_ENDING. Disposes the battle
|
||||
* (see battleDispose) and switches back to SCENE_TYPE_OVERWORLD.
|
||||
*/
|
||||
void battleStateEndingInit(void);
|
||||
|
||||
/**
|
||||
* Updates BATTLE_STATE_ENDING for one frame.
|
||||
*/
|
||||
void battleStateEndingUpdate(void);
|
||||
|
||||
/**
|
||||
* Called when the battle leaves BATTLE_STATE_ENDING.
|
||||
*/
|
||||
void battleStateEndingDispose(void);
|
||||
@@ -0,0 +1,116 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "battlestateexecuting.h"
|
||||
#include "rpg/battle/battle.h"
|
||||
#include "rpg/cutscene/item/cutsceneitem.h"
|
||||
#include "rpg/cutscene/cutscenesystem.h"
|
||||
#include "rpg/cutscene/cutscene.h"
|
||||
#include "asset/assetfile.h"
|
||||
#include "ui/overlay/uifatalerror.h"
|
||||
#include "assert/assert.h"
|
||||
#include "util/string.h"
|
||||
|
||||
|
||||
void battleStateExecutingResolveFighterIndex(
|
||||
battlefighterpos_t *fighterIndex,
|
||||
const uint8_t userIndex,
|
||||
const battlemoveaction_t *action
|
||||
) {
|
||||
if(*fighterIndex == BATTLE_FIGHTER_POS_TARGET) {
|
||||
*fighterIndex = action->targets[0];
|
||||
} else if(*fighterIndex == BATTLE_FIGHTER_POS_USER) {
|
||||
*fighterIndex = userIndex;
|
||||
}
|
||||
}
|
||||
|
||||
void battleStateExecutingInit(void) {
|
||||
cutsceneitem_t items[BATTLE_STATE_EXECUTING_ITEMS_MAX];
|
||||
uint8_t itemsCount = 0;
|
||||
|
||||
// For each action (just queued during selection). actionsPending is
|
||||
// indexed by fighter position, not sequentially, so scan every position
|
||||
// and stop early once every pending action has been processed.
|
||||
uint8_t processed = 0;
|
||||
for(
|
||||
uint8_t i = 0;
|
||||
i < BATTLE_FIGHTER_POS_COUNT && processed < BATTLE.actionsPendingCount;
|
||||
i++
|
||||
) {
|
||||
// Get action.
|
||||
battlemoveaction_t *action = &BATTLE.actionsPending[i];
|
||||
if(action->move == BATTLE_MOVE_ID_NULL) continue;
|
||||
processed++;
|
||||
|
||||
assertTrue(action->move < BATTLE_MOVE_COUNT, "Invalid move id");
|
||||
|
||||
// Get ability.
|
||||
battleability_t *ability = &BATTLE_MOVES[action->move];
|
||||
assertStrLenMin(ability->name, 1, "Invalid move name");
|
||||
|
||||
// Determine cutscene path
|
||||
char_t path[ASSET_FILE_NAME_MAX];
|
||||
stringFormat(
|
||||
path, ASSET_FILE_NAME_MAX - 1,
|
||||
"cutscenes/battle/moves/%s.jsonc", ability->name
|
||||
);
|
||||
|
||||
// Load and parse the move's cutscene straight into the tail of items.
|
||||
const uint8_t moveItemsStart = itemsCount;
|
||||
cutscene_t scene;
|
||||
errorret_t result = cutsceneLoadParse(
|
||||
path, &scene, &items[itemsCount],
|
||||
BATTLE_STATE_EXECUTING_ITEMS_MAX - itemsCount
|
||||
);
|
||||
if(errorIsNotOk(result)) {
|
||||
errorCatch(errorPrint(result));
|
||||
uiFatalErrorOpen(NULL);
|
||||
continue;
|
||||
}
|
||||
itemsCount += scene.itemCount;
|
||||
|
||||
// Resolve BATTLE_FIGHTER_POS_TARGET/USER sentinels against this
|
||||
// action, now that its user/target fighters are known.
|
||||
for(uint8_t j = moveItemsStart; j < itemsCount; j++) {
|
||||
cutsceneitem_t *moveItem = &items[j];
|
||||
if(moveItem->type == CUTSCENE_ITEM_TYPE_BATTLE_FIGHTER_DAMAGE) {
|
||||
battleStateExecutingResolveFighterIndex(
|
||||
&moveItem->battleFighterDamage.fighterIndex, i, action
|
||||
);
|
||||
} else if(moveItem->type == CUTSCENE_ITEM_TYPE_BATTLE_FIGHTER_HEAL) {
|
||||
battleStateExecutingResolveFighterIndex(
|
||||
&moveItem->battleFighterHeal.fighterIndex, i, action
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Insert end of attack cutscene item.
|
||||
items[itemsCount++] = (cutsceneitem_t){
|
||||
.type = CUTSCENE_ITEM_TYPE_BATTLE_POST_MOVE
|
||||
};
|
||||
|
||||
// This action has now been resolved - clear it so it isn't
|
||||
// re-executed (and doesn't block re-selection) next round.
|
||||
action->move = BATTLE_MOVE_ID_NULL;
|
||||
}
|
||||
BATTLE.actionsPendingCount = 0;
|
||||
|
||||
// After all moves completed, transition to pre-round for next round.
|
||||
items[itemsCount++] = (cutsceneitem_t){
|
||||
.type = CUTSCENE_ITEM_TYPE_BATTLE_SET_STATE,
|
||||
.battleSetState = { .state = BATTLE_STATE_PRE_ROUND }
|
||||
};
|
||||
cutsceneSystemInsertItemsNext(items, itemsCount);
|
||||
}
|
||||
|
||||
void battleStateExecutingUpdate(void) {
|
||||
|
||||
}
|
||||
|
||||
void battleStateExecutingDispose(void) {
|
||||
|
||||
}
|
||||
@@ -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"
|
||||
#include "rpg/battle/fighter/battlefighterpos.h"
|
||||
#include "rpg/battle/move/battlemoveaction.h"
|
||||
|
||||
typedef struct {
|
||||
|
||||
} battlestateexecuting_t;
|
||||
|
||||
#define BATTLE_STATE_EXECUTING_ITEMS_MAX 64
|
||||
|
||||
/**
|
||||
* Called when the battle enters BATTLE_STATE_EXECUTING.
|
||||
*/
|
||||
void battleStateExecutingInit(void);
|
||||
|
||||
/**
|
||||
* Resolves a BATTLE_FIGHTER_POS_TARGET/BATTLE_FIGHTER_POS_USER sentinel
|
||||
* (as parsed onto a BATTLE_FIGHTER_DAMAGE/BATTLE_FIGHTER_HEAL item inside
|
||||
* a move's own cutscene) against the action currently being resolved
|
||||
* into cutscene items. Leaves fighterIndex untouched if it's already a
|
||||
* concrete BATTLE.fighters index.
|
||||
*
|
||||
* @param fighterIndex The field to resolve in place.
|
||||
* @param userIndex Index into BATTLE.fighters of the fighter performing
|
||||
* the action.
|
||||
* @param action The action being resolved.
|
||||
*/
|
||||
void battleStateExecutingResolveFighterIndex(
|
||||
battlefighterpos_t *fighterIndex,
|
||||
const uint8_t userIndex,
|
||||
const battlemoveaction_t *action
|
||||
);
|
||||
|
||||
/**
|
||||
* Updates BATTLE_STATE_EXECUTING for one frame.
|
||||
*/
|
||||
void battleStateExecutingUpdate(void);
|
||||
|
||||
/**
|
||||
* Called when the battle leaves BATTLE_STATE_EXECUTING.
|
||||
*/
|
||||
void battleStateExecutingDispose(void);
|
||||
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "battlestatefled.h"
|
||||
#include "rpg/cutscene/item/cutsceneitem.h"
|
||||
#include "rpg/cutscene/cutscenesystem.h"
|
||||
|
||||
void battleStateFledInit(void) {
|
||||
const cutsceneitem_t items[] = {
|
||||
{
|
||||
.type = CUTSCENE_ITEM_TYPE_TEXT,
|
||||
.text = { .text = "Got away safely!" }
|
||||
},
|
||||
{
|
||||
.type = CUTSCENE_ITEM_TYPE_BATTLE_SET_STATE,
|
||||
.battleSetState = { .state = BATTLE_STATE_ENDING }
|
||||
}
|
||||
};
|
||||
cutsceneSystemInsertItemsNext(items, sizeof(items) / sizeof(cutsceneitem_t));
|
||||
}
|
||||
|
||||
void battleStateFledUpdate(void) {
|
||||
|
||||
}
|
||||
|
||||
void battleStateFledDispose(void) {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* 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 {
|
||||
|
||||
} battlestatefled_t;
|
||||
|
||||
/**
|
||||
* Called when the battle enters BATTLE_STATE_FLED. Queues a textbox
|
||||
* announcing the escape, then transitions to BATTLE_STATE_ENDING once
|
||||
* dismissed.
|
||||
*/
|
||||
void battleStateFledInit(void);
|
||||
|
||||
/**
|
||||
* Updates BATTLE_STATE_FLED for one frame.
|
||||
*/
|
||||
void battleStateFledUpdate(void);
|
||||
|
||||
/**
|
||||
* Called when the battle leaves BATTLE_STATE_FLED.
|
||||
*/
|
||||
void battleStateFledDispose(void);
|
||||
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "battlestatelost.h"
|
||||
#include "rpg/cutscene/item/cutsceneitem.h"
|
||||
#include "rpg/cutscene/cutscenesystem.h"
|
||||
|
||||
void battleStateLostInit(void) {
|
||||
const cutsceneitem_t items[] = {
|
||||
{
|
||||
.type = CUTSCENE_ITEM_TYPE_TEXT,
|
||||
.text = { .text = "You were defeated..." }
|
||||
},
|
||||
{
|
||||
.type = CUTSCENE_ITEM_TYPE_BATTLE_SET_STATE,
|
||||
.battleSetState = { .state = BATTLE_STATE_ENDING }
|
||||
}
|
||||
};
|
||||
cutsceneSystemInsertItemsNext(items, sizeof(items) / sizeof(cutsceneitem_t));
|
||||
}
|
||||
|
||||
void battleStateLostUpdate(void) {
|
||||
|
||||
}
|
||||
|
||||
void battleStateLostDispose(void) {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* 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 {
|
||||
|
||||
} battlestatelost_t;
|
||||
|
||||
/**
|
||||
* Called when the battle enters BATTLE_STATE_LOST. Queues a textbox
|
||||
* announcing the loss, then transitions to BATTLE_STATE_ENDING once
|
||||
* dismissed.
|
||||
*/
|
||||
void battleStateLostInit(void);
|
||||
|
||||
/**
|
||||
* Updates BATTLE_STATE_LOST for one frame.
|
||||
*/
|
||||
void battleStateLostUpdate(void);
|
||||
|
||||
/**
|
||||
* Called when the battle leaves BATTLE_STATE_LOST.
|
||||
*/
|
||||
void battleStateLostDispose(void);
|
||||
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "battlestateopening.h"
|
||||
#include "rpg/cutscene/item/cutsceneitem.h"
|
||||
#include "rpg/cutscene/cutscenesystem.h"
|
||||
|
||||
void battleStateOpeningInit(void) {
|
||||
const cutsceneitem_t items[] = {
|
||||
{
|
||||
.type = CUTSCENE_ITEM_TYPE_BATTLE_SET_STATE,
|
||||
.battleSetState = { .state = BATTLE_STATE_PRE_ROUND }
|
||||
}
|
||||
};
|
||||
cutsceneSystemInsertItemsNext(items, sizeof(items) / sizeof(cutsceneitem_t));
|
||||
}
|
||||
|
||||
void battleStateOpeningUpdate(void) {
|
||||
|
||||
}
|
||||
|
||||
void battleStateOpeningDispose(void) {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* 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 {
|
||||
|
||||
} battlestateopening_t;
|
||||
|
||||
/**
|
||||
* Called when the battle enters BATTLE_STATE_OPENING.
|
||||
*/
|
||||
void battleStateOpeningInit(void);
|
||||
|
||||
/**
|
||||
* Updates BATTLE_STATE_OPENING for one frame.
|
||||
*/
|
||||
void battleStateOpeningUpdate(void);
|
||||
|
||||
/**
|
||||
* Called when the battle leaves BATTLE_STATE_OPENING.
|
||||
*/
|
||||
void battleStateOpeningDispose(void);
|
||||
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "battlestatepostround.h"
|
||||
|
||||
void battleStatePostRoundInit(void) {
|
||||
|
||||
}
|
||||
|
||||
void battleStatePostRoundUpdate(void) {
|
||||
|
||||
}
|
||||
|
||||
void battleStatePostRoundDispose(void) {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* 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 {
|
||||
|
||||
} battlestatepostround_t;
|
||||
|
||||
/**
|
||||
* Called when the battle enters BATTLE_STATE_POST_ROUND.
|
||||
*/
|
||||
void battleStatePostRoundInit(void);
|
||||
|
||||
/**
|
||||
* Updates BATTLE_STATE_POST_ROUND for one frame.
|
||||
*/
|
||||
void battleStatePostRoundUpdate(void);
|
||||
|
||||
/**
|
||||
* Called when the battle leaves BATTLE_STATE_POST_ROUND.
|
||||
*/
|
||||
void battleStatePostRoundDispose(void);
|
||||
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "battlestatepreround.h"
|
||||
#include "rpg/battle/battle.h"
|
||||
#include "rpg/cutscene/item/cutsceneitem.h"
|
||||
#include "rpg/cutscene/cutscenesystem.h"
|
||||
|
||||
void battleStatePreRoundInit(void) {
|
||||
// TODO: Determine selection order and who gets to act.
|
||||
BATTLE.fighterOrderCount = 0;
|
||||
for(uint8_t i = 0; i < BATTLE_FIGHTER_POS_COUNT; i++) {
|
||||
if(BATTLE.fighters[i] == NULL) continue;
|
||||
if(!battleFighterIsAlive(BATTLE.fighters[i])) continue;
|
||||
BATTLE.fighterOrder[BATTLE.fighterOrderCount++] = i;
|
||||
}
|
||||
|
||||
// Queue up the set state and wait for selection to stop cutscene ending.
|
||||
cutsceneitem_t items[] = {
|
||||
{
|
||||
.type = CUTSCENE_ITEM_TYPE_BATTLE_SET_STATE,
|
||||
.battleSetState = { .state = BATTLE_STATE_SELECTION }
|
||||
},
|
||||
{
|
||||
.type = CUTSCENE_ITEM_TYPE_BATTLE_WAIT_SELECTION
|
||||
}
|
||||
};
|
||||
cutsceneSystemInsertItemsNext(items, sizeof(items) / sizeof(cutsceneitem_t));
|
||||
}
|
||||
|
||||
void battleStatePreRoundUpdate(void) {
|
||||
|
||||
}
|
||||
|
||||
void battleStatePreRoundDispose(void) {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* 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 {
|
||||
|
||||
} battlestatepreround_t;
|
||||
|
||||
/**
|
||||
* Called when the battle enters BATTLE_STATE_PRE_ROUND.
|
||||
*/
|
||||
void battleStatePreRoundInit(void);
|
||||
|
||||
/**
|
||||
* Updates BATTLE_STATE_PRE_ROUND for one frame.
|
||||
*/
|
||||
void battleStatePreRoundUpdate(void);
|
||||
|
||||
/**
|
||||
* Called when the battle leaves BATTLE_STATE_PRE_ROUND.
|
||||
*/
|
||||
void battleStatePreRoundDispose(void);
|
||||
@@ -0,0 +1,146 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "battlestateselection.h"
|
||||
#include "rpg/battle/battle.h"
|
||||
#include "assert/assert.h"
|
||||
|
||||
void battleStateSelectionInit(void) {
|
||||
BATTLE.stateData.selection.selectionIndex = 0;
|
||||
}
|
||||
|
||||
bool_t battleStateSelectionFighterHasDecided(const uint8_t fighterIndex) {
|
||||
assertTrue(fighterIndex < BATTLE_FIGHTER_POS_COUNT, "Fighter OOB");
|
||||
return BATTLE.actionsPending[fighterIndex].move != BATTLE_MOVE_ID_NULL;
|
||||
}
|
||||
|
||||
bool_t battleStateSelectionFighterNeedsDecision(const uint8_t fighterIndex) {
|
||||
assertTrue(fighterIndex < BATTLE_FIGHTER_POS_COUNT, "Fighter OOB");
|
||||
|
||||
battlefighter_t *fighter = BATTLE.fighters[fighterIndex];
|
||||
if(fighter == NULL) return false;
|
||||
if(!battleFighterIsAlive(fighter)) return false;
|
||||
|
||||
return !battleStateSelectionFighterHasDecided(fighterIndex);
|
||||
}
|
||||
|
||||
bool_t battleStateSelectionIsComplete(void) {
|
||||
return BATTLE.stateData.selection.selectionIndex >= BATTLE.fighterOrderCount;
|
||||
}
|
||||
|
||||
void battleStateSelectionAiDecide(const uint8_t fighterIndex) {
|
||||
battlefighter_t *fighter = BATTLE.fighters[fighterIndex];
|
||||
const battlefighterteam_t enemyTeam =
|
||||
fighter->team == BATTLE_FIGHTER_TEAM_ALLY ?
|
||||
BATTLE_FIGHTER_TEAM_ENEMY : BATTLE_FIGHTER_TEAM_ALLY;
|
||||
|
||||
// Simple placeholder AI: attack the first living fighter on the
|
||||
// opposing team. No move variety or target scoring yet.
|
||||
for(uint8_t i = 0; i < BATTLE_FIGHTER_POS_COUNT; i++) {
|
||||
battlefighter_t *candidate = BATTLE.fighters[i];
|
||||
if(candidate == NULL) continue;
|
||||
if(candidate->team != enemyTeam) continue;
|
||||
if(!battleFighterIsAlive(candidate)) continue;
|
||||
|
||||
battleStateSelectionQueueAttack(fighterIndex, i);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void battleStateSelectionUpdate(void) {
|
||||
battlestateselection_t *state = &BATTLE.stateData.selection;
|
||||
|
||||
// Are we still waiting for a selection?
|
||||
while(state->selectionIndex < BATTLE.fighterOrderCount) {
|
||||
const uint8_t fighterIndex = BATTLE.fighterOrder[state->selectionIndex];
|
||||
|
||||
// Is this fighter still owed a decision?
|
||||
if(!battleStateSelectionFighterNeedsDecision(fighterIndex)) {
|
||||
state->selectionIndex++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Players will act on their own.
|
||||
battlefighter_t *fighter = BATTLE.fighters[fighterIndex];
|
||||
if(fighter->controller != BATTLE_FIGHTER_CONTROLLER_AI) return;
|
||||
|
||||
battleStateSelectionAiDecide(fighterIndex);
|
||||
state->selectionIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
void battleStateSelectionDispose(void) {
|
||||
|
||||
}
|
||||
|
||||
void battleStateSelectionQueueAttack(
|
||||
const uint8_t fighterIndex,
|
||||
const uint8_t targetIndex
|
||||
) {
|
||||
assertTrue(fighterIndex < BATTLE_FIGHTER_POS_COUNT, "Fighter OOB");
|
||||
assertTrue(targetIndex < BATTLE_FIGHTER_POS_COUNT, "Target OOB");
|
||||
assertNotNull(BATTLE.fighters[fighterIndex], "Fighter cannot be NULL");
|
||||
assertNotNull(BATTLE.fighters[targetIndex], "Target cannot be NULL");
|
||||
|
||||
const battlemoveid_t move = battleMoveGetIdByName("slash");
|
||||
assertTrue(move != BATTLE_MOVE_ID_NULL, "Missing 'slash' battle move");
|
||||
|
||||
battlemoveaction_t *action = &BATTLE.actionsPending[fighterIndex];
|
||||
if(action->move == BATTLE_MOVE_ID_NULL) BATTLE.actionsPendingCount++;
|
||||
action->move = move;
|
||||
action->targets[0] = targetIndex;
|
||||
action->user = BATTLE.fighters[fighterIndex];
|
||||
}
|
||||
|
||||
battlefighter_t *battleStateSelectionGetCurrentFighter(void) {
|
||||
if(BATTLE.state != BATTLE_STATE_SELECTION) return NULL;
|
||||
|
||||
const uint8_t index = BATTLE.stateData.selection.selectionIndex;
|
||||
if(index >= BATTLE.fighterOrderCount) return NULL;
|
||||
|
||||
return BATTLE.fighters[BATTLE.fighterOrder[index]];
|
||||
}
|
||||
|
||||
void battleStateSelectionPlayerAttack(const uint8_t targetIndex) {
|
||||
battlefighter_t *fighter = battleStateSelectionGetCurrentFighter();
|
||||
if(fighter == NULL) return;
|
||||
|
||||
const uint8_t selectionIndex = BATTLE.stateData.selection.selectionIndex;
|
||||
const uint8_t fighterIndex = BATTLE.fighterOrder[selectionIndex];
|
||||
battleStateSelectionQueueAttack(fighterIndex, targetIndex);
|
||||
BATTLE.stateData.selection.selectionIndex++;
|
||||
}
|
||||
|
||||
void battleStateSelectionGoBack(void) {
|
||||
battlestateselection_t *state = &BATTLE.stateData.selection;
|
||||
uint8_t index = state->selectionIndex;
|
||||
|
||||
while(index > 0) {
|
||||
index--;
|
||||
|
||||
const uint8_t fighterIndex = BATTLE.fighterOrder[index];
|
||||
battlefighter_t *fighter = BATTLE.fighters[fighterIndex];
|
||||
if(fighter == NULL) continue;
|
||||
if(fighter->controller != BATTLE_FIGHTER_CONTROLLER_PLAYER) continue;
|
||||
if(!battleStateSelectionFighterHasDecided(fighterIndex)) continue;
|
||||
|
||||
battlemoveaction_t *action = &BATTLE.actionsPending[fighterIndex];
|
||||
action->move = BATTLE_MOVE_ID_NULL;
|
||||
BATTLE.actionsPendingCount--;
|
||||
|
||||
state->selectionIndex = index;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void battleStateSelectionPlayerFlee(void) {
|
||||
battlefighter_t *fighter = battleStateSelectionGetCurrentFighter();
|
||||
if(fighter == NULL) return;
|
||||
|
||||
BATTLE.result = BATTLE_RESULT_FLED;
|
||||
battleSetState(BATTLE_STATE_FLED);
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "rpg/battle/fighter/battlefighter.h"
|
||||
|
||||
typedef struct {
|
||||
// Cursor into BATTLE.fighters (by position index) for the fighter
|
||||
// currently deciding a move this round.
|
||||
uint8_t selectionIndex;
|
||||
} battlestateselection_t;
|
||||
|
||||
/**
|
||||
* Called when the battle enters BATTLE_STATE_SELECTION.
|
||||
*/
|
||||
void battleStateSelectionInit(void);
|
||||
|
||||
/**
|
||||
* Updates BATTLE_STATE_SELECTION for one frame: auto-decides (see
|
||||
* battleStateSelectionAiDecide) for every AI-controlled fighter the
|
||||
* cursor passes, stopping once it reaches a player-controlled fighter
|
||||
* awaiting input (see battleStateSelectionGetCurrentFighter). Once every
|
||||
* position has decided, BATTLE_WAIT_SELECTION (see
|
||||
* cutsceneBattleWaitSelectionUpdate) queues the move to
|
||||
* BATTLE_STATE_EXECUTING.
|
||||
*/
|
||||
void battleStateSelectionUpdate(void);
|
||||
|
||||
/**
|
||||
* Called when the battle leaves BATTLE_STATE_SELECTION.
|
||||
*/
|
||||
void battleStateSelectionDispose(void);
|
||||
|
||||
/**
|
||||
* Checks whether the fighter at fighterIndex has already queued a move
|
||||
* this round.
|
||||
*
|
||||
* @param fighterIndex Index into BATTLE.fighters to check.
|
||||
* @return True if an action is already pending for this fighter.
|
||||
*/
|
||||
bool_t battleStateSelectionFighterHasDecided(const uint8_t fighterIndex);
|
||||
|
||||
/**
|
||||
* Checks whether the fighter at fighterIndex is a live, undecided
|
||||
* candidate that BATTLE_STATE_SELECTION should still be deciding a move
|
||||
* for.
|
||||
*
|
||||
* @param fighterIndex Index into BATTLE.fighters to check.
|
||||
* @return True if the fighter is alive and has no move queued yet.
|
||||
*/
|
||||
bool_t battleStateSelectionFighterNeedsDecision(const uint8_t fighterIndex);
|
||||
|
||||
/**
|
||||
* Decides and queues a move for an AI-controlled fighter: currently
|
||||
* always a basic attack against the first living fighter on the
|
||||
* opposing team (see battleStateSelectionQueueAttack). No-ops if there's
|
||||
* no living target (the battle should already be over by then via
|
||||
* BATTLE_POST_MOVE).
|
||||
*
|
||||
* @param fighterIndex Index into BATTLE.fighters of the deciding fighter.
|
||||
*/
|
||||
void battleStateSelectionAiDecide(const uint8_t fighterIndex);
|
||||
|
||||
/**
|
||||
* Checks whether every fighter in BATTLE.fighterOrder has decided a move
|
||||
* this round.
|
||||
*
|
||||
* @return True once the selection cursor has passed every fighter.
|
||||
*/
|
||||
bool_t battleStateSelectionIsComplete(void);
|
||||
|
||||
/**
|
||||
* Queues a basic attack (the "slash" move) from fighterIndex against
|
||||
* targetIndex.
|
||||
*
|
||||
* @param fighterIndex Index into BATTLE.fighters of the deciding fighter.
|
||||
* @param targetIndex Index into BATTLE.fighters of the target.
|
||||
*/
|
||||
void battleStateSelectionQueueAttack(
|
||||
const uint8_t fighterIndex,
|
||||
const uint8_t targetIndex
|
||||
);
|
||||
|
||||
/**
|
||||
* Returns the fighter currently awaiting a decision in
|
||||
* BATTLE_STATE_SELECTION.
|
||||
*
|
||||
* @return Pointer to the fighter awaiting a decision, or NULL if the
|
||||
* battle isn't in BATTLE_STATE_SELECTION or every position has
|
||||
* already decided.
|
||||
*/
|
||||
battlefighter_t *battleStateSelectionGetCurrentFighter(void);
|
||||
|
||||
/**
|
||||
* Submits the currently-selecting fighter's attack against a target, if
|
||||
* the battle is in BATTLE_STATE_SELECTION and awaiting a decision. Queues
|
||||
* the move and advances the selection cursor.
|
||||
*
|
||||
* @param targetIndex Index into BATTLE.fighters of the target.
|
||||
*/
|
||||
void battleStateSelectionPlayerAttack(const uint8_t targetIndex);
|
||||
|
||||
/**
|
||||
* Ends the battle immediately with BATTLE_STATE_FLED, if the battle is in
|
||||
* BATTLE_STATE_SELECTION and awaiting a decision.
|
||||
*/
|
||||
void battleStateSelectionPlayerFlee(void);
|
||||
|
||||
/**
|
||||
* Rewinds the selection cursor to the nearest earlier player-controlled
|
||||
* fighter that has already decided a move this round, clearing its
|
||||
* pending action so it can be re-decided. AI-decided fighters along the
|
||||
* way are left alone (skipped over, not rewound onto). No-ops if there's
|
||||
* no such fighter (e.g. already at the first decision of the round).
|
||||
*/
|
||||
void battleStateSelectionGoBack(void);
|
||||
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "battlestatewon.h"
|
||||
#include "rpg/cutscene/item/cutsceneitem.h"
|
||||
#include "rpg/cutscene/cutscenesystem.h"
|
||||
|
||||
void battleStateWonInit(void) {
|
||||
const cutsceneitem_t items[] = {
|
||||
{
|
||||
.type = CUTSCENE_ITEM_TYPE_TEXT,
|
||||
.text = { .text = "You won the battle!" }
|
||||
},
|
||||
{
|
||||
.type = CUTSCENE_ITEM_TYPE_BATTLE_SET_STATE,
|
||||
.battleSetState = { .state = BATTLE_STATE_ENDING }
|
||||
}
|
||||
};
|
||||
cutsceneSystemInsertItemsNext(items, sizeof(items) / sizeof(cutsceneitem_t));
|
||||
}
|
||||
|
||||
void battleStateWonUpdate(void) {
|
||||
|
||||
}
|
||||
|
||||
void battleStateWonDispose(void) {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* 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 {
|
||||
|
||||
} battlestatewon_t;
|
||||
|
||||
/**
|
||||
* Called when the battle enters BATTLE_STATE_WON. Queues a textbox
|
||||
* announcing the win, then transitions to BATTLE_STATE_ENDING once
|
||||
* dismissed.
|
||||
*/
|
||||
void battleStateWonInit(void);
|
||||
|
||||
/**
|
||||
* Updates BATTLE_STATE_WON for one frame.
|
||||
*/
|
||||
void battleStateWonUpdate(void);
|
||||
|
||||
/**
|
||||
* Called when the battle leaves BATTLE_STATE_WON.
|
||||
*/
|
||||
void battleStateWonDispose(void);
|
||||
@@ -1,43 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "testbattle.h"
|
||||
#include "rpg/battle/battle.h"
|
||||
#include "scene/scene.h"
|
||||
|
||||
void testBattleStart(void) {
|
||||
battleInit();
|
||||
|
||||
const battlefighterstats_t allyOneStats =
|
||||
{ .attack = 10, .defense = 5, .magic = 0, .speed = 10, .luck = 0 };
|
||||
const battlefighterstats_t allyTwoStats =
|
||||
{ .attack = 8, .defense = 4, .magic = 0, .speed = 8, .luck = 0 };
|
||||
const battlefighterstats_t enemyOneStats =
|
||||
{ .attack = 6, .defense = 3, .magic = 0, .speed = 6, .luck = 0 };
|
||||
const battlefighterstats_t enemyTwoStats =
|
||||
{ .attack = 7, .defense = 3, .magic = 0, .speed = 5, .luck = 0 };
|
||||
|
||||
battleAddFighter(
|
||||
BATTLE_FIGHTER_TEAM_ALLY, BATTLE_FIGHTER_CONTROLLER_PLAYER,
|
||||
allyOneStats, 30, 10
|
||||
);
|
||||
battleAddFighter(
|
||||
BATTLE_FIGHTER_TEAM_ALLY, BATTLE_FIGHTER_CONTROLLER_PLAYER,
|
||||
allyTwoStats, 25, 10
|
||||
);
|
||||
battleAddFighter(
|
||||
BATTLE_FIGHTER_TEAM_ENEMY, BATTLE_FIGHTER_CONTROLLER_AI,
|
||||
enemyOneStats, 20, 5
|
||||
);
|
||||
battleAddFighter(
|
||||
BATTLE_FIGHTER_TEAM_ENEMY, BATTLE_FIGHTER_CONTROLLER_AI,
|
||||
enemyTwoStats, 20, 5
|
||||
);
|
||||
|
||||
battleStart(BATTLE_ENCOUNTER_REGULAR, true);
|
||||
sceneSet(SCENE_TYPE_BATTLE);
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "error/error.h"
|
||||
|
||||
/**
|
||||
* TEMPORARY test hook: sets up a hardcoded mock battle and switches to the
|
||||
* battle scene, so the battle scene (camera/fighters/HUD) can be seen and
|
||||
* played without a real encounter trigger yet.
|
||||
*/
|
||||
void testBattleStart(void);
|
||||
@@ -6,6 +6,8 @@
|
||||
# Sources
|
||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
PUBLIC
|
||||
cutscene.c
|
||||
cutsceneref.c
|
||||
cutscenesystem.c
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "cutscene.h"
|
||||
#include "item/json/cutscenejsonpauseflags.h"
|
||||
#include "util/memory.h"
|
||||
#include "assert/assert.h"
|
||||
#include "asset/asset.h"
|
||||
#include "asset/loader/assetloader.h"
|
||||
|
||||
errorret_t cutsceneParseItem(
|
||||
yyjson_val *itemObj,
|
||||
cutsceneitem_t *item,
|
||||
const uint8_t index
|
||||
) {
|
||||
assertNotNull(itemObj, "Item object cannot be NULL");
|
||||
assertNotNull(item, "Item cannot be NULL");
|
||||
|
||||
if(!yyjson_is_obj(itemObj)) {
|
||||
errorThrow("Cutscene item %u is not an object", (uint32_t)index);
|
||||
}
|
||||
|
||||
const char_t *typeName = yyjson_get_str(yyjson_obj_get(itemObj, "type"));
|
||||
if(!cutsceneItemLookupType(typeName, &item->type)) {
|
||||
errorThrow(
|
||||
"Cutscene item %u: missing/unsupported item type %s",
|
||||
(uint32_t)index, typeName
|
||||
);
|
||||
}
|
||||
|
||||
cutsceneitemloadcallback_t *load = CUTSCENE_ITEM_CALLBACKS[item->type].load;
|
||||
if(load == NULL) {
|
||||
errorThrow(
|
||||
"Cutscene item %u: unhandled item type %u",
|
||||
(uint32_t)index, (uint32_t)item->type
|
||||
);
|
||||
}
|
||||
|
||||
errorret_t result = load(itemObj, item);
|
||||
if(errorIsNotOk(result)) errorChain(result);
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
size_t cutsceneCountItems(yyjson_doc *doc) {
|
||||
assertNotNull(doc, "Doc cannot be NULL");
|
||||
|
||||
yyjson_val *root = yyjson_doc_get_root(doc);
|
||||
yyjson_val *itemsArr = yyjson_obj_get(root, "items");
|
||||
if(!yyjson_is_arr(itemsArr)) return 0;
|
||||
return yyjson_arr_size(itemsArr);
|
||||
}
|
||||
|
||||
errorret_t cutsceneParseDoc(
|
||||
yyjson_doc *doc,
|
||||
cutscene_t *scene,
|
||||
cutsceneitem_t *items,
|
||||
size_t itemsMax
|
||||
) {
|
||||
assertNotNull(doc, "Doc cannot be NULL");
|
||||
assertNotNull(scene, "Scene cannot be NULL");
|
||||
assertNotNull(items, "Items cannot be NULL");
|
||||
assertTrue(itemsMax > 0, "Items max cannot be 0");
|
||||
|
||||
yyjson_val *root = yyjson_doc_get_root(doc);
|
||||
yyjson_val *itemsArr = yyjson_obj_get(root, "items");
|
||||
if(!yyjson_is_arr(itemsArr)) {
|
||||
errorThrow("Cutscene JSON is missing an 'items' array");
|
||||
}
|
||||
|
||||
size_t itemCount = yyjson_arr_size(itemsArr);
|
||||
if(itemCount > itemsMax) {
|
||||
errorThrow("Cutscene has more items than its target buffer allows");
|
||||
}
|
||||
|
||||
cutscenepause_t pause = CUTSCENE_PAUSE_DEFAULT;
|
||||
yyjson_val *pauseArr = yyjson_obj_get(root, "pause");
|
||||
if(yyjson_is_arr(pauseArr)) {
|
||||
if(!cutsceneJsonParsePauseFlags(pauseArr, &pause)) {
|
||||
errorThrow("Cutscene JSON has an invalid 'pause' flag");
|
||||
}
|
||||
}
|
||||
|
||||
memoryZero(items, itemCount * sizeof(cutsceneitem_t));
|
||||
|
||||
yyjson_arr_iter itemIter = yyjson_arr_iter_with(itemsArr);
|
||||
yyjson_val *itemVal;
|
||||
uint8_t index = 0;
|
||||
while((itemVal = yyjson_arr_iter_next(&itemIter)) != NULL) {
|
||||
errorret_t itemResult = cutsceneParseItem(itemVal, &items[index], index);
|
||||
if(errorIsNotOk(itemResult)) errorChain(itemResult);
|
||||
index++;
|
||||
}
|
||||
|
||||
scene->itemCount = (uint8_t)itemCount;
|
||||
scene->itemsMax = itemsMax;
|
||||
scene->pause = pause;
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
void cutsceneAppendAt(
|
||||
cutscene_t *scene,
|
||||
const uint8_t atIndex,
|
||||
const cutsceneitem_t *items,
|
||||
const uint8_t count
|
||||
) {
|
||||
assertNotNull(scene, "Scene cannot be NULL");
|
||||
assertNotNull(items, "Items cannot be NULL");
|
||||
assertTrue(count > 0, "Count cannot be 0");
|
||||
assertTrue(atIndex <= scene->itemCount, "atIndex out of bounds");
|
||||
assertTrue(
|
||||
(size_t)scene->itemCount + count <= scene->itemsMax,
|
||||
"Cutscene item append exceeds itemsMax"
|
||||
);
|
||||
|
||||
const size_t tailCount = (size_t)scene->itemCount - atIndex;
|
||||
if(tailCount > 0) {
|
||||
memoryMove(
|
||||
&scene->items[atIndex + count],
|
||||
&scene->items[atIndex],
|
||||
tailCount * sizeof(cutsceneitem_t)
|
||||
);
|
||||
}
|
||||
memoryCopy(&scene->items[atIndex], items, count * sizeof(cutsceneitem_t));
|
||||
scene->itemCount += count;
|
||||
}
|
||||
|
||||
void cutsceneAppend(
|
||||
cutscene_t *scene,
|
||||
const cutsceneitem_t *items,
|
||||
const uint8_t count
|
||||
) {
|
||||
cutsceneAppendAt(scene, scene->itemCount, items, count);
|
||||
}
|
||||
|
||||
void cutsceneAppendNext(
|
||||
cutscene_t *scene,
|
||||
const uint8_t currentIndex,
|
||||
const cutsceneitem_t *items,
|
||||
const uint8_t count
|
||||
) {
|
||||
cutsceneAppendAt(scene, (uint8_t)(currentIndex + 1), items, count);
|
||||
}
|
||||
|
||||
errorret_t cutsceneLoadParse(
|
||||
const char_t *path,
|
||||
cutscene_t *scene,
|
||||
cutsceneitem_t *items,
|
||||
const size_t itemsMax
|
||||
) {
|
||||
assertNotNull(path, "Path cannot be NULL");
|
||||
assertNotNull(scene, "Scene cannot be NULL");
|
||||
assertNotNull(items, "Items cannot be NULL");
|
||||
|
||||
// Lock the asset, require it loaded.
|
||||
assetentry_t *entry = assetLock(path, ASSET_LOADER_TYPE_JSON, NULL);
|
||||
errorret_t result = assetRequireLoaded(entry);
|
||||
if(errorIsNotOk(result)) {
|
||||
assetUnlockEntry(entry);
|
||||
errorChain(result);
|
||||
}
|
||||
|
||||
// Parse the loaded JSON
|
||||
errorret_t parseResult = cutsceneParseDoc(
|
||||
entry->data.json, scene, items, itemsMax
|
||||
);
|
||||
|
||||
// Free up the asset entry.
|
||||
assetUnlockEntry(entry);
|
||||
errorChain(parseResult);
|
||||
|
||||
errorOk();
|
||||
}
|
||||
+133
-492
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Dominic Masters
|
||||
*
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
@@ -10,504 +10,145 @@
|
||||
#include "rpg/cutscene/cutscenepause.h"
|
||||
|
||||
typedef struct cutscene_s {
|
||||
const cutsceneitem_t *items;
|
||||
cutsceneitem_t *items;
|
||||
uint8_t itemCount;
|
||||
// Capacity of items - see cutsceneAppend/cutsceneAppendAt/
|
||||
// cutsceneAppendNext, which assert against this rather than taking a
|
||||
// capacity parameter of their own. Set once, either by cutsceneParseDoc
|
||||
// (to whatever itemsMax it was given) or by whoever declares a
|
||||
// C-authored cutscene_t directly (to items' true array capacity, which
|
||||
// may be larger than itemCount to leave room for a later append/insert).
|
||||
size_t itemsMax;
|
||||
cutscenepause_t pause;
|
||||
|
||||
// Size in bytes of this cutscene's custom user data, carved out of
|
||||
// CUTSCENE_SYSTEM.data while the cutscene is running.
|
||||
size_t dataSize;
|
||||
} cutscene_t;
|
||||
|
||||
#define CUTSCENE(NAME, SIZE, PAUSE_TYPE, ...) \
|
||||
static const cutsceneitem_t CUTSCENE_##NAME##_ITEMS[] = { __VA_ARGS__ }; \
|
||||
static const cutscene_t CUTSCENE_##NAME = { \
|
||||
.items = CUTSCENE_##NAME##_ITEMS, \
|
||||
.itemCount = sizeof(CUTSCENE_##NAME##_ITEMS) / sizeof(cutsceneitem_t), \
|
||||
.pause = CUTSCENE_PAUSE_##PAUSE_TYPE, \
|
||||
.dataSize = SIZE \
|
||||
};
|
||||
/**
|
||||
* Parses one cutscene item object into item. Every field/enum name is
|
||||
* validated as real (untrusted, authored) file content - failures are
|
||||
* real errors, not asserts. Dispatches to the matching type's Load
|
||||
* callback (see CUTSCENE_ITEM_CALLBACKS in item/cutsceneitem.h) - the
|
||||
* per-type parsing logic itself lives there, colocated with that type's
|
||||
* runtime behavior.
|
||||
*
|
||||
* @param itemObj The item's JSON object.
|
||||
* @param item Destination item, already zeroed by the caller.
|
||||
* @param index Index of this item within the cutscene, for error messages.
|
||||
* @return Error code indicating success or failure of the parse.
|
||||
*/
|
||||
errorret_t cutsceneParseItem(
|
||||
yyjson_val *itemObj,
|
||||
cutsceneitem_t *item,
|
||||
const uint8_t index
|
||||
);
|
||||
|
||||
#define CUTSCENE_REFERENCE(CUTSCENE) \
|
||||
&CUTSCENE_##CUTSCENE
|
||||
/**
|
||||
* Counts how many entries are in doc's top-level "items" array, without
|
||||
* parsing any of them - e.g. so a caller that wants to malloc its own
|
||||
* items array instead of using a fixed one could size it first (we don't
|
||||
* do that ourselves, but the option stays open).
|
||||
*
|
||||
* @param doc Already-parsed JSON document.
|
||||
* @return Number of entries in "items", or 0 if it's missing/not an array.
|
||||
*/
|
||||
size_t cutsceneCountItems(yyjson_doc *doc);
|
||||
|
||||
#define CUTSCENE_TEXT(TEXT) \
|
||||
{ .type = CUTSCENE_ITEM_TYPE_TEXT, .text = { .text = TEXT } }
|
||||
/**
|
||||
* Parses doc's "items" (and optional "pause") into items, and updates
|
||||
* scene's itemCount/itemsMax/pause - scene->items is left untouched, since
|
||||
* the caller already points it at items. Purely a parser: doesn't touch
|
||||
* the asset system, doesn't lock/load anything, and doesn't take
|
||||
* ownership of doc - the caller is responsible for both loading doc
|
||||
* beforehand and for its lifetime afterward (though as of cutscene items
|
||||
* owning their own string fields by value, nothing parsed here actually
|
||||
* needs doc to survive past this call returning).
|
||||
*
|
||||
* @param doc Already-parsed JSON document to read from.
|
||||
* @param scene Destination scene - itemCount/itemsMax/pause are
|
||||
* overwritten.
|
||||
* @param items Destination item array, capacity itemsMax.
|
||||
* @param itemsMax Capacity of items.
|
||||
* @return Error code indicating success or failure of the parse.
|
||||
*/
|
||||
errorret_t cutsceneParseDoc(
|
||||
yyjson_doc *doc,
|
||||
cutscene_t *scene,
|
||||
cutsceneitem_t *items,
|
||||
size_t itemsMax
|
||||
);
|
||||
|
||||
#define CUTSCENE_TEXT_MINI(TEXT, X, Y, Z, DURATION) \
|
||||
{ \
|
||||
.type = CUTSCENE_ITEM_TYPE_TEXT_MINI, \
|
||||
.textMini = { \
|
||||
.text = TEXT, \
|
||||
.position = { X, Y, Z }, \
|
||||
.duration = DURATION \
|
||||
} \
|
||||
}
|
||||
/**
|
||||
* Inserts items into scene->items starting at atIndex, growing
|
||||
* scene->itemCount. Every existing item from atIndex onward shifts right
|
||||
* by count to make room. Asserts if this would exceed scene->itemsMax.
|
||||
*
|
||||
* @param scene Cutscene to insert into.
|
||||
* @param atIndex Index the first inserted item should end up at - must be
|
||||
* <= scene->itemCount.
|
||||
* @param items Items to insert, copied in order.
|
||||
* @param count Number of entries in items.
|
||||
*/
|
||||
void cutsceneAppendAt(
|
||||
cutscene_t *scene,
|
||||
const uint8_t atIndex,
|
||||
const cutsceneitem_t *items,
|
||||
const uint8_t count
|
||||
);
|
||||
|
||||
#define CUTSCENE_TEXT_MINI_HIDE(INDEX) \
|
||||
{ \
|
||||
.type = CUTSCENE_ITEM_TYPE_TEXT_MINI_HIDE, \
|
||||
.textMiniHide = { .index = INDEX } \
|
||||
}
|
||||
/**
|
||||
* Same as cutsceneAppendAt, but always inserts at the end of
|
||||
* scene->items.
|
||||
*
|
||||
* @param scene Cutscene to append to.
|
||||
* @param items Items to append, copied in order.
|
||||
* @param count Number of entries in items.
|
||||
*/
|
||||
void cutsceneAppend(
|
||||
cutscene_t *scene,
|
||||
const cutsceneitem_t *items,
|
||||
const uint8_t count
|
||||
);
|
||||
|
||||
#define CUTSCENE_WAIT(WAIT) \
|
||||
{ .type = CUTSCENE_ITEM_TYPE_WAIT, .wait = WAIT }
|
||||
/**
|
||||
* Same as cutsceneAppendAt, but inserts right after currentIndex (i.e. at
|
||||
* currentIndex + 1) instead of at a literal index - e.g. pass
|
||||
* CUTSCENE_SYSTEM.currentItem to make these items run immediately after
|
||||
* the currently-executing one. currentIndex may be 0xFF (nothing has run
|
||||
* yet), which wraps to insert at index 0, matching that sentinel's usual
|
||||
* meaning elsewhere in the cutscene system.
|
||||
*
|
||||
* @param scene Cutscene to insert into.
|
||||
* @param currentIndex Index to insert after.
|
||||
* @param items Items to insert, copied in order.
|
||||
* @param count Number of entries in items.
|
||||
*/
|
||||
void cutsceneAppendNext(
|
||||
cutscene_t *scene,
|
||||
const uint8_t currentIndex,
|
||||
const cutsceneitem_t *items,
|
||||
const uint8_t count
|
||||
);
|
||||
|
||||
// A named, otherwise no-op position in the item list that cutsceneGoTo
|
||||
// can jump execution straight to. NAME is matched with stringEquals,
|
||||
// not pointer identity, so it's safe to use separate string literals
|
||||
// with the same contents at the marker and at each call site.
|
||||
#define CUTSCENE_MARKER(NAME) \
|
||||
{ .type = CUTSCENE_ITEM_TYPE_MARKER, .marker = { .name = NAME } }
|
||||
|
||||
// Restarts the currently running cutscene from its first item,
|
||||
// preserving whatever interact/interacted entities triggered it.
|
||||
#define CUTSCENE_RESTART() \
|
||||
{ .type = CUTSCENE_ITEM_TYPE_RESTART }
|
||||
|
||||
// Blocks the cutscene here indefinitely - it never completes on its own,
|
||||
// only cutsceneGoTo (called externally, e.g. from a UI callback) can move
|
||||
// execution past it.
|
||||
#define CUTSCENE_IDLE() \
|
||||
{ .type = CUTSCENE_ITEM_TYPE_IDLE }
|
||||
|
||||
// Opens a full-screen UI panel (e.g. the main menu) declaratively, then
|
||||
// immediately continues on to whatever follows this item.
|
||||
#define CUTSCENE_UI_SHOW(SCREEN) \
|
||||
{ .type = CUTSCENE_ITEM_TYPE_UI_SHOW, .uiShow = { .screen = SCREEN } }
|
||||
|
||||
// Requests a switch to a different SCENE_TYPE via sceneSet, then
|
||||
// immediately continues on to whatever follows this item - the switch
|
||||
// itself doesn't happen until the next sceneUpdate() tick, so it does
|
||||
// not take effect this frame.
|
||||
#define CUTSCENE_SCENE(TYPE) \
|
||||
{ .type = CUTSCENE_ITEM_TYPE_SCENE, .sceneChange = { .type = TYPE } }
|
||||
|
||||
#define CUTSCENE_CUTSCENE(CUTSCENE) \
|
||||
{ \
|
||||
.type = CUTSCENE_ITEM_TYPE_CUTSCENE, \
|
||||
.cutscene = CUTSCENE_REFERENCE(CUTSCENE) \
|
||||
}
|
||||
|
||||
#define CUTSCENE_CALLBACK(CALLBACK) \
|
||||
{ .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = CALLBACK }
|
||||
|
||||
#define CUTSCENE_PRINT(TEXT) \
|
||||
{ .type = CUTSCENE_ITEM_TYPE_PRINT, .print = { .text = TEXT } }
|
||||
|
||||
// Shows a message-only modal (no option buttons) and immediately
|
||||
// continues on to whatever follows this item - it does not wait for
|
||||
// the dialog to be dismissed. Script the rest of the interaction (e.g.
|
||||
// CUTSCENE_CALLBACK to kick off work, CUTSCENE_WAIT, then
|
||||
// CUTSCENE_MODAL_CLOSE) as later items in the same cutscene.
|
||||
// TITLE and MESSAGE are each displayed as-is unless they match a
|
||||
// locale message ID, in which case the translated string is shown
|
||||
// instead - see uiModalLocalize.
|
||||
#define CUTSCENE_MODAL(TITLE, MESSAGE) \
|
||||
{ \
|
||||
.type = CUTSCENE_ITEM_TYPE_MODAL, \
|
||||
.modal = { .title = TITLE, .message = MESSAGE } \
|
||||
}
|
||||
|
||||
// Shows a modal with option buttons and immediately continues on, same
|
||||
// as CUTSCENE_MODAL - it does not block waiting for a selection.
|
||||
// Back/cancel input is disabled while it's open (see
|
||||
// uiMenuSetDisableBack), so it must be dismissed by picking one; CALLBACK
|
||||
// then fires with the selected option index once the dialog closes.
|
||||
// Option labels are passed as trailing arguments, e.g.
|
||||
// CUTSCENE_MODAL_OPTIONS(title, message, callback, "Retry", "Cancel") -
|
||||
// their strings are not copied by this item, so they must stay valid
|
||||
// until the modal opens (string literals are fine). Like TITLE and
|
||||
// MESSAGE, each option is translated if it matches a locale message ID.
|
||||
#define CUTSCENE_MODAL_OPTIONS(TITLE, MESSAGE, CALLBACK, ...) \
|
||||
{ \
|
||||
.type = CUTSCENE_ITEM_TYPE_MODAL, \
|
||||
.modal = { \
|
||||
.title = TITLE, .message = MESSAGE, \
|
||||
.options = (const char_t *[]){ __VA_ARGS__ }, \
|
||||
.optionCount = (uint8_t)( \
|
||||
sizeof((const char_t *[]){ __VA_ARGS__ }) / sizeof(const char_t *) \
|
||||
), \
|
||||
.callback = CALLBACK \
|
||||
} \
|
||||
}
|
||||
|
||||
// Same as CUTSCENE_MODAL_OPTIONS, but fixed to exactly one option and
|
||||
// MARKER1 to jump straight to via cutsceneGoTo once it's selected - no
|
||||
// callback function to write. Does not fall through to whatever follows
|
||||
// this item, so MARKER1 must be scripted elsewhere in the same cutscene.
|
||||
#define CUTSCENE_MODAL_OPTIONS_ONE(TITLE, MESSAGE, OPTION1, MARKER1) \
|
||||
{ \
|
||||
.type = CUTSCENE_ITEM_TYPE_MODAL_OPTIONS_MARKERS, \
|
||||
.modalOptionsMarkers = { \
|
||||
.title = TITLE, .message = MESSAGE, \
|
||||
.options = { OPTION1 }, \
|
||||
.markers = { MARKER1 }, \
|
||||
.optionCount = 1 \
|
||||
} \
|
||||
}
|
||||
|
||||
// Same as CUTSCENE_MODAL_OPTIONS, but fixed to exactly two options,
|
||||
// jumping straight to OPTION1_MARKER or OPTION2_MARKER via cutsceneGoTo
|
||||
// once the corresponding option is selected - no callback function to
|
||||
// write. Does not fall through to whatever follows this item, so both
|
||||
// markers must be scripted elsewhere in the same cutscene.
|
||||
#define CUTSCENE_MODAL_OPTIONS_TWO( \
|
||||
TITLE, MESSAGE, OPTION1, OPTION1_MARKER, OPTION2, OPTION2_MARKER \
|
||||
) \
|
||||
{ \
|
||||
.type = CUTSCENE_ITEM_TYPE_MODAL_OPTIONS_MARKERS, \
|
||||
.modalOptionsMarkers = { \
|
||||
.title = TITLE, .message = MESSAGE, \
|
||||
.options = { OPTION1, OPTION2 }, \
|
||||
.markers = { OPTION1_MARKER, OPTION2_MARKER }, \
|
||||
.optionCount = 2 \
|
||||
} \
|
||||
}
|
||||
|
||||
// Closes the currently open modal (if any). Useful when a modal was
|
||||
// opened outside of a blocking CUTSCENE_MODAL item (e.g. directly via
|
||||
// uiModalOpen) and this cutscene just needs to dismiss it and continue
|
||||
// on to whatever follows this item in the sequence.
|
||||
#define CUTSCENE_MODAL_CLOSE() \
|
||||
{ .type = CUTSCENE_ITEM_TYPE_MODAL_CLOSE }
|
||||
|
||||
// Opens the on-screen keyboard and blocks the cutscene until it closes,
|
||||
// then caches whatever was typed - see cutsceneSystemGetTextCache to
|
||||
// read it back afterwards. __VA_ARGS__ are uikeyboardopen_t designated
|
||||
// initializers, e.g. CUTSCENE_KEYBOARD(.cancel = true, .maxLength = 8).
|
||||
#define CUTSCENE_KEYBOARD(...) \
|
||||
{ \
|
||||
.type = CUTSCENE_ITEM_TYPE_KEYBOARD, \
|
||||
.keyboard = { .open = { __VA_ARGS__ } } \
|
||||
}
|
||||
|
||||
// (Re)requests an available save device and jumps straight to
|
||||
// SUCCESS_MARKER or FAILURE_MARKER once it resolves, same as a
|
||||
// CUTSCENE_MODAL_OPTIONS callback - it does not fall through to
|
||||
// whatever follows this item, so both markers must be scripted
|
||||
// elsewhere in the same cutscene.
|
||||
#define CUTSCENE_SAVE_DEVICE_CHECK(SUCCESS_MARKER, FAILURE_MARKER) \
|
||||
{ \
|
||||
.type = CUTSCENE_ITEM_TYPE_SAVE_DEVICE_CHECK, \
|
||||
.saveDeviceCheck = { \
|
||||
.successMarker = SUCCESS_MARKER, \
|
||||
.failureMarker = FAILURE_MARKER \
|
||||
} \
|
||||
}
|
||||
|
||||
// (Re)loads every save slot via saveLoadAllSlots() and jumps straight to
|
||||
// SUCCESS_MARKER or FAILURE_MARKER once it resolves, same shape as
|
||||
// CUTSCENE_SAVE_DEVICE_CHECK - it does not fall through to whatever
|
||||
// follows this item, so both markers must be scripted elsewhere in the
|
||||
// same cutscene.
|
||||
#define CUTSCENE_SAVE_LOAD_ALL_SLOTS(SUCCESS_MARKER, FAILURE_MARKER) \
|
||||
{ \
|
||||
.type = CUTSCENE_ITEM_TYPE_SAVE_LOAD_ALL_SLOTS, \
|
||||
.saveLoadAllSlots = { \
|
||||
.successMarker = SUCCESS_MARKER, \
|
||||
.failureMarker = FAILURE_MARKER \
|
||||
} \
|
||||
}
|
||||
|
||||
#define CUTSCENE_ENTITY_WALK_TO(ENTITY_INDEX, X, Y, Z) \
|
||||
{ \
|
||||
.type = CUTSCENE_ITEM_TYPE_ENTITY_WALK_TO, \
|
||||
.entityWalkTo = { \
|
||||
.entityIndex = ENTITY_INDEX, \
|
||||
.positions = (const worldpos_t[]){ { X, Y, Z } }, \
|
||||
.count = 1, \
|
||||
.walkAround = true \
|
||||
} \
|
||||
}
|
||||
|
||||
#define CUTSCENE_ENTITY_WALK_PATH(NAME, ENTITY_INDEX, ...) \
|
||||
static const worldpos_t CUTSCENE_##NAME##_POSITIONS[] = { __VA_ARGS__ }; \
|
||||
static const cutsceneitem_t CUTSCENE_##NAME = { \
|
||||
.type = CUTSCENE_ITEM_TYPE_ENTITY_WALK_TO, \
|
||||
.entityWalkTo = { \
|
||||
.entityIndex = ENTITY_INDEX, \
|
||||
.positions = CUTSCENE_##NAME##_POSITIONS, \
|
||||
.count = sizeof(CUTSCENE_##NAME##_POSITIONS) / sizeof(worldpos_t), \
|
||||
.walkAround = true \
|
||||
} \
|
||||
}
|
||||
|
||||
#define CUTSCENE_ENTITY_REMOVE(ENTITY_INDEX) \
|
||||
{ \
|
||||
.type = CUTSCENE_ITEM_TYPE_ENTITY_REMOVE, \
|
||||
.entityRemove = { .entityIndex = ENTITY_INDEX } \
|
||||
}
|
||||
|
||||
#define CUTSCENE_ENTITY_ADD(TYPE, X, Y, Z) \
|
||||
{ \
|
||||
.type = CUTSCENE_ITEM_TYPE_ENTITY_ADD, \
|
||||
.entityAdd = { .entityType = TYPE, .position = { X, Y, Z } } \
|
||||
}
|
||||
|
||||
#define CUTSCENE_ENTITY_TURN(ENTITY_INDEX, DIRECTION) \
|
||||
{ \
|
||||
.type = CUTSCENE_ITEM_TYPE_ENTITY_TURN, \
|
||||
.entityTurn = { .entityIndex = ENTITY_INDEX, .direction = DIRECTION } \
|
||||
}
|
||||
|
||||
// Walks ENTITY_INDEX to stand beside TARGET_ENTITY_INDEX, offset by
|
||||
// (OFFSET_X, OFFSET_Y) on the 2D plane. The destination Z is resolved
|
||||
// from nearby terrain each frame, so ramps between the two entities are
|
||||
// accounted for automatically.
|
||||
#define CUTSCENE_ENTITY_WALK_TO_ENTITY( \
|
||||
ENTITY_INDEX, TARGET_ENTITY_INDEX, OFFSET_X, OFFSET_Y \
|
||||
) \
|
||||
{ \
|
||||
.type = CUTSCENE_ITEM_TYPE_ENTITY_WALK_TO_ENTITY, \
|
||||
.entityWalkToEntity = { \
|
||||
.entityIndex = ENTITY_INDEX, \
|
||||
.targetEntityIndex = TARGET_ENTITY_INDEX, \
|
||||
.offsetX = OFFSET_X, \
|
||||
.offsetY = OFFSET_Y \
|
||||
} \
|
||||
}
|
||||
|
||||
#define CUTSCENE_ENTITY_TELEPORT(ENTITY_INDEX, X, Y, Z) \
|
||||
{ \
|
||||
.type = CUTSCENE_ITEM_TYPE_ENTITY_TELEPORT, \
|
||||
.entityTeleport = { .entityIndex = ENTITY_INDEX, .target = { X, Y, Z } } \
|
||||
}
|
||||
|
||||
#define CUTSCENE_FADE(FROM, TO, DURATION, EASING) \
|
||||
{ \
|
||||
.type = CUTSCENE_ITEM_TYPE_FADE, \
|
||||
.fade = { .from = FROM, .to = TO, .duration = DURATION, .easing = EASING } \
|
||||
}
|
||||
|
||||
#define CUTSCENE_FADE_TO_BLACK(DURATION) \
|
||||
CUTSCENE_FADE(COLOR_TRANSPARENT_BLACK, COLOR_BLACK, DURATION, EASING_LINEAR)
|
||||
|
||||
#define CUTSCENE_FADE_FROM_BLACK(DURATION) \
|
||||
CUTSCENE_FADE(COLOR_BLACK, COLOR_TRANSPARENT_BLACK, DURATION, EASING_LINEAR)
|
||||
|
||||
#define CUTSCENE_FADE_TO_WHITE(DURATION) \
|
||||
CUTSCENE_FADE(COLOR_TRANSPARENT_WHITE, COLOR_WHITE, DURATION, EASING_LINEAR)
|
||||
|
||||
#define CUTSCENE_FADE_FROM_WHITE(DURATION) \
|
||||
CUTSCENE_FADE(COLOR_WHITE, COLOR_TRANSPARENT_WHITE, DURATION, EASING_LINEAR)
|
||||
|
||||
#define CUTSCENE_EMOJI(ENTITY_INDEX, EMOJI_TYPE, DURATION) \
|
||||
{ \
|
||||
.type = CUTSCENE_ITEM_TYPE_EMOJI, \
|
||||
.emoji = { \
|
||||
.entityIndex = ENTITY_INDEX, \
|
||||
.emojiType = EMOJI_TYPE, \
|
||||
.duration = DURATION \
|
||||
} \
|
||||
}
|
||||
|
||||
// AMOUNT ranges 0 (no shake) to 4 (three tiles): 1 is half a tile, 2 is
|
||||
// a full tile, 3 is two tiles, and 4 is three tiles.
|
||||
#define CUTSCENE_SHAKE(AMOUNT, DURATION) \
|
||||
{ \
|
||||
.type = CUTSCENE_ITEM_TYPE_SHAKE, \
|
||||
.shake = { .amount = AMOUNT, .duration = DURATION } \
|
||||
}
|
||||
|
||||
// Waits until BATTLE.state reaches STATE. Put this BEFORE
|
||||
// CUTSCENE_SET_PAUSE(CUTSCENE_PAUSE_BATTLE), not after -- pausing first
|
||||
// freezes BATTLE.state wherever it already is, so it would never reach
|
||||
// STATE on its own to satisfy the wait. Waiting unpaused, then pausing the
|
||||
// moment it's satisfied, catches the battle right at STATE before it can
|
||||
// advance further.
|
||||
#define CUTSCENE_BATTLE_WAIT_STATE(STATE) \
|
||||
{ \
|
||||
.type = CUTSCENE_ITEM_TYPE_BATTLE_WAIT_STATE, \
|
||||
.battleWaitState = { .state = STATE } \
|
||||
}
|
||||
|
||||
// Immediately queues an attack for FIGHTER_INDEX against TARGET_INDEX,
|
||||
// bypassing normal player/AI selection for that fighter this round.
|
||||
#define CUTSCENE_BATTLE_FORCE_ACTION(FIGHTER_INDEX, TARGET_INDEX) \
|
||||
{ \
|
||||
.type = CUTSCENE_ITEM_TYPE_BATTLE_FORCE_ACTION, \
|
||||
.battleForceAction = { \
|
||||
.fighterIndex = FIGHTER_INDEX, .targetIndex = TARGET_INDEX \
|
||||
} \
|
||||
}
|
||||
|
||||
#define CUTSCENE_SET_PAUSE(FLAGS) \
|
||||
{ .type = CUTSCENE_ITEM_TYPE_SET_PAUSE, .setPause = (FLAGS) }
|
||||
|
||||
#define CUTSCENE_ITEM_GIVE(ITEM_ID, QUANTITY) \
|
||||
{ \
|
||||
.type = CUTSCENE_ITEM_TYPE_ITEM_GIVE, \
|
||||
.itemGive = { .item = ITEM_ID, .quantity = QUANTITY } \
|
||||
}
|
||||
|
||||
// Runs all listed items simultaneously and waits until all are done.
|
||||
// Concurrent items cannot be nested inside another CUTSCENE_CONCURRENT.
|
||||
#define CUTSCENE_CONCURRENT(...) \
|
||||
{ \
|
||||
.type = CUTSCENE_ITEM_TYPE_CONCURRENT, \
|
||||
.concurrent = { \
|
||||
.items = (const cutsceneitem_t[]){ __VA_ARGS__ }, \
|
||||
.count = (uint8_t)( \
|
||||
sizeof((cutsceneitem_t[]){ __VA_ARGS__ }) / \
|
||||
sizeof(cutsceneitem_t) \
|
||||
) \
|
||||
} \
|
||||
}
|
||||
|
||||
#define CUTSCENE_MAP_AREA_ADD( \
|
||||
MIN_X, MIN_Y, MIN_Z, MAX_X, MAX_Y, MAX_Z, CALLBACK, NOTIFY, TRIGGER \
|
||||
) \
|
||||
{ \
|
||||
.type = CUTSCENE_ITEM_TYPE_MAP_AREA_ADD, \
|
||||
.mapAreaAdd = { \
|
||||
.min = { MIN_X, MIN_Y, MIN_Z }, \
|
||||
.max = { MAX_X, MAX_Y, MAX_Z }, \
|
||||
.callback = CALLBACK, \
|
||||
.notify = NOTIFY, \
|
||||
.trigger = TRIGGER \
|
||||
} \
|
||||
}
|
||||
|
||||
#define CUTSCENE_MAP_AREA_REMOVE(AREA_ID) \
|
||||
{ \
|
||||
.type = CUTSCENE_ITEM_TYPE_MAP_AREA_REMOVE, \
|
||||
.mapAreaRemove = { .areaId = AREA_ID } \
|
||||
}
|
||||
|
||||
// Waits until any one of the given map area IDs has its callback invoked.
|
||||
// Accepts CUTSCENE_AREA_LAST_CREATED in place of a literal area ID.
|
||||
#define CUTSCENE_MAP_AREA_WAIT(...) \
|
||||
{ \
|
||||
.type = CUTSCENE_ITEM_TYPE_MAP_AREA_WAIT, \
|
||||
.mapAreaWait = { \
|
||||
.areaIds = (const uint8_t[]){ __VA_ARGS__ }, \
|
||||
.count = (uint8_t)( \
|
||||
sizeof((const uint8_t[]){ __VA_ARGS__ }) / sizeof(uint8_t) \
|
||||
) \
|
||||
} \
|
||||
}
|
||||
|
||||
// Adds a map area, waits for it to be triggered once, then removes it
|
||||
// before the cutscene continues. Uses a no-op callback since the wait is
|
||||
// driven by the area's trigger count rather than callback logic.
|
||||
#define CUTSCENE_MAP_AREA_TRIGGER_ONCE( \
|
||||
MIN_X, MIN_Y, MIN_Z, MAX_X, MAX_Y, MAX_Z, NOTIFY, TRIGGER \
|
||||
) \
|
||||
CUTSCENE_MAP_AREA_ADD( \
|
||||
MIN_X, MIN_Y, MIN_Z, MAX_X, MAX_Y, MAX_Z, \
|
||||
mapAreaNoopCallback, NOTIFY, TRIGGER \
|
||||
), \
|
||||
CUTSCENE_MAP_AREA_WAIT(CUTSCENE_AREA_LAST_CREATED), \
|
||||
CUTSCENE_MAP_AREA_REMOVE(CUTSCENE_AREA_LAST_CREATED)
|
||||
|
||||
// Longhand - every audioMixerQueuePlay() parameter. See
|
||||
// CUTSCENE_AUDIO_PLAY_SIMPLE/CUTSCENE_AUDIO_PLAY_LOOPED below for the
|
||||
// shorthands.
|
||||
#define CUTSCENE_AUDIO_PLAY( \
|
||||
FILE, CHANNEL, VOLUME, PAN, LOOPING, LOOP_COUNT, LOOP_START, LOOP_TO \
|
||||
) \
|
||||
{ \
|
||||
.type = CUTSCENE_ITEM_TYPE_AUDIO_PLAY, \
|
||||
.audioPlay = { \
|
||||
.file = FILE, \
|
||||
.channel = CHANNEL, \
|
||||
.volume = VOLUME, \
|
||||
.pan = PAN, \
|
||||
.looping = LOOPING, \
|
||||
.loopCount = LOOP_COUNT, \
|
||||
.loopStart = LOOP_START, \
|
||||
.loopTo = LOOP_TO \
|
||||
} \
|
||||
}
|
||||
|
||||
// Shorthand: play FILE on CHANNEL once, full volume, centered (0.0f, same
|
||||
// as AUDIO_STREAM_CENTER), no loop.
|
||||
#define CUTSCENE_AUDIO_PLAY_SIMPLE(FILE, CHANNEL) \
|
||||
CUTSCENE_AUDIO_PLAY(FILE, CHANNEL, 1.0f, 0.0f, false, 0, -1.0f, 0.0f)
|
||||
|
||||
// Shorthand: play FILE on CHANNEL looping the whole clip forever, full
|
||||
// volume, centered (0.0f, same as AUDIO_STREAM_CENTER).
|
||||
#define CUTSCENE_AUDIO_PLAY_LOOPED(FILE, CHANNEL) \
|
||||
CUTSCENE_AUDIO_PLAY(FILE, CHANNEL, 1.0f, 0.0f, true, 0, -1.0f, 0.0f)
|
||||
|
||||
#define CUTSCENE_AUDIO_STOP(CHANNEL) \
|
||||
{ .type = CUTSCENE_ITEM_TYPE_AUDIO_STOP, .audioStopChannel = CHANNEL }
|
||||
|
||||
#define CUTSCENE_AUDIO_PAUSE(CHANNEL) \
|
||||
{ .type = CUTSCENE_ITEM_TYPE_AUDIO_PAUSE, .audioPauseChannel = CHANNEL }
|
||||
|
||||
#define CUTSCENE_AUDIO_RESUME(CHANNEL) \
|
||||
{ .type = CUTSCENE_ITEM_TYPE_AUDIO_RESUME, .audioResumeChannel = CHANNEL }
|
||||
|
||||
// Longhand - starts CHANNEL fading from FROM to TO over DURATION seconds,
|
||||
// eased by EASING. Doesn't wait for it - pair with
|
||||
// CUTSCENE_AUDIO_FADE_WAIT(CHANNEL) to block until it finishes. See
|
||||
// CUTSCENE_AUDIO_FADE_OUT/CUTSCENE_AUDIO_FADE_IN below for the shorthands;
|
||||
// use this directly for a custom range, e.g.
|
||||
// CUTSCENE_AUDIO_FADE(CHANNEL, 1.0f, 0.3f, 5.0f, EASING_LINEAR) to fade
|
||||
// down to "playing quietly" rather than silent.
|
||||
#define CUTSCENE_AUDIO_FADE(CHANNEL, FROM, TO, DURATION, EASING) \
|
||||
{ \
|
||||
.type = CUTSCENE_ITEM_TYPE_AUDIO_FADE, \
|
||||
.audioFade = { \
|
||||
.channel = CHANNEL, \
|
||||
.from = FROM, \
|
||||
.to = TO, \
|
||||
.duration = DURATION, \
|
||||
.easing = EASING \
|
||||
} \
|
||||
}
|
||||
|
||||
// Shorthand: fade CHANNEL from full volume to silent over DURATION seconds.
|
||||
#define CUTSCENE_AUDIO_FADE_OUT(CHANNEL, DURATION) \
|
||||
CUTSCENE_AUDIO_FADE(CHANNEL, 1.0f, 0.0f, DURATION, EASING_LINEAR)
|
||||
|
||||
// Shorthand: fade CHANNEL from silent to full volume over DURATION seconds.
|
||||
#define CUTSCENE_AUDIO_FADE_IN(CHANNEL, DURATION) \
|
||||
CUTSCENE_AUDIO_FADE(CHANNEL, 0.0f, 1.0f, DURATION, EASING_LINEAR)
|
||||
|
||||
// Waits until CHANNEL's fade (started elsewhere by CUTSCENE_AUDIO_FADE)
|
||||
// has finished - a no-op wait if CHANNEL isn't fading.
|
||||
#define CUTSCENE_AUDIO_FADE_WAIT(CHANNEL) \
|
||||
{ \
|
||||
.type = CUTSCENE_ITEM_TYPE_AUDIO_FADE_WAIT, \
|
||||
.audioFadeWaitChannel = CHANNEL \
|
||||
}
|
||||
|
||||
#define CUTSCENE_AUDIO_SET_PAN(CHANNEL, PAN) \
|
||||
{ \
|
||||
.type = CUTSCENE_ITEM_TYPE_AUDIO_SET_PAN, \
|
||||
.audioSetPan = { .channel = CHANNEL, .pan = PAN } \
|
||||
}
|
||||
|
||||
#define CUTSCENE_AUDIO_SET_LOOP( \
|
||||
CHANNEL, LOOPING, LOOP_COUNT, LOOP_START, LOOP_TO \
|
||||
) \
|
||||
{ \
|
||||
.type = CUTSCENE_ITEM_TYPE_AUDIO_SET_LOOP, \
|
||||
.audioSetLoop = { \
|
||||
.channel = CHANNEL, \
|
||||
.looping = LOOPING, \
|
||||
.loopCount = LOOP_COUNT, \
|
||||
.loopStart = LOOP_START, \
|
||||
.loopTo = LOOP_TO \
|
||||
} \
|
||||
}
|
||||
|
||||
// Combined form of CUTSCENE_AUDIO_SET_PAN + CUTSCENE_AUDIO_SET_LOOP, for
|
||||
// setting both at once rather than as two separate items.
|
||||
#define CUTSCENE_AUDIO_SET( \
|
||||
CHANNEL, PAN, LOOPING, LOOP_COUNT, LOOP_START, LOOP_TO \
|
||||
) \
|
||||
{ \
|
||||
.type = CUTSCENE_ITEM_TYPE_AUDIO_SET, \
|
||||
.audioSet = { \
|
||||
.channel = CHANNEL, \
|
||||
.pan = PAN, \
|
||||
.looping = LOOPING, \
|
||||
.loopCount = LOOP_COUNT, \
|
||||
.loopStart = LOOP_START, \
|
||||
.loopTo = LOOP_TO \
|
||||
} \
|
||||
}
|
||||
/**
|
||||
* Loads a cutscene JSON asset by path and parses it into scene/items (see
|
||||
* cutsceneParseDoc) - the common "lock the asset, require it loaded,
|
||||
* parse its 'items' array, unlock it again" sequence shared by every
|
||||
* place that resolves a cutscene by file path (cutsceneSystemLoad,
|
||||
* cutsceneCutsceneResolve, cutsceneInsertResolveAndSplice,
|
||||
* battleStateExecutingInit, cutsceneSystemGoTo's reload). Purely propagates the
|
||||
* error on failure (like cutsceneParseDoc) - this is core cutscene logic,
|
||||
* not UI, so it doesn't reach for uiFatalErrorOpen itself; each caller
|
||||
* decides how to surface the failure (typically errorCatch(errorPrint())
|
||||
* + uiFatalErrorOpen(NULL), same as any other asset load failure).
|
||||
*
|
||||
* @param path Asset path of the cutscene JSON file to load.
|
||||
* @param scene Destination scene - itemCount/itemsMax/pause overwritten.
|
||||
* @param items Destination item array, capacity itemsMax.
|
||||
* @param itemsMax Capacity of items.
|
||||
* @return Error code indicating success or failure of the load/parse.
|
||||
*/
|
||||
errorret_t cutsceneLoadParse(
|
||||
const char_t *path,
|
||||
cutscene_t *scene,
|
||||
cutsceneitem_t *items,
|
||||
const size_t itemsMax
|
||||
);
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "cutsceneref.h"
|
||||
#include "assert/assert.h"
|
||||
|
||||
void cutsceneRefReset(
|
||||
cutsceneref_t *refs,
|
||||
entity_t *interact,
|
||||
entity_t *interacted
|
||||
) {
|
||||
assertNotNull(refs, "Refs cannot be null");
|
||||
|
||||
refs->entityInteract = interact;
|
||||
refs->entityInteracted = interacted;
|
||||
refs->entityLastCreated = NULL;
|
||||
refs->entityLastRef = NULL;
|
||||
refs->areaLastCreated = CUTSCENE_REF_AREA_LAST_CREATED;
|
||||
refs->textMiniLastCreated = CUTSCENE_REF_TEXT_MINI_LAST_CREATED;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* 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 entity_s entity_t;
|
||||
|
||||
#define CUTSCENE_REF_AREA_LAST_CREATED ((uint8_t)0xFF)
|
||||
#define CUTSCENE_REF_TEXT_MINI_LAST_CREATED ((uint8_t)0xFA)
|
||||
|
||||
typedef struct {
|
||||
entity_t *entityInteract;
|
||||
entity_t *entityInteracted;
|
||||
entity_t *entityLastCreated;
|
||||
entity_t *entityLastRef;
|
||||
uint8_t areaLastCreated;
|
||||
uint8_t textMiniLastCreated;
|
||||
} cutsceneref_t;
|
||||
|
||||
/**
|
||||
* Resets refs to an empty state.
|
||||
*
|
||||
* @param refs Refs to reset.
|
||||
* @param interact The entity that initiated the interaction, or NULL.
|
||||
* @param interacted The entity being interacted with, or NULL.
|
||||
*/
|
||||
void cutsceneRefReset(
|
||||
cutsceneref_t *refs,
|
||||
entity_t *interact,
|
||||
entity_t *interacted
|
||||
);
|
||||
@@ -10,89 +10,15 @@
|
||||
#include "util/memory.h"
|
||||
#include "util/string.h"
|
||||
#include "assert/assert.h"
|
||||
#include "asset/asset.h"
|
||||
#include "ui/overlay/uifatalerror.h"
|
||||
#include "save/slot/saveslot.h"
|
||||
|
||||
cutscenesystem_t CUTSCENE_SYSTEM;
|
||||
|
||||
void cutsceneSystemInit() {
|
||||
memoryZero(&CUTSCENE_SYSTEM, sizeof(cutscenesystem_t));
|
||||
}
|
||||
|
||||
void cutsceneSystemPrepare(
|
||||
const cutscene_t *cutscene,
|
||||
entity_t *interact,
|
||||
entity_t *interacted
|
||||
) {
|
||||
assertTrue(
|
||||
cutscene->dataSize < CUTSCENE_SYSTEM_SIZE_MAX,
|
||||
"Cutscene data size exceeds CUTSCENE_SYSTEM_SIZE_MAX"
|
||||
);
|
||||
|
||||
CUTSCENE_SYSTEM.scene = cutscene;
|
||||
CUTSCENE_SYSTEM.pause = cutscene->pause;
|
||||
CUTSCENE_SYSTEM.entityInteract = interact;
|
||||
CUTSCENE_SYSTEM.entityInteracted = interacted;
|
||||
CUTSCENE_SYSTEM.entityLastCreated = NULL;
|
||||
CUTSCENE_SYSTEM.entityLastRef = NULL;
|
||||
CUTSCENE_SYSTEM.areaLastCreated = CUTSCENE_AREA_LAST_CREATED;
|
||||
CUTSCENE_SYSTEM.textMiniLastCreated = CUTSCENE_TEXT_MINI_LAST_CREATED;
|
||||
CUTSCENE_SYSTEM.textCache[0] = '\0';
|
||||
CUTSCENE_SYSTEM.currentItem = 0xFF;// Set to 0xFF so Next wraps to 0.
|
||||
CUTSCENE_SYSTEM.onComplete = NULL;
|
||||
}
|
||||
|
||||
void cutsceneSystemStartCutscene(const cutscene_t *cutscene) {
|
||||
cutsceneSystemStartCutsceneWith(cutscene, NULL, NULL);
|
||||
}
|
||||
|
||||
void cutsceneSystemStartCutsceneWith(
|
||||
const cutscene_t *cutscene,
|
||||
entity_t *interact,
|
||||
entity_t *interacted
|
||||
) {
|
||||
cutsceneSystemPrepare(cutscene, interact, interacted);
|
||||
cutsceneSystemNext();
|
||||
}
|
||||
|
||||
void cutsceneSystemStartCutsceneAndGoToMarker(
|
||||
const cutscene_t *cutscene,
|
||||
const char_t *marker
|
||||
) {
|
||||
cutsceneSystemPrepare(cutscene, NULL, NULL);
|
||||
cutsceneGoTo(marker);
|
||||
}
|
||||
|
||||
void cutsceneSystemLoad(const char_t *file) {
|
||||
char_t path[ASSET_FILE_NAME_MAX];
|
||||
stringFormat(path, sizeof(path), "cutscenes/%s", file);
|
||||
|
||||
assetentry_t *entry = assetLock(path, ASSET_LOADER_TYPE_CUTSCENE, NULL);
|
||||
errorret_t result = assetRequireLoaded(entry);
|
||||
if(errorIsNotOk(result)) {
|
||||
errorCatch(errorPrint(result));
|
||||
uiFatalErrorOpen(NULL);
|
||||
return;
|
||||
}
|
||||
|
||||
cutsceneSystemStartCutscene(&entry->data.cutscene.cutscene);
|
||||
}
|
||||
|
||||
void cutsceneRestart(void) {
|
||||
assertNotNull(
|
||||
CUTSCENE_SYSTEM.scene, "cutsceneRestart called with no cutscene running"
|
||||
);
|
||||
|
||||
// A restart is the same logical run trying again (e.g. retrying a failed
|
||||
// save-device check), not a fresh unrelated start, so it should not
|
||||
// silently drop a completion callback the caller already armed.
|
||||
cutscenecallback_t onComplete = CUTSCENE_SYSTEM.onComplete;
|
||||
cutsceneSystemStartCutsceneWith(
|
||||
CUTSCENE_SYSTEM.scene,
|
||||
CUTSCENE_SYSTEM.entityInteract,
|
||||
CUTSCENE_SYSTEM.entityInteracted
|
||||
);
|
||||
CUTSCENE_SYSTEM.onComplete = onComplete;
|
||||
CUTSCENE_SYSTEM.loadedScene.items = CUTSCENE_SYSTEM.loadedItems;
|
||||
CUTSCENE_SYSTEM.loadedScene.itemsMax = CUTSCENE_LOADED_ITEMS_MAX;
|
||||
}
|
||||
|
||||
void cutsceneSystemUpdate() {
|
||||
@@ -102,42 +28,158 @@ void cutsceneSystemUpdate() {
|
||||
if(cutsceneItemUpdate(item, &CUTSCENE_SYSTEM.data)) cutsceneSystemNext();
|
||||
}
|
||||
|
||||
void cutsceneSystemNext() {
|
||||
if(CUTSCENE_SYSTEM.scene == NULL) return;
|
||||
void cutsceneSystemDispose() {
|
||||
CUTSCENE_SYSTEM.scene = NULL;
|
||||
CUTSCENE_SYSTEM.currentItem = CUTSCENE_CURRENT_ITEM_NONE;
|
||||
CUTSCENE_SYSTEM.pause = CUTSCENE_PAUSE_NONE;
|
||||
cutsceneRefReset(&CUTSCENE_SYSTEM.refs, NULL, NULL);
|
||||
CUTSCENE_SYSTEM.textCache[0] = '\0';
|
||||
CUTSCENE_SYSTEM.onComplete = NULL;
|
||||
CUTSCENE_SYSTEM.loadedFile[0] = '\0';
|
||||
}
|
||||
|
||||
CUTSCENE_SYSTEM.currentItem++;
|
||||
void cutsceneSystemPrepare(
|
||||
cutscene_t *cutscene,
|
||||
entity_t *interact,
|
||||
entity_t *interacted
|
||||
) {
|
||||
assertNotNull(cutscene, "Cutscene cannot be NULL");
|
||||
|
||||
// End of the cutscene?
|
||||
if(
|
||||
CUTSCENE_SYSTEM.currentItem >= CUTSCENE_SYSTEM.scene->itemCount
|
||||
) {
|
||||
// Saved and cleared before firing so a callback that immediately
|
||||
// starts another cutscene (or sets its own onComplete) isn't clobbered
|
||||
// by this function's own cleanup running after it - same reentrancy
|
||||
// hazard as uiFocusPop, see src/dusk/ui/focus/uifocus.c.
|
||||
cutscenecallback_t onComplete = CUTSCENE_SYSTEM.onComplete;
|
||||
void *userData = CUTSCENE_SYSTEM.userData;
|
||||
CUTSCENE_SYSTEM.scene = cutscene;
|
||||
CUTSCENE_SYSTEM.pause = cutscene->pause;
|
||||
cutsceneRefReset(&CUTSCENE_SYSTEM.refs, interact, interacted);
|
||||
CUTSCENE_SYSTEM.textCache[0] = '\0';
|
||||
CUTSCENE_SYSTEM.onComplete = NULL;
|
||||
|
||||
CUTSCENE_SYSTEM.scene = NULL;
|
||||
CUTSCENE_SYSTEM.currentItem = 0xFF;
|
||||
CUTSCENE_SYSTEM.pause = CUTSCENE_PAUSE_NONE;
|
||||
CUTSCENE_SYSTEM.entityInteract = NULL;
|
||||
CUTSCENE_SYSTEM.entityInteracted = NULL;
|
||||
CUTSCENE_SYSTEM.entityLastCreated = NULL;
|
||||
CUTSCENE_SYSTEM.entityLastRef = NULL;
|
||||
CUTSCENE_SYSTEM.areaLastCreated = CUTSCENE_AREA_LAST_CREATED;
|
||||
CUTSCENE_SYSTEM.textMiniLastCreated = CUTSCENE_TEXT_MINI_LAST_CREATED;
|
||||
CUTSCENE_SYSTEM.textCache[0] = '\0';
|
||||
CUTSCENE_SYSTEM.onComplete = NULL;
|
||||
// cutsceneNext will cause overflow to index 0 (intentional).
|
||||
CUTSCENE_SYSTEM.currentItem = CUTSCENE_CURRENT_ITEM_NONE;
|
||||
|
||||
if(onComplete != NULL) onComplete(userData);
|
||||
// Clear loaded file if cutscene not running from file.
|
||||
if(cutscene != &CUTSCENE_SYSTEM.loadedScene) {
|
||||
CUTSCENE_SYSTEM.loadedFile[0] = '\0';
|
||||
}
|
||||
}
|
||||
|
||||
void cutsceneSystemStartCutscene(cutscene_t *cutscene) {
|
||||
cutsceneSystemStartCutsceneWith(cutscene, NULL, NULL);
|
||||
}
|
||||
|
||||
void cutsceneSystemStartCutsceneWith(
|
||||
cutscene_t *cutscene,
|
||||
entity_t *interact,
|
||||
entity_t *interacted
|
||||
) {
|
||||
assertNotNull(cutscene, "Cutscene cannot be NULL");
|
||||
assertTrue(
|
||||
cutscene->itemCount <= cutscene->itemsMax,
|
||||
"Cutscene itemCount exceeds itemsMax"
|
||||
);
|
||||
|
||||
// Prep cutscene, save it to the save slot, and advance, aka overflow to 0.
|
||||
cutsceneSystemPrepare(cutscene, interact, interacted);
|
||||
saveSlotSetCurrentCutscene(CUTSCENE_SYSTEM.loadedFile);
|
||||
cutsceneSystemNext();
|
||||
}
|
||||
|
||||
void cutsceneSystemStartCutsceneAndGoToMarker(
|
||||
cutscene_t *cutscene,
|
||||
const char_t *marker
|
||||
) {
|
||||
assertNotNull(marker, "Marker cannot be NULL");
|
||||
|
||||
// Restarts the cutscene
|
||||
cutsceneSystemPrepare(cutscene, NULL, NULL);
|
||||
cutsceneSystemGoTo(marker);
|
||||
}
|
||||
|
||||
void cutsceneSystemInsertItemsNext(
|
||||
const cutsceneitem_t *items,
|
||||
const size_t itemCount
|
||||
) {
|
||||
assertNotNull(items, "Items cannot be NULL");
|
||||
assertNotNull(
|
||||
CUTSCENE_SYSTEM.scene,
|
||||
"cutsceneSystemInsertItemsNext called with no cutscene running"
|
||||
);
|
||||
cutsceneAppendNext(
|
||||
CUTSCENE_SYSTEM.scene, CUTSCENE_SYSTEM.currentItem, items, itemCount
|
||||
);
|
||||
}
|
||||
|
||||
void cutsceneSystemInsertItems(
|
||||
const cutsceneitem_t *items,
|
||||
const size_t itemCount
|
||||
) {
|
||||
cutsceneSystemInsertItemsNext(items, itemCount);
|
||||
cutsceneSystemNext();
|
||||
}
|
||||
|
||||
void cutsceneSystemInsertCutscene(const cutscene_t *cutscene) {
|
||||
assertNotNull(cutscene, "Cutscene cannot be NULL");
|
||||
cutsceneSystemInsertItems(cutscene->items, cutscene->itemCount);
|
||||
}
|
||||
|
||||
void cutsceneSystemClearAfter(const uint8_t index) {
|
||||
assertNotNull(
|
||||
CUTSCENE_SYSTEM.scene,
|
||||
"cutsceneSystemClearAfter called with no cutscene running"
|
||||
);
|
||||
assertTrue(
|
||||
index < CUTSCENE_SYSTEM.scene->itemCount,
|
||||
"cutsceneSystemClearAfter index out of range"
|
||||
);
|
||||
|
||||
CUTSCENE_SYSTEM.scene->itemCount = index + 1;
|
||||
}
|
||||
|
||||
void cutsceneSystemLoad(const char_t *file) {
|
||||
assertNotNull(file, "File cannot be NULL");
|
||||
|
||||
// Take a copy of file, as it may be a part of the cutscene itself.
|
||||
char_t fileCopy[ASSET_FILE_NAME_MAX];
|
||||
stringCopy(fileCopy, file, ASSET_FILE_NAME_MAX - 1);
|
||||
|
||||
// Load the cutscene asset, parse it, and start it.
|
||||
errorret_t result = cutsceneLoadParse(
|
||||
fileCopy, &CUTSCENE_SYSTEM.loadedScene, CUTSCENE_SYSTEM.loadedItems,
|
||||
CUTSCENE_LOADED_ITEMS_MAX
|
||||
);
|
||||
if(errorIsNotOk(result)) {
|
||||
errorCatch(errorPrint(result));
|
||||
uiFatalErrorOpen(NULL);
|
||||
return;
|
||||
}
|
||||
|
||||
// Store the loaded file so we can restart if needed.
|
||||
stringCopy(CUTSCENE_SYSTEM.loadedFile, fileCopy, ASSET_FILE_NAME_MAX - 1);
|
||||
cutsceneSystemStartCutscene(&CUTSCENE_SYSTEM.loadedScene);
|
||||
}
|
||||
|
||||
// Start item.
|
||||
const cutsceneitem_t *item = cutsceneSystemGetCurrentItem();
|
||||
memset(&CUTSCENE_SYSTEM.data, 0, sizeof(CUTSCENE_SYSTEM.data));
|
||||
cutsceneItemStart(item, &CUTSCENE_SYSTEM.data);
|
||||
void cutsceneRestart(void) {
|
||||
assertNotNull(
|
||||
CUTSCENE_SYSTEM.scene, "cutsceneRestart called with no cutscene running"
|
||||
);
|
||||
assertTrue(
|
||||
CUTSCENE_SYSTEM.scene == &CUTSCENE_SYSTEM.loadedScene,
|
||||
"cutsceneRestart called with a cutscene not loaded from file"
|
||||
);
|
||||
assertTrue(
|
||||
CUTSCENE_SYSTEM.loadedFile[0] != '\0',
|
||||
"cutsceneRestart called with no loaded file"
|
||||
);
|
||||
|
||||
// Copy the refs and callbacks
|
||||
cutscenecallback_t onComplete = CUTSCENE_SYSTEM.onComplete;
|
||||
cutsceneref_t refs = CUTSCENE_SYSTEM.refs;
|
||||
|
||||
// Reload the file.
|
||||
char_t file[ASSET_FILE_NAME_MAX];
|
||||
stringCopy(file, CUTSCENE_SYSTEM.loadedFile, ASSET_FILE_NAME_MAX - 1);
|
||||
cutsceneSystemLoad(file);
|
||||
|
||||
// Restore the refs and callbacks.
|
||||
CUTSCENE_SYSTEM.onComplete = onComplete;
|
||||
CUTSCENE_SYSTEM.refs = refs;
|
||||
}
|
||||
|
||||
void cutsceneSystemSetOnComplete(cutscenecallback_t onComplete) {
|
||||
@@ -148,60 +190,33 @@ void cutsceneSystemSetOnComplete(cutscenecallback_t onComplete) {
|
||||
CUTSCENE_SYSTEM.onComplete = onComplete;
|
||||
}
|
||||
|
||||
void cutsceneGoTo(const char_t *name) {
|
||||
assertNotNull(
|
||||
CUTSCENE_SYSTEM.scene, "cutsceneGoTo called with no cutscene running"
|
||||
);
|
||||
|
||||
for(uint8_t i = 0; i < CUTSCENE_SYSTEM.scene->itemCount; i++) {
|
||||
const cutsceneitem_t *item = &CUTSCENE_SYSTEM.scene->items[i];
|
||||
if(
|
||||
item->type == CUTSCENE_ITEM_TYPE_MARKER &&
|
||||
stringEquals(item->marker.name, name)
|
||||
) {
|
||||
CUTSCENE_SYSTEM.currentItem = i;
|
||||
memoryZero(&CUTSCENE_SYSTEM.data, sizeof(CUTSCENE_SYSTEM.data));
|
||||
cutsceneItemStart(item, &CUTSCENE_SYSTEM.data);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
assertTrue(false, "cutsceneGoTo: no marker found with that name");
|
||||
}
|
||||
|
||||
const cutsceneitem_t * cutsceneSystemGetCurrentItem() {
|
||||
if(CUTSCENE_SYSTEM.scene == NULL) return NULL;
|
||||
|
||||
return &CUTSCENE_SYSTEM.scene->items[CUTSCENE_SYSTEM.currentItem];
|
||||
}
|
||||
|
||||
entity_t * cutsceneSystemGetEntity(const uint8_t entityIndex) {
|
||||
entity_t *entity;
|
||||
|
||||
if(entityIndex == CUTSCENE_ENTITY_INTERACT) {
|
||||
assertNotNull(
|
||||
CUTSCENE_SYSTEM.entityInteract,
|
||||
CUTSCENE_SYSTEM.refs.entityInteract,
|
||||
"CUTSCENE_ENTITY_INTERACT used but no interact entity is set"
|
||||
);
|
||||
entity = CUTSCENE_SYSTEM.entityInteract;
|
||||
entity = CUTSCENE_SYSTEM.refs.entityInteract;
|
||||
} else if(entityIndex == CUTSCENE_ENTITY_INTERACTED) {
|
||||
assertNotNull(
|
||||
CUTSCENE_SYSTEM.entityInteracted,
|
||||
CUTSCENE_SYSTEM.refs.entityInteracted,
|
||||
"CUTSCENE_ENTITY_INTERACTED used but no interacted entity is set"
|
||||
);
|
||||
entity = CUTSCENE_SYSTEM.entityInteracted;
|
||||
entity = CUTSCENE_SYSTEM.refs.entityInteracted;
|
||||
} else if(entityIndex == CUTSCENE_ENTITY_LAST_CREATED) {
|
||||
assertNotNull(
|
||||
CUTSCENE_SYSTEM.entityLastCreated,
|
||||
CUTSCENE_SYSTEM.refs.entityLastCreated,
|
||||
"CUTSCENE_ENTITY_LAST_CREATED used but no entity has been created"
|
||||
);
|
||||
entity = CUTSCENE_SYSTEM.entityLastCreated;
|
||||
entity = CUTSCENE_SYSTEM.refs.entityLastCreated;
|
||||
} else if(entityIndex == CUTSCENE_ENTITY_LAST_REF) {
|
||||
assertNotNull(
|
||||
CUTSCENE_SYSTEM.entityLastRef,
|
||||
CUTSCENE_SYSTEM.refs.entityLastRef,
|
||||
"CUTSCENE_ENTITY_LAST_REF used but no entity has been referenced"
|
||||
);
|
||||
entity = CUTSCENE_SYSTEM.entityLastRef;
|
||||
entity = CUTSCENE_SYSTEM.refs.entityLastRef;
|
||||
} else {
|
||||
assertTrue(
|
||||
entityIndex < ENTITY_COUNT,
|
||||
@@ -210,29 +225,30 @@ entity_t * cutsceneSystemGetEntity(const uint8_t entityIndex) {
|
||||
entity = &ENTITIES[entityIndex];
|
||||
}
|
||||
|
||||
CUTSCENE_SYSTEM.entityLastRef = entity;
|
||||
CUTSCENE_SYSTEM.refs.entityLastRef = entity;
|
||||
return entity;
|
||||
}
|
||||
|
||||
uint8_t cutsceneSystemGetAreaId(const uint8_t areaId) {
|
||||
if(areaId == CUTSCENE_AREA_LAST_CREATED) {
|
||||
if(areaId == CUTSCENE_REF_AREA_LAST_CREATED) {
|
||||
assertTrue(
|
||||
CUTSCENE_SYSTEM.areaLastCreated != CUTSCENE_AREA_LAST_CREATED,
|
||||
"CUTSCENE_AREA_LAST_CREATED used but no map area has been created"
|
||||
CUTSCENE_SYSTEM.refs.areaLastCreated != CUTSCENE_REF_AREA_LAST_CREATED,
|
||||
"CUTSCENE_REF_AREA_LAST_CREATED used but no map area has been created"
|
||||
);
|
||||
return CUTSCENE_SYSTEM.areaLastCreated;
|
||||
return CUTSCENE_SYSTEM.refs.areaLastCreated;
|
||||
}
|
||||
return areaId;
|
||||
}
|
||||
|
||||
uint8_t cutsceneSystemGetTextMiniId(const uint8_t index) {
|
||||
if(index == CUTSCENE_TEXT_MINI_LAST_CREATED) {
|
||||
if(index == CUTSCENE_REF_TEXT_MINI_LAST_CREATED) {
|
||||
assertTrue(
|
||||
CUTSCENE_SYSTEM.textMiniLastCreated != CUTSCENE_TEXT_MINI_LAST_CREATED,
|
||||
"CUTSCENE_TEXT_MINI_LAST_CREATED used but no mini textbox has been "
|
||||
CUTSCENE_SYSTEM.refs.textMiniLastCreated !=
|
||||
CUTSCENE_REF_TEXT_MINI_LAST_CREATED,
|
||||
"CUTSCENE_REF_TEXT_MINI_LAST_CREATED used but no mini textbox has been "
|
||||
"shown"
|
||||
);
|
||||
return CUTSCENE_SYSTEM.textMiniLastCreated;
|
||||
return CUTSCENE_SYSTEM.refs.textMiniLastCreated;
|
||||
}
|
||||
return index;
|
||||
}
|
||||
@@ -242,21 +258,101 @@ const char_t * cutsceneSystemGetTextCache(void) {
|
||||
}
|
||||
|
||||
void cutsceneSystemSetTextCache(const char_t *text) {
|
||||
assertNotNull(text, "Text cannot be NULL");
|
||||
|
||||
stringCopy(
|
||||
CUTSCENE_SYSTEM.textCache, text, CUTSCENE_TEXT_CACHE_MAX - 1
|
||||
);
|
||||
}
|
||||
|
||||
void cutsceneSystemDispose() {
|
||||
CUTSCENE_SYSTEM.scene = NULL;
|
||||
CUTSCENE_SYSTEM.currentItem = 0xFF;
|
||||
CUTSCENE_SYSTEM.pause = CUTSCENE_PAUSE_NONE;
|
||||
CUTSCENE_SYSTEM.entityInteract = NULL;
|
||||
CUTSCENE_SYSTEM.entityInteracted = NULL;
|
||||
CUTSCENE_SYSTEM.entityLastCreated = NULL;
|
||||
CUTSCENE_SYSTEM.entityLastRef = NULL;
|
||||
CUTSCENE_SYSTEM.areaLastCreated = CUTSCENE_AREA_LAST_CREATED;
|
||||
CUTSCENE_SYSTEM.textMiniLastCreated = CUTSCENE_TEXT_MINI_LAST_CREATED;
|
||||
CUTSCENE_SYSTEM.textCache[0] = '\0';
|
||||
CUTSCENE_SYSTEM.onComplete = NULL;
|
||||
void cutsceneSystemNext() {
|
||||
if(CUTSCENE_SYSTEM.scene == NULL) return;
|
||||
|
||||
// Pop the just-finished item off the front.
|
||||
if(CUTSCENE_SYSTEM.currentItem == CUTSCENE_CURRENT_ITEM_NONE) {
|
||||
CUTSCENE_SYSTEM.currentItem = 0;
|
||||
} else {
|
||||
cutscene_t *scene = CUTSCENE_SYSTEM.scene;
|
||||
const size_t tailCount = (size_t)scene->itemCount - 1;
|
||||
if(tailCount > 0) {
|
||||
memoryMove(
|
||||
&scene->items[0], &scene->items[1], tailCount * sizeof(cutsceneitem_t)
|
||||
);
|
||||
}
|
||||
scene->itemCount--;
|
||||
}
|
||||
|
||||
// End of the cutscene?
|
||||
if(CUTSCENE_SYSTEM.scene->itemCount == 0) {
|
||||
cutscenecallback_t onComplete = CUTSCENE_SYSTEM.onComplete;
|
||||
|
||||
CUTSCENE_SYSTEM.scene = NULL;
|
||||
CUTSCENE_SYSTEM.pause = CUTSCENE_PAUSE_NONE;
|
||||
|
||||
if(onComplete != NULL) onComplete(NULL);
|
||||
return;
|
||||
}
|
||||
|
||||
// Start next (now current) item
|
||||
const cutsceneitem_t *item = cutsceneSystemGetCurrentItem();
|
||||
memset(&CUTSCENE_SYSTEM.data, 0, sizeof(CUTSCENE_SYSTEM.data));
|
||||
cutsceneItemStart(item, &CUTSCENE_SYSTEM.data);
|
||||
}
|
||||
|
||||
void cutsceneSystemGoTo(const char_t *name) {
|
||||
assertNotNull(name, "Name cannot be NULL");
|
||||
assertNotNull(
|
||||
CUTSCENE_SYSTEM.scene, "cutsceneSystemGoTo called with no cutscene running"
|
||||
);
|
||||
assertTrue(
|
||||
CUTSCENE_SYSTEM.scene == &CUTSCENE_SYSTEM.loadedScene,
|
||||
"cutsceneSystemGoTo requires the running cutscene file reference (1)"
|
||||
);
|
||||
assertTrue(
|
||||
CUTSCENE_SYSTEM.loadedFile[0] != '\0',
|
||||
"cutsceneSystemGoTo requires the running cutscene file reference (2)"
|
||||
);
|
||||
|
||||
cutscene_t freshScene = { .items = CUTSCENE_SYSTEM.gotoScratchItems };
|
||||
errorret_t result = cutsceneLoadParse(
|
||||
CUTSCENE_SYSTEM.loadedFile, &freshScene, CUTSCENE_SYSTEM.gotoScratchItems,
|
||||
CUTSCENE_LOADED_ITEMS_MAX
|
||||
);
|
||||
if(errorIsNotOk(result)) {
|
||||
errorCatch(errorPrint(result));
|
||||
uiFatalErrorOpen(NULL);
|
||||
return;
|
||||
}
|
||||
|
||||
// Search for the marker with the given name...
|
||||
for(uint8_t i = 0; i < freshScene.itemCount; i++) {
|
||||
const cutsceneitem_t *item = &freshScene.items[i];
|
||||
if(
|
||||
item->type != CUTSCENE_ITEM_TYPE_MARKER ||
|
||||
!stringEquals(item->marker.name, name)
|
||||
) continue;
|
||||
|
||||
// Found, so splice the cutscene at that point and start it, the remaining
|
||||
// items being the rest of the cutscene after the marker.
|
||||
const uint8_t remaining = freshScene.itemCount - i;
|
||||
memoryCopy(
|
||||
CUTSCENE_SYSTEM.loadedItems, &freshScene.items[i],
|
||||
remaining * sizeof(cutsceneitem_t)
|
||||
);
|
||||
CUTSCENE_SYSTEM.loadedScene.itemCount = remaining;
|
||||
CUTSCENE_SYSTEM.currentItem = 0;
|
||||
|
||||
memoryZero(&CUTSCENE_SYSTEM.data, sizeof(CUTSCENE_SYSTEM.data));
|
||||
cutsceneItemStart(
|
||||
&CUTSCENE_SYSTEM.loadedItems[0], &CUTSCENE_SYSTEM.data
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
assertTrue(false, "cutsceneSystemGoTo: no marker found with that name");
|
||||
}
|
||||
|
||||
const cutsceneitem_t * cutsceneSystemGetCurrentItem() {
|
||||
if(CUTSCENE_SYSTEM.scene == NULL) return NULL;
|
||||
return &CUTSCENE_SYSTEM.scene->items[0];
|
||||
}
|
||||
|
||||
@@ -1,57 +1,48 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Dominic Masters
|
||||
*
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "cutscene.h"
|
||||
#include "cutsceneref.h"
|
||||
|
||||
typedef struct entity_s entity_t;
|
||||
#define CUTSCENE_ENTITY_INTERACT ((uint8_t)0xFE)
|
||||
#define CUTSCENE_ENTITY_INTERACTED ((uint8_t)0xFD)
|
||||
#define CUTSCENE_ENTITY_LAST_CREATED ((uint8_t)0xFC)
|
||||
#define CUTSCENE_ENTITY_LAST_REF ((uint8_t)0xFB)
|
||||
#define CUTSCENE_CURRENT_ITEM_NONE ((uint8_t)0xFF)
|
||||
|
||||
#define CUTSCENE_ENTITY_INTERACT ((uint8_t)0xFE)
|
||||
#define CUTSCENE_ENTITY_INTERACTED ((uint8_t)0xFD)
|
||||
#define CUTSCENE_ENTITY_LAST_CREATED ((uint8_t)0xFC)
|
||||
#define CUTSCENE_ENTITY_LAST_REF ((uint8_t)0xFB)
|
||||
#define CUTSCENE_AREA_LAST_CREATED ((uint8_t)0xFF)
|
||||
#define CUTSCENE_TEXT_MINI_LAST_CREATED ((uint8_t)0xFA)
|
||||
|
||||
// Maximum number of bytes a running cutscene may request via
|
||||
// cutscene_t.dataSize.
|
||||
#define CUTSCENE_SYSTEM_SIZE_MAX 8192
|
||||
|
||||
// Size of CUTSCENE_SYSTEM.textCache - keep >= UI_KEYBOARD_TEXT_MAX (see
|
||||
// ui/dialog/keyboard/uikeyboard.h) so text entered via a
|
||||
// CUTSCENE_ITEM_TYPE_KEYBOARD item is never truncated caching it here.
|
||||
#define CUTSCENE_TEXT_CACHE_MAX 64
|
||||
#define CUTSCENE_LOADED_ITEMS_MAX 128
|
||||
#define CUTSCENE_INSERT_ITEMS_MAX 16
|
||||
|
||||
typedef struct {
|
||||
const cutscene_t *scene;
|
||||
cutscene_t *scene;
|
||||
uint8_t currentItem;
|
||||
cutscenepause_t pause;
|
||||
entity_t *entityInteract;
|
||||
entity_t *entityInteracted;
|
||||
entity_t *entityLastCreated;
|
||||
entity_t *entityLastRef;
|
||||
uint8_t areaLastCreated;
|
||||
uint8_t textMiniLastCreated;
|
||||
|
||||
// Free-form text cache, e.g. holding whatever was last typed via a
|
||||
// CUTSCENE_ITEM_TYPE_KEYBOARD item - see cutsceneSystemGetTextCache/
|
||||
// cutsceneSystemSetTextCache. Not tied to any one item type; any item
|
||||
// may read or write it.
|
||||
cutsceneref_t refs;
|
||||
|
||||
// Free-form text cache for the running cutscene - see
|
||||
// cutsceneSystemGetTextCache/cutsceneSystemSetTextCache.
|
||||
char_t textCache[CUTSCENE_TEXT_CACHE_MAX];
|
||||
|
||||
// Data (used by the current item).
|
||||
// Runtime data for the current item.
|
||||
cutsceneitemdata_t data;
|
||||
|
||||
// Custom user data for the running cutscene, sized per-scene by
|
||||
// cutscene_t.dataSize.
|
||||
uint8_t userData[CUTSCENE_SYSTEM_SIZE_MAX];
|
||||
|
||||
// See cutsceneSystemSetOnComplete.
|
||||
cutscenecallback_t onComplete;
|
||||
|
||||
// Cache and storage used for the dynamically loaded cutscenes.
|
||||
cutsceneitem_t loadedItems[CUTSCENE_LOADED_ITEMS_MAX];
|
||||
cutscene_t loadedScene;
|
||||
char_t loadedFile[ASSET_FILE_NAME_MAX];
|
||||
|
||||
// Cutscene items scratch, mostly used for CUTSCENE_INSERT items.
|
||||
cutsceneitem_t gotoScratchItems[CUTSCENE_LOADED_ITEMS_MAX];
|
||||
} cutscenesystem_t;
|
||||
|
||||
extern cutscenesystem_t CUTSCENE_SYSTEM;
|
||||
@@ -61,12 +52,32 @@ extern cutscenesystem_t CUTSCENE_SYSTEM;
|
||||
*/
|
||||
void cutsceneSystemInit();
|
||||
|
||||
/**
|
||||
* Disposes of the cutscene system, stopping any active cutscene.
|
||||
*/
|
||||
void cutsceneSystemDispose();
|
||||
|
||||
/**
|
||||
* Resets CUTSCENE_SYSTEM to run cutscene from its first item, binding
|
||||
* interact/interacted entities. Does not itself advance to the first item.
|
||||
*
|
||||
* @param cutscene Pointer to the cutscene to prepare.
|
||||
* @param interact The entity that initiated the interaction (player), or
|
||||
* NULL.
|
||||
* @param interacted The entity being interacted with (NPC), or NULL.
|
||||
*/
|
||||
void cutsceneSystemPrepare(
|
||||
cutscene_t *cutscene,
|
||||
entity_t *interact,
|
||||
entity_t *interacted
|
||||
);
|
||||
|
||||
/**
|
||||
* Start a cutscene with no bound entities.
|
||||
*
|
||||
* @param cutscene Pointer to the cutscene to start.
|
||||
*/
|
||||
void cutsceneSystemStartCutscene(const cutscene_t *cutscene);
|
||||
void cutsceneSystemStartCutscene(cutscene_t *cutscene);
|
||||
|
||||
/**
|
||||
* Start a cutscene with the two entities that triggered it.
|
||||
@@ -76,74 +87,95 @@ void cutsceneSystemStartCutscene(const cutscene_t *cutscene);
|
||||
* @param interacted The entity that was interacted with (NPC).
|
||||
*/
|
||||
void cutsceneSystemStartCutsceneWith(
|
||||
const cutscene_t *cutscene,
|
||||
cutscene_t *cutscene,
|
||||
entity_t *interact,
|
||||
entity_t *interacted
|
||||
);
|
||||
|
||||
/**
|
||||
* Starts a cutscene with no bound entities, jumping straight to the
|
||||
* CUTSCENE_MARKER item with the given name instead of running from the
|
||||
* first item - as if cutsceneGoTo(marker) had been called immediately
|
||||
* after cutsceneSystemStartCutscene. Asserts if no marker with that
|
||||
* name exists in the cutscene.
|
||||
* marker with the given name instead of running from the first item.
|
||||
* Asserts if no marker with that name exists.
|
||||
*
|
||||
* @param cutscene Pointer to the cutscene to start.
|
||||
* @param marker Marker name to jump to, matched with stringEquals.
|
||||
* @param marker Marker name to jump to.
|
||||
*/
|
||||
void cutsceneSystemStartCutsceneAndGoToMarker(
|
||||
const cutscene_t *cutscene,
|
||||
cutscene_t *cutscene,
|
||||
const char_t *marker
|
||||
);
|
||||
|
||||
/**
|
||||
* Locks, loads and immediately starts a cutscene asset by file name,
|
||||
* e.g. cutsceneSystemLoad("main_menu.jsonc") loads and starts
|
||||
* assets/cutscenes/main_menu.jsonc - same lock/assetRequireLoaded/start
|
||||
* sequence as sceneInitialInit's INITIAL_CUTSCENE_ENTRY. The entry is
|
||||
* locked forever (never unlocked), same lifetime convention as
|
||||
* LOCALE.entry - it's fine to call this again later for the same file,
|
||||
* it'll just resolve to the same already-loaded entry. Opens the fatal
|
||||
* error overlay (see uiFatalErrorOpen) instead of starting anything if
|
||||
* the asset fails to load.
|
||||
* Splices items into the running cutscene so that items[0] becomes the
|
||||
* very next item, without otherwise disturbing the currently-running
|
||||
* item's progress.
|
||||
*
|
||||
* @param file Cutscene file name (with .jsonc extension), relative to
|
||||
* assets/cutscenes/.
|
||||
* @param items Items to splice in.
|
||||
* @param itemCount Number of items in items.
|
||||
*/
|
||||
void cutsceneSystemInsertItemsNext(
|
||||
const cutsceneitem_t *items,
|
||||
const size_t itemCount
|
||||
);
|
||||
|
||||
/**
|
||||
* Splices items into the running cutscene right after the current item
|
||||
* (see cutsceneSystemInsertItemsNext), then immediately advances into the
|
||||
* first spliced item.
|
||||
*
|
||||
* @param items Items to splice in.
|
||||
* @param itemCount Number of items in items.
|
||||
*/
|
||||
void cutsceneSystemInsertItems(
|
||||
const cutsceneitem_t *items,
|
||||
const size_t itemCount
|
||||
);
|
||||
|
||||
/**
|
||||
* Splices cutscene's items into the running cutscene.
|
||||
*
|
||||
* @param cutscene The cutscene whose items to splice in.
|
||||
*/
|
||||
void cutsceneSystemInsertCutscene(const cutscene_t *cutscene);
|
||||
|
||||
/**
|
||||
* Discards every item queued after index (e.g. index 0 keeps only the
|
||||
* currently-running item and drops everything spliced in behind it),
|
||||
* without disturbing index's own progress - e.g. so a callback that
|
||||
* decides the rest of a batch-spliced sequence is now moot (a battle
|
||||
* ending mid-round, with other fighters' still-queued move cutscenes and
|
||||
* the round's trailing state transition left behind) can drop it before
|
||||
* splicing in its own follow-up items, instead of leaving it to run
|
||||
* afterward against torn-down state.
|
||||
*
|
||||
* @param index Index to keep - everything after it is discarded. Must be
|
||||
* < the running cutscene's current item count.
|
||||
*/
|
||||
void cutsceneSystemClearAfter(const uint8_t index);
|
||||
|
||||
/**
|
||||
* Loads and immediately starts a cutscene asset by file name.
|
||||
*
|
||||
* @param file Cutscene file name.
|
||||
*/
|
||||
void cutsceneSystemLoad(const char_t *file);
|
||||
|
||||
/**
|
||||
* Restarts the currently running cutscene from its first item,
|
||||
* preserving whatever interact/interacted entities triggered it.
|
||||
* Asserts if no cutscene is running.
|
||||
* Restarts the currently running cutscene. Will perform a load if the cutscene
|
||||
* was initially from a file.
|
||||
*/
|
||||
void cutsceneRestart(void);
|
||||
|
||||
/**
|
||||
* Sets a native callback to fire once when the currently running cutscene
|
||||
* finishes by running off the end of its item list. A fresh
|
||||
* cutsceneSystemStartCutscene* call clears any previously set callback, so
|
||||
* call this again after starting a new cutscene to arm it - but
|
||||
* cutsceneRestart() preserves whatever was armed, since a restart (e.g.
|
||||
* retrying a failed check) is the same logical run trying again, not a new
|
||||
* one. Invoked with CUTSCENE_SYSTEM.userData, same as CUTSCENE_CALLBACK.
|
||||
* finishes.
|
||||
*
|
||||
* Exists so a runtime-loaded cutscene file (which can't store a native
|
||||
* function pointer) can still hand off to native code once it's done,
|
||||
* without needing a whole name->function registry: the file just ends
|
||||
* normally, and whoever started it supplies what happens next.
|
||||
*
|
||||
* @param onComplete Callback to fire on natural completion. May be NULL
|
||||
* to clear a previously set one.
|
||||
* @param onComplete Callback to fire on completion.
|
||||
*/
|
||||
void cutsceneSystemSetOnComplete(cutscenecallback_t onComplete);
|
||||
|
||||
/**
|
||||
* Resolves a raw entity index (or sentinel) to an entity pointer.
|
||||
* Handles CUTSCENE_ENTITY_INTERACT, CUTSCENE_ENTITY_INTERACTED,
|
||||
* CUTSCENE_ENTITY_LAST_CREATED and CUTSCENE_ENTITY_LAST_REF.
|
||||
* Updates CUTSCENE_SYSTEM.entityLastRef to the resolved entity.
|
||||
* Asserts the resolved entity is within bounds.
|
||||
*
|
||||
* @param entityIndex Raw entity index or sentinel value.
|
||||
* @returns Pointer to the resolved entity.
|
||||
@@ -151,7 +183,7 @@ void cutsceneSystemSetOnComplete(cutscenecallback_t onComplete);
|
||||
entity_t * cutsceneSystemGetEntity(const uint8_t entityIndex);
|
||||
|
||||
/**
|
||||
* Resolves a raw map area ID (or CUTSCENE_AREA_LAST_CREATED sentinel) to
|
||||
* Resolves a raw map area ID (or CUTSCENE_REF_AREA_LAST_CREATED sentinel) to
|
||||
* a concrete map area ID.
|
||||
*
|
||||
* @param areaId Raw map area ID or sentinel value.
|
||||
@@ -160,7 +192,7 @@ entity_t * cutsceneSystemGetEntity(const uint8_t entityIndex);
|
||||
uint8_t cutsceneSystemGetAreaId(const uint8_t areaId);
|
||||
|
||||
/**
|
||||
* Resolves a raw mini textbox slot index (or CUTSCENE_TEXT_MINI_LAST_CREATED
|
||||
* Resolves a raw mini textbox slot index (or CUTSCENE_REF_TEXT_MINI_LAST_CREATED
|
||||
* sentinel) to a concrete UI_TEXTBOX_MINI_LIST slot index.
|
||||
*
|
||||
* @param index Raw slot index or sentinel value.
|
||||
@@ -169,23 +201,17 @@ uint8_t cutsceneSystemGetAreaId(const uint8_t areaId);
|
||||
uint8_t cutsceneSystemGetTextMiniId(const uint8_t index);
|
||||
|
||||
/**
|
||||
* Returns CUTSCENE_SYSTEM.textCache - whatever was last written there via
|
||||
* cutsceneSystemSetTextCache (e.g. by a CUTSCENE_ITEM_TYPE_KEYBOARD item
|
||||
* once its keyboard closes). Empty ("") if nothing has been cached yet
|
||||
* for the running cutscene.
|
||||
* Returns the running cutscene's cached text - see
|
||||
* cutsceneSystemSetTextCache. Empty ("") if nothing has been cached yet.
|
||||
*
|
||||
* @returns The cached text.
|
||||
*/
|
||||
const char_t * cutsceneSystemGetTextCache(void);
|
||||
|
||||
/**
|
||||
* Overwrites CUTSCENE_SYSTEM.textCache with a copy of text. Any item may
|
||||
* call this - it isn't tied to any one item type - so later items can
|
||||
* read back whatever the caller wants to pass along, up to
|
||||
* CUTSCENE_TEXT_CACHE_MAX - 1 characters.
|
||||
* Overwrites the running cutscene's cached text.
|
||||
*
|
||||
* @param text The text to cache; copied internally, safe to be
|
||||
* transient. Must not exceed CUTSCENE_TEXT_CACHE_MAX - 1 characters.
|
||||
* @param text The text to cache; copied internally.
|
||||
*/
|
||||
void cutsceneSystemSetTextCache(const char_t *text);
|
||||
|
||||
@@ -195,16 +221,12 @@ void cutsceneSystemSetTextCache(const char_t *text);
|
||||
void cutsceneSystemNext();
|
||||
|
||||
/**
|
||||
* Jumps the running cutscene directly to the CUTSCENE_MARKER item with
|
||||
* the given name and starts it immediately, as if cutsceneSystemNext()
|
||||
* had advanced straight to it. Intended to be called from within
|
||||
* another item's start/update (e.g. a CUTSCENE_CALLBACK) to implement
|
||||
* flow control. Asserts if no cutscene is running or no marker with
|
||||
* that name exists in it.
|
||||
* Jumps to a cutscene marker directly. If the cutscene is a loaded cutscene,
|
||||
* this will result in it being reloaded (due to unshift nature of cutscenes).
|
||||
*
|
||||
* @param name Marker name to search for, matched with stringEquals.
|
||||
* @param name Marker name to search for.
|
||||
*/
|
||||
void cutsceneGoTo(const char_t *name);
|
||||
void cutsceneSystemGoTo(const char_t *name);
|
||||
|
||||
/**
|
||||
* Update the cutscene system for one frame.
|
||||
@@ -217,8 +239,3 @@ void cutsceneSystemUpdate();
|
||||
* @return Pointer to the current cutscene item.
|
||||
*/
|
||||
const cutsceneitem_t * cutsceneSystemGetCurrentItem();
|
||||
|
||||
/**
|
||||
* Disposes of the cutscene system, stopping any active cutscene.
|
||||
*/
|
||||
void cutsceneSystemDispose();
|
||||
@@ -20,3 +20,5 @@ add_subdirectory(battle)
|
||||
add_subdirectory(save)
|
||||
add_subdirectory(audio)
|
||||
add_subdirectory(json)
|
||||
add_subdirectory(story)
|
||||
add_subdirectory(party)
|
||||
|
||||
@@ -33,9 +33,7 @@ bool_t cutsceneAudioFadeUpdate(
|
||||
|
||||
errorret_t cutsceneAudioFadeLoad(
|
||||
yyjson_val *itemObj,
|
||||
cutsceneitem_t *item,
|
||||
uint8_t *pool,
|
||||
size_t *poolOffset
|
||||
cutsceneitem_t *item
|
||||
) {
|
||||
audiomixerchannel_t channelValue;
|
||||
if(!cutsceneJsonLookupAudioChannel(
|
||||
|
||||
@@ -48,13 +48,9 @@ bool_t cutsceneAudioFadeUpdate(
|
||||
*
|
||||
* @param itemObj The item's JSON object.
|
||||
* @param item Destination item, already zeroed by the caller.
|
||||
* @param pool Unused (AUDIO_FADE needs no pool storage).
|
||||
* @param poolOffset Unused (AUDIO_FADE needs no pool storage).
|
||||
* @return Error code indicating success or failure of the parse.
|
||||
*/
|
||||
errorret_t cutsceneAudioFadeLoad(
|
||||
yyjson_val *itemObj,
|
||||
cutsceneitem_t *item,
|
||||
uint8_t *pool,
|
||||
size_t *poolOffset
|
||||
cutsceneitem_t *item
|
||||
);
|
||||
|
||||
@@ -18,9 +18,7 @@ bool_t cutsceneAudioFadeWaitUpdate(
|
||||
|
||||
errorret_t cutsceneAudioFadeWaitLoad(
|
||||
yyjson_val *itemObj,
|
||||
cutsceneitem_t *item,
|
||||
uint8_t *pool,
|
||||
size_t *poolOffset
|
||||
cutsceneitem_t *item
|
||||
) {
|
||||
return cutsceneJsonLoadAudioChannelOnly(itemObj, &item->audioFadeWaitChannel);
|
||||
}
|
||||
|
||||
@@ -28,13 +28,9 @@ bool_t cutsceneAudioFadeWaitUpdate(
|
||||
*
|
||||
* @param itemObj The item's JSON object.
|
||||
* @param item Destination item, already zeroed by the caller.
|
||||
* @param pool Unused (AUDIO_FADE_WAIT needs no pool storage).
|
||||
* @param poolOffset Unused (AUDIO_FADE_WAIT needs no pool storage).
|
||||
* @return Error code indicating success or failure of the parse.
|
||||
*/
|
||||
errorret_t cutsceneAudioFadeWaitLoad(
|
||||
yyjson_val *itemObj,
|
||||
cutsceneitem_t *item,
|
||||
uint8_t *pool,
|
||||
size_t *poolOffset
|
||||
cutsceneitem_t *item
|
||||
);
|
||||
|
||||
@@ -25,9 +25,7 @@ bool_t cutsceneAudioPauseUpdate(
|
||||
|
||||
errorret_t cutsceneAudioPauseLoad(
|
||||
yyjson_val *itemObj,
|
||||
cutsceneitem_t *item,
|
||||
uint8_t *pool,
|
||||
size_t *poolOffset
|
||||
cutsceneitem_t *item
|
||||
) {
|
||||
return cutsceneJsonLoadAudioChannelOnly(itemObj, &item->audioPauseChannel);
|
||||
}
|
||||
|
||||
@@ -37,13 +37,9 @@ bool_t cutsceneAudioPauseUpdate(
|
||||
*
|
||||
* @param itemObj The item's JSON object.
|
||||
* @param item Destination item, already zeroed by the caller.
|
||||
* @param pool Unused (AUDIO_PAUSE needs no pool storage).
|
||||
* @param poolOffset Unused (AUDIO_PAUSE needs no pool storage).
|
||||
* @return Error code indicating success or failure of the parse.
|
||||
*/
|
||||
errorret_t cutsceneAudioPauseLoad(
|
||||
yyjson_val *itemObj,
|
||||
cutsceneitem_t *item,
|
||||
uint8_t *pool,
|
||||
size_t *poolOffset
|
||||
cutsceneitem_t *item
|
||||
);
|
||||
|
||||
@@ -34,9 +34,7 @@ bool_t cutsceneAudioPlayUpdate(
|
||||
|
||||
errorret_t cutsceneAudioPlayLoad(
|
||||
yyjson_val *itemObj,
|
||||
cutsceneitem_t *item,
|
||||
uint8_t *pool,
|
||||
size_t *poolOffset
|
||||
cutsceneitem_t *item
|
||||
) {
|
||||
if(!cutsceneJsonCopyString(
|
||||
itemObj, "file", NULL, item->audioPlay.file, AUDIO_PATH_MAX
|
||||
|
||||
@@ -50,13 +50,9 @@ bool_t cutsceneAudioPlayUpdate(
|
||||
*
|
||||
* @param itemObj The item's JSON object.
|
||||
* @param item Destination item, already zeroed by the caller.
|
||||
* @param pool Unused (AUDIO_PLAY needs no pool storage).
|
||||
* @param poolOffset Unused (AUDIO_PLAY needs no pool storage).
|
||||
* @return Error code indicating success or failure of the parse.
|
||||
*/
|
||||
errorret_t cutsceneAudioPlayLoad(
|
||||
yyjson_val *itemObj,
|
||||
cutsceneitem_t *item,
|
||||
uint8_t *pool,
|
||||
size_t *poolOffset
|
||||
cutsceneitem_t *item
|
||||
);
|
||||
|
||||
@@ -25,9 +25,7 @@ bool_t cutsceneAudioResumeUpdate(
|
||||
|
||||
errorret_t cutsceneAudioResumeLoad(
|
||||
yyjson_val *itemObj,
|
||||
cutsceneitem_t *item,
|
||||
uint8_t *pool,
|
||||
size_t *poolOffset
|
||||
cutsceneitem_t *item
|
||||
) {
|
||||
return cutsceneJsonLoadAudioChannelOnly(itemObj, &item->audioResumeChannel);
|
||||
}
|
||||
|
||||
@@ -37,13 +37,9 @@ bool_t cutsceneAudioResumeUpdate(
|
||||
*
|
||||
* @param itemObj The item's JSON object.
|
||||
* @param item Destination item, already zeroed by the caller.
|
||||
* @param pool Unused (AUDIO_RESUME needs no pool storage).
|
||||
* @param poolOffset Unused (AUDIO_RESUME needs no pool storage).
|
||||
* @return Error code indicating success or failure of the parse.
|
||||
*/
|
||||
errorret_t cutsceneAudioResumeLoad(
|
||||
yyjson_val *itemObj,
|
||||
cutsceneitem_t *item,
|
||||
uint8_t *pool,
|
||||
size_t *poolOffset
|
||||
cutsceneitem_t *item
|
||||
);
|
||||
|
||||
@@ -33,9 +33,7 @@ bool_t cutsceneAudioSetUpdate(
|
||||
|
||||
errorret_t cutsceneAudioSetLoad(
|
||||
yyjson_val *itemObj,
|
||||
cutsceneitem_t *item,
|
||||
uint8_t *pool,
|
||||
size_t *poolOffset
|
||||
cutsceneitem_t *item
|
||||
) {
|
||||
audiomixerchannel_t channelValue;
|
||||
if(!cutsceneJsonLookupAudioChannel(
|
||||
|
||||
@@ -49,13 +49,9 @@ bool_t cutsceneAudioSetUpdate(
|
||||
*
|
||||
* @param itemObj The item's JSON object.
|
||||
* @param item Destination item, already zeroed by the caller.
|
||||
* @param pool Unused (AUDIO_SET needs no pool storage).
|
||||
* @param poolOffset Unused (AUDIO_SET needs no pool storage).
|
||||
* @return Error code indicating success or failure of the parse.
|
||||
*/
|
||||
errorret_t cutsceneAudioSetLoad(
|
||||
yyjson_val *itemObj,
|
||||
cutsceneitem_t *item,
|
||||
uint8_t *pool,
|
||||
size_t *poolOffset
|
||||
cutsceneitem_t *item
|
||||
);
|
||||
|
||||
@@ -32,9 +32,7 @@ bool_t cutsceneAudioSetLoopUpdate(
|
||||
|
||||
errorret_t cutsceneAudioSetLoopLoad(
|
||||
yyjson_val *itemObj,
|
||||
cutsceneitem_t *item,
|
||||
uint8_t *pool,
|
||||
size_t *poolOffset
|
||||
cutsceneitem_t *item
|
||||
) {
|
||||
audiomixerchannel_t channelValue;
|
||||
if(!cutsceneJsonLookupAudioChannel(
|
||||
|
||||
@@ -46,13 +46,9 @@ bool_t cutsceneAudioSetLoopUpdate(
|
||||
*
|
||||
* @param itemObj The item's JSON object.
|
||||
* @param item Destination item, already zeroed by the caller.
|
||||
* @param pool Unused (AUDIO_SET_LOOP needs no pool storage).
|
||||
* @param poolOffset Unused (AUDIO_SET_LOOP needs no pool storage).
|
||||
* @return Error code indicating success or failure of the parse.
|
||||
*/
|
||||
errorret_t cutsceneAudioSetLoopLoad(
|
||||
yyjson_val *itemObj,
|
||||
cutsceneitem_t *item,
|
||||
uint8_t *pool,
|
||||
size_t *poolOffset
|
||||
cutsceneitem_t *item
|
||||
);
|
||||
|
||||
@@ -26,9 +26,7 @@ bool_t cutsceneAudioSetPanUpdate(
|
||||
|
||||
errorret_t cutsceneAudioSetPanLoad(
|
||||
yyjson_val *itemObj,
|
||||
cutsceneitem_t *item,
|
||||
uint8_t *pool,
|
||||
size_t *poolOffset
|
||||
cutsceneitem_t *item
|
||||
) {
|
||||
audiomixerchannel_t channelValue;
|
||||
if(!cutsceneJsonLookupAudioChannel(
|
||||
|
||||
@@ -43,13 +43,9 @@ bool_t cutsceneAudioSetPanUpdate(
|
||||
*
|
||||
* @param itemObj The item's JSON object.
|
||||
* @param item Destination item, already zeroed by the caller.
|
||||
* @param pool Unused (AUDIO_SET_PAN needs no pool storage).
|
||||
* @param poolOffset Unused (AUDIO_SET_PAN needs no pool storage).
|
||||
* @return Error code indicating success or failure of the parse.
|
||||
*/
|
||||
errorret_t cutsceneAudioSetPanLoad(
|
||||
yyjson_val *itemObj,
|
||||
cutsceneitem_t *item,
|
||||
uint8_t *pool,
|
||||
size_t *poolOffset
|
||||
cutsceneitem_t *item
|
||||
);
|
||||
|
||||
@@ -25,9 +25,7 @@ bool_t cutsceneAudioStopUpdate(
|
||||
|
||||
errorret_t cutsceneAudioStopLoad(
|
||||
yyjson_val *itemObj,
|
||||
cutsceneitem_t *item,
|
||||
uint8_t *pool,
|
||||
size_t *poolOffset
|
||||
cutsceneitem_t *item
|
||||
) {
|
||||
return cutsceneJsonLoadAudioChannelOnly(itemObj, &item->audioStopChannel);
|
||||
}
|
||||
|
||||
@@ -37,13 +37,9 @@ bool_t cutsceneAudioStopUpdate(
|
||||
*
|
||||
* @param itemObj The item's JSON object.
|
||||
* @param item Destination item, already zeroed by the caller.
|
||||
* @param pool Unused (AUDIO_STOP needs no pool storage).
|
||||
* @param poolOffset Unused (AUDIO_STOP needs no pool storage).
|
||||
* @return Error code indicating success or failure of the parse.
|
||||
*/
|
||||
errorret_t cutsceneAudioStopLoad(
|
||||
yyjson_val *itemObj,
|
||||
cutsceneitem_t *item,
|
||||
uint8_t *pool,
|
||||
size_t *poolOffset
|
||||
cutsceneitem_t *item
|
||||
);
|
||||
|
||||
@@ -5,7 +5,10 @@
|
||||
|
||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
PUBLIC
|
||||
cutscenestartbattle.c
|
||||
cutscenebattlewaitstate.c
|
||||
cutscenebattleforceaction.c
|
||||
cutscenebattlestart.c
|
||||
cutscenebattlesetstate.c
|
||||
cutscenebattlewaitselection.c
|
||||
cutscenebattlepostmove.c
|
||||
cutscenebattlefighterdamage.c
|
||||
cutscenebattlefighterheal.c
|
||||
)
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "rpg/cutscene/item/cutsceneitem.h"
|
||||
#include "rpg/battle/battle.h"
|
||||
#include "rpg/cutscene/item/json/cutscenejsonfield.h"
|
||||
#include "assert/assert.h"
|
||||
#include "util/string.h"
|
||||
|
||||
#define CUTSCENE_BATTLE_FIGHTER_REF_MAX 16
|
||||
|
||||
void cutsceneBattleFighterDamageStart(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
) {
|
||||
const cutscenebattlefighterdamage_t *damage = &item->battleFighterDamage;
|
||||
assertTrue(
|
||||
damage->fighterIndex < BATTLE_FIGHTER_POS_COUNT,
|
||||
"fighterIndex sentinel was never resolved"
|
||||
);
|
||||
battlefighter_t *fighter = BATTLE.fighters[damage->fighterIndex];
|
||||
assertNotNull(fighter, "Fighter cannot be NULL");
|
||||
|
||||
battleFighterDamage(fighter, damage->amount);
|
||||
}
|
||||
|
||||
bool_t cutsceneBattleFighterDamageUpdate(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
errorret_t cutsceneBattleFighterDamageLoad(
|
||||
yyjson_val *itemObj,
|
||||
cutsceneitem_t *item
|
||||
) {
|
||||
char_t fighterRef[CUTSCENE_BATTLE_FIGHTER_REF_MAX];
|
||||
if(!cutsceneJsonCopyString(
|
||||
itemObj, "fighter", NULL, fighterRef, CUTSCENE_BATTLE_FIGHTER_REF_MAX
|
||||
)) {
|
||||
errorThrow("Cutscene item (BATTLE_FIGHTER_DAMAGE): missing 'fighter'");
|
||||
}
|
||||
if(stringEquals(fighterRef, "target")) {
|
||||
item->battleFighterDamage.fighterIndex = BATTLE_FIGHTER_POS_TARGET;
|
||||
} else if(stringEquals(fighterRef, "user")) {
|
||||
item->battleFighterDamage.fighterIndex = BATTLE_FIGHTER_POS_USER;
|
||||
} else {
|
||||
errorThrow(
|
||||
"Cutscene item (BATTLE_FIGHTER_DAMAGE): 'fighter' must be "
|
||||
"'target' or 'user'"
|
||||
);
|
||||
}
|
||||
|
||||
const int32_t amount = cutsceneJsonGetInt(itemObj, "amount", -1);
|
||||
if(amount < 0 || amount > UINT16_MAX) {
|
||||
errorThrow(
|
||||
"Cutscene item (BATTLE_FIGHTER_DAMAGE): invalid/missing 'amount'"
|
||||
);
|
||||
}
|
||||
item->battleFighterDamage.amount = (uint16_t)amount;
|
||||
|
||||
errorOk();
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "rpg/cutscene/item/cutsceneitembase.h"
|
||||
#include "rpg/battle/fighter/battlefighterpos.h"
|
||||
|
||||
typedef struct {
|
||||
// Index into BATTLE.fighters of the fighter to damage, or one of
|
||||
// BATTLE_FIGHTER_POS_TARGET/BATTLE_FIGHTER_POS_USER, resolved against
|
||||
// the currently-executing action by battleStateExecutingInit before
|
||||
// this item ever runs.
|
||||
battlefighterpos_t fighterIndex;
|
||||
uint16_t amount;
|
||||
} cutscenebattlefighterdamage_t;
|
||||
|
||||
/**
|
||||
* Starts a BATTLE_FIGHTER_DAMAGE item, applying the configured damage to
|
||||
* BATTLE.fighters[fighterIndex] immediately.
|
||||
*
|
||||
* @param item The cutscene item.
|
||||
* @param data Runtime data storage.
|
||||
*/
|
||||
void cutsceneBattleFighterDamageStart(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
);
|
||||
|
||||
/**
|
||||
* Updates a BATTLE_FIGHTER_DAMAGE item (always completes immediately).
|
||||
*
|
||||
* @param item The cutscene item.
|
||||
* @param data Runtime data storage.
|
||||
* @returns true always.
|
||||
*/
|
||||
bool_t cutsceneBattleFighterDamageUpdate(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
);
|
||||
|
||||
/**
|
||||
* Parses a BATTLE_FIGHTER_DAMAGE cutscene item's JSON fields into item.
|
||||
*
|
||||
* @param itemObj The item's JSON object.
|
||||
* @param item Destination item, already zeroed by the caller.
|
||||
* @return Error code indicating success or failure of the parse.
|
||||
*/
|
||||
errorret_t cutsceneBattleFighterDamageLoad(
|
||||
yyjson_val *itemObj,
|
||||
cutsceneitem_t *item
|
||||
);
|
||||
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "rpg/cutscene/item/cutsceneitem.h"
|
||||
#include "rpg/battle/battle.h"
|
||||
#include "rpg/cutscene/item/json/cutscenejsonfield.h"
|
||||
#include "assert/assert.h"
|
||||
#include "util/string.h"
|
||||
|
||||
#define CUTSCENE_BATTLE_FIGHTER_REF_MAX 16
|
||||
|
||||
void cutsceneBattleFighterHealStart(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
) {
|
||||
const cutscenebattlefighterheal_t *heal = &item->battleFighterHeal;
|
||||
assertTrue(
|
||||
heal->fighterIndex < BATTLE_FIGHTER_POS_COUNT,
|
||||
"fighterIndex sentinel was never resolved"
|
||||
);
|
||||
battlefighter_t *fighter = BATTLE.fighters[heal->fighterIndex];
|
||||
assertNotNull(fighter, "Fighter cannot be NULL");
|
||||
|
||||
battleFighterHeal(fighter, heal->amount);
|
||||
}
|
||||
|
||||
bool_t cutsceneBattleFighterHealUpdate(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
errorret_t cutsceneBattleFighterHealLoad(
|
||||
yyjson_val *itemObj,
|
||||
cutsceneitem_t *item
|
||||
) {
|
||||
char_t fighterRef[CUTSCENE_BATTLE_FIGHTER_REF_MAX];
|
||||
if(!cutsceneJsonCopyString(
|
||||
itemObj, "fighter", NULL, fighterRef, CUTSCENE_BATTLE_FIGHTER_REF_MAX
|
||||
)) {
|
||||
errorThrow("Cutscene item (BATTLE_FIGHTER_HEAL): missing 'fighter'");
|
||||
}
|
||||
if(stringEquals(fighterRef, "target")) {
|
||||
item->battleFighterHeal.fighterIndex = BATTLE_FIGHTER_POS_TARGET;
|
||||
} else if(stringEquals(fighterRef, "user")) {
|
||||
item->battleFighterHeal.fighterIndex = BATTLE_FIGHTER_POS_USER;
|
||||
} else {
|
||||
errorThrow(
|
||||
"Cutscene item (BATTLE_FIGHTER_HEAL): 'fighter' must be "
|
||||
"'target' or 'user'"
|
||||
);
|
||||
}
|
||||
|
||||
const int32_t amount = cutsceneJsonGetInt(itemObj, "amount", -1);
|
||||
if(amount < 0 || amount > UINT16_MAX) {
|
||||
errorThrow(
|
||||
"Cutscene item (BATTLE_FIGHTER_HEAL): invalid/missing 'amount'"
|
||||
);
|
||||
}
|
||||
item->battleFighterHeal.amount = (uint16_t)amount;
|
||||
|
||||
errorOk();
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "rpg/cutscene/item/cutsceneitembase.h"
|
||||
#include "rpg/battle/fighter/battlefighterpos.h"
|
||||
|
||||
typedef struct {
|
||||
// Index into BATTLE.fighters of the fighter to heal, or one of
|
||||
// BATTLE_FIGHTER_POS_TARGET/BATTLE_FIGHTER_POS_USER, resolved against
|
||||
// the currently-executing action by battleStateExecutingInit before
|
||||
// this item ever runs.
|
||||
battlefighterpos_t fighterIndex;
|
||||
uint16_t amount;
|
||||
} cutscenebattlefighterheal_t;
|
||||
|
||||
/**
|
||||
* Starts a BATTLE_FIGHTER_HEAL item, applying the configured heal to
|
||||
* BATTLE.fighters[fighterIndex] immediately.
|
||||
*
|
||||
* @param item The cutscene item.
|
||||
* @param data Runtime data storage.
|
||||
*/
|
||||
void cutsceneBattleFighterHealStart(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
);
|
||||
|
||||
/**
|
||||
* Updates a BATTLE_FIGHTER_HEAL item (always completes immediately).
|
||||
*
|
||||
* @param item The cutscene item.
|
||||
* @param data Runtime data storage.
|
||||
* @returns true always.
|
||||
*/
|
||||
bool_t cutsceneBattleFighterHealUpdate(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
);
|
||||
|
||||
/**
|
||||
* Parses a BATTLE_FIGHTER_HEAL cutscene item's JSON fields into item.
|
||||
*
|
||||
* @param itemObj The item's JSON object.
|
||||
* @param item Destination item, already zeroed by the caller.
|
||||
* @return Error code indicating success or failure of the parse.
|
||||
*/
|
||||
errorret_t cutsceneBattleFighterHealLoad(
|
||||
yyjson_val *itemObj,
|
||||
cutsceneitem_t *item
|
||||
);
|
||||
@@ -1,39 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "rpg/cutscene/item/cutsceneitem.h"
|
||||
|
||||
void cutsceneBattleForceActionStart(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
) {
|
||||
battleQueueAction(
|
||||
item->battleForceAction.fighterIndex, BATTLE_ACTION_ATTACK,
|
||||
item->battleForceAction.targetIndex
|
||||
);
|
||||
}
|
||||
|
||||
bool_t cutsceneBattleForceActionUpdate(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
errorret_t cutsceneBattleForceActionLoad(
|
||||
yyjson_val *itemObj,
|
||||
cutsceneitem_t *item,
|
||||
uint8_t *pool,
|
||||
size_t *poolOffset
|
||||
) {
|
||||
item->battleForceAction.fighterIndex =
|
||||
(uint8_t)yyjson_get_int(yyjson_obj_get(itemObj, "fighterIndex"));
|
||||
item->battleForceAction.targetIndex =
|
||||
(uint8_t)yyjson_get_int(yyjson_obj_get(itemObj, "targetIndex"));
|
||||
|
||||
errorOk();
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "rpg/battle/battle.h"
|
||||
#include "rpg/cutscene/item/cutsceneitembase.h"
|
||||
|
||||
typedef struct {
|
||||
uint8_t fighterIndex;
|
||||
uint8_t targetIndex;
|
||||
} cutscenebattleforceaction_t;
|
||||
|
||||
/**
|
||||
* Starts a battle force-action step: immediately queues an attack for the
|
||||
* given fighter against the given target, bypassing normal player/AI
|
||||
* selection for that fighter this round.
|
||||
*
|
||||
* @param item The cutscene item.
|
||||
* @param data Runtime data storage.
|
||||
*/
|
||||
void cutsceneBattleForceActionStart(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
);
|
||||
|
||||
/**
|
||||
* Updates a battle force-action step (always completes immediately).
|
||||
*
|
||||
* @param item The cutscene item.
|
||||
* @param data Runtime data storage.
|
||||
* @returns true always.
|
||||
*/
|
||||
bool_t cutsceneBattleForceActionUpdate(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
);
|
||||
|
||||
/**
|
||||
* Parses a BATTLE_FORCE_ACTION cutscene item's JSON fields into item.
|
||||
*
|
||||
* @param itemObj The item's JSON object.
|
||||
* @param item Destination item, already zeroed by the caller.
|
||||
* @param pool Unused (BATTLE_FORCE_ACTION needs no pool storage).
|
||||
* @param poolOffset Unused (BATTLE_FORCE_ACTION needs no pool storage).
|
||||
* @return Error code indicating success or failure of the parse.
|
||||
*/
|
||||
errorret_t cutsceneBattleForceActionLoad(
|
||||
yyjson_val *itemObj,
|
||||
cutsceneitem_t *item,
|
||||
uint8_t *pool,
|
||||
size_t *poolOffset
|
||||
);
|
||||
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "rpg/cutscene/item/cutsceneitem.h"
|
||||
#include "rpg/battle/battle.h"
|
||||
#include "rpg/cutscene/cutscenesystem.h"
|
||||
#include "console/console.h"
|
||||
|
||||
void cutsceneBattlePostMoveStart(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
) {
|
||||
consolePrint("BATTLE_POST_MOVE\n");
|
||||
|
||||
// Once a result has already been decided (e.g. the player fled), there
|
||||
// is nothing left to check.
|
||||
if(BATTLE.result != BATTLE_RESULT_NONE) return;
|
||||
|
||||
bool_t allyAlive = false;
|
||||
bool_t enemyAlive = false;
|
||||
for(uint8_t i = 0; i < BATTLE_FIGHTER_POS_COUNT; i++) {
|
||||
battlefighter_t *fighter = BATTLE.fighters[i];
|
||||
if(fighter == NULL) continue;
|
||||
if(!battleFighterIsAlive(fighter)) continue;
|
||||
|
||||
if(fighter->team == BATTLE_FIGHTER_TEAM_ALLY) allyAlive = true;
|
||||
else enemyAlive = true;
|
||||
}
|
||||
|
||||
if(allyAlive && enemyAlive) return;
|
||||
|
||||
BATTLE.result = allyAlive ? BATTLE_RESULT_WIN : BATTLE_RESULT_LOSS;
|
||||
|
||||
// Drop the rest of this round's already-spliced items (other fighters'
|
||||
// still-pending move cutscenes, the trailing BATTLE_SET_STATE(PRE_ROUND) -
|
||||
// see battleStateExecutingInit) - the battle is over, so none of that
|
||||
// should run against the WON/LOST/ENDING transition about to replace it.
|
||||
cutsceneSystemClearAfter(CUTSCENE_SYSTEM.currentItem);
|
||||
|
||||
const cutsceneitem_t items[] = {
|
||||
{
|
||||
.type = CUTSCENE_ITEM_TYPE_BATTLE_SET_STATE,
|
||||
.battleSetState = {
|
||||
.state = allyAlive ? BATTLE_STATE_WON : BATTLE_STATE_LOST
|
||||
}
|
||||
}
|
||||
};
|
||||
cutsceneSystemInsertItemsNext(items, sizeof(items) / sizeof(cutsceneitem_t));
|
||||
}
|
||||
|
||||
bool_t cutsceneBattlePostMoveUpdate(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "rpg/cutscene/item/cutsceneitembase.h"
|
||||
|
||||
/**
|
||||
* Starts a BATTLE_POST_MOVE item. Runs after each queued move's cutscene
|
||||
* finishes: checks whether every ally or every enemy fighter has been
|
||||
* knocked out and, if so, sets BATTLE.result and splices in a
|
||||
* BATTLE_SET_STATE to BATTLE_STATE_WON/LOST. No-ops the health check if
|
||||
* BATTLE.result is already set (e.g. the player fled). Not
|
||||
* JSON-authorable -- constructed dynamically by battleStateExecutingInit.
|
||||
*
|
||||
* @param item The cutscene item.
|
||||
* @param data Runtime data storage.
|
||||
*/
|
||||
void cutsceneBattlePostMoveStart(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
);
|
||||
|
||||
/**
|
||||
* Updates a BATTLE_POST_MOVE item (always completes immediately).
|
||||
*
|
||||
* @param item The cutscene item.
|
||||
* @param data Runtime data storage.
|
||||
* @returns true always.
|
||||
*/
|
||||
bool_t cutsceneBattlePostMoveUpdate(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
);
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user