From fed04b66b8ab2657d440c85d3181bcccabd6fc1b Mon Sep 17 00:00:00 2001 From: Dominic Masters Date: Tue, 22 Sep 2026 18:44:45 -0500 Subject: [PATCH] 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 --- assets/cutscenes/new_game.jsonc | 11 ++++++++++ assets/party.jsonc | 22 ++++++++++++------- .../rpg/battle/fighter/battlefighterspecies.h | 22 +++++++++++++++++++ src/dusk/rpg/cutscene/cutscenesystem.c | 11 +++++++++- src/dusk/save/slot/saveslot.c | 15 +++++++++++++ src/dusk/save/slot/saveslot.h | 17 ++++++++++++++ src/dusk/save/slot/saveslotcurrent.c | 9 ++++++++ src/dusk/save/slot/saveslotver1.c | 7 ++++++ src/dusk/save/slot/saveslotver1.h | 6 +++++ src/dusk/ui/screen/mainmenu/uimainmenu.c | 4 ++++ 10 files changed, 115 insertions(+), 9 deletions(-) create mode 100644 assets/cutscenes/new_game.jsonc create mode 100644 src/dusk/rpg/battle/fighter/battlefighterspecies.h diff --git a/assets/cutscenes/new_game.jsonc b/assets/cutscenes/new_game.jsonc new file mode 100644 index 00000000..01bd3ac2 --- /dev/null +++ b/assets/cutscenes/new_game.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 + } + ] +} diff --git a/assets/party.jsonc b/assets/party.jsonc index 32c82a22..260acbc1 100644 --- a/assets/party.jsonc +++ b/assets/party.jsonc @@ -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": { "hero": { - "attack": 10, "defense": 5, "magic": 0, "speed": 10, "luck": 0, - "healthMax": 30, "mpMax": 10 + "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 + "attack": 8, + "defense": 6, + "magic": 4, + "speed": 8, + "luck": 1, + "healthMax": 25, + "mpMax": 15 } } } diff --git a/src/dusk/rpg/battle/fighter/battlefighterspecies.h b/src/dusk/rpg/battle/fighter/battlefighterspecies.h new file mode 100644 index 00000000..92162b98 --- /dev/null +++ b/src/dusk/rpg/battle/fighter/battlefighterspecies.h @@ -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; diff --git a/src/dusk/rpg/cutscene/cutscenesystem.c b/src/dusk/rpg/cutscene/cutscenesystem.c index 1b9d55e1..8bd18a4d 100644 --- a/src/dusk/rpg/cutscene/cutscenesystem.c +++ b/src/dusk/rpg/cutscene/cutscenesystem.c @@ -11,6 +11,7 @@ #include "util/string.h" #include "assert/assert.h" #include "ui/overlay/uifatalerror.h" +#include "save/slot/saveslot.h" cutscenesystem_t CUTSCENE_SYSTEM; @@ -84,6 +85,8 @@ void cutsceneSystemStartCutsceneWith( ); cutsceneSystemPrepare(cutscene, interact, interacted); + saveSlotSetCurrentCutscene(CUTSCENE_SYSTEM.loadedFile); + cutsceneSystemNext(); } @@ -160,8 +163,14 @@ void cutsceneSystemLoad(const char_t *file) { 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); + cutsceneSystemStartCutscene(&CUTSCENE_SYSTEM.loadedScene); } void cutsceneRestart(void) { diff --git a/src/dusk/save/slot/saveslot.c b/src/dusk/save/slot/saveslot.c index a4eeff45..cb966442 100644 --- a/src/dusk/save/slot/saveslot.c +++ b/src/dusk/save/slot/saveslot.c @@ -27,6 +27,12 @@ errorret_t saveSlotInit(saveslot_t *slot) { // Set the default party. errorChain(partyInit(&slot->party)); + // Set the default current cutscene. + stringCopy( + slot->currentCutscene, SAVE_SLOT_CUTSCENE_DEFAULT, + sizeof(slot->currentCutscene) - 1 + ); + errorOk(); } @@ -41,6 +47,15 @@ errorret_t saveSlotNewGame(void) { 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) { assertNotNull(slot, "Slot cannot be null"); return slot->name[0] != '\0'; diff --git a/src/dusk/save/slot/saveslot.h b/src/dusk/save/slot/saveslot.h index 7ce56691..6a934f08 100644 --- a/src/dusk/save/slot/saveslot.h +++ b/src/dusk/save/slot/saveslot.h @@ -11,6 +11,7 @@ #include "rpg/battle/party.h" #include "rpg/story/story.h" #include "time/timeepoch.h" +#include "asset/assetfile.h" #include "yyjson.h" #define SAVE_SLOT_NAME_LENGTH 8 @@ -19,6 +20,8 @@ #endif #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 { char_t name[SAVE_SLOT_NAME_LENGTH + 1];// 8 characters + null terminator @@ -34,6 +37,10 @@ typedef struct saveslot_s { saveslotcache_t cachedData; party_t party; 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; /** @@ -59,6 +66,16 @@ errorret_t saveSlotInit(saveslot_t *slot); */ 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 * player name is set or not. diff --git a/src/dusk/save/slot/saveslotcurrent.c b/src/dusk/save/slot/saveslotcurrent.c index 64d4b84c..76c199e4 100644 --- a/src/dusk/save/slot/saveslotcurrent.c +++ b/src/dusk/save/slot/saveslotcurrent.c @@ -37,6 +37,10 @@ errorret_t saveSlotCurrentWriteJSON( memoryCopy(ver1.storyFlags, slot->storyFlags, sizeof(ver1.storyFlags)); + stringCopy( + ver1.currentCutscene, slot->currentCutscene, sizeof(ver1.currentCutscene) + ); + errorChain(saveSlotVer1WriteJSON(&ver1, doc, object)); errorOk(); } @@ -66,5 +70,10 @@ errorret_t saveSlotCurrentReadJSON(saveslot_t *slot, yyjson_val *object) { memoryCopy(slot->storyFlags, ver1.storyFlags, sizeof(slot->storyFlags)); + stringCopy( + slot->currentCutscene, ver1.currentCutscene, + sizeof(slot->currentCutscene) + ); + errorOk(); } diff --git a/src/dusk/save/slot/saveslotver1.c b/src/dusk/save/slot/saveslotver1.c index 7774dc3b..247166b6 100644 --- a/src/dusk/save/slot/saveslotver1.c +++ b/src/dusk/save/slot/saveslotver1.c @@ -41,6 +41,8 @@ errorret_t saveSlotVer1WriteJSON( ); } + writeString("currentCutscene", slot->currentCutscene); + 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(); } diff --git a/src/dusk/save/slot/saveslotver1.h b/src/dusk/save/slot/saveslotver1.h index 41acba00..77040c74 100644 --- a/src/dusk/save/slot/saveslotver1.h +++ b/src/dusk/save/slot/saveslotver1.h @@ -36,6 +36,10 @@ // bump instead. #define SAVE_SLOT_VER1_NAME_LENGTH 8 #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 // 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]; storyflagval_t storyFlags[STORY_FLAG_COUNT_MAX]; + + char_t currentCutscene[SAVE_SLOT_VER1_CUTSCENE_NAME_MAX]; } saveslotver1_t; /** diff --git a/src/dusk/ui/screen/mainmenu/uimainmenu.c b/src/dusk/ui/screen/mainmenu/uimainmenu.c index 8ab856b4..2e5b972d 100644 --- a/src/dusk/ui/screen/mainmenu/uimainmenu.c +++ b/src/dusk/ui/screen/mainmenu/uimainmenu.c @@ -63,6 +63,10 @@ void uiMainMenuSelectSaveResult(const uint8_t slotIndex, void *user) { } sceneSet(SCENE_TYPE_OVERWORLD); + + if(SAVE.slot.currentCutscene[0] != '\0') { + cutsceneSystemLoad(SAVE.slot.currentCutscene); + } } void uiMainMenuOpenSelectSave(void *userData) {