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": [
|
"items": [
|
||||||
{
|
{
|
||||||
"type": "MODAL",
|
"type": "MODAL",
|
||||||
"title": "BATTLE TEST",
|
"title": "Fire",
|
||||||
"message": "BATTLE TEST"
|
"message": "A burst of flame scorches the target."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "WAIT",
|
||||||
|
"seconds": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "MODAL_CLOSE"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,16 @@
|
|||||||
{
|
{
|
||||||
"items": [
|
"items": [
|
||||||
{
|
{
|
||||||
"type": "MODAL",
|
"type": "MODAL",
|
||||||
"title": "BATTLE TEST",
|
"title": "Slash",
|
||||||
"message": "BATTLE TEST"
|
"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.
|
// Order of who gets to act.
|
||||||
uint8_t fighterOrder[BATTLE_FIGHTER_POS_COUNT];
|
uint8_t fighterOrder[BATTLE_FIGHTER_POS_COUNT];
|
||||||
|
uint8_t fighterOrderCount;
|
||||||
|
|
||||||
// Actions to be executed.
|
// Actions to be executed.
|
||||||
battlemoveaction_t actionsPending[BATTLE_FIGHTER_POS_COUNT];
|
battlemoveaction_t actionsPending[BATTLE_FIGHTER_POS_COUNT];
|
||||||
|
uint8_t actionsPendingCount;
|
||||||
|
|
||||||
battlestate_t state;
|
battlestate_t state;
|
||||||
battlestatedata_t stateData;
|
battlestatedata_t stateData;
|
||||||
|
|||||||
@@ -6,9 +6,92 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#include "battlestateexecuting.h"
|
#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) {
|
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) {
|
void battleStateExecutingUpdate(void) {
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ typedef struct {
|
|||||||
|
|
||||||
} battlestateexecuting_t;
|
} battlestateexecuting_t;
|
||||||
|
|
||||||
|
#define BATTLE_STATE_EXECUTING_ITEMS_MAX 64
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when the battle enters BATTLE_STATE_EXECUTING.
|
* Called when the battle enters BATTLE_STATE_EXECUTING.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -6,11 +6,21 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#include "battlestatepreround.h"
|
#include "battlestatepreround.h"
|
||||||
|
#include "rpg/battle/battle.h"
|
||||||
#include "rpg/cutscene/item/cutsceneitem.h"
|
#include "rpg/cutscene/item/cutsceneitem.h"
|
||||||
#include "rpg/cutscene/cutscenesystem.h"
|
#include "rpg/cutscene/cutscenesystem.h"
|
||||||
|
|
||||||
void battleStatePreRoundInit(void) {
|
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,
|
.type = CUTSCENE_ITEM_TYPE_BATTLE_SET_STATE,
|
||||||
.battleSetState = { .state = BATTLE_STATE_SELECTION }
|
.battleSetState = { .state = BATTLE_STATE_SELECTION }
|
||||||
|
|||||||
@@ -7,87 +7,73 @@
|
|||||||
|
|
||||||
#include "battlestateselection.h"
|
#include "battlestateselection.h"
|
||||||
#include "rpg/battle/battle.h"
|
#include "rpg/battle/battle.h"
|
||||||
#include "rpg/cutscene/item/cutsceneitem.h"
|
|
||||||
#include "rpg/cutscene/cutscenesystem.h"
|
|
||||||
#include "assert/assert.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) {
|
void battleStateSelectionInit(void) {
|
||||||
BATTLE.stateData.selection.selectionIndex = 0;
|
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) {
|
void battleStateSelectionUpdate(void) {
|
||||||
battlestateselection_t *state = &BATTLE.stateData.selection;
|
battlestateselection_t *state = &BATTLE.stateData.selection;
|
||||||
|
|
||||||
while(state->selectionIndex < BATTLE_FIGHTER_POS_COUNT) {
|
// Are we still waiting for a selection?
|
||||||
const uint8_t fighterIndex = state->selectionIndex;
|
while(state->selectionIndex < BATTLE.fighterOrderCount) {
|
||||||
|
const uint8_t fighterIndex = BATTLE.fighterOrder[state->selectionIndex];
|
||||||
|
|
||||||
|
// Is this fighter still owed a decision?
|
||||||
if(!battleStateSelectionFighterNeedsDecision(fighterIndex)) {
|
if(!battleStateSelectionFighterNeedsDecision(fighterIndex)) {
|
||||||
state->selectionIndex++;
|
state->selectionIndex++;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Players will act on their own.
|
||||||
battlefighter_t *fighter = BATTLE.fighters[fighterIndex];
|
battlefighter_t *fighter = BATTLE.fighters[fighterIndex];
|
||||||
if(fighter->controller != BATTLE_FIGHTER_CONTROLLER_AI) {
|
if(fighter->controller != BATTLE_FIGHTER_CONTROLLER_AI) return;
|
||||||
// Player-controlled: wait for battleStateSelectionPlayerAttack/Flee.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const battlefighterteam_t enemyTeam =
|
// This is AI, in future I'll have proper AI here, for now I'm actually not
|
||||||
fighter->team == BATTLE_FIGHTER_TEAM_ALLY ?
|
// going to do anything.
|
||||||
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);
|
|
||||||
}
|
|
||||||
state->selectionIndex++;
|
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) {
|
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(
|
void battleStateSelectionQueueAttack(
|
||||||
const uint8_t fighterIndex,
|
const uint8_t fighterIndex,
|
||||||
const uint8_t targetIndex
|
const uint8_t targetIndex
|
||||||
) {
|
) {
|
||||||
assertTrue(fighterIndex < BATTLE_FIGHTER_POS_COUNT, "fighterIndex out of bounds");
|
assertTrue(fighterIndex < BATTLE_FIGHTER_POS_COUNT, "Fighter OOB");
|
||||||
assertTrue(targetIndex < BATTLE_FIGHTER_POS_COUNT, "targetIndex out of bounds");
|
assertTrue(targetIndex < BATTLE_FIGHTER_POS_COUNT, "Target OOB");
|
||||||
assertNotNull(BATTLE.fighters[fighterIndex], "Fighter cannot be NULL");
|
assertNotNull(BATTLE.fighters[fighterIndex], "Fighter cannot be NULL");
|
||||||
assertNotNull(BATTLE.fighters[targetIndex], "Target 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];
|
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->targets[0] = targetIndex;
|
||||||
action->user = BATTLE.fighters[fighterIndex];
|
action->user = BATTLE.fighters[fighterIndex];
|
||||||
}
|
}
|
||||||
@@ -96,18 +82,18 @@ battlefighter_t *battleStateSelectionGetCurrentFighter(void) {
|
|||||||
if(BATTLE.state != BATTLE_STATE_SELECTION) return NULL;
|
if(BATTLE.state != BATTLE_STATE_SELECTION) return NULL;
|
||||||
|
|
||||||
const uint8_t index = BATTLE.stateData.selection.selectionIndex;
|
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) {
|
void battleStateSelectionPlayerAttack(const uint8_t targetIndex) {
|
||||||
battlefighter_t *fighter = battleStateSelectionGetCurrentFighter();
|
battlefighter_t *fighter = battleStateSelectionGetCurrentFighter();
|
||||||
if(fighter == NULL) return;
|
if(fighter == NULL) return;
|
||||||
|
|
||||||
battleStateSelectionQueueAttack(
|
const uint8_t selectionIndex = BATTLE.stateData.selection.selectionIndex;
|
||||||
BATTLE.stateData.selection.selectionIndex, targetIndex
|
const uint8_t fighterIndex = BATTLE.fighterOrder[selectionIndex];
|
||||||
);
|
battleStateSelectionQueueAttack(fighterIndex, targetIndex);
|
||||||
BATTLE.stateData.selection.selectionIndex++;
|
BATTLE.stateData.selection.selectionIndex++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -33,6 +33,15 @@ void battleStateSelectionUpdate(void);
|
|||||||
*/
|
*/
|
||||||
void battleStateSelectionDispose(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
|
* Checks whether the fighter at fighterIndex is a live, undecided
|
||||||
* candidate that BATTLE_STATE_SELECTION should still be deciding a move
|
* 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);
|
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 fighterIndex Index into BATTLE.fighters of the deciding fighter.
|
||||||
* @param targetIndex Index into BATTLE.fighters of the target.
|
* @param targetIndex Index into BATTLE.fighters of the target.
|
||||||
|
|||||||
@@ -8,4 +8,5 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
|||||||
cutscenebattlestart.c
|
cutscenebattlestart.c
|
||||||
cutscenebattlesetstate.c
|
cutscenebattlesetstate.c
|
||||||
cutscenebattlewaitselection.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/item/cutsceneitem.h"
|
||||||
|
#include "rpg/cutscene/cutscenesystem.h"
|
||||||
#include "rpg/battle/battle.h"
|
#include "rpg/battle/battle.h"
|
||||||
|
#include "rpg/battle/state/battlestateselection.h"
|
||||||
|
|
||||||
bool_t cutsceneBattleWaitSelectionUpdate(
|
bool_t cutsceneBattleWaitSelectionUpdate(
|
||||||
const cutsceneitem_t *item,
|
const cutsceneitem_t *item,
|
||||||
cutsceneitemdata_t *data
|
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"
|
#include "rpg/cutscene/item/cutsceneitembase.h"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates a BATTLE_WAIT_SELECTION step, completing once the battle is no
|
* Updates a BATTLE_WAIT_SELECTION step, completing once every fighter has
|
||||||
* longer in BATTLE_STATE_SELECTION -- i.e. every fighter has decided a
|
* decided a move this round (letting the queued BATTLE_SET_STATE step
|
||||||
* move (moving on to BATTLE_STATE_EXECUTING) or the selecting fighter has
|
* that follows move the battle on to BATTLE_STATE_EXECUTING) or once the
|
||||||
* fled (moving on to BATTLE_STATE_FLED). Has no Start callback -- there's
|
* selecting fighter has fled (moving on to BATTLE_STATE_FLED). Has no
|
||||||
* nothing to do until selection actually finishes.
|
* Start callback -- there's nothing to do until selection actually
|
||||||
|
* finishes.
|
||||||
*
|
*
|
||||||
* @param item The cutscene item.
|
* @param item The cutscene item.
|
||||||
* @param data Runtime data storage.
|
* @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(
|
bool_t cutsceneBattleWaitSelectionUpdate(
|
||||||
const cutsceneitem_t *item,
|
const cutsceneitem_t *item,
|
||||||
|
|||||||
@@ -265,6 +265,11 @@ cutsceneitemcallbacks_t CUTSCENE_ITEM_CALLBACKS[CUTSCENE_ITEM_TYPE_COUNT] = {
|
|||||||
.load = cutsceneItemLoadNoop
|
.load = cutsceneItemLoadNoop
|
||||||
},
|
},
|
||||||
|
|
||||||
|
[CUTSCENE_ITEM_TYPE_BATTLE_POST_MOVE] = {
|
||||||
|
.init = cutsceneBattlePostMoveStart,
|
||||||
|
.update = cutsceneBattlePostMoveUpdate
|
||||||
|
},
|
||||||
|
|
||||||
// Save
|
// Save
|
||||||
[CUTSCENE_ITEM_TYPE_SAVE_DEVICE_CHECK] = {
|
[CUTSCENE_ITEM_TYPE_SAVE_DEVICE_CHECK] = {
|
||||||
.init = cutsceneSaveDeviceCheckStart,
|
.init = cutsceneSaveDeviceCheckStart,
|
||||||
|
|||||||
@@ -43,6 +43,7 @@
|
|||||||
#include "battle/cutscenebattlestart.h"
|
#include "battle/cutscenebattlestart.h"
|
||||||
#include "battle/cutscenebattlesetstate.h"
|
#include "battle/cutscenebattlesetstate.h"
|
||||||
#include "battle/cutscenebattlewaitselection.h"
|
#include "battle/cutscenebattlewaitselection.h"
|
||||||
|
#include "battle/cutscenebattlepostmove.h"
|
||||||
#include "save/cutscenesavedevicecheck.h"
|
#include "save/cutscenesavedevicecheck.h"
|
||||||
#include "save/cutscenesaveloadallslots.h"
|
#include "save/cutscenesaveloadallslots.h"
|
||||||
#include "audio/cutsceneaudioplay.h"
|
#include "audio/cutsceneaudioplay.h"
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ typedef enum {
|
|||||||
CUTSCENE_ITEM_TYPE_BATTLE_START,
|
CUTSCENE_ITEM_TYPE_BATTLE_START,
|
||||||
CUTSCENE_ITEM_TYPE_BATTLE_SET_STATE,
|
CUTSCENE_ITEM_TYPE_BATTLE_SET_STATE,
|
||||||
CUTSCENE_ITEM_TYPE_BATTLE_WAIT_SELECTION,
|
CUTSCENE_ITEM_TYPE_BATTLE_WAIT_SELECTION,
|
||||||
|
CUTSCENE_ITEM_TYPE_BATTLE_POST_MOVE,
|
||||||
|
|
||||||
// Save
|
// Save
|
||||||
CUTSCENE_ITEM_TYPE_SAVE_DEVICE_CHECK,
|
CUTSCENE_ITEM_TYPE_SAVE_DEVICE_CHECK,
|
||||||
|
|||||||
@@ -36,7 +36,8 @@ bool_t cutsceneModalUpdate(
|
|||||||
const cutsceneitem_t *item,
|
const cutsceneitem_t *item,
|
||||||
cutsceneitemdata_t *data
|
cutsceneitemdata_t *data
|
||||||
) {
|
) {
|
||||||
return item->modal.optionCount == 0;
|
if(item->modal.optionCount == 0 && !item->modal.blocking) return true;
|
||||||
|
return !uiModalIsOpen();
|
||||||
}
|
}
|
||||||
|
|
||||||
void cutsceneModalCloseStart(
|
void cutsceneModalCloseStart(
|
||||||
@@ -72,6 +73,7 @@ errorret_t cutsceneModalLoad(
|
|||||||
)) {
|
)) {
|
||||||
errorThrow("Cutscene item (MODAL): missing/too-long 'message'");
|
errorThrow("Cutscene item (MODAL): missing/too-long 'message'");
|
||||||
}
|
}
|
||||||
|
item->modal.blocking = cutsceneJsonGetBool(itemObj, "blocking", false);
|
||||||
|
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,6 +40,12 @@ typedef struct {
|
|||||||
uint8_t optionCount;
|
uint8_t optionCount;
|
||||||
|
|
||||||
cutscenemodaloptioncallback_t callback;
|
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;
|
} cutscenemodal_t;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -57,23 +63,24 @@ void cutsceneModalStart(
|
|||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates a modal item. A message-only modal (optionCount == 0, e.g.
|
* Updates a modal item.
|
||||||
* from CUTSCENE_MODAL) always completes immediately - the cutscene
|
*
|
||||||
* continues on to whatever follows this item right away, it does not
|
* A message-only modal (optionCount == 0, e.g. from CUTSCENE_MODAL)
|
||||||
* wait for the dialog to be dismissed. Script what should happen while
|
* completes immediately unless item->modal.blocking is set, in which
|
||||||
* it's up, and how it gets closed, as later items in the same cutscene
|
* case it waits for the dialog to close - either the player pressing
|
||||||
* (e.g. CUTSCENE_CALLBACK, CUTSCENE_WAIT, CUTSCENE_MODAL_CLOSE).
|
* accept/cancel (see uiModalMessageFocusSelected) or an explicit
|
||||||
|
* CUTSCENE_MODAL_CLOSE item elsewhere in the cutscene.
|
||||||
*
|
*
|
||||||
* A modal with options (optionCount > 0, e.g. from
|
* A modal with options (optionCount > 0, e.g. from
|
||||||
* CUTSCENE_MODAL_OPTIONS) never completes on its own - the cutscene
|
* CUTSCENE_MODAL_OPTIONS) always blocks regardless of the blocking
|
||||||
* blocks here indefinitely. The option callback fires once the user
|
* field - it's dismissed by the player picking one of them, and the
|
||||||
* picks something, and it alone is responsible for moving the cutscene
|
* option callback is responsible for moving the cutscene on from there
|
||||||
* on from there (typically via cutsceneGoTo or cutsceneRestart).
|
* (typically via cutsceneGoTo or cutsceneRestart).
|
||||||
*
|
*
|
||||||
* @param item The cutscene item.
|
* @param item The cutscene item.
|
||||||
* @param data Runtime data storage.
|
* @param data Runtime data storage.
|
||||||
* @returns true once immediately for a message-only modal; false
|
* @returns true once the modal has closed, or immediately for a
|
||||||
* always for a modal with options.
|
* non-blocking message-only modal.
|
||||||
*/
|
*/
|
||||||
bool_t cutsceneModalUpdate(
|
bool_t cutsceneModalUpdate(
|
||||||
const cutsceneitem_t *item,
|
const cutsceneitem_t *item,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
#include "uimodal.h"
|
#include "uimodal.h"
|
||||||
#include "ui/widget/uiframe.h"
|
#include "ui/widget/uiframe.h"
|
||||||
|
#include "ui/focus/uifocus.h"
|
||||||
#include "assert/assert.h"
|
#include "assert/assert.h"
|
||||||
#include "util/memory.h"
|
#include "util/memory.h"
|
||||||
#include "util/string.h"
|
#include "util/string.h"
|
||||||
@@ -24,6 +25,11 @@
|
|||||||
|
|
||||||
uimodal_t UI_MODAL;
|
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(
|
void uiModalSelected(
|
||||||
const uimenu_t *menu,
|
const uimenu_t *menu,
|
||||||
const uint8_t index,
|
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) {
|
errorret_t uiModalInit(void) {
|
||||||
memoryZero(&UI_MODAL, sizeof(uimodal_t));
|
memoryZero(&UI_MODAL, sizeof(uimodal_t));
|
||||||
|
|
||||||
@@ -212,6 +230,11 @@ void uiModalOpen(
|
|||||||
uiMenuOpen(&UI_MODAL.menu);
|
uiMenuOpen(&UI_MODAL.menu);
|
||||||
} else {
|
} else {
|
||||||
uiMenuInit(&UI_MODAL.menu, uiModalSelected, uiModalClosed, NULL);
|
uiMenuInit(&UI_MODAL.menu, uiModalSelected, uiModalClosed, NULL);
|
||||||
|
messageFocus = uiFocusPush(
|
||||||
|
1, 1,
|
||||||
|
uiModalMessageFocusSelected, NULL, uiModalMessageFocusClosed,
|
||||||
|
NULL, NULL
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(UI_MODAL.onOpen != NULL) {
|
if(UI_MODAL.onOpen != NULL) {
|
||||||
@@ -225,6 +248,8 @@ void uiModalClose(uimodalclosedcallback_t callback) {
|
|||||||
|
|
||||||
if(uiMenuIsActive(&UI_MODAL.menu)) {
|
if(uiMenuIsActive(&UI_MODAL.menu)) {
|
||||||
uiMenuClose(&UI_MODAL.menu);
|
uiMenuClose(&UI_MODAL.menu);
|
||||||
|
} else if(messageFocus != NULL) {
|
||||||
|
uiFocusPopItem(messageFocus);
|
||||||
} else {
|
} else {
|
||||||
uiModalClosed(NULL);
|
uiModalClosed(NULL);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user