Resolve moves into per-move cutscenes during battle execution
Each queued action now resolves to its move's cutscene (cutscenes/battle/moves/<name>.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 <[email protected]>
This commit is contained in:
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -12,6 +12,8 @@ typedef struct {
|
||||
|
||||
} battlestateexecuting_t;
|
||||
|
||||
#define BATTLE_STATE_EXECUTING_ITEMS_MAX 64
|
||||
|
||||
/**
|
||||
* Called when the battle enters BATTLE_STATE_EXECUTING.
|
||||
*/
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
@@ -8,4 +8,5 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
cutscenebattlestart.c
|
||||
cutscenebattlesetstate.c
|
||||
cutscenebattlewaitselection.c
|
||||
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;
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user