Drive battle rounds through cutscene items, fix render/init bugs

Adds BATTLE_START, BATTLE_SET_STATE and BATTLE_WAIT_SELECTION cutscene
items so a battle's state transitions (opening -> pre-round -> selection
-> executing) are chained through the running cutscene, with
cutsceneSystemInsertItemsNext/InsertItems splitting "splice" from
"splice and advance immediately". Adds battlestateselection's
get-current-fighter/player-attack/player-flee API backing the battle UI,
and battleMoveGetIdByName for JSON-authored ability lookups.

Fixes: off-by-one in battleMoveInit that let the first-defined move
alias BATTLE_MOVE_ID_NULL; a segfault in scenebattle.c from rendering
unfilled (NULL) fighter slots; stale battleGetCurrentFighter/
battlePlayerAttack/battlePlayerFlee references in the battle UI.

Adds assertions across the battle/scene/UI code for fighter-index
bounds and non-null invariants. Updates battletest.jsonc for the new
BATTLE_START item schema.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
2026-09-20 13:32:52 -05:00
co-authored by Claude Sonnet 5
parent 3675ec634f
commit e3f56c4d12
28 changed files with 747 additions and 34 deletions
+10 -12
View File
@@ -1,24 +1,22 @@
{
"items": [
{
"type": "START_BATTLE",
"encounterType": "REGULAR",
"fleeAvailable": true,
"type": "BATTLE_START",
"enemies": [
{
"stats": { "attack": 8, "defense": 4, "magic": 0, "speed": 5, "luck": 2 },
"healthMax": 30,
"mpMax": 0
"stats": {
"attack": 8, "defense": 4, "magic": 0, "speed": 5, "luck": 2,
"healthMax": 30, "mpMax": 0
},
"abilities": ["fire"]
},
{
"stats": { "attack": 6, "defense": 3, "magic": 0, "speed": 7, "luck": 2 },
"healthMax": 20,
"mpMax": 0
"stats": {
"attack": 6, "defense": 3, "magic": 0, "speed": 7, "luck": 2,
"healthMax": 20, "mpMax": 0
}
}
]
},
{
"type": "REGULAR_BATTLE"
}
]
}
+25 -3
View File
@@ -9,6 +9,7 @@
#include "assert/assert.h"
#include "util/memory.h"
#include "rpg/cutscene/cutscenesystem.h"
#include "scene/scene.h"
battle_t BATTLE;
@@ -22,7 +23,8 @@ void battleStart(
assertNotNull(fighters, "Fighters cannot be NULL");
#if DUSK_ASSERTIONS
// Must be at least two non null fighter present.
// Must be at least two non null fighter present, may need to ensure they
// are from seperate teams also.
uint8_t nonNullCount = 0;
for(uint8_t i = 0; i < BATTLE_FIGHTER_POS_COUNT; i++) {
if(fighters[i] != NULL) nonNullCount++;
@@ -39,18 +41,38 @@ void battleStart(
sizeof(battlefighter_t *) * BATTLE_FIGHTER_POS_COUNT
);
// Call init on opening state.
BATTLE.state = BATTLE_STATE_OPENING;
// Enter the opening state.
battleSetState(BATTLE_STATE_OPENING);
// Change scene
sceneSet(SCENE_TYPE_BATTLE);
}
void battleSetState(const battlestate_t next) {
assertTrue(next < BATTLE_STATE_COUNT, "Invalid battle state");
assertNotNull(BATTLE_STATE_FUNCTIONS[next].init, "Missing state init function");
assertNotNull(BATTLE_STATE_FUNCTIONS[next].update, "Missing state update function");
assertNotNull(BATTLE_STATE_FUNCTIONS[next].dispose, "Missing state dispose function");
if(BATTLE.state != BATTLE_STATE_NULL) {
BATTLE_STATE_FUNCTIONS[BATTLE.state].dispose();
}
BATTLE.state = next;
BATTLE_STATE_FUNCTIONS[BATTLE.state].init();
}
void battleUpdate(void) {
if(BATTLE.state == BATTLE_STATE_NULL) return;
assertTrue(BATTLE.state < BATTLE_STATE_COUNT, "Invalid battle state");
assertNotNull(BATTLE_STATE_FUNCTIONS[BATTLE.state].update, "Missing state update function");
BATTLE_STATE_FUNCTIONS[BATTLE.state].update();
}
void battleDispose(void) {
if(BATTLE.state == BATTLE_STATE_NULL) return;
assertTrue(BATTLE.state < BATTLE_STATE_COUNT, "Invalid battle state");
assertNotNull(BATTLE_STATE_FUNCTIONS[BATTLE.state].dispose, "Missing state dispose function");
BATTLE_STATE_FUNCTIONS[BATTLE.state].dispose();
memoryZero(&BATTLE, sizeof(battle_t));
+8
View File
@@ -49,6 +49,14 @@ void battleStart(
battlefighter_t *fighters[BATTLE_FIGHTER_POS_COUNT]
);
/**
* Transitions the battle to a new state: disposes the current state (if
* any) and initializes next.
*
* @param next The state to transition to.
*/
void battleSetState(const battlestate_t next);
/**
* Updates the battle system.
*/
+12 -1
View File
@@ -8,6 +8,7 @@
#include "battlemove.h"
#include "asset/asset.h"
#include "asset/loader/assetloader.h"
#include "assert/assert.h"
#include "util/memory.h"
#include "util/string.h"
#include <string.h>
@@ -17,7 +18,7 @@ uint8_t BATTLE_MOVE_COUNT;
errorret_t battleMoveInit(void) {
memoryZero(BATTLE_MOVES, sizeof(BATTLE_MOVES));
BATTLE_MOVE_COUNT = BATTLE_MOVE_ID_NULL;
BATTLE_MOVE_COUNT = BATTLE_MOVE_ID_NULL + 1;// Reserve NULL.
// Get moves JSON.
assetentry_t *entry = assetLock(
@@ -126,3 +127,13 @@ errorret_t battleMoveInit(void) {
assetUnlockEntry(entry);
errorOk();
}
battlemoveid_t battleMoveGetIdByName(const char_t *name) {
assertNotNull(name, "name must not be NULL");
for(battlemoveid_t id = BATTLE_MOVE_ID_NULL + 1; id < BATTLE_MOVE_COUNT; id++) {
if(stringEquals(BATTLE_MOVES[id].name, name)) return id;
}
return BATTLE_MOVE_ID_NULL;
}
+8
View File
@@ -32,3 +32,11 @@ extern battleability_t BATTLE_MOVES[BATTLE_MOVE_COUNT_MAX];
* @return Any error that occurs.
*/
errorret_t battleMoveInit(void);
/**
* Finds a move's id by name (exact match).
*
* @param name The move name to search for.
* @return The matching move's id, or BATTLE_MOVE_ID_NULL if not found.
*/
battlemoveid_t battleMoveGetIdByName(const char_t *name);
@@ -6,9 +6,17 @@
*/
#include "battlestateopening.h"
#include "rpg/cutscene/item/cutsceneitem.h"
#include "rpg/cutscene/cutscenesystem.h"
void battleStateOpeningInit(void) {
const cutsceneitem_t items[] = {
{
.type = CUTSCENE_ITEM_TYPE_BATTLE_SET_STATE,
.battleSetState = { .state = BATTLE_STATE_PRE_ROUND }
}
};
cutsceneSystemInsertItemsNext(items, sizeof(items) / sizeof(cutsceneitem_t));
}
void battleStateOpeningUpdate(void) {
@@ -6,9 +6,20 @@
*/
#include "battlestatepreround.h"
#include "rpg/cutscene/item/cutsceneitem.h"
#include "rpg/cutscene/cutscenesystem.h"
void battleStatePreRoundInit(void) {
const cutsceneitem_t items[] = {
{
.type = CUTSCENE_ITEM_TYPE_BATTLE_SET_STATE,
.battleSetState = { .state = BATTLE_STATE_SELECTION }
},
{
.type = CUTSCENE_ITEM_TYPE_BATTLE_WAIT_SELECTION
}
};
cutsceneSystemInsertItemsNext(items, sizeof(items) / sizeof(cutsceneitem_t));
}
void battleStatePreRoundUpdate(void) {
@@ -6,15 +6,114 @@
*/
#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;
}
void battleStateSelectionUpdate(void) {
battlestateselection_t *state = &BATTLE.stateData.selection;
while(state->selectionIndex < BATTLE_FIGHTER_POS_COUNT) {
const uint8_t fighterIndex = state->selectionIndex;
if(!battleStateSelectionFighterNeedsDecision(fighterIndex)) {
state->selectionIndex++;
continue;
}
battlefighter_t *fighter = BATTLE.fighters[fighterIndex];
if(fighter->controller != BATTLE_FIGHTER_CONTROLLER_AI) {
// Player-controlled: wait for battleStateSelectionPlayerAttack/Flee.
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);
}
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");
assertNotNull(BATTLE.fighters[fighterIndex], "Fighter cannot be NULL");
assertNotNull(BATTLE.fighters[targetIndex], "Target cannot be NULL");
battlemoveaction_t *action = &BATTLE.actionsPending[fighterIndex];
action->move = BATTLE_MOVE_ID_ATTACK;
action->targets[0] = targetIndex;
action->user = BATTLE.fighters[fighterIndex];
}
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;
return BATTLE.fighters[index];
}
void battleStateSelectionPlayerAttack(const uint8_t targetIndex) {
battlefighter_t *fighter = battleStateSelectionGetCurrentFighter();
if(fighter == NULL) return;
battleStateSelectionQueueAttack(
BATTLE.stateData.selection.selectionIndex, targetIndex
);
BATTLE.stateData.selection.selectionIndex++;
}
void battleStateSelectionPlayerFlee(void) {
battlefighter_t *fighter = battleStateSelectionGetCurrentFighter();
if(fighter == NULL) return;
battleSetState(BATTLE_STATE_FLED);
}
@@ -6,10 +6,12 @@
*/
#pragma once
#include "dusk.h"
#include "rpg/battle/fighter/battlefighter.h"
typedef struct {
// Cursor into BATTLE.fighters (by position index) for the fighter
// currently deciding a move this round.
uint8_t selectionIndex;
} battlestateselection_t;
/**
@@ -18,7 +20,11 @@ typedef struct {
void battleStateSelectionInit(void);
/**
* Updates BATTLE_STATE_SELECTION for one frame.
* Updates BATTLE_STATE_SELECTION for one frame: auto-decides for every
* AI-controlled fighter the cursor passes, stopping once it reaches a
* player-controlled fighter awaiting input (see
* battleStateSelectionGetCurrentFighter), or transitions to
* BATTLE_STATE_EXECUTING once every position has decided.
*/
void battleStateSelectionUpdate(void);
@@ -26,3 +32,49 @@ void battleStateSelectionUpdate(void);
* Called when the battle leaves BATTLE_STATE_SELECTION.
*/
void battleStateSelectionDispose(void);
/**
* Checks whether the fighter at fighterIndex is a live, undecided
* candidate that BATTLE_STATE_SELECTION should still be deciding a move
* for.
*
* @param fighterIndex Index into BATTLE.fighters to check.
* @return True if the fighter is alive and has no move queued yet.
*/
bool_t battleStateSelectionFighterNeedsDecision(const uint8_t fighterIndex);
/**
* Queues a basic attack from fighterIndex against targetIndex.
*
* @param fighterIndex Index into BATTLE.fighters of the deciding fighter.
* @param targetIndex Index into BATTLE.fighters of the target.
*/
void battleStateSelectionQueueAttack(
const uint8_t fighterIndex,
const uint8_t targetIndex
);
/**
* Returns the fighter currently awaiting a decision in
* BATTLE_STATE_SELECTION.
*
* @return Pointer to the fighter awaiting a decision, or NULL if the
* battle isn't in BATTLE_STATE_SELECTION or every position has
* already decided.
*/
battlefighter_t *battleStateSelectionGetCurrentFighter(void);
/**
* Submits the currently-selecting fighter's attack against a target, if
* the battle is in BATTLE_STATE_SELECTION and awaiting a decision. Queues
* the move and advances the selection cursor.
*
* @param targetIndex Index into BATTLE.fighters of the target.
*/
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);
+19 -5
View File
@@ -87,20 +87,34 @@ void cutsceneSystemStartCutsceneAndGoToMarker(
cutsceneGoTo(marker);
}
void cutsceneSystemInsertCutscene(const cutscene_t *cutscene) {
assertNotNull(cutscene, "Cutscene cannot be NULL");
void cutsceneSystemInsertItemsNext(
const cutsceneitem_t *items,
const size_t itemCount
) {
assertNotNull(items, "Items cannot be NULL");
assertNotNull(
CUTSCENE_SYSTEM.scene,
"cutsceneSystemInsertCutscene called with no cutscene running"
"cutsceneSystemInsertItemsNext called with no cutscene running"
);
cutsceneAppendNext(
CUTSCENE_SYSTEM.scene, CUTSCENE_SYSTEM.currentItem,
cutscene->items, cutscene->itemCount
CUTSCENE_SYSTEM.scene, CUTSCENE_SYSTEM.currentItem, items, itemCount
);
}
void cutsceneSystemInsertItems(
const cutsceneitem_t *items,
const size_t itemCount
) {
cutsceneSystemInsertItemsNext(items, itemCount);
cutsceneSystemNext();
}
void cutsceneSystemInsertCutscene(const cutscene_t *cutscene) {
assertNotNull(cutscene, "Cutscene cannot be NULL");
cutsceneSystemInsertItems(cutscene->items, cutscene->itemCount);
}
void cutsceneSystemLoad(const char_t *file) {
assertNotNull(file, "File cannot be NULL");
+26
View File
@@ -109,6 +109,32 @@ void cutsceneSystemStartCutsceneAndGoToMarker(
const char_t *marker
);
/**
* Splices items into the running cutscene so that items[0] becomes the
* very next item, without otherwise disturbing the currently-running
* item's progress.
*
* @param items Items to splice in.
* @param itemCount Number of items in items.
*/
void cutsceneSystemInsertItemsNext(
const cutsceneitem_t *items,
const size_t itemCount
);
/**
* Splices items into the running cutscene right after the current item
* (see cutsceneSystemInsertItemsNext), then immediately advances into the
* first spliced item.
*
* @param items Items to splice in.
* @param itemCount Number of items in items.
*/
void cutsceneSystemInsertItems(
const cutsceneitem_t *items,
const size_t itemCount
);
/**
* Splices cutscene's items into the running cutscene.
*
@@ -16,6 +16,7 @@ add_subdirectory(entity)
add_subdirectory(item)
add_subdirectory(maparea)
add_subdirectory(ui)
add_subdirectory(battle)
add_subdirectory(save)
add_subdirectory(audio)
add_subdirectory(json)
@@ -0,0 +1,11 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
cutscenebattlestart.c
cutscenebattlesetstate.c
cutscenebattlewaitselection.c
)
@@ -0,0 +1,41 @@
/**
* 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 "rpg/battle/battle.h"
#include "rpg/cutscene/item/json/cutscenejsonbattlestate.h"
#include "assert/assert.h"
void cutsceneBattleSetStateStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
assertNotNull(item, "item cannot be NULL");
battleSetState(item->battleSetState.state);
}
bool_t cutsceneBattleSetStateUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
return true;
}
errorret_t cutsceneBattleSetStateLoad(
yyjson_val *itemObj,
cutsceneitem_t *item
) {
battlestate_t state;
if(!cutsceneJsonLookupBattleState(
yyjson_get_str(yyjson_obj_get(itemObj, "state")), &state
)) {
errorThrow("Cutscene item (BATTLE_SET_STATE): invalid 'state'");
}
item->battleSetState.state = state;
errorOk();
}
@@ -0,0 +1,50 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "rpg/battle/state/battlestate.h"
#include "rpg/cutscene/item/cutsceneitembase.h"
typedef struct {
battlestate_t state;
} cutscenebattlesetstate_t;
/**
* Starts a BATTLE_SET_STATE item, transitioning the battle to the
* configured state immediately.
*
* @param item The cutscene item.
* @param data Runtime data storage.
*/
void cutsceneBattleSetStateStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
/**
* Updates a BATTLE_SET_STATE item (always completes immediately).
*
* @param item The cutscene item.
* @param data Runtime data storage.
* @returns true always.
*/
bool_t cutsceneBattleSetStateUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
/**
* Parses a BATTLE_SET_STATE cutscene item's JSON fields into item.
*
* @param itemObj The item's JSON object.
* @param item Destination item, already zeroed by the caller.
* @return Error code indicating success or failure of the parse.
*/
errorret_t cutsceneBattleSetStateLoad(
yyjson_val *itemObj,
cutsceneitem_t *item
);
@@ -0,0 +1,144 @@
/**
* 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 "rpg/battle/battle.h"
#include "rpg/battle/party.h"
#include "assert/assert.h"
#include "util/memory.h"
battlefighter_t CUTSCENE_BATTLE_START_ENEMY_POOL[
CUTSCENE_BATTLE_START_ENEMY_COUNT_MAX
];
void cutsceneBattleStartStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
assertNotNull(item, "item cannot be NULL");
const cutscenebattlestart_t *config = &item->battleStart;
assertTrue(
config->enemyCount <= CUTSCENE_BATTLE_START_ENEMY_COUNT_MAX,
"enemyCount out of bounds"
);
battlefighter_t *fighters[BATTLE_FIGHTER_POS_COUNT] = { 0 };
memoryZero(
CUTSCENE_BATTLE_START_ENEMY_POOL,
sizeof(CUTSCENE_BATTLE_START_ENEMY_POOL)
);
for(uint8_t i = 0; i < config->enemyCount; i++) {
const cutscenebattlestartenemy_t *enemyConfig = &config->enemies[i];
battlefighter_t *enemy = &CUTSCENE_BATTLE_START_ENEMY_POOL[i];
battleFighterInit(
enemy, BATTLE_FIGHTER_TEAM_ENEMY, BATTLE_FIGHTER_CONTROLLER_AI,
enemyConfig->stats
);
for(uint8_t a = 0; a < enemyConfig->abilityCount; a++) {
battleFighterAbilityAdd(enemy, enemyConfig->abilities[a]);
}
fighters[i] = enemy;
}
for(uint8_t i = 0; i < PARTY_ACTIVE_SIZE_MAX; i++) {
battlefighter_t *member = partyGetOrderMember(i);
if(member == NULL) continue;
fighters[BATTLE_FIGHTER_POS_4 + i] = member;
}
battleStart(fighters);
}
bool_t cutsceneBattleStartUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
return true;
}
errorret_t cutsceneBattleStartLoad(
yyjson_val *itemObj,
cutsceneitem_t *item
) {
yyjson_val *enemies = yyjson_obj_get(itemObj, "enemies");
size_t enemyCount = yyjson_arr_size(enemies);
if(
!yyjson_is_arr(enemies) || enemyCount == 0 ||
enemyCount > CUTSCENE_BATTLE_START_ENEMY_COUNT_MAX
) {
errorThrow(
"Cutscene item (BATTLE_START): 'enemies' must have 1-%u entries",
(uint32_t)CUTSCENE_BATTLE_START_ENEMY_COUNT_MAX
);
}
item->battleStart.enemyCount = (uint8_t)enemyCount;
yyjson_arr_iter iter = yyjson_arr_iter_with(enemies);
yyjson_val *enemyVal;
size_t e = 0;
while((enemyVal = yyjson_arr_iter_next(&iter)) != NULL) {
cutscenebattlestartenemy_t *enemy = &item->battleStart.enemies[e];
yyjson_val *stats = yyjson_obj_get(enemyVal, "stats");
if(!yyjson_is_obj(stats)) {
errorThrow(
"Cutscene item (BATTLE_START): enemies[%u] is missing 'stats'",
(uint32_t)e
);
}
enemy->stats.attack = (uint16_t)yyjson_get_int(yyjson_obj_get(stats, "attack"));
enemy->stats.defense = (uint16_t)yyjson_get_int(yyjson_obj_get(stats, "defense"));
enemy->stats.magic = (uint16_t)yyjson_get_int(yyjson_obj_get(stats, "magic"));
enemy->stats.speed = (uint16_t)yyjson_get_int(yyjson_obj_get(stats, "speed"));
enemy->stats.luck = (uint16_t)yyjson_get_int(yyjson_obj_get(stats, "luck"));
enemy->stats.healthMax = (uint16_t)yyjson_get_int(yyjson_obj_get(stats, "healthMax"));
enemy->stats.mpMax = (uint16_t)yyjson_get_int(yyjson_obj_get(stats, "mpMax"));
yyjson_val *abilities = yyjson_obj_get(enemyVal, "abilities");
if(abilities != NULL) {
size_t abilityCount = yyjson_arr_size(abilities);
if(
!yyjson_is_arr(abilities) ||
abilityCount > BATTLE_FIGHTER_ABILITY_COUNT_MAX
) {
errorThrow(
"Cutscene item (BATTLE_START): enemies[%u].abilities must have "
"0-%u entries",
(uint32_t)e, (uint32_t)BATTLE_FIGHTER_ABILITY_COUNT_MAX
);
}
yyjson_arr_iter abilityIter = yyjson_arr_iter_with(abilities);
yyjson_val *abilityVal;
size_t a = 0;
while((abilityVal = yyjson_arr_iter_next(&abilityIter)) != NULL) {
const char_t *name = yyjson_get_str(abilityVal);
battlemoveid_t ability = name == NULL
? BATTLE_MOVE_ID_NULL : battleMoveGetIdByName(name);
if(ability == BATTLE_MOVE_ID_NULL) {
errorThrow(
"Cutscene item (BATTLE_START): enemies[%u].abilities[%u] is "
"invalid",
(uint32_t)e, (uint32_t)a
);
}
enemy->abilities[a] = ability;
a++;
}
enemy->abilityCount = (uint8_t)abilityCount;
}
e++;
}
errorOk();
}
@@ -0,0 +1,62 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "rpg/battle/fighter/battlefighter.h"
#include "rpg/cutscene/item/cutsceneitembase.h"
// Enemies always occupy the left-side positions (BATTLE_FIGHTER_POS_0-3).
#define CUTSCENE_BATTLE_START_ENEMY_COUNT_MAX 4
typedef struct {
battlefighterstats_t stats;
battlemoveid_t abilities[BATTLE_FIGHTER_ABILITY_COUNT_MAX];
uint8_t abilityCount;
} cutscenebattlestartenemy_t;
typedef struct {
cutscenebattlestartenemy_t enemies[CUTSCENE_BATTLE_START_ENEMY_COUNT_MAX];
uint8_t enemyCount;
} cutscenebattlestart_t;
/**
* Starts a BATTLE_START item: spawns the configured enemies, fills the
* remaining positions with the active party (via partyGetOrderMember), and
* calls battleStart. Always completes immediately - the battle scene takes
* over from here.
*
* @param item The cutscene item.
* @param data Runtime data storage.
*/
void cutsceneBattleStartStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
/**
* Updates a BATTLE_START item (always completes immediately).
*
* @param item The cutscene item.
* @param data Runtime data storage.
* @returns true always.
*/
bool_t cutsceneBattleStartUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
/**
* Parses a BATTLE_START cutscene item's JSON fields into item.
*
* @param itemObj The item's JSON object.
* @param item Destination item, already zeroed by the caller.
* @return Error code indicating success or failure of the parse.
*/
errorret_t cutsceneBattleStartLoad(
yyjson_val *itemObj,
cutsceneitem_t *item
);
@@ -0,0 +1,16 @@
/**
* 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 "rpg/battle/battle.h"
bool_t cutsceneBattleWaitSelectionUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
return BATTLE.state != BATTLE_STATE_SELECTION;
}
@@ -0,0 +1,25 @@
/**
* 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"
/**
* 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.
*
* @param item The cutscene item.
* @param data Runtime data storage.
* @returns true once BATTLE.state is no longer BATTLE_STATE_SELECTION.
*/
bool_t cutsceneBattleWaitSelectionUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
+21
View File
@@ -244,6 +244,27 @@ cutsceneitemcallbacks_t CUTSCENE_ITEM_CALLBACKS[CUTSCENE_ITEM_TYPE_COUNT] = {
.load = cutsceneMapAreaWaitLoad
},
// Battle
[CUTSCENE_ITEM_TYPE_BATTLE_START] = {
.init = cutsceneBattleStartStart,
.update = cutsceneBattleStartUpdate,
.jsonName = "BATTLE_START",
.load = cutsceneBattleStartLoad
},
[CUTSCENE_ITEM_TYPE_BATTLE_SET_STATE] = {
.init = cutsceneBattleSetStateStart,
.update = cutsceneBattleSetStateUpdate,
.jsonName = "BATTLE_SET_STATE",
.load = cutsceneBattleSetStateLoad
},
[CUTSCENE_ITEM_TYPE_BATTLE_WAIT_SELECTION] = {
.update = cutsceneBattleWaitSelectionUpdate,
.jsonName = "BATTLE_WAIT_SELECTION",
.load = cutsceneItemLoadNoop
},
// Save
[CUTSCENE_ITEM_TYPE_SAVE_DEVICE_CHECK] = {
.init = cutsceneSaveDeviceCheckStart,
@@ -40,6 +40,9 @@
#include "maparea/cutscenemapareaadd.h"
#include "maparea/cutscenemaparearemove.h"
#include "maparea/cutscenemapareawait.h"
#include "battle/cutscenebattlestart.h"
#include "battle/cutscenebattlesetstate.h"
#include "battle/cutscenebattlewaitselection.h"
#include "save/cutscenesavedevicecheck.h"
#include "save/cutscenesaveloadallslots.h"
#include "audio/cutsceneaudioplay.h"
@@ -112,6 +115,8 @@ struct cutsceneitem_s {
cutscenemapareaadd_t mapAreaAdd;
cutscenemaparearemove_t mapAreaRemove;
cutscenemapareawait_t mapAreaWait;
cutscenebattlestart_t battleStart;
cutscenebattlesetstate_t battleSetState;
cutsceneemoji_t emoji;
cutsceneshake_t shake;
cutscenemodal_t modal;
@@ -59,6 +59,11 @@ typedef enum {
CUTSCENE_ITEM_TYPE_MAP_AREA_REMOVE,
CUTSCENE_ITEM_TYPE_MAP_AREA_WAIT,
// Battle
CUTSCENE_ITEM_TYPE_BATTLE_START,
CUTSCENE_ITEM_TYPE_BATTLE_SET_STATE,
CUTSCENE_ITEM_TYPE_BATTLE_WAIT_SELECTION,
// Save
CUTSCENE_ITEM_TYPE_SAVE_DEVICE_CHECK,
CUTSCENE_ITEM_TYPE_SAVE_LOAD_ALL_SLOTS,
@@ -14,6 +14,7 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
cutscenejsonemoji.c
cutscenejsonentitytype.c
cutscenejsonentitydir.c
cutscenejsonbattlestate.c
cutscenejsonscenetype.c
cutscenejsonuiscreentype.c
cutscenejsonaudiochannel.c
@@ -0,0 +1,38 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "cutscenejsonbattlestate.h"
#include "cutscenejsonenum.h"
static const cutscenejsonenumentry_t CUTSCENE_JSON_BATTLE_STATE_TABLE[] = {
{ "OPENING", BATTLE_STATE_OPENING },
{ "PRE_ROUND", BATTLE_STATE_PRE_ROUND },
{ "SELECTION", BATTLE_STATE_SELECTION },
{ "EXECUTING", BATTLE_STATE_EXECUTING },
{ "POST_ROUND", BATTLE_STATE_POST_ROUND },
{ "LOST", BATTLE_STATE_LOST },
{ "WON", BATTLE_STATE_WON },
{ "FLED", BATTLE_STATE_FLED },
{ "ENDING", BATTLE_STATE_ENDING },
};
#define CUTSCENE_JSON_BATTLE_STATE_TABLE_COUNT \
(sizeof(CUTSCENE_JSON_BATTLE_STATE_TABLE) / sizeof(cutscenejsonenumentry_t))
bool_t cutsceneJsonLookupBattleState(
const char_t *name,
battlestate_t *out
) {
int32_t value;
if(!cutsceneJsonLookupEnumInsensitive(
CUTSCENE_JSON_BATTLE_STATE_TABLE,
CUTSCENE_JSON_BATTLE_STATE_TABLE_COUNT, name, &value
)) {
return false;
}
*out = (battlestate_t)value;
return true;
}
@@ -0,0 +1,21 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "rpg/battle/state/battlestate.h"
/**
* Looks up name (case-insensitive) against every battlestate_t name.
*
* @param name Name to search for.
* @param out Set to the matching state on success.
* @return true if a matching entry was found, false otherwise.
*/
bool_t cutsceneJsonLookupBattleState(
const char_t *name,
battlestate_t *out
);
+14 -2
View File
@@ -6,6 +6,7 @@
*/
#include "scenebattle.h"
#include "assert/assert.h"
#include "display/display.h"
#include "display/displaystate.h"
#include "display/shader/shader.h"
@@ -80,6 +81,10 @@ void sceneBattleWorldToScreen(
vec3 renderPos,
vec2 out
) {
assertNotNull(battle, "battle cannot be NULL");
assertNotNull(renderPos, "renderPos cannot be NULL");
assertNotNull(out, "out cannot be NULL");
mat4 viewProj;
glm_mat4_mul(battle->projection, battle->eye, viewProj);
@@ -94,15 +99,20 @@ void sceneBattleWorldToScreen(
}
void sceneBattleGetFighterPosition(const uint8_t fighterIndex, vec3 out) {
assertTrue(fighterIndex < BATTLE_FIGHTER_POS_COUNT, "fighterIndex out of bounds");
const battlefighter_t *fighter = BATTLE.fighters[fighterIndex];
assertNotNull(fighter, "Fighter cannot be NULL");
assertNotNull(out, "out cannot be NULL");
// Ordinal (and count) of this fighter among its living-slot teammates,
// in slot order, so each team lays out as an evenly spaced row.
uint8_t ordinal = 0;
uint8_t teamCount = 0;
for(uint8_t i = 0; i < BATTLE_FIGHTER_POS_COUNT; i++) {
if(BATTLE.fighters[i]->status == BATTLE_FIGHTER_STATUS_NULL) continue;
if(BATTLE.fighters[i]->team != fighter->team) continue;
const battlefighter_t *candidate = BATTLE.fighters[i];
if(candidate == NULL) continue;
if(candidate->status == BATTLE_FIGHTER_STATUS_NULL) continue;
if(candidate->team != fighter->team) continue;
if(i < fighterIndex) ordinal++;
teamCount++;
}
@@ -119,7 +129,9 @@ void sceneBattleGetFighterPosition(const uint8_t fighterIndex, vec3 out) {
}
errorret_t sceneBattleDrawFighter(const uint8_t fighterIndex) {
assertTrue(fighterIndex < BATTLE_FIGHTER_POS_COUNT, "fighterIndex out of bounds");
const battlefighter_t *fighter = BATTLE.fighters[fighterIndex];
if(fighter == NULL) errorOk();
if(fighter->status == BATTLE_FIGHTER_STATUS_NULL) errorOk();
vec3 position;
+2 -1
View File
@@ -42,12 +42,13 @@ errorret_t uiBattleHudInit(void) {
}
errorret_t uiBattleHudDraw(void) {
if(BATTLE.state == BATTLE_STATE_NONE) errorOk();
if(BATTLE.state == BATTLE_STATE_NULL) errorOk();
scenebattle_t *battle = &SCENE.data.battle;
for(uint8_t i = 0; i < BATTLE_FIGHTER_POS_COUNT; i++) {
const battlefighter_t *fighter = BATTLE.fighters[i];
if(fighter == NULL) continue;
if(fighter->status == BATTLE_FIGHTER_STATUS_NULL) continue;
vec3 position;
+6 -4
View File
@@ -22,13 +22,14 @@ void uiBattleMenuTargetSelected(
const uint8_t index,
const uimenuitem_t *item
) {
battlePlayerAttack(UI_BATTLE_MENU.targetFighterIndex[index]);
assertTrue(index < UI_BATTLE_MENU_TARGET_ITEM_COUNT, "index out of bounds");
battleStateSelectionPlayerAttack(UI_BATTLE_MENU.targetFighterIndex[index]);
uiMenuClose(&UI_BATTLE_MENU.targetMenu);
uiMenuClose(&UI_BATTLE_MENU.actionMenu);
}
void uiBattleMenuOpenTargets(void) {
battlefighter_t *current = battleGetCurrentFighter();
battlefighter_t *current = battleStateSelectionGetCurrentFighter();
if(current == NULL) return;
const battlefighterteam_t enemyTeam =
@@ -42,6 +43,7 @@ void uiBattleMenuOpenTargets(void) {
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;
@@ -71,7 +73,7 @@ void uiBattleMenuActionSelected(
break;
case UI_BATTLE_MENU_ACTION_INDEX_FLEE:
battlePlayerFlee();
battleStateSelectionPlayerFlee();
uiMenuClose(&UI_BATTLE_MENU.actionMenu);
break;
@@ -116,7 +118,7 @@ errorret_t uiBattleMenuInit(void) {
errorret_t uiBattleMenuUpdate(void) {
if(uiMenuIsActive(&UI_BATTLE_MENU.actionMenu)) errorOk();
battlefighter_t *current = battleGetCurrentFighter();
battlefighter_t *current = battleStateSelectionGetCurrentFighter();
if(current == NULL) errorOk();
if(current->controller != BATTLE_FIGHTER_CONTROLLER_PLAYER) errorOk();