Persist and resume in-progress cutscene via save slot; add species stub

Save slots now track the currently-active cutscene file (defaulting to a
new placeholder new_game.jsonc intro) so loading a save resumes it;
cutsceneSystemStartCutsceneWith persists this on every cutscene start, and
a cutsceneSystemLoad ordering bug (loadedFile stamped after starting) is
fixed so the persisted path is always correct. Also adds a
battlefighterspecies_t/battlespeciesid_t stub for upcoming species data.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
2026-09-22 18:44:45 -05:00
co-authored by Claude Sonnet 5
parent a4ca718070
commit fed04b66b8
10 changed files with 115 additions and 9 deletions
+11
View File
@@ -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
}
]
}
+14 -8
View File
@@ -1,16 +1,22 @@
{ {
// Starting party roster, loaded by partyInit into a fresh save slot.
// Each key is a stable identifier cutscene content can reference
// directly (see PARTY_ADD_MEMBER/PARTY_REMOVE_MEMBER/PARTY_HEAL_MEMBER's
// "key" field) instead of a fragile positional index.
"members": { "members": {
"hero": { "hero": {
"attack": 10, "defense": 5, "magic": 0, "speed": 10, "luck": 0, "attack": 10,
"healthMax": 30, "mpMax": 10 "defense": 5,
"magic": 0,
"speed": 10,
"luck": 0,
"healthMax": 30,
"mpMax": 10
}, },
"companion": { "companion": {
"attack": 8, "defense": 6, "magic": 4, "speed": 8, "luck": 1, "attack": 8,
"healthMax": 25, "mpMax": 15 "defense": 6,
"magic": 4,
"speed": 8,
"luck": 1,
"healthMax": 25,
"mpMax": 15
} }
} }
} }
@@ -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;
+10 -1
View File
@@ -11,6 +11,7 @@
#include "util/string.h" #include "util/string.h"
#include "assert/assert.h" #include "assert/assert.h"
#include "ui/overlay/uifatalerror.h" #include "ui/overlay/uifatalerror.h"
#include "save/slot/saveslot.h"
cutscenesystem_t CUTSCENE_SYSTEM; cutscenesystem_t CUTSCENE_SYSTEM;
@@ -84,6 +85,8 @@ void cutsceneSystemStartCutsceneWith(
); );
cutsceneSystemPrepare(cutscene, interact, interacted); cutsceneSystemPrepare(cutscene, interact, interacted);
saveSlotSetCurrentCutscene(CUTSCENE_SYSTEM.loadedFile);
cutsceneSystemNext(); cutsceneSystemNext();
} }
@@ -160,8 +163,14 @@ void cutsceneSystemLoad(const char_t *file) {
return; return;
} }
cutsceneSystemStartCutscene(&CUTSCENE_SYSTEM.loadedScene); // Stamped before starting (not after) - cutsceneSystemStartCutsceneWith
// reads loadedFile to persist the in-progress cutscene to the save (see
// saveSlotSetCurrentCutscene), so it must already be correct by then.
// Safe: cutsceneSystemPrepare (called from within Start) only clears
// loadedFile when switching away from loadedScene entirely, which this
// isn't.
stringCopy(CUTSCENE_SYSTEM.loadedFile, fileCopy, ASSET_FILE_NAME_MAX - 1); stringCopy(CUTSCENE_SYSTEM.loadedFile, fileCopy, ASSET_FILE_NAME_MAX - 1);
cutsceneSystemStartCutscene(&CUTSCENE_SYSTEM.loadedScene);
} }
void cutsceneRestart(void) { void cutsceneRestart(void) {
+15
View File
@@ -27,6 +27,12 @@ errorret_t saveSlotInit(saveslot_t *slot) {
// Set the default party. // Set the default party.
errorChain(partyInit(&slot->party)); errorChain(partyInit(&slot->party));
// Set the default current cutscene.
stringCopy(
slot->currentCutscene, SAVE_SLOT_CUTSCENE_DEFAULT,
sizeof(slot->currentCutscene) - 1
);
errorOk(); errorOk();
} }
@@ -41,6 +47,15 @@ errorret_t saveSlotNewGame(void) {
errorOk(); errorOk();
} }
void saveSlotSetCurrentCutscene(const char_t *file) {
assertNotNull(file, "File cannot be null");
stringCopy(
SAVE.slot.currentCutscene, file, sizeof(SAVE.slot.currentCutscene) - 1
);
SAVE.slotDirty = true;
}
bool_t saveSlotInUse(saveslotcache_t *slot) { bool_t saveSlotInUse(saveslotcache_t *slot) {
assertNotNull(slot, "Slot cannot be null"); assertNotNull(slot, "Slot cannot be null");
return slot->name[0] != '\0'; return slot->name[0] != '\0';
+17
View File
@@ -11,6 +11,7 @@
#include "rpg/battle/party.h" #include "rpg/battle/party.h"
#include "rpg/story/story.h" #include "rpg/story/story.h"
#include "time/timeepoch.h" #include "time/timeepoch.h"
#include "asset/assetfile.h"
#include "yyjson.h" #include "yyjson.h"
#define SAVE_SLOT_NAME_LENGTH 8 #define SAVE_SLOT_NAME_LENGTH 8
@@ -19,6 +20,8 @@
#endif #endif
#define SAVE_SLOT_MAP_NAME_DEFAULT "overworld" #define SAVE_SLOT_MAP_NAME_DEFAULT "overworld"
#define SAVE_SLOT_CUTSCENE_NAME_MAX ASSET_FILE_NAME_MAX
#define SAVE_SLOT_CUTSCENE_DEFAULT "cutscenes/new_game.jsonc"
typedef struct { typedef struct {
char_t name[SAVE_SLOT_NAME_LENGTH + 1];// 8 characters + null terminator char_t name[SAVE_SLOT_NAME_LENGTH + 1];// 8 characters + null terminator
@@ -34,6 +37,10 @@ typedef struct saveslot_s {
saveslotcache_t cachedData; saveslotcache_t cachedData;
party_t party; party_t party;
storyflagval_t storyFlags[STORY_FLAG_COUNT_MAX]; storyflagval_t storyFlags[STORY_FLAG_COUNT_MAX];
// Asset path of the cutscene currently in progress for this slot (e.g.
// resumed on load) - defaults to SAVE_SLOT_CUTSCENE_DEFAULT on a fresh
// slot (see saveSlotInit).
char_t currentCutscene[SAVE_SLOT_CUTSCENE_NAME_MAX];
} saveslot_t; } saveslot_t;
/** /**
@@ -59,6 +66,16 @@ errorret_t saveSlotInit(saveslot_t *slot);
*/ */
errorret_t saveSlotNewGame(void); errorret_t saveSlotNewGame(void);
/**
* Sets the current save slot's (SAVE.slot) in-progress cutscene and
* dirties the save (SAVE.slotDirty), same as storySetFlagValue - so the
* next save write persists it. Use this (not writing SAVE.slot.
* currentCutscene directly) anywhere gameplay needs to change it.
*
* @param file Asset path of the cutscene now in progress, or "" for none.
*/
void saveSlotSetCurrentCutscene(const char_t *file);
/** /**
* Checks if the save slot is in use, this is determined by checking if the * Checks if the save slot is in use, this is determined by checking if the
* player name is set or not. * player name is set or not.
+9
View File
@@ -37,6 +37,10 @@ errorret_t saveSlotCurrentWriteJSON(
memoryCopy(ver1.storyFlags, slot->storyFlags, sizeof(ver1.storyFlags)); memoryCopy(ver1.storyFlags, slot->storyFlags, sizeof(ver1.storyFlags));
stringCopy(
ver1.currentCutscene, slot->currentCutscene, sizeof(ver1.currentCutscene)
);
errorChain(saveSlotVer1WriteJSON(&ver1, doc, object)); errorChain(saveSlotVer1WriteJSON(&ver1, doc, object));
errorOk(); errorOk();
} }
@@ -66,5 +70,10 @@ errorret_t saveSlotCurrentReadJSON(saveslot_t *slot, yyjson_val *object) {
memoryCopy(slot->storyFlags, ver1.storyFlags, sizeof(slot->storyFlags)); memoryCopy(slot->storyFlags, ver1.storyFlags, sizeof(slot->storyFlags));
stringCopy(
slot->currentCutscene, ver1.currentCutscene,
sizeof(slot->currentCutscene)
);
errorOk(); errorOk();
} }
+7
View File
@@ -41,6 +41,8 @@ errorret_t saveSlotVer1WriteJSON(
); );
} }
writeString("currentCutscene", slot->currentCutscene);
errorOk(); errorOk();
} }
@@ -94,5 +96,10 @@ errorret_t saveSlotVer1ReadJSON(saveslotver1_t *slot, yyjson_val *object) {
} }
} }
readString(
"currentCutscene", slot->currentCutscene, SAVE_SLOT_VER1_CUTSCENE_DEFAULT,
SAVE_SLOT_VER1_CUTSCENE_NAME_MAX - 1
);
errorOk(); errorOk();
} }
+6
View File
@@ -36,6 +36,10 @@
// bump instead. // bump instead.
#define SAVE_SLOT_VER1_NAME_LENGTH 8 #define SAVE_SLOT_VER1_NAME_LENGTH 8
#define SAVE_SLOT_VER1_MAP_NAME_MAX 10 #define SAVE_SLOT_VER1_MAP_NAME_MAX 10
#define SAVE_SLOT_VER1_CUTSCENE_NAME_MAX 128
// Own frozen copy of save/slot/saveslot.h's SAVE_SLOT_CUTSCENE_DEFAULT, for
// the same reason as the sizes above - never reference the live constant.
#define SAVE_SLOT_VER1_CUTSCENE_DEFAULT "cutscenes/new_game.jsonc"
// Independent from saveslotcache_t (save/slot/saveslot.h) by design - this // Independent from saveslotcache_t (save/slot/saveslot.h) by design - this
// is what version 1 actually put on disk, frozen forever. saveSlotCurrent // is what version 1 actually put on disk, frozen forever. saveSlotCurrent
@@ -52,6 +56,8 @@ typedef struct saveslotver1_s {
uint8_t partyOrder[PARTY_ACTIVE_SIZE_MAX]; uint8_t partyOrder[PARTY_ACTIVE_SIZE_MAX];
storyflagval_t storyFlags[STORY_FLAG_COUNT_MAX]; storyflagval_t storyFlags[STORY_FLAG_COUNT_MAX];
char_t currentCutscene[SAVE_SLOT_VER1_CUTSCENE_NAME_MAX];
} saveslotver1_t; } saveslotver1_t;
/** /**
+4
View File
@@ -63,6 +63,10 @@ void uiMainMenuSelectSaveResult(const uint8_t slotIndex, void *user) {
} }
sceneSet(SCENE_TYPE_OVERWORLD); sceneSet(SCENE_TYPE_OVERWORLD);
if(SAVE.slot.currentCutscene[0] != '\0') {
cutsceneSystemLoad(SAVE.slot.currentCutscene);
}
} }
void uiMainMenuOpenSelectSave(void *userData) { void uiMainMenuOpenSelectSave(void *userData) {