From 37e7e95acecb564093d0e69f7fa4097f34aa8d3b Mon Sep 17 00:00:00 2001 From: Dominic Masters Date: Sun, 20 Sep 2026 17:49:06 -0500 Subject: [PATCH] Resolve moves into per-move cutscenes during battle execution Each queued action now resolves to its move's cutscene (cutscenes/battle/moves/.jsonc), spliced into the executing state with a BATTLE_POST_MOVE checkpoint after each one. Fighter turn order is tracked via fighterOrder/fighterOrderCount, and resolved actions/pending counts are cleared so fighters aren't skipped in later rounds. Also fixes a BATTLE_WAIT_SELECTION deadlock and adds an optional "blocking" flag to MODAL cutscene items. Co-Authored-By: Claude Sonnet 5 --- assets/cutscenes/battle/moves/fire.jsonc | 15 ++- assets/cutscenes/battle/moves/slash.jsonc | 15 ++- src/dusk/rpg/battle/battle.h | 4 +- .../rpg/battle/state/battlestateexecuting.c | 83 ++++++++++++++++ .../rpg/battle/state/battlestateexecuting.h | 2 + .../rpg/battle/state/battlestatepreround.c | 12 ++- .../rpg/battle/state/battlestateselection.c | 96 ++++++++----------- .../rpg/battle/state/battlestateselection.h | 22 ++++- .../rpg/cutscene/item/battle/CMakeLists.txt | 1 + .../item/battle/cutscenebattlepostmove.c | 25 +++++ .../item/battle/cutscenebattlepostmove.h | 36 +++++++ .../item/battle/cutscenebattlewaitselection.c | 15 ++- .../item/battle/cutscenebattlewaitselection.h | 14 +-- src/dusk/rpg/cutscene/item/cutsceneitem.c | 5 + src/dusk/rpg/cutscene/item/cutsceneitem.h | 1 + src/dusk/rpg/cutscene/item/cutsceneitembase.h | 1 + src/dusk/rpg/cutscene/item/ui/cutscenemodal.c | 4 +- src/dusk/rpg/cutscene/item/ui/cutscenemodal.h | 31 +++--- src/dusk/ui/widget/uimodal.c | 25 +++++ 19 files changed, 320 insertions(+), 87 deletions(-) create mode 100644 src/dusk/rpg/cutscene/item/battle/cutscenebattlepostmove.c create mode 100644 src/dusk/rpg/cutscene/item/battle/cutscenebattlepostmove.h diff --git a/assets/cutscenes/battle/moves/fire.jsonc b/assets/cutscenes/battle/moves/fire.jsonc index 0b455d28..b4760f18 100644 --- a/assets/cutscenes/battle/moves/fire.jsonc +++ b/assets/cutscenes/battle/moves/fire.jsonc @@ -1,9 +1,16 @@ { "items": [ - { + { "type": "MODAL", - "title": "BATTLE TEST", - "message": "BATTLE TEST" + "title": "Fire", + "message": "A burst of flame scorches the target." + }, + { + "type": "WAIT", + "seconds": 2 + }, + { + "type": "MODAL_CLOSE" } ] -} \ No newline at end of file +} diff --git a/assets/cutscenes/battle/moves/slash.jsonc b/assets/cutscenes/battle/moves/slash.jsonc index 0b455d28..9726503b 100644 --- a/assets/cutscenes/battle/moves/slash.jsonc +++ b/assets/cutscenes/battle/moves/slash.jsonc @@ -1,9 +1,16 @@ { "items": [ - { + { "type": "MODAL", - "title": "BATTLE TEST", - "message": "BATTLE TEST" + "title": "Slash", + "message": "A quick blade strike cuts across the target." + }, + { + "type": "WAIT", + "seconds": 2 + }, + { + "type": "MODAL_CLOSE" } ] -} \ No newline at end of file +} diff --git a/src/dusk/rpg/battle/battle.h b/src/dusk/rpg/battle/battle.h index 2c86fa37..9c20aacb 100644 --- a/src/dusk/rpg/battle/battle.h +++ b/src/dusk/rpg/battle/battle.h @@ -25,9 +25,11 @@ typedef struct { // Order of who gets to act. uint8_t fighterOrder[BATTLE_FIGHTER_POS_COUNT]; + uint8_t fighterOrderCount; // Actions to be executed. - battlemoveaction_t actionsPending[BATTLE_FIGHTER_POS_COUNT]; + battlemoveaction_t actionsPending[BATTLE_FIGHTER_POS_COUNT]; + uint8_t actionsPendingCount; battlestate_t state; battlestatedata_t stateData; diff --git a/src/dusk/rpg/battle/state/battlestateexecuting.c b/src/dusk/rpg/battle/state/battlestateexecuting.c index c2a058a8..948a217c 100644 --- a/src/dusk/rpg/battle/state/battlestateexecuting.c +++ b/src/dusk/rpg/battle/state/battlestateexecuting.c @@ -6,9 +6,92 @@ */ #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/asset.h" +#include "asset/loader/assetloader.h" +#include "asset/assetfile.h" +#include "ui/overlay/uifatalerror.h" +#include "assert/assert.h" +#include "util/string.h" + 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 cutscene asset. + assetentry_t *entry = assetLock(path, ASSET_LOADER_TYPE_JSON, NULL); + errorret_t result = assetRequireLoaded(entry); + if(errorIsNotOk(result)) { + assetUnlockEntry(entry); + errorCatch(errorPrint(result)); + uiFatalErrorOpen(NULL); + continue; + } + + // Parse out cutscene items and append to the list. + cutscene_t scene; + errorret_t parseResult = cutsceneParseDoc( + entry->data.json, &scene, &items[itemsCount], + BATTLE_STATE_EXECUTING_ITEMS_MAX - itemsCount + ); + itemsCount += scene.itemCount; + + // Release asset + assetUnlockEntry(entry); + if(errorIsNotOk(parseResult)) { + errorCatch(errorPrint(parseResult)); + uiFatalErrorOpen(NULL); + continue; + } + + // 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) { diff --git a/src/dusk/rpg/battle/state/battlestateexecuting.h b/src/dusk/rpg/battle/state/battlestateexecuting.h index 1dc49c9d..a6f8f75a 100644 --- a/src/dusk/rpg/battle/state/battlestateexecuting.h +++ b/src/dusk/rpg/battle/state/battlestateexecuting.h @@ -12,6 +12,8 @@ typedef struct { } battlestateexecuting_t; +#define BATTLE_STATE_EXECUTING_ITEMS_MAX 64 + /** * Called when the battle enters BATTLE_STATE_EXECUTING. */ diff --git a/src/dusk/rpg/battle/state/battlestatepreround.c b/src/dusk/rpg/battle/state/battlestatepreround.c index 28f1d2be..7b85ece7 100644 --- a/src/dusk/rpg/battle/state/battlestatepreround.c +++ b/src/dusk/rpg/battle/state/battlestatepreround.c @@ -6,11 +6,21 @@ */ #include "battlestatepreround.h" +#include "rpg/battle/battle.h" #include "rpg/cutscene/item/cutsceneitem.h" #include "rpg/cutscene/cutscenesystem.h" void battleStatePreRoundInit(void) { - const cutsceneitem_t items[] = { + // 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 } diff --git a/src/dusk/rpg/battle/state/battlestateselection.c b/src/dusk/rpg/battle/state/battlestateselection.c index 3d45a11b..168e7c2b 100644 --- a/src/dusk/rpg/battle/state/battlestateselection.c +++ b/src/dusk/rpg/battle/state/battlestateselection.c @@ -7,87 +7,73 @@ #include "battlestateselection.h" #include "rpg/battle/battle.h" -#include "rpg/cutscene/item/cutsceneitem.h" -#include "rpg/cutscene/cutscenesystem.h" #include "assert/assert.h" -// TEMPORARY: hardcoded pending a real moves.jsonc-backed registry lookup. -#define BATTLE_MOVE_ID_ATTACK ((battlemoveid_t)1) - 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 battleStateSelectionUpdate(void) { battlestateselection_t *state = &BATTLE.stateData.selection; - while(state->selectionIndex < BATTLE_FIGHTER_POS_COUNT) { - const uint8_t fighterIndex = state->selectionIndex; + // 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) { - // Player-controlled: wait for battleStateSelectionPlayerAttack/Flee. - return; - } + if(fighter->controller != BATTLE_FIGHTER_CONTROLLER_AI) return; - const battlefighterteam_t enemyTeam = - fighter->team == BATTLE_FIGHTER_TEAM_ALLY ? - BATTLE_FIGHTER_TEAM_ENEMY : BATTLE_FIGHTER_TEAM_ALLY; - - uint8_t targetIndex = 0xFF; - 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; - targetIndex = i; - break; - } - - if(targetIndex != 0xFF) { - battleStateSelectionQueueAttack(fighterIndex, targetIndex); - } + // This is AI, in future I'll have proper AI here, for now I'm actually not + // going to do anything. state->selectionIndex++; } - - const cutsceneitem_t items[] = { - { - .type = CUTSCENE_ITEM_TYPE_BATTLE_SET_STATE, - .battleSetState = { .state = BATTLE_STATE_EXECUTING } - } - }; - cutsceneSystemInsertItems(items, sizeof(items) / sizeof(cutsceneitem_t)); } void battleStateSelectionDispose(void) { } -bool_t battleStateSelectionFighterNeedsDecision(const uint8_t fighterIndex) { - assertTrue(fighterIndex < BATTLE_FIGHTER_POS_COUNT, "fighterIndex out of bounds"); - - battlefighter_t *fighter = BATTLE.fighters[fighterIndex]; - if(fighter == NULL) return false; - if(!battleFighterIsAlive(fighter)) return false; - - return BATTLE.actionsPending[fighterIndex].move == BATTLE_MOVE_ID_NULL; -} - void battleStateSelectionQueueAttack( const uint8_t fighterIndex, const uint8_t targetIndex ) { - assertTrue(fighterIndex < BATTLE_FIGHTER_POS_COUNT, "fighterIndex out of bounds"); - assertTrue(targetIndex < BATTLE_FIGHTER_POS_COUNT, "targetIndex out of bounds"); + 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]; - action->move = BATTLE_MOVE_ID_ATTACK; + if(action->move == BATTLE_MOVE_ID_NULL) BATTLE.actionsPendingCount++; + action->move = move; action->targets[0] = targetIndex; action->user = BATTLE.fighters[fighterIndex]; } @@ -96,18 +82,18 @@ battlefighter_t *battleStateSelectionGetCurrentFighter(void) { if(BATTLE.state != BATTLE_STATE_SELECTION) return NULL; const uint8_t index = BATTLE.stateData.selection.selectionIndex; - if(index >= BATTLE_FIGHTER_POS_COUNT) return NULL; + if(index >= BATTLE.fighterOrderCount) return NULL; - return BATTLE.fighters[index]; + return BATTLE.fighters[BATTLE.fighterOrder[index]]; } void battleStateSelectionPlayerAttack(const uint8_t targetIndex) { battlefighter_t *fighter = battleStateSelectionGetCurrentFighter(); if(fighter == NULL) return; - battleStateSelectionQueueAttack( - BATTLE.stateData.selection.selectionIndex, targetIndex - ); + const uint8_t selectionIndex = BATTLE.stateData.selection.selectionIndex; + const uint8_t fighterIndex = BATTLE.fighterOrder[selectionIndex]; + battleStateSelectionQueueAttack(fighterIndex, targetIndex); BATTLE.stateData.selection.selectionIndex++; } @@ -116,4 +102,4 @@ void battleStateSelectionPlayerFlee(void) { if(fighter == NULL) return; battleSetState(BATTLE_STATE_FLED); -} +} \ No newline at end of file diff --git a/src/dusk/rpg/battle/state/battlestateselection.h b/src/dusk/rpg/battle/state/battlestateselection.h index ad93cfb2..71d8ee69 100644 --- a/src/dusk/rpg/battle/state/battlestateselection.h +++ b/src/dusk/rpg/battle/state/battlestateselection.h @@ -33,6 +33,15 @@ void battleStateSelectionUpdate(void); */ 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 @@ -44,7 +53,16 @@ void battleStateSelectionDispose(void); bool_t battleStateSelectionFighterNeedsDecision(const uint8_t fighterIndex); /** - * Queues a basic attack from fighterIndex against targetIndex. + * 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. @@ -77,4 +95,4 @@ 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); +void battleStateSelectionPlayerFlee(void); \ No newline at end of file diff --git a/src/dusk/rpg/cutscene/item/battle/CMakeLists.txt b/src/dusk/rpg/cutscene/item/battle/CMakeLists.txt index 2faba70b..80b57f57 100644 --- a/src/dusk/rpg/cutscene/item/battle/CMakeLists.txt +++ b/src/dusk/rpg/cutscene/item/battle/CMakeLists.txt @@ -8,4 +8,5 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME} cutscenebattlestart.c cutscenebattlesetstate.c cutscenebattlewaitselection.c + cutscenebattlepostmove.c ) diff --git a/src/dusk/rpg/cutscene/item/battle/cutscenebattlepostmove.c b/src/dusk/rpg/cutscene/item/battle/cutscenebattlepostmove.c new file mode 100644 index 00000000..77efff1a --- /dev/null +++ b/src/dusk/rpg/cutscene/item/battle/cutscenebattlepostmove.c @@ -0,0 +1,25 @@ +/** + * 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 "console/console.h" + +void cutsceneBattlePostMoveStart( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +) { + // TODO: check fighter health etc. here to short-circuit into a + // win/loss (BATTLE_STATE_WON/LOST) once a side has been wiped out. + consolePrint("BATTLE_POST_MOVE\n"); +} + +bool_t cutsceneBattlePostMoveUpdate( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +) { + return true; +} diff --git a/src/dusk/rpg/cutscene/item/battle/cutscenebattlepostmove.h b/src/dusk/rpg/cutscene/item/battle/cutscenebattlepostmove.h new file mode 100644 index 00000000..6a7ff0ca --- /dev/null +++ b/src/dusk/rpg/cutscene/item/battle/cutscenebattlepostmove.h @@ -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 "rpg/cutscene/item/cutsceneitembase.h" + +/** + * Starts a BATTLE_POST_MOVE item. Runs after each queued move's cutscene + * finishes - eventually this is where we'll check things like fighter + * health to short-circuit into a win/loss, but for now it just prints to + * the console. 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 +); diff --git a/src/dusk/rpg/cutscene/item/battle/cutscenebattlewaitselection.c b/src/dusk/rpg/cutscene/item/battle/cutscenebattlewaitselection.c index 70f3582d..763dbbff 100644 --- a/src/dusk/rpg/cutscene/item/battle/cutscenebattlewaitselection.c +++ b/src/dusk/rpg/cutscene/item/battle/cutscenebattlewaitselection.c @@ -6,11 +6,24 @@ */ #include "rpg/cutscene/item/cutsceneitem.h" +#include "rpg/cutscene/cutscenesystem.h" #include "rpg/battle/battle.h" +#include "rpg/battle/state/battlestateselection.h" bool_t cutsceneBattleWaitSelectionUpdate( const cutsceneitem_t *item, cutsceneitemdata_t *data ) { - return BATTLE.state != BATTLE_STATE_SELECTION; + if(BATTLE.state != BATTLE_STATE_SELECTION) return true; + if(!battleStateSelectionIsComplete()) return false; + + // Every fighter has decided - queue the move to BATTLE_STATE_EXECUTING. + const cutsceneitem_t items[] = { + { + .type = CUTSCENE_ITEM_TYPE_BATTLE_SET_STATE, + .battleSetState = { .state = BATTLE_STATE_EXECUTING } + } + }; + cutsceneSystemInsertItemsNext(items, sizeof(items) / sizeof(cutsceneitem_t)); + return true; } diff --git a/src/dusk/rpg/cutscene/item/battle/cutscenebattlewaitselection.h b/src/dusk/rpg/cutscene/item/battle/cutscenebattlewaitselection.h index ba66dc24..c5235734 100644 --- a/src/dusk/rpg/cutscene/item/battle/cutscenebattlewaitselection.h +++ b/src/dusk/rpg/cutscene/item/battle/cutscenebattlewaitselection.h @@ -9,15 +9,17 @@ #include "rpg/cutscene/item/cutsceneitembase.h" /** - * Updates a BATTLE_WAIT_SELECTION step, completing once the battle is no - * longer in BATTLE_STATE_SELECTION -- i.e. every fighter has decided a - * move (moving on to BATTLE_STATE_EXECUTING) or the selecting fighter has - * fled (moving on to BATTLE_STATE_FLED). Has no Start callback -- there's - * nothing to do until selection actually finishes. + * Updates a BATTLE_WAIT_SELECTION step, completing once every fighter has + * decided a move this round (letting the queued BATTLE_SET_STATE step + * that follows move the battle on to BATTLE_STATE_EXECUTING) or once the + * selecting fighter has fled (moving on to BATTLE_STATE_FLED). Has no + * Start callback -- there's nothing to do until selection actually + * finishes. * * @param item The cutscene item. * @param data Runtime data storage. - * @returns true once BATTLE.state is no longer BATTLE_STATE_SELECTION. + * @returns true once selection has completed or the battle has left + * BATTLE_STATE_SELECTION. */ bool_t cutsceneBattleWaitSelectionUpdate( const cutsceneitem_t *item, diff --git a/src/dusk/rpg/cutscene/item/cutsceneitem.c b/src/dusk/rpg/cutscene/item/cutsceneitem.c index dd126730..ba44a1bb 100644 --- a/src/dusk/rpg/cutscene/item/cutsceneitem.c +++ b/src/dusk/rpg/cutscene/item/cutsceneitem.c @@ -265,6 +265,11 @@ cutsceneitemcallbacks_t CUTSCENE_ITEM_CALLBACKS[CUTSCENE_ITEM_TYPE_COUNT] = { .load = cutsceneItemLoadNoop }, + [CUTSCENE_ITEM_TYPE_BATTLE_POST_MOVE] = { + .init = cutsceneBattlePostMoveStart, + .update = cutsceneBattlePostMoveUpdate + }, + // Save [CUTSCENE_ITEM_TYPE_SAVE_DEVICE_CHECK] = { .init = cutsceneSaveDeviceCheckStart, diff --git a/src/dusk/rpg/cutscene/item/cutsceneitem.h b/src/dusk/rpg/cutscene/item/cutsceneitem.h index e4e4745f..3530fa2e 100644 --- a/src/dusk/rpg/cutscene/item/cutsceneitem.h +++ b/src/dusk/rpg/cutscene/item/cutsceneitem.h @@ -43,6 +43,7 @@ #include "battle/cutscenebattlestart.h" #include "battle/cutscenebattlesetstate.h" #include "battle/cutscenebattlewaitselection.h" +#include "battle/cutscenebattlepostmove.h" #include "save/cutscenesavedevicecheck.h" #include "save/cutscenesaveloadallslots.h" #include "audio/cutsceneaudioplay.h" diff --git a/src/dusk/rpg/cutscene/item/cutsceneitembase.h b/src/dusk/rpg/cutscene/item/cutsceneitembase.h index be187c26..b3f7dcc7 100644 --- a/src/dusk/rpg/cutscene/item/cutsceneitembase.h +++ b/src/dusk/rpg/cutscene/item/cutsceneitembase.h @@ -63,6 +63,7 @@ typedef enum { CUTSCENE_ITEM_TYPE_BATTLE_START, CUTSCENE_ITEM_TYPE_BATTLE_SET_STATE, CUTSCENE_ITEM_TYPE_BATTLE_WAIT_SELECTION, + CUTSCENE_ITEM_TYPE_BATTLE_POST_MOVE, // Save CUTSCENE_ITEM_TYPE_SAVE_DEVICE_CHECK, diff --git a/src/dusk/rpg/cutscene/item/ui/cutscenemodal.c b/src/dusk/rpg/cutscene/item/ui/cutscenemodal.c index e4c23db3..bad23afd 100644 --- a/src/dusk/rpg/cutscene/item/ui/cutscenemodal.c +++ b/src/dusk/rpg/cutscene/item/ui/cutscenemodal.c @@ -36,7 +36,8 @@ bool_t cutsceneModalUpdate( const cutsceneitem_t *item, cutsceneitemdata_t *data ) { - return item->modal.optionCount == 0; + if(item->modal.optionCount == 0 && !item->modal.blocking) return true; + return !uiModalIsOpen(); } void cutsceneModalCloseStart( @@ -72,6 +73,7 @@ errorret_t cutsceneModalLoad( )) { errorThrow("Cutscene item (MODAL): missing/too-long 'message'"); } + item->modal.blocking = cutsceneJsonGetBool(itemObj, "blocking", false); errorOk(); } diff --git a/src/dusk/rpg/cutscene/item/ui/cutscenemodal.h b/src/dusk/rpg/cutscene/item/ui/cutscenemodal.h index ff7c73f5..d7f74c19 100644 --- a/src/dusk/rpg/cutscene/item/ui/cutscenemodal.h +++ b/src/dusk/rpg/cutscene/item/ui/cutscenemodal.h @@ -40,6 +40,12 @@ typedef struct { uint8_t optionCount; cutscenemodaloptioncallback_t callback; + + // Whether the cutscene should wait for this dialog to close before + // continuing. Always treated as true when optionCount > 0 (there's no + // other way to move on). Ignored/false by default for a message-only + // dialog, e.g. from CUTSCENE_MODAL - see cutsceneModalUpdate. + bool_t blocking; } cutscenemodal_t; /** @@ -57,23 +63,24 @@ void cutsceneModalStart( ); /** - * Updates a modal item. A message-only modal (optionCount == 0, e.g. - * from CUTSCENE_MODAL) always completes immediately - the cutscene - * continues on to whatever follows this item right away, it does not - * wait for the dialog to be dismissed. Script what should happen while - * it's up, and how it gets closed, as later items in the same cutscene - * (e.g. CUTSCENE_CALLBACK, CUTSCENE_WAIT, CUTSCENE_MODAL_CLOSE). + * Updates a modal item. + * + * A message-only modal (optionCount == 0, e.g. from CUTSCENE_MODAL) + * completes immediately unless item->modal.blocking is set, in which + * case it waits for the dialog to close - either the player pressing + * accept/cancel (see uiModalMessageFocusSelected) or an explicit + * CUTSCENE_MODAL_CLOSE item elsewhere in the cutscene. * * A modal with options (optionCount > 0, e.g. from - * CUTSCENE_MODAL_OPTIONS) never completes on its own - the cutscene - * blocks here indefinitely. The option callback fires once the user - * picks something, and it alone is responsible for moving the cutscene - * on from there (typically via cutsceneGoTo or cutsceneRestart). + * CUTSCENE_MODAL_OPTIONS) always blocks regardless of the blocking + * field - it's dismissed by the player picking one of them, and the + * option callback is responsible for moving the cutscene on from there + * (typically via cutsceneGoTo or cutsceneRestart). * * @param item The cutscene item. * @param data Runtime data storage. - * @returns true once immediately for a message-only modal; false - * always for a modal with options. + * @returns true once the modal has closed, or immediately for a + * non-blocking message-only modal. */ bool_t cutsceneModalUpdate( const cutsceneitem_t *item, diff --git a/src/dusk/ui/widget/uimodal.c b/src/dusk/ui/widget/uimodal.c index 37e0eac6..9276f26b 100644 --- a/src/dusk/ui/widget/uimodal.c +++ b/src/dusk/ui/widget/uimodal.c @@ -7,6 +7,7 @@ #include "uimodal.h" #include "ui/widget/uiframe.h" +#include "ui/focus/uifocus.h" #include "assert/assert.h" #include "util/memory.h" #include "util/string.h" @@ -24,6 +25,11 @@ uimodal_t UI_MODAL; +// Focus item backing a message-only modal (no options) - lets a plain +// MODAL still block on player confirmation. NULL whenever a +// message-only modal isn't open. Mirrors uitextboxmain.c's pattern. +static uifocusitem_t *messageFocus = NULL; + void uiModalSelected( const uimenu_t *menu, const uint8_t index, @@ -39,6 +45,18 @@ void uiModalClosed(const uimenu_t *menu) { } } +bool_t uiModalMessageFocusSelected(const uifocusitem_t *item) { + uiFocusPopItem(messageFocus); + return true; +} + +bool_t uiModalMessageFocusClosed(const uifocusitem_t *item) { + messageFocus = NULL; + UI_MODAL.open = false; + uiModalClosed(NULL); + return true; +} + errorret_t uiModalInit(void) { memoryZero(&UI_MODAL, sizeof(uimodal_t)); @@ -212,6 +230,11 @@ void uiModalOpen( uiMenuOpen(&UI_MODAL.menu); } else { uiMenuInit(&UI_MODAL.menu, uiModalSelected, uiModalClosed, NULL); + messageFocus = uiFocusPush( + 1, 1, + uiModalMessageFocusSelected, NULL, uiModalMessageFocusClosed, + NULL, NULL + ); } if(UI_MODAL.onOpen != NULL) { @@ -225,6 +248,8 @@ void uiModalClose(uimodalclosedcallback_t callback) { if(uiMenuIsActive(&UI_MODAL.menu)) { uiMenuClose(&UI_MODAL.menu); + } else if(messageFocus != NULL) { + uiFocusPopItem(messageFocus); } else { uiModalClosed(NULL); }