Merge branch 'battle-refactor'
This commit is contained in:
@@ -1,11 +1,21 @@
|
||||
{
|
||||
"moves": {
|
||||
"fire": {
|
||||
"ability": "FIRE",
|
||||
"power": 12,
|
||||
"mpCost": 4,
|
||||
"target": "SINGLE_ENEMY",
|
||||
"cutscene": "cutscenes/battle/fire.jsonc"
|
||||
// Target flags
|
||||
"enemies": true,
|
||||
"allies": true,
|
||||
"multiple": false,
|
||||
"self": true,
|
||||
"default": "enemy",
|
||||
"target": "SINGLE_ENEMY"
|
||||
},
|
||||
|
||||
"slash": {
|
||||
"enemies": true,
|
||||
"allies": true,
|
||||
"self": false,
|
||||
"default": "enemy",
|
||||
"target": "SINGLE_ENEMY"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"type": "MODAL",
|
||||
"title": "Fire",
|
||||
"message": "A burst of flame scorches the target."
|
||||
},
|
||||
{
|
||||
"type": "WAIT",
|
||||
"seconds": 2
|
||||
},
|
||||
{
|
||||
"type": "MODAL_CLOSE"
|
||||
},
|
||||
{
|
||||
"type": "BATTLE_FIGHTER_DAMAGE",
|
||||
"fighter": "target",
|
||||
"amount": 12
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"type": "MODAL",
|
||||
"title": "Slash",
|
||||
"message": "A quick blade strike cuts across the target."
|
||||
},
|
||||
{
|
||||
"type": "WAIT",
|
||||
"seconds": 2
|
||||
},
|
||||
{
|
||||
"type": "MODAL_CLOSE"
|
||||
},
|
||||
{
|
||||
"type": "BATTLE_FIGHTER_DAMAGE",
|
||||
"fighter": "target",
|
||||
"amount": 8
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -266,6 +266,10 @@ msgstr "loading"
|
||||
msgid "ui.autosave.saving"
|
||||
msgstr "SAVING"
|
||||
|
||||
|
||||
#
|
||||
# Items
|
||||
#
|
||||
msgid "item.potion.name"
|
||||
msgstr "Potion"
|
||||
|
||||
@@ -274,3 +278,12 @@ msgstr "Potato"
|
||||
|
||||
msgid "item.apple.name"
|
||||
msgstr "Apple"
|
||||
|
||||
#
|
||||
# Moves
|
||||
#
|
||||
msgid "move.fire.name"
|
||||
msgstr "Fire"
|
||||
|
||||
msgid "move.slash.name"
|
||||
msgstr "Slash"
|
||||
@@ -12,13 +12,16 @@
|
||||
// "DSK2" - distinct from a plain zip's "PK\x03\x04" so a stray plain zip
|
||||
// never gets misread as a valid dusk.dsk.
|
||||
#define ASSET_DSK_MAGIC_SIZE 4
|
||||
#define ASSET_DSK_MAGIC "DSK2"
|
||||
#define ASSET_DSK_MAGIC "DSK"
|
||||
#define ASSET_DSK_VERSION 2
|
||||
|
||||
// magic(4) + version(4) + size(4) + checksum(4), all little-endian
|
||||
// regardless of host - see assetDskParseHeader. The zip archive itself
|
||||
// starts immediately after, at ASSET_DSK_HEADER_SIZE.
|
||||
#define ASSET_DSK_HEADER_SIZE 16
|
||||
// Magic + version + size + checksum
|
||||
#define ASSET_DSK_HEADER_SIZE ( \
|
||||
ASSET_DSK_MAGIC_SIZE + \
|
||||
sizeof(uint32_t) + \
|
||||
sizeof(uint32_t) + \
|
||||
sizeof(uint32_t) \
|
||||
)
|
||||
|
||||
/**
|
||||
* Parsed dusk.dsk (DSK2 format) header. dusk.dsk is a single uncompressed
|
||||
|
||||
@@ -7,6 +7,9 @@
|
||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
PUBLIC
|
||||
battle.c
|
||||
battlefighter.c
|
||||
party.c
|
||||
)
|
||||
|
||||
add_subdirectory(fighter)
|
||||
add_subdirectory(move)
|
||||
add_subdirectory(state)
|
||||
|
||||
+41
-290
@@ -9,320 +9,71 @@
|
||||
#include "assert/assert.h"
|
||||
#include "util/memory.h"
|
||||
#include "rpg/cutscene/cutscenesystem.h"
|
||||
#include "scene/scene.h"
|
||||
|
||||
battle_t BATTLE;
|
||||
|
||||
void battleInit(void) {
|
||||
memoryZero(&BATTLE, sizeof(battle_t));
|
||||
for(uint8_t i = 0; i < BATTLE_FIGHTER_COUNT_MAX; i++) {
|
||||
BATTLE.fighters[i].id = i;
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t battleGetAvailableFighter(void) {
|
||||
for(uint8_t i = 0; i < BATTLE_FIGHTER_COUNT_MAX; i++) {
|
||||
if(BATTLE.fighters[i].status == BATTLE_FIGHTER_STATUS_NULL) return i;
|
||||
}
|
||||
|
||||
return 0xFF;
|
||||
}
|
||||
|
||||
battlefighter_t *battleAddFighter(
|
||||
const battlefighterteam_t team,
|
||||
const battlefightercontroller_t controller,
|
||||
const battlefighterstats_t stats,
|
||||
const uint16_t healthMax,
|
||||
const uint16_t mpMax
|
||||
) {
|
||||
const uint8_t index = battleGetAvailableFighter();
|
||||
if(index == 0xFF) return NULL;
|
||||
|
||||
battlefighter_t *fighter = &BATTLE.fighters[index];
|
||||
battleFighterInit(fighter, team, controller, stats, healthMax, mpMax);
|
||||
return fighter;
|
||||
}
|
||||
|
||||
void battleStart(
|
||||
const battleencountertype_t encounterType,
|
||||
const bool_t fleeAvailable
|
||||
battlefighter_t *fighters[BATTLE_FIGHTER_POS_COUNT]
|
||||
) {
|
||||
assertTrue(encounterType < BATTLE_ENCOUNTER_COUNT, "Invalid encounter type");
|
||||
assertNotNull(fighters, "Fighters cannot be NULL");
|
||||
|
||||
BATTLE.encounterType = encounterType;
|
||||
BATTLE.fleeAvailable = fleeAvailable;
|
||||
BATTLE.result = BATTLE_RESULT_NONE;
|
||||
BATTLE.round = 1;
|
||||
|
||||
battleSetState(BATTLE_STATE_OPENING);
|
||||
}
|
||||
|
||||
void battleDispose(void) {
|
||||
battleInit();
|
||||
}
|
||||
|
||||
battlefighter_t *battleGetCurrentFighter(void) {
|
||||
if(BATTLE.state != BATTLE_STATE_PLAYER_SELECTION) return NULL;
|
||||
if(BATTLE.selectionIndex >= BATTLE.executionCount) return NULL;
|
||||
return &BATTLE.fighters[BATTLE.executionOrder[BATTLE.selectionIndex]];
|
||||
}
|
||||
|
||||
uint8_t battleGetAliveCount(const battlefighterteam_t team) {
|
||||
uint8_t count = 0;
|
||||
for(uint8_t i = 0; i < BATTLE_FIGHTER_COUNT_MAX; i++) {
|
||||
if(BATTLE.fighters[i].team != team) continue;
|
||||
if(!battleFighterIsAlive(&BATTLE.fighters[i])) continue;
|
||||
count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
void battleResolveAttack(
|
||||
battlefighter_t *attacker,
|
||||
battlefighter_t *defender
|
||||
) {
|
||||
assertNotNull(attacker, "Attacker cannot be NULL");
|
||||
assertNotNull(defender, "Defender cannot be NULL");
|
||||
|
||||
const int32_t rawDamage =
|
||||
(int32_t)attacker->stats.attack - (int32_t)defender->stats.defense;
|
||||
const uint16_t damage = rawDamage > 0 ? (uint16_t)rawDamage : 1;
|
||||
|
||||
defender->health = damage >= defender->health ? 0 : defender->health - damage;
|
||||
if(defender->health == 0) defender->status = BATTLE_FIGHTER_STATUS_DEAD;
|
||||
}
|
||||
|
||||
battleresult_t battleCheckResult(void) {
|
||||
if(BATTLE.result != BATTLE_RESULT_NONE) return BATTLE.result;
|
||||
|
||||
if(battleGetAliveCount(BATTLE_FIGHTER_TEAM_ALLY) == 0) {
|
||||
battleSetResult(BATTLE_RESULT_LOSS);
|
||||
} else if(battleGetAliveCount(BATTLE_FIGHTER_TEAM_ENEMY) == 0) {
|
||||
battleSetResult(BATTLE_RESULT_WIN);
|
||||
}
|
||||
|
||||
return BATTLE.result;
|
||||
}
|
||||
|
||||
void battleQueueAction(
|
||||
const uint8_t fighterIndex,
|
||||
const battleactiontype_t type,
|
||||
const uint8_t targetIndex
|
||||
) {
|
||||
battleaction_t *action = &BATTLE.actions[fighterIndex];
|
||||
action->type = type;
|
||||
action->targetIndex = targetIndex;
|
||||
|
||||
if(BATTLE.onActionDecided != NULL) {
|
||||
BATTLE.onActionDecided(&BATTLE.fighters[fighterIndex], action);
|
||||
}
|
||||
}
|
||||
|
||||
void battlePlayerAttack(const uint8_t targetIndex) {
|
||||
battlefighter_t *fighter = battleGetCurrentFighter();
|
||||
if(fighter == NULL) return;
|
||||
if(targetIndex >= BATTLE_FIGHTER_COUNT_MAX) return;
|
||||
if(!battleFighterIsAlive(&BATTLE.fighters[targetIndex])) return;
|
||||
|
||||
battleQueueAction(fighter->id, BATTLE_ACTION_ATTACK, targetIndex);
|
||||
BATTLE.selectionIndex++;
|
||||
battleAdvanceSelection();
|
||||
}
|
||||
|
||||
void battlePlayerFlee(void) {
|
||||
battlefighter_t *fighter = battleGetCurrentFighter();
|
||||
if(fighter == NULL) return;
|
||||
if(!BATTLE.fleeAvailable) return;
|
||||
|
||||
battleSetResult(BATTLE_RESULT_FLED);
|
||||
}
|
||||
|
||||
void battleUpdate(void) {
|
||||
if(BATTLE.state == BATTLE_STATE_NONE) return;
|
||||
if(BATTLE.state == BATTLE_STATE_ENDED) return;
|
||||
if(CUTSCENE_SYSTEM.pause & CUTSCENE_PAUSE_BATTLE) return;
|
||||
|
||||
switch(BATTLE.state) {
|
||||
case BATTLE_STATE_OPENING:
|
||||
battleSetState(BATTLE_STATE_PRE_ROUND);
|
||||
break;
|
||||
|
||||
case BATTLE_STATE_PRE_ROUND:
|
||||
battleUpdatePreRound();
|
||||
break;
|
||||
|
||||
case BATTLE_STATE_AI_SELECTION:
|
||||
battleUpdateAiSelection();
|
||||
break;
|
||||
|
||||
case BATTLE_STATE_MOVES_EXECUTING:
|
||||
battleUpdateMovesExecuting();
|
||||
break;
|
||||
|
||||
case BATTLE_STATE_POST_ROUND:
|
||||
battleUpdatePostRound();
|
||||
break;
|
||||
|
||||
default:
|
||||
// BATTLE_STATE_PLAYER_SELECTION: waits on battlePlayerAttack/Flee.
|
||||
// BATTLE_STATE_NONE/ENDED: handled above.
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void battleBuildExecutionOrder(const bool_t applyEncounterBias) {
|
||||
BATTLE.executionCount = 0;
|
||||
for(uint8_t i = 0; i < BATTLE_FIGHTER_COUNT_MAX; i++) {
|
||||
if(!battleFighterIsAlive(&BATTLE.fighters[i])) continue;
|
||||
BATTLE.executionOrder[BATTLE.executionCount++] = i;
|
||||
}
|
||||
|
||||
// Insertion sort by speed descending -- fine for BATTLE_FIGHTER_COUNT_MAX.
|
||||
for(uint8_t i = 1; i < BATTLE.executionCount; i++) {
|
||||
const uint8_t key = BATTLE.executionOrder[i];
|
||||
const uint16_t keySpeed = BATTLE.fighters[key].stats.speed;
|
||||
|
||||
int8_t j = (int8_t)i - 1;
|
||||
while(
|
||||
j >= 0 &&
|
||||
BATTLE.fighters[BATTLE.executionOrder[j]].stats.speed < keySpeed
|
||||
) {
|
||||
BATTLE.executionOrder[j + 1] = BATTLE.executionOrder[j];
|
||||
j--;
|
||||
#if DUSK_ASSERTIONS
|
||||
// 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++;
|
||||
}
|
||||
BATTLE.executionOrder[j + 1] = key;
|
||||
}
|
||||
assertTrue(nonNullCount >= 2, "Must have at least two fighters");
|
||||
#endif
|
||||
|
||||
if(!applyEncounterBias) return;
|
||||
|
||||
if(BATTLE.encounterType == BATTLE_ENCOUNTER_PLAYER_ADVANTAGE) {
|
||||
battleMoveTeamFirst(BATTLE_FIGHTER_TEAM_ALLY);
|
||||
} else if(BATTLE.encounterType == BATTLE_ENCOUNTER_BACK_ATTACK) {
|
||||
battleMoveTeamFirst(BATTLE_FIGHTER_TEAM_ENEMY);
|
||||
}
|
||||
}
|
||||
|
||||
void battleMoveTeamFirst(const battlefighterteam_t team) {
|
||||
uint8_t sorted[BATTLE_FIGHTER_COUNT_MAX];
|
||||
uint8_t count = 0;
|
||||
|
||||
for(uint8_t i = 0; i < BATTLE.executionCount; i++) {
|
||||
if(BATTLE.fighters[BATTLE.executionOrder[i]].team != team) continue;
|
||||
sorted[count++] = BATTLE.executionOrder[i];
|
||||
}
|
||||
for(uint8_t i = 0; i < BATTLE.executionCount; i++) {
|
||||
if(BATTLE.fighters[BATTLE.executionOrder[i]].team == team) continue;
|
||||
sorted[count++] = BATTLE.executionOrder[i];
|
||||
}
|
||||
|
||||
// TODO: I may not need to zero entire battle.
|
||||
memoryZero(&BATTLE, sizeof(battle_t));
|
||||
memoryCopy(
|
||||
BATTLE.executionOrder, sorted, sizeof(uint8_t) * BATTLE.executionCount
|
||||
BATTLE.fighters,
|
||||
fighters,
|
||||
sizeof(battlefighter_t *) * BATTLE_FIGHTER_POS_COUNT
|
||||
);
|
||||
}
|
||||
|
||||
battlefighter_t *battleAIChooseTarget(const battlefighter_t *fighter) {
|
||||
const battlefighterteam_t enemyTeam =
|
||||
fighter->team == BATTLE_FIGHTER_TEAM_ALLY ?
|
||||
BATTLE_FIGHTER_TEAM_ENEMY : BATTLE_FIGHTER_TEAM_ALLY;
|
||||
// Enter the opening state.
|
||||
battleSetState(BATTLE_STATE_OPENING);
|
||||
|
||||
battlefighter_t *weakest = NULL;
|
||||
for(uint8_t i = 0; i < BATTLE_FIGHTER_COUNT_MAX; i++) {
|
||||
battlefighter_t *candidate = &BATTLE.fighters[i];
|
||||
if(candidate->team != enemyTeam) continue;
|
||||
if(!battleFighterIsAlive(candidate)) continue;
|
||||
if(weakest == NULL || candidate->health < weakest->health) {
|
||||
weakest = candidate;
|
||||
}
|
||||
}
|
||||
|
||||
return weakest;
|
||||
// Change scene
|
||||
sceneSet(SCENE_TYPE_BATTLE);
|
||||
}
|
||||
|
||||
void battleSetState(const battlestate_t next) {
|
||||
const battlestate_t previous = BATTLE.state;
|
||||
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;
|
||||
if(BATTLE.onStateChanged != NULL) BATTLE.onStateChanged(previous, next);
|
||||
BATTLE_STATE_FUNCTIONS[BATTLE.state].init();
|
||||
}
|
||||
|
||||
void battleSetResult(const battleresult_t result) {
|
||||
BATTLE.result = result;
|
||||
battleSetState(BATTLE_STATE_ENDED);
|
||||
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();
|
||||
}
|
||||
|
||||
bool_t battleFighterNeedsDecision(
|
||||
const uint8_t fighterIndex,
|
||||
const battlefightercontroller_t controller
|
||||
) {
|
||||
return battleFighterIsAlive(&BATTLE.fighters[fighterIndex])
|
||||
&& BATTLE.fighters[fighterIndex].controller == controller
|
||||
&& BATTLE.actions[fighterIndex].type == BATTLE_ACTION_NONE;
|
||||
}
|
||||
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");
|
||||
|
||||
void battleAdvanceSelection(void) {
|
||||
while(BATTLE.selectionIndex < BATTLE.executionCount) {
|
||||
const uint8_t fighterIndex = BATTLE.executionOrder[BATTLE.selectionIndex];
|
||||
if(
|
||||
battleFighterNeedsDecision(fighterIndex, BATTLE_FIGHTER_CONTROLLER_PLAYER)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
BATTLE.selectionIndex++;
|
||||
}
|
||||
|
||||
battleSetState(BATTLE_STATE_AI_SELECTION);
|
||||
}
|
||||
|
||||
void battleUpdatePreRound(void) {
|
||||
// No need to clear BATTLE.actions here: every living fighter's action is
|
||||
// unconditionally reset to BATTLE_ACTION_NONE as it's processed in
|
||||
// battleUpdateMovesExecuting, and round 1 starts pre-zeroed by
|
||||
// battleInit(). Clearing it here would also wipe out any action a
|
||||
// cutscene force-queued while parked at BATTLE_STATE_PRE_ROUND.
|
||||
battleBuildExecutionOrder(BATTLE.round == 1);
|
||||
|
||||
BATTLE.selectionIndex = 0;
|
||||
battleSetState(BATTLE_STATE_PLAYER_SELECTION);
|
||||
battleAdvanceSelection();
|
||||
}
|
||||
|
||||
void battleUpdateAiSelection(void) {
|
||||
for(uint8_t i = 0; i < BATTLE.executionCount; i++) {
|
||||
const uint8_t fighterIndex = BATTLE.executionOrder[i];
|
||||
if(
|
||||
!battleFighterNeedsDecision(fighterIndex, BATTLE_FIGHTER_CONTROLLER_AI)
|
||||
) continue;
|
||||
|
||||
battlefighter_t *target =
|
||||
battleAIChooseTarget(&BATTLE.fighters[fighterIndex]);
|
||||
if(target == NULL) continue;
|
||||
|
||||
battleQueueAction(fighterIndex, BATTLE_ACTION_ATTACK, target->id);
|
||||
}
|
||||
|
||||
BATTLE.executionIndex = 0;
|
||||
battleSetState(BATTLE_STATE_MOVES_EXECUTING);
|
||||
}
|
||||
|
||||
void battleUpdateMovesExecuting(void) {
|
||||
if(BATTLE.executionIndex >= BATTLE.executionCount) {
|
||||
battleSetState(BATTLE_STATE_POST_ROUND);
|
||||
return;
|
||||
}
|
||||
|
||||
const uint8_t fighterIndex = BATTLE.executionOrder[BATTLE.executionIndex++];
|
||||
battlefighter_t *fighter = &BATTLE.fighters[fighterIndex];
|
||||
if(!battleFighterIsAlive(fighter)) return;
|
||||
|
||||
battleaction_t *action = &BATTLE.actions[fighterIndex];
|
||||
if(action->type == BATTLE_ACTION_ATTACK) {
|
||||
battlefighter_t *target = &BATTLE.fighters[action->targetIndex];
|
||||
if(battleFighterIsAlive(target)) battleResolveAttack(fighter, target);
|
||||
}
|
||||
action->type = BATTLE_ACTION_NONE;
|
||||
|
||||
battleCheckResult();
|
||||
}
|
||||
|
||||
void battleUpdatePostRound(void) {
|
||||
BATTLE.round++;
|
||||
battleSetState(BATTLE_STATE_PRE_ROUND);
|
||||
BATTLE_STATE_FUNCTIONS[BATTLE.state].dispose();
|
||||
memoryZero(&BATTLE, sizeof(battle_t));
|
||||
}
|
||||
+28
-246
@@ -6,69 +6,38 @@
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "battlefighter.h"
|
||||
#include "battleaction.h"
|
||||
|
||||
#define BATTLE_FIGHTER_COUNT_MAX 8
|
||||
|
||||
typedef enum {
|
||||
BATTLE_ENCOUNTER_REGULAR,
|
||||
BATTLE_ENCOUNTER_PLAYER_ADVANTAGE,
|
||||
BATTLE_ENCOUNTER_BACK_ATTACK,
|
||||
|
||||
BATTLE_ENCOUNTER_COUNT
|
||||
} battleencountertype_t;
|
||||
#include "rpg/battle/fighter/battlefighter.h"
|
||||
#include "rpg/battle/move/battlemove.h"
|
||||
#include "rpg/battle/move/battlemoveaction.h"
|
||||
#include "rpg/battle/fighter/battlefighterpos.h"
|
||||
#include "rpg/battle/state/battlestate.h"
|
||||
|
||||
typedef enum {
|
||||
BATTLE_RESULT_NONE,
|
||||
BATTLE_RESULT_WIN,
|
||||
BATTLE_RESULT_LOSS,
|
||||
BATTLE_RESULT_FLED,
|
||||
|
||||
BATTLE_RESULT_COUNT
|
||||
} battleresult_t;
|
||||
|
||||
typedef enum {
|
||||
BATTLE_STATE_NONE,
|
||||
|
||||
BATTLE_STATE_OPENING,
|
||||
BATTLE_STATE_PRE_ROUND,
|
||||
BATTLE_STATE_PLAYER_SELECTION,
|
||||
BATTLE_STATE_AI_SELECTION,
|
||||
BATTLE_STATE_MOVES_EXECUTING,
|
||||
BATTLE_STATE_POST_ROUND,
|
||||
|
||||
BATTLE_STATE_ENDED,
|
||||
|
||||
BATTLE_STATE_COUNT
|
||||
} battlestate_t;
|
||||
|
||||
typedef void (*battlestatechangedcallback_t)(
|
||||
const battlestate_t previous,
|
||||
const battlestate_t next
|
||||
);
|
||||
|
||||
typedef struct {
|
||||
battlefighter_t *fighters[BATTLE_FIGHTER_POS_COUNT];
|
||||
|
||||
// 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];
|
||||
uint8_t actionsPendingCount;
|
||||
|
||||
battlestate_t state;
|
||||
battlefighter_t fighters[BATTLE_FIGHTER_COUNT_MAX];
|
||||
battleaction_t actions[BATTLE_FIGHTER_COUNT_MAX];
|
||||
battlestatedata_t stateData;
|
||||
|
||||
battleencountertype_t encounterType;
|
||||
bool_t fleeAvailable;
|
||||
// Set once a side has been wiped out (see cutsceneBattlePostMoveStart)
|
||||
// or the player has fled (see battleStateSelectionPlayerFlee). Stays
|
||||
// BATTLE_RESULT_NONE for the rest of the battle otherwise.
|
||||
battleresult_t result;
|
||||
|
||||
// Fighter indices (into fighters[]), sorted for the current round.
|
||||
uint8_t executionOrder[BATTLE_FIGHTER_COUNT_MAX];
|
||||
uint8_t executionCount;
|
||||
uint8_t executionIndex;
|
||||
uint16_t round;
|
||||
|
||||
// Cursor into executionOrder used by BATTLE_STATE_PLAYER_SELECTION to find
|
||||
// the next player-controlled fighter that still needs a decision.
|
||||
uint8_t selectionIndex;
|
||||
|
||||
battlestatechangedcallback_t onStateChanged;
|
||||
battleactiondecidedcallback_t onActionDecided;
|
||||
} battle_t;
|
||||
|
||||
extern battle_t BATTLE;
|
||||
@@ -79,215 +48,28 @@ extern battle_t BATTLE;
|
||||
void battleInit(void);
|
||||
|
||||
/**
|
||||
* Gets an available (unused) fighter slot index.
|
||||
* Starts a battle.
|
||||
*
|
||||
* @return The index of an available fighter slot, or 0xFF if none are
|
||||
* available.
|
||||
*/
|
||||
uint8_t battleGetAvailableFighter(void);
|
||||
|
||||
/**
|
||||
* Adds a fighter to the battle in the next available slot.
|
||||
*
|
||||
* @param team The team the fighter belongs to.
|
||||
* @param controller Who makes decisions for the fighter.
|
||||
* @param stats The fighter's base combat stats.
|
||||
* @param healthMax The fighter's maximum health.
|
||||
* @param mpMax The fighter's maximum mp.
|
||||
* @return Pointer to the newly added fighter, or NULL if the battle is
|
||||
* already full.
|
||||
*/
|
||||
battlefighter_t *battleAddFighter(
|
||||
const battlefighterteam_t team,
|
||||
const battlefightercontroller_t controller,
|
||||
const battlefighterstats_t stats,
|
||||
const uint16_t healthMax,
|
||||
const uint16_t mpMax
|
||||
);
|
||||
|
||||
/**
|
||||
* Starts the battle: enters BATTLE_STATE_OPENING and marks the battle
|
||||
* active. Call once every fighter has been added via battleAddFighter.
|
||||
*
|
||||
* @param encounterType Determines the opening round's execution order.
|
||||
* @param fleeAvailable Whether the party may attempt to flee this battle.
|
||||
* @param fighters Fighters in the battle.
|
||||
*/
|
||||
void battleStart(
|
||||
const battleencountertype_t encounterType,
|
||||
const bool_t fleeAvailable
|
||||
battlefighter_t *fighters[BATTLE_FIGHTER_POS_COUNT]
|
||||
);
|
||||
|
||||
/**
|
||||
* Disposes of the battle, clearing all fighters and marking it inactive.
|
||||
*/
|
||||
void battleDispose(void);
|
||||
|
||||
/**
|
||||
* Returns the fighter currently awaiting a player decision.
|
||||
*
|
||||
* @return Pointer to the fighter awaiting a decision, or NULL if the battle
|
||||
* isn't in BATTLE_STATE_PLAYER_SELECTION or every player-controlled
|
||||
* fighter has already decided.
|
||||
*/
|
||||
battlefighter_t *battleGetCurrentFighter(void);
|
||||
|
||||
/**
|
||||
* Returns the number of living fighters on a team.
|
||||
*
|
||||
* @param team The team to count.
|
||||
* @return Count of living fighters on that team.
|
||||
*/
|
||||
uint8_t battleGetAliveCount(const battlefighterteam_t team);
|
||||
|
||||
/**
|
||||
* Resolves a physical attack from attacker onto defender: damage is the
|
||||
* attacker's attack stat minus the defender's defense stat (minimum 1),
|
||||
* subtracted from the defender's health. The defender is marked dead
|
||||
* once health reaches 0.
|
||||
*
|
||||
* @param attacker The attacking fighter.
|
||||
* @param defender The defending fighter.
|
||||
*/
|
||||
void battleResolveAttack(
|
||||
battlefighter_t *attacker,
|
||||
battlefighter_t *defender
|
||||
);
|
||||
|
||||
/**
|
||||
* Checks whether the battle has been won or lost, transitioning to
|
||||
* BATTLE_STATE_ENDED and updating BATTLE.result if so. Does nothing if a
|
||||
* result has already been set (e.g. by a successful flee).
|
||||
*
|
||||
* @return The battle's current result.
|
||||
*/
|
||||
battleresult_t battleCheckResult(void);
|
||||
|
||||
/**
|
||||
* Queues an action for a fighter to perform once BATTLE_STATE_MOVES_EXECUTING
|
||||
* reaches them this round, overwriting any action already queued for that
|
||||
* fighter. Fires BATTLE.onActionDecided.
|
||||
*
|
||||
* @param fighterIndex Index into BATTLE.fighters of the deciding fighter.
|
||||
* @param type The type of action to perform.
|
||||
* @param targetIndex Index into BATTLE.fighters of the target, meaningful
|
||||
* for BATTLE_ACTION_ATTACK.
|
||||
*/
|
||||
void battleQueueAction(
|
||||
const uint8_t fighterIndex,
|
||||
const battleactiontype_t type,
|
||||
const uint8_t targetIndex
|
||||
);
|
||||
|
||||
/**
|
||||
* Submits the currently-selecting fighter's attack against a target, if the
|
||||
* battle is in BATTLE_STATE_PLAYER_SELECTION and awaiting a decision.
|
||||
* Queues the action and advances the selection cursor.
|
||||
*
|
||||
* @param targetIndex Index into BATTLE.fighters of the target.
|
||||
*/
|
||||
void battlePlayerAttack(const uint8_t targetIndex);
|
||||
|
||||
/**
|
||||
* Submits a flee attempt for the currently-selecting fighter, if the battle
|
||||
* is in BATTLE_STATE_PLAYER_SELECTION and fleeing is available for this
|
||||
* battle. Always succeeds, ending the battle with BATTLE_RESULT_FLED.
|
||||
*/
|
||||
void battlePlayerFlee(void);
|
||||
|
||||
/**
|
||||
* Updates the battle simulation for one frame, dispatching on BATTLE.state.
|
||||
* No-op if the battle isn't active, has already ended, or
|
||||
* CUTSCENE_PAUSE_BATTLE is set.
|
||||
*/
|
||||
void battleUpdate(void);
|
||||
|
||||
/**
|
||||
* Rebuilds BATTLE.executionOrder/executionCount from every currently living
|
||||
* fighter, sorted by speed descending.
|
||||
*
|
||||
* @param applyEncounterBias If true, reorders the freshly speed-sorted
|
||||
* queue so BATTLE.encounterType's favoured team goes first (used only
|
||||
* for the opening round).
|
||||
*/
|
||||
void battleBuildExecutionOrder(const bool_t applyEncounterBias);
|
||||
|
||||
/**
|
||||
* Stably partitions BATTLE.executionOrder so every fighter on the given team
|
||||
* comes first, preserving each side's relative (speed-sorted) order.
|
||||
*
|
||||
* @param team The team to move to the front of the execution order.
|
||||
*/
|
||||
void battleMoveTeamFirst(const battlefighterteam_t team);
|
||||
|
||||
/**
|
||||
* Picks an AI target for fighter: the lowest-health living fighter on
|
||||
* the opposing team.
|
||||
*
|
||||
* @param fighter The AI-controlled fighter choosing a target.
|
||||
* @return The chosen target, or NULL if the opposing team has no
|
||||
* living fighters.
|
||||
*/
|
||||
battlefighter_t *battleAIChooseTarget(const battlefighter_t *fighter);
|
||||
|
||||
/**
|
||||
* Sets BATTLE.state and fires BATTLE.onStateChanged with the previous and
|
||||
* new state.
|
||||
* 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);
|
||||
|
||||
/**
|
||||
* Sets BATTLE.result and transitions to BATTLE_STATE_ENDED.
|
||||
*
|
||||
* @param result The result to end the battle with.
|
||||
* Updates the battle system.
|
||||
*/
|
||||
void battleSetResult(const battleresult_t result);
|
||||
void battleUpdate(void);
|
||||
|
||||
/**
|
||||
* Checks whether a fighter is a live, undecided candidate for the given
|
||||
* controller -- i.e. whether PLAYER_SELECTION or AI_SELECTION should still
|
||||
* be deciding an action for it this round.
|
||||
*
|
||||
* @param fighterIndex Index into BATTLE.fighters to check.
|
||||
* @param controller The controller PLAYER_SELECTION/AI_SELECTION is
|
||||
* currently deciding for.
|
||||
* @return True if the fighter is alive, matches controller, and has no
|
||||
* action queued yet.
|
||||
* Disposes of the battle, clearing all fighters and marking it inactive.
|
||||
*/
|
||||
bool_t battleFighterNeedsDecision(
|
||||
const uint8_t fighterIndex,
|
||||
const battlefightercontroller_t controller
|
||||
);
|
||||
|
||||
/**
|
||||
* Advances BATTLE.selectionIndex to the next player-controlled fighter that
|
||||
* still needs a decision, or transitions to BATTLE_STATE_AI_SELECTION once
|
||||
* none remain.
|
||||
*/
|
||||
void battleAdvanceSelection(void);
|
||||
|
||||
/**
|
||||
* Handles BATTLE_STATE_PRE_ROUND: rebuilds the execution order and moves on
|
||||
* to BATTLE_STATE_PLAYER_SELECTION, positioning the selection cursor.
|
||||
*/
|
||||
void battleUpdatePreRound(void);
|
||||
|
||||
/**
|
||||
* Handles BATTLE_STATE_AI_SELECTION: queues an attack for every undecided
|
||||
* AI-controlled fighter, then moves on to BATTLE_STATE_MOVES_EXECUTING.
|
||||
*/
|
||||
void battleUpdateAiSelection(void);
|
||||
|
||||
/**
|
||||
* Handles BATTLE_STATE_MOVES_EXECUTING: resolves one queued action from
|
||||
* BATTLE.executionOrder per call, or transitions to BATTLE_STATE_POST_ROUND
|
||||
* once every fighter this round has been processed.
|
||||
*/
|
||||
void battleUpdateMovesExecuting(void);
|
||||
|
||||
/**
|
||||
* Handles BATTLE_STATE_POST_ROUND: advances BATTLE.round and transitions
|
||||
* back to BATTLE_STATE_PRE_ROUND.
|
||||
*/
|
||||
void battleUpdatePostRound(void);
|
||||
void battleDispose(void);
|
||||
@@ -1,27 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "error/error.h"
|
||||
#include "battlefighter.h"
|
||||
|
||||
typedef enum {
|
||||
BATTLE_ACTION_NONE, // No action decided yet for this fighter this round.
|
||||
BATTLE_ACTION_ATTACK,
|
||||
|
||||
BATTLE_ACTION_COUNT
|
||||
} battleactiontype_t;
|
||||
|
||||
typedef struct {
|
||||
battleactiontype_t type;
|
||||
uint8_t targetIndex; // Meaningful for BATTLE_ACTION_ATTACK.
|
||||
} battleaction_t;
|
||||
|
||||
typedef void (*battleactiondecidedcallback_t)(
|
||||
const battlefighter_t *fighter,
|
||||
const battleaction_t *action
|
||||
);
|
||||
@@ -1,43 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "battlefighter.h"
|
||||
#include "assert/assert.h"
|
||||
#include "util/memory.h"
|
||||
|
||||
void battleFighterInit(
|
||||
battlefighter_t *fighter,
|
||||
const battlefighterteam_t team,
|
||||
const battlefightercontroller_t controller,
|
||||
const battlefighterstats_t stats,
|
||||
const uint16_t healthMax,
|
||||
const uint16_t mpMax
|
||||
) {
|
||||
assertNotNull(fighter, "Fighter pointer cannot be NULL");
|
||||
assertTrue(team < BATTLE_FIGHTER_TEAM_COUNT, "Invalid fighter team");
|
||||
assertTrue(
|
||||
controller < BATTLE_FIGHTER_CONTROLLER_COUNT,
|
||||
"Invalid fighter controller"
|
||||
);
|
||||
|
||||
const uint8_t id = fighter->id;
|
||||
memoryZero(fighter, sizeof(battlefighter_t));
|
||||
fighter->id = id;
|
||||
fighter->status = BATTLE_FIGHTER_STATUS_NORMAL;
|
||||
fighter->team = team;
|
||||
fighter->controller = controller;
|
||||
fighter->stats = stats;
|
||||
fighter->healthMax = healthMax;
|
||||
fighter->health = healthMax;
|
||||
fighter->mpMax = mpMax;
|
||||
fighter->mp = mpMax;
|
||||
}
|
||||
|
||||
bool_t battleFighterIsAlive(const battlefighter_t *fighter) {
|
||||
assertNotNull(fighter, "Fighter pointer cannot be NULL");
|
||||
return fighter->status == BATTLE_FIGHTER_STATUS_NORMAL;
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "error/error.h"
|
||||
#include "battleability.h"
|
||||
|
||||
#define BATTLE_FIGHTER_ABILITY_COUNT_MAX 8
|
||||
|
||||
// An empty status means the slot in BATTLE.fighters is unused.
|
||||
typedef enum {
|
||||
BATTLE_FIGHTER_STATUS_NULL,
|
||||
|
||||
BATTLE_FIGHTER_STATUS_NORMAL,
|
||||
BATTLE_FIGHTER_STATUS_DEAD,
|
||||
|
||||
BATTLE_FIGHTER_STATUS_COUNT
|
||||
} battlefighterstatus_t;
|
||||
|
||||
typedef enum {
|
||||
BATTLE_FIGHTER_TEAM_ALLY,
|
||||
BATTLE_FIGHTER_TEAM_ENEMY,
|
||||
|
||||
BATTLE_FIGHTER_TEAM_COUNT
|
||||
} battlefighterteam_t;
|
||||
|
||||
typedef enum {
|
||||
BATTLE_FIGHTER_CONTROLLER_PLAYER,
|
||||
BATTLE_FIGHTER_CONTROLLER_AI,
|
||||
|
||||
BATTLE_FIGHTER_CONTROLLER_COUNT
|
||||
} battlefightercontroller_t;
|
||||
|
||||
// Base combat stats, kept separate from the resource pools (health/mp) on
|
||||
// battlefighter_t so that equipment/buffs can later modify them without
|
||||
// touching current health/mp state.
|
||||
typedef struct {
|
||||
uint16_t attack;
|
||||
uint16_t defense;
|
||||
uint16_t magic;
|
||||
uint16_t speed;
|
||||
uint16_t luck;
|
||||
} battlefighterstats_t;
|
||||
|
||||
typedef struct {
|
||||
uint8_t id;
|
||||
battlefighterstatus_t status;
|
||||
battlefighterteam_t team;
|
||||
battlefightercontroller_t controller;
|
||||
|
||||
uint16_t health;
|
||||
uint16_t healthMax;
|
||||
uint16_t mp;
|
||||
uint16_t mpMax;
|
||||
|
||||
battlefighterstats_t stats;
|
||||
battleability_t abilities[BATTLE_FIGHTER_ABILITY_COUNT_MAX];
|
||||
} battlefighter_t;
|
||||
|
||||
/**
|
||||
* Initializes a battle fighter in place, filling health/mp to their maximum
|
||||
* values and setting its status to normal.
|
||||
*
|
||||
* @param fighter Pointer to the fighter to initialize.
|
||||
* @param team The team the fighter belongs to.
|
||||
* @param controller Who makes decisions for the fighter.
|
||||
* @param stats The fighter's base combat stats.
|
||||
* @param healthMax The fighter's maximum health.
|
||||
* @param mpMax The fighter's maximum mp.
|
||||
*/
|
||||
void battleFighterInit(
|
||||
battlefighter_t *fighter,
|
||||
const battlefighterteam_t team,
|
||||
const battlefightercontroller_t controller,
|
||||
const battlefighterstats_t stats,
|
||||
const uint16_t healthMax,
|
||||
const uint16_t mpMax
|
||||
);
|
||||
|
||||
/**
|
||||
* Returns true if the fighter is in a state where it can still act (i.e.
|
||||
* is not dead).
|
||||
*
|
||||
* @param fighter Pointer to the fighter to check.
|
||||
* @returns True if the fighter can act.
|
||||
*/
|
||||
bool_t battleFighterIsAlive(const battlefighter_t *fighter);
|
||||
@@ -0,0 +1,10 @@
|
||||
# Copyright (c) 2026 Dominic Masters
|
||||
#
|
||||
# This software is released under the MIT License.
|
||||
# https://opensource.org/licenses/MIT
|
||||
|
||||
# Sources
|
||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
PUBLIC
|
||||
battlefighter.c
|
||||
)
|
||||
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "battlefighter.h"
|
||||
#include "assert/assert.h"
|
||||
#include "util/memory.h"
|
||||
|
||||
void battleFighterInit(
|
||||
battlefighter_t *fighter,
|
||||
const battlefighterteam_t team,
|
||||
const battlefightercontroller_t controller,
|
||||
const battlefighterstats_t stats
|
||||
) {
|
||||
assertNotNull(fighter, "Fighter pointer cannot be NULL");
|
||||
assertTrue(team < BATTLE_FIGHTER_TEAM_COUNT, "Invalid fighter team");
|
||||
assertTrue(
|
||||
controller < BATTLE_FIGHTER_CONTROLLER_COUNT,
|
||||
"Invalid fighter controller"
|
||||
);
|
||||
|
||||
const uint8_t id = fighter->id;
|
||||
memoryZero(fighter, sizeof(battlefighter_t));
|
||||
fighter->id = id;
|
||||
fighter->status = BATTLE_FIGHTER_STATUS_NORMAL;
|
||||
fighter->team = team;
|
||||
fighter->controller = controller;
|
||||
fighter->stats = stats;
|
||||
fighter->stats.health = stats.healthMax;
|
||||
fighter->stats.mp = stats.mpMax;
|
||||
}
|
||||
|
||||
bool_t battleFighterIsAlive(const battlefighter_t *fighter) {
|
||||
assertNotNull(fighter, "Fighter pointer cannot be NULL");
|
||||
return fighter->status == BATTLE_FIGHTER_STATUS_NORMAL;
|
||||
}
|
||||
|
||||
void battleFighterAbilityAdd(
|
||||
battlefighter_t *fighter,
|
||||
const battlemoveid_t ability
|
||||
) {
|
||||
assertNotNull(fighter, "Fighter pointer cannot be NULL");
|
||||
assertTrue(
|
||||
fighter->abilityCount < BATTLE_FIGHTER_ABILITY_COUNT_MAX,
|
||||
"Fighter already has the maximum number of abilities"
|
||||
);
|
||||
|
||||
fighter->abilities[fighter->abilityCount++] = ability;
|
||||
}
|
||||
|
||||
void battleFighterDamage(
|
||||
battlefighter_t *fighter,
|
||||
const uint16_t amount
|
||||
) {
|
||||
assertNotNull(fighter, "Fighter pointer cannot be NULL");
|
||||
|
||||
fighter->stats.health =
|
||||
amount >= fighter->stats.health ? 0 : fighter->stats.health - amount;
|
||||
if(fighter->stats.health == 0) fighter->status = BATTLE_FIGHTER_STATUS_DEAD;
|
||||
}
|
||||
|
||||
void battleFighterHeal(
|
||||
battlefighter_t *fighter,
|
||||
const uint16_t amount
|
||||
) {
|
||||
assertNotNull(fighter, "Fighter pointer cannot be NULL");
|
||||
if(!battleFighterIsAlive(fighter)) return;
|
||||
|
||||
const uint32_t healed = (uint32_t)fighter->stats.health + amount;
|
||||
fighter->stats.health = healed > fighter->stats.healthMax
|
||||
? fighter->stats.healthMax : (uint16_t)healed;
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "error/error.h"
|
||||
#include "rpg/battle/move/battlemove.h"
|
||||
#include "rpg/battle/fighter/battlefighterstatus.h"
|
||||
#include "rpg/battle/fighter/battlefighterteam.h"
|
||||
#include "rpg/battle/fighter/battlefightercontroller.h"
|
||||
#include "rpg/battle/fighter/battlefighterstats.h"
|
||||
#include "rpg/battle/fighter/battlefighterpos.h"
|
||||
|
||||
#define BATTLE_FIGHTER_ABILITY_COUNT_MAX 8
|
||||
|
||||
typedef struct battlefighter_s {
|
||||
uint8_t id;
|
||||
battlefighterstatus_t status;
|
||||
battlefighterteam_t team;
|
||||
battlefightercontroller_t controller;
|
||||
|
||||
|
||||
battlefighterstats_t stats;
|
||||
|
||||
battlemoveid_t abilities[BATTLE_FIGHTER_ABILITY_COUNT_MAX];
|
||||
battlemoveid_t abilityCount;
|
||||
} battlefighter_t;
|
||||
|
||||
/**
|
||||
* Initializes a battle fighter in place, filling health/mp to the maximum
|
||||
* values carried on stats and setting its status to normal.
|
||||
*
|
||||
* @param fighter Pointer to the fighter to initialize.
|
||||
* @param team The team the fighter belongs to.
|
||||
* @param controller Who makes decisions for the fighter.
|
||||
* @param stats The fighter's base combat stats, including healthMax/mpMax.
|
||||
*/
|
||||
void battleFighterInit(
|
||||
battlefighter_t *fighter,
|
||||
const battlefighterteam_t team,
|
||||
const battlefightercontroller_t controller,
|
||||
const battlefighterstats_t stats
|
||||
);
|
||||
|
||||
/**
|
||||
* Returns true if the fighter is in a state where it can still act (i.e.
|
||||
* is not dead).
|
||||
*
|
||||
* @param fighter Pointer to the fighter to check.
|
||||
* @returns True if the fighter can act.
|
||||
*/
|
||||
bool_t battleFighterIsAlive(const battlefighter_t *fighter);
|
||||
|
||||
/**
|
||||
* Appends an ability to the fighter's ability list.
|
||||
*
|
||||
* @param fighter Pointer to the fighter to add the ability to.
|
||||
* @param ability The ability to add.
|
||||
*/
|
||||
void battleFighterAbilityAdd(
|
||||
battlefighter_t *fighter,
|
||||
const battlemoveid_t ability
|
||||
);
|
||||
|
||||
/**
|
||||
* Subtracts amount from the fighter's health (floored at 0), marking it
|
||||
* dead once health reaches 0.
|
||||
*
|
||||
* @param fighter Pointer to the fighter to damage.
|
||||
* @param amount The amount of damage to apply.
|
||||
*/
|
||||
void battleFighterDamage(
|
||||
battlefighter_t *fighter,
|
||||
const uint16_t amount
|
||||
);
|
||||
|
||||
/**
|
||||
* Adds amount to the fighter's health (capped at healthMax). No-op if the
|
||||
* fighter isn't alive -- healing can't revive a dead fighter.
|
||||
*
|
||||
* @param fighter Pointer to the fighter to heal.
|
||||
* @param amount The amount to heal.
|
||||
*/
|
||||
void battleFighterHeal(
|
||||
battlefighter_t *fighter,
|
||||
const uint16_t amount
|
||||
);
|
||||
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "error/error.h"
|
||||
|
||||
typedef enum {
|
||||
BATTLE_FIGHTER_CONTROLLER_NONE,
|
||||
|
||||
BATTLE_FIGHTER_CONTROLLER_PLAYER,
|
||||
BATTLE_FIGHTER_CONTROLLER_AI,
|
||||
|
||||
BATTLE_FIGHTER_CONTROLLER_COUNT
|
||||
} battlefightercontroller_t;
|
||||
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "dusk.h"
|
||||
|
||||
typedef uint8_t battlefighterpos_t;
|
||||
|
||||
#define BATTLE_FIGHTER_POS(x) ((battlefighterpos_t)(x))
|
||||
|
||||
// Left (Enemy side)
|
||||
#define BATTLE_FIGHTER_POS_0 BATTLE_FIGHTER_POS(0)
|
||||
#define BATTLE_FIGHTER_POS_1 BATTLE_FIGHTER_POS(1)
|
||||
#define BATTLE_FIGHTER_POS_2 BATTLE_FIGHTER_POS(2)
|
||||
#define BATTLE_FIGHTER_POS_3 BATTLE_FIGHTER_POS(3)
|
||||
|
||||
// Right (Ally side)
|
||||
#define BATTLE_FIGHTER_POS_4 BATTLE_FIGHTER_POS(4)
|
||||
#define BATTLE_FIGHTER_POS_5 BATTLE_FIGHTER_POS(5)
|
||||
#define BATTLE_FIGHTER_POS_6 BATTLE_FIGHTER_POS(6)
|
||||
#define BATTLE_FIGHTER_POS_7 BATTLE_FIGHTER_POS(7)
|
||||
|
||||
#define BATTLE_FIGHTER_POS_COUNT (BATTLE_FIGHTER_POS_7 + 1)
|
||||
|
||||
// Sentinels resolved by battleStateExecutingInit against the currently
|
||||
// resolving action, for cutscene items authored inside a move's own
|
||||
// cutscene (e.g. cutscenes/battle/moves/slash.jsonc) that need to refer
|
||||
// to "whoever is being hit"/"whoever is attacking" without knowing their
|
||||
// concrete BATTLE.fighters index ahead of time.
|
||||
#define BATTLE_FIGHTER_POS_TARGET BATTLE_FIGHTER_POS(0xFE)
|
||||
#define BATTLE_FIGHTER_POS_USER BATTLE_FIGHTER_POS(0xFD)
|
||||
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "dusk.h"
|
||||
|
||||
typedef struct {
|
||||
uint16_t health;
|
||||
uint16_t mp;
|
||||
|
||||
uint16_t healthMax;
|
||||
uint16_t mpMax;
|
||||
uint16_t attack;
|
||||
uint16_t defense;
|
||||
uint16_t magic;
|
||||
uint16_t speed;
|
||||
uint16_t luck;
|
||||
} battlefighterstats_t;
|
||||
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "dusk.h"
|
||||
|
||||
typedef enum {
|
||||
BATTLE_FIGHTER_STATUS_NULL,
|
||||
|
||||
BATTLE_FIGHTER_STATUS_NORMAL,
|
||||
BATTLE_FIGHTER_STATUS_DEAD,
|
||||
|
||||
// Poisoned, burned, confused, etc.
|
||||
|
||||
BATTLE_FIGHTER_STATUS_COUNT
|
||||
} battlefighterstatus_t;
|
||||
+5
-6
@@ -6,12 +6,11 @@
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "error/error.h"
|
||||
#include "dusk.h"
|
||||
|
||||
typedef enum {
|
||||
BATTLE_ABILITY_NULL,
|
||||
BATTLE_FIGHTER_TEAM_ALLY,
|
||||
BATTLE_FIGHTER_TEAM_ENEMY,
|
||||
|
||||
BATTLE_ABILITY_FIRE,
|
||||
|
||||
BATTLE_ABILITY_COUNT
|
||||
} battleability_t;
|
||||
BATTLE_FIGHTER_TEAM_COUNT
|
||||
} battlefighterteam_t;
|
||||
@@ -0,0 +1,10 @@
|
||||
# Copyright (c) 2026 Dominic Masters
|
||||
#
|
||||
# This software is released under the MIT License.
|
||||
# https://opensource.org/licenses/MIT
|
||||
|
||||
# Sources
|
||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
PUBLIC
|
||||
battlemove.c
|
||||
)
|
||||
@@ -0,0 +1,139 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#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>
|
||||
|
||||
battleability_t BATTLE_MOVES[BATTLE_MOVE_COUNT_MAX];
|
||||
uint8_t BATTLE_MOVE_COUNT;
|
||||
|
||||
errorret_t battleMoveInit(void) {
|
||||
memoryZero(BATTLE_MOVES, sizeof(BATTLE_MOVES));
|
||||
BATTLE_MOVE_COUNT = BATTLE_MOVE_ID_NULL + 1;// Reserve NULL.
|
||||
|
||||
// Get moves JSON.
|
||||
assetentry_t *entry = assetLock(
|
||||
"battle/moves.jsonc", ASSET_LOADER_TYPE_JSON, NULL
|
||||
);
|
||||
errorret_t ret = assetRequireLoaded(entry);
|
||||
if(errorIsNotOk(ret)) {
|
||||
assetUnlockEntry(entry);
|
||||
errorChain(ret);
|
||||
}
|
||||
|
||||
yyjson_val *root = yyjson_doc_get_root(entry->data.json);
|
||||
yyjson_val *moves = yyjson_obj_get(root, "moves");
|
||||
yyjson_obj_iter iter = yyjson_obj_iter_with(moves);
|
||||
yyjson_val *key;
|
||||
while((key = yyjson_obj_iter_next(&iter))) {
|
||||
if(BATTLE_MOVE_COUNT >= BATTLE_MOVE_COUNT_MAX) {
|
||||
assetUnlockEntry(entry);
|
||||
errorThrow(
|
||||
"Battle move: too many moves defined (max %u)",
|
||||
(uint32_t)BATTLE_MOVE_COUNT_MAX
|
||||
);
|
||||
}
|
||||
|
||||
battlemoveid_t id = BATTLE_MOVE_COUNT++;
|
||||
battleability_t *move = &BATTLE_MOVES[id];
|
||||
|
||||
// Name
|
||||
if(!yyjson_is_str(key)) {
|
||||
assetUnlockEntry(entry);
|
||||
errorThrow("Battle move: keys in 'moves' must be strings");
|
||||
}
|
||||
const char_t *name = yyjson_get_str(key);
|
||||
stringCopy(move->name, name, BATTLE_MOVE_NAME_MAX);
|
||||
|
||||
if(strlen(name) >= BATTLE_MOVE_NAME_MAX) {
|
||||
assetUnlockEntry(entry);
|
||||
errorThrow(
|
||||
"Battle move '%s': name must be less than %u characters",
|
||||
name, (uint32_t)BATTLE_MOVE_NAME_MAX
|
||||
);
|
||||
}
|
||||
|
||||
// Must me object
|
||||
yyjson_val *val = yyjson_obj_iter_get_val(key);
|
||||
if(!yyjson_is_obj(val)) {
|
||||
assetUnlockEntry(entry);
|
||||
errorThrow("Battle move '%s': value must be an object", name);
|
||||
}
|
||||
|
||||
// Valid targets
|
||||
yyjson_val *enemies = yyjson_obj_get(val, "enemies");
|
||||
if(enemies != NULL && !yyjson_is_bool(enemies)) {
|
||||
assetUnlockEntry(entry);
|
||||
errorThrow("Battle move '%s': 'enemies' must be a boolean", name);
|
||||
}
|
||||
yyjson_val *allies = yyjson_obj_get(val, "allies");
|
||||
if(allies != NULL && !yyjson_is_bool(allies)) {
|
||||
assetUnlockEntry(entry);
|
||||
errorThrow("Battle move '%s': 'allies' must be a boolean", name);
|
||||
}
|
||||
yyjson_val *multiple = yyjson_obj_get(val, "multiple");
|
||||
if(multiple != NULL && !yyjson_is_bool(multiple)) {
|
||||
assetUnlockEntry(entry);
|
||||
errorThrow("Battle move '%s': 'multiple' must be a boolean", name);
|
||||
}
|
||||
yyjson_val *self = yyjson_obj_get(val, "self");
|
||||
if(self != NULL && !yyjson_is_bool(self)) {
|
||||
assetUnlockEntry(entry);
|
||||
errorThrow("Battle move '%s': 'self' must be a boolean", name);
|
||||
}
|
||||
|
||||
battletarget_t target = 0;
|
||||
if(yyjson_get_bool(enemies)) target |= BATTLE_TARGET_ENEMIES;
|
||||
if(yyjson_get_bool(allies)) target |= BATTLE_TARGET_ALLIES;
|
||||
if(yyjson_get_bool(multiple)) target |= BATTLE_TARGET_MULTIPLE;
|
||||
if(yyjson_get_bool(self)) target |= BATTLE_TARGET_SELF;
|
||||
move->target = target;
|
||||
if(move->target == 0) {
|
||||
assetUnlockEntry(entry);
|
||||
errorThrow("Battle move '%s': missing target type", name);
|
||||
}
|
||||
|
||||
// Default target (optional)
|
||||
yyjson_val *defaultVal = yyjson_obj_get(val, "default");
|
||||
if(defaultVal != NULL && !yyjson_is_str(defaultVal)) {
|
||||
assetUnlockEntry(entry);
|
||||
errorThrow("Battle move '%s': 'default' must be a string", name);
|
||||
}
|
||||
|
||||
const char_t *defaultTarget = yyjson_get_str(defaultVal);
|
||||
if(defaultTarget == NULL) {
|
||||
move->targetDefault = 0;
|
||||
} else if(stringCompareInsensitive(defaultTarget, "enemy") == 0) {
|
||||
move->targetDefault = BATTLE_TARGET_ENEMIES;
|
||||
} else if(stringCompareInsensitive(defaultTarget, "ally") == 0) {
|
||||
move->targetDefault = BATTLE_TARGET_ALLIES;
|
||||
} else if(stringCompareInsensitive(defaultTarget, "self") == 0) {
|
||||
move->targetDefault = BATTLE_TARGET_SELF;
|
||||
} else {
|
||||
assetUnlockEntry(entry);
|
||||
errorThrow("Battle move %s has invalid default target.", name);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "asset/asset.h"
|
||||
#include "battletarget.h"
|
||||
|
||||
typedef uint8_t battlemoveid_t;
|
||||
|
||||
#define BATTLE_MOVE_NAME_MAX 16
|
||||
#define BATTLE_MOVE_COUNT_MAX 250
|
||||
|
||||
#define BATTLE_MOVE_ID_NULL ((battlemoveid_t)0)
|
||||
|
||||
extern uint8_t BATTLE_MOVE_COUNT;
|
||||
|
||||
typedef struct {
|
||||
char_t name[BATTLE_MOVE_NAME_MAX];
|
||||
battletarget_t target;
|
||||
battletarget_t targetDefault;
|
||||
} battleability_t;
|
||||
|
||||
extern battleability_t BATTLE_MOVES[BATTLE_MOVE_COUNT_MAX];
|
||||
|
||||
/**
|
||||
* Initializes the battle move subsystem, loading assets/battle/moves.jsonc.
|
||||
*
|
||||
* @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);
|
||||
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "rpg/battle/move/battlemove.h"
|
||||
#include "rpg/battle/fighter/battlefighterpos.h"
|
||||
|
||||
typedef struct battlefighter_s battlefighter_t;
|
||||
|
||||
typedef struct {
|
||||
battlemoveid_t move;
|
||||
battlefighterpos_t targets[BATTLE_FIGHTER_POS_COUNT];
|
||||
battlefighter_t *user;
|
||||
} battlemoveaction_t;
|
||||
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "dusk.h"
|
||||
|
||||
typedef uint8_t battletarget_t;
|
||||
|
||||
#define BATTLE_TARGET_ENEMIES (1 << 0)
|
||||
#define BATTLE_TARGET_ALLIES (1 << 1)
|
||||
#define BATTLE_TARGET_MULTIPLE (1 << 2)
|
||||
#define BATTLE_TARGET_SELF (1 << 3)
|
||||
@@ -33,9 +33,7 @@ uint8_t partyGetAvailableMember(void) {
|
||||
}
|
||||
|
||||
battlefighter_t *partyAddMember(
|
||||
const battlefighterstats_t stats,
|
||||
const uint16_t healthMax,
|
||||
const uint16_t mpMax
|
||||
const battlefighterstats_t stats
|
||||
) {
|
||||
party_t *party = &SAVE.slot.party;
|
||||
|
||||
@@ -44,8 +42,7 @@ battlefighter_t *partyAddMember(
|
||||
|
||||
battlefighter_t *member = &party->members[index];
|
||||
battleFighterInit(
|
||||
member, BATTLE_FIGHTER_TEAM_ALLY, BATTLE_FIGHTER_CONTROLLER_PLAYER,
|
||||
stats, healthMax, mpMax
|
||||
member, BATTLE_FIGHTER_TEAM_ALLY, BATTLE_FIGHTER_CONTROLLER_PLAYER, stats
|
||||
);
|
||||
|
||||
for(uint8_t i = 0; i < PARTY_ACTIVE_SIZE_MAX; i++) {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "battlefighter.h"
|
||||
#include "rpg/battle/fighter/battlefighter.h"
|
||||
|
||||
#define PARTY_MEMBER_COUNT_MAX 4
|
||||
#define PARTY_ACTIVE_SIZE_MAX 3
|
||||
@@ -14,12 +14,8 @@
|
||||
|
||||
typedef struct {
|
||||
battlefighter_t members[PARTY_MEMBER_COUNT_MAX];
|
||||
|
||||
// Maps an active battle slot to the roster member filling it, or
|
||||
// PARTY_ORDER_EMPTY if the slot is unfilled. Only the first
|
||||
// PARTY_ACTIVE_SIZE_MAX of the PARTY_MEMBER_COUNT_MAX roster members
|
||||
// can be in the active lineup at once.
|
||||
uint8_t order[PARTY_ACTIVE_SIZE_MAX];
|
||||
// TODO: Whether party members are "front" or "back".
|
||||
} party_t;
|
||||
|
||||
/**
|
||||
@@ -42,16 +38,12 @@ uint8_t partyGetAvailableMember(void);
|
||||
* player. If there is a free active order slot, the new member is placed
|
||||
* into it.
|
||||
*
|
||||
* @param stats The member's base combat stats.
|
||||
* @param healthMax The member's maximum health.
|
||||
* @param mpMax The member's maximum mp.
|
||||
* @param stats The member's base combat stats, including healthMax/mpMax.
|
||||
* @return Pointer to the newly added party member, or NULL if the party is
|
||||
* already full.
|
||||
*/
|
||||
battlefighter_t *partyAddMember(
|
||||
const battlefighterstats_t stats,
|
||||
const uint16_t healthMax,
|
||||
const uint16_t mpMax
|
||||
const battlefighterstats_t stats
|
||||
);
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
# Copyright (c) 2026 Dominic Masters
|
||||
#
|
||||
# This software is released under the MIT License.
|
||||
# https://opensource.org/licenses/MIT
|
||||
|
||||
# Sources
|
||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
PUBLIC
|
||||
battlestate.c
|
||||
battlestateopening.c
|
||||
battlestatepreround.c
|
||||
battlestateselection.c
|
||||
battlestateexecuting.c
|
||||
battlestatepostround.c
|
||||
battlestatelost.c
|
||||
battlestatewon.c
|
||||
battlestatefled.c
|
||||
battlestateending.c
|
||||
)
|
||||
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "battlestate.h"
|
||||
|
||||
battlestatefunctions_t BATTLE_STATE_FUNCTIONS[BATTLE_STATE_COUNT] = {
|
||||
[BATTLE_STATE_NULL] = { 0 },
|
||||
|
||||
[BATTLE_STATE_OPENING] = {
|
||||
.init = battleStateOpeningInit,
|
||||
.update = battleStateOpeningUpdate,
|
||||
.dispose = battleStateOpeningDispose
|
||||
},
|
||||
|
||||
[BATTLE_STATE_PRE_ROUND] = {
|
||||
.init = battleStatePreRoundInit,
|
||||
.update = battleStatePreRoundUpdate,
|
||||
.dispose = battleStatePreRoundDispose
|
||||
},
|
||||
|
||||
[BATTLE_STATE_SELECTION] = {
|
||||
.init = battleStateSelectionInit,
|
||||
.update = battleStateSelectionUpdate,
|
||||
.dispose = battleStateSelectionDispose
|
||||
},
|
||||
|
||||
[BATTLE_STATE_EXECUTING] = {
|
||||
.init = battleStateExecutingInit,
|
||||
.update = battleStateExecutingUpdate,
|
||||
.dispose = battleStateExecutingDispose
|
||||
},
|
||||
|
||||
[BATTLE_STATE_POST_ROUND] = {
|
||||
.init = battleStatePostRoundInit,
|
||||
.update = battleStatePostRoundUpdate,
|
||||
.dispose = battleStatePostRoundDispose
|
||||
},
|
||||
|
||||
[BATTLE_STATE_LOST] = {
|
||||
.init = battleStateLostInit,
|
||||
.update = battleStateLostUpdate,
|
||||
.dispose = battleStateLostDispose
|
||||
},
|
||||
|
||||
[BATTLE_STATE_WON] = {
|
||||
.init = battleStateWonInit,
|
||||
.update = battleStateWonUpdate,
|
||||
.dispose = battleStateWonDispose
|
||||
},
|
||||
|
||||
[BATTLE_STATE_FLED] = {
|
||||
.init = battleStateFledInit,
|
||||
.update = battleStateFledUpdate,
|
||||
.dispose = battleStateFledDispose
|
||||
},
|
||||
|
||||
[BATTLE_STATE_ENDING] = {
|
||||
.init = battleStateEndingInit,
|
||||
.update = battleStateEndingUpdate,
|
||||
.dispose = battleStateEndingDispose
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "battlestateopening.h"
|
||||
#include "battlestatepreround.h"
|
||||
#include "battlestateselection.h"
|
||||
#include "battlestateexecuting.h"
|
||||
#include "battlestatepostround.h"
|
||||
#include "battlestatelost.h"
|
||||
#include "battlestatewon.h"
|
||||
#include "battlestatefled.h"
|
||||
#include "battlestateending.h"
|
||||
|
||||
typedef enum {
|
||||
// No active battle.
|
||||
BATTLE_STATE_NULL,
|
||||
|
||||
// Battle hasn't actually started yet, equiv to trainer intro in Pokemon.
|
||||
BATTLE_STATE_OPENING,
|
||||
// Pre round, anything that happens before AI or Player can act.
|
||||
BATTLE_STATE_PRE_ROUND,
|
||||
// Player is deciding what to do, waiting for their input
|
||||
BATTLE_STATE_SELECTION,
|
||||
// Currently executing the moves.
|
||||
BATTLE_STATE_EXECUTING,
|
||||
// Post round, anything that happens after AI or Player has acted, loops back
|
||||
// after this point
|
||||
BATTLE_STATE_POST_ROUND,
|
||||
// Battle has ended with a player loss
|
||||
BATTLE_STATE_LOST,
|
||||
// Battle has ended with a player win
|
||||
BATTLE_STATE_WON,
|
||||
// Battle has ended with a player fleeing
|
||||
BATTLE_STATE_FLED,
|
||||
// Battle state is done, this is fade out or whatever.
|
||||
BATTLE_STATE_ENDING,
|
||||
|
||||
BATTLE_STATE_COUNT
|
||||
} battlestate_t;
|
||||
|
||||
typedef union {
|
||||
battlestateopening_t opening;
|
||||
battlestatepreround_t preRound;
|
||||
battlestateselection_t selection;
|
||||
battlestateexecuting_t executing;
|
||||
battlestatepostround_t postRound;
|
||||
battlestatelost_t lost;
|
||||
battlestatewon_t won;
|
||||
battlestatefled_t fled;
|
||||
battlestateending_t ending;
|
||||
} battlestatedata_t;
|
||||
|
||||
typedef struct {
|
||||
void (*init)();
|
||||
void (*update)();
|
||||
void (*dispose)();
|
||||
} battlestatefunctions_t;
|
||||
|
||||
extern battlestatefunctions_t BATTLE_STATE_FUNCTIONS[BATTLE_STATE_COUNT];
|
||||
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "battlestateending.h"
|
||||
#include "rpg/battle/battle.h"
|
||||
#include "scene/scene.h"
|
||||
|
||||
void battleStateEndingInit(void) {
|
||||
// Tear down the battle and hand control back to the overworld. The
|
||||
// overworld scene keeps no state of its own in scenedata_t (player/map
|
||||
// state lives in the separate global entity/map systems, untouched by
|
||||
// switching to/from SCENE_TYPE_BATTLE), so there's nothing else to
|
||||
// restore here.
|
||||
battleDispose();
|
||||
sceneSet(SCENE_TYPE_OVERWORLD);
|
||||
}
|
||||
|
||||
void battleStateEndingUpdate(void) {
|
||||
|
||||
}
|
||||
|
||||
void battleStateEndingDispose(void) {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "dusk.h"
|
||||
|
||||
typedef struct {
|
||||
|
||||
} battlestateending_t;
|
||||
|
||||
/**
|
||||
* Called when the battle enters BATTLE_STATE_ENDING. Disposes the battle
|
||||
* (see battleDispose) and switches back to SCENE_TYPE_OVERWORLD.
|
||||
*/
|
||||
void battleStateEndingInit(void);
|
||||
|
||||
/**
|
||||
* Updates BATTLE_STATE_ENDING for one frame.
|
||||
*/
|
||||
void battleStateEndingUpdate(void);
|
||||
|
||||
/**
|
||||
* Called when the battle leaves BATTLE_STATE_ENDING.
|
||||
*/
|
||||
void battleStateEndingDispose(void);
|
||||
@@ -0,0 +1,116 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#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/assetfile.h"
|
||||
#include "ui/overlay/uifatalerror.h"
|
||||
#include "assert/assert.h"
|
||||
#include "util/string.h"
|
||||
|
||||
|
||||
void battleStateExecutingResolveFighterIndex(
|
||||
battlefighterpos_t *fighterIndex,
|
||||
const uint8_t userIndex,
|
||||
const battlemoveaction_t *action
|
||||
) {
|
||||
if(*fighterIndex == BATTLE_FIGHTER_POS_TARGET) {
|
||||
*fighterIndex = action->targets[0];
|
||||
} else if(*fighterIndex == BATTLE_FIGHTER_POS_USER) {
|
||||
*fighterIndex = userIndex;
|
||||
}
|
||||
}
|
||||
|
||||
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 and parse the move's cutscene straight into the tail of items.
|
||||
const uint8_t moveItemsStart = itemsCount;
|
||||
cutscene_t scene;
|
||||
errorret_t result = cutsceneLoadParse(
|
||||
path, &scene, &items[itemsCount],
|
||||
BATTLE_STATE_EXECUTING_ITEMS_MAX - itemsCount
|
||||
);
|
||||
if(errorIsNotOk(result)) {
|
||||
errorCatch(errorPrint(result));
|
||||
uiFatalErrorOpen(NULL);
|
||||
continue;
|
||||
}
|
||||
itemsCount += scene.itemCount;
|
||||
|
||||
// Resolve BATTLE_FIGHTER_POS_TARGET/USER sentinels against this
|
||||
// action, now that its user/target fighters are known.
|
||||
for(uint8_t j = moveItemsStart; j < itemsCount; j++) {
|
||||
cutsceneitem_t *moveItem = &items[j];
|
||||
if(moveItem->type == CUTSCENE_ITEM_TYPE_BATTLE_FIGHTER_DAMAGE) {
|
||||
battleStateExecutingResolveFighterIndex(
|
||||
&moveItem->battleFighterDamage.fighterIndex, i, action
|
||||
);
|
||||
} else if(moveItem->type == CUTSCENE_ITEM_TYPE_BATTLE_FIGHTER_HEAL) {
|
||||
battleStateExecutingResolveFighterIndex(
|
||||
&moveItem->battleFighterHeal.fighterIndex, i, action
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 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 battleStateExecutingDispose(void) {
|
||||
|
||||
}
|
||||
@@ -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 "dusk.h"
|
||||
#include "rpg/battle/fighter/battlefighterpos.h"
|
||||
#include "rpg/battle/move/battlemoveaction.h"
|
||||
|
||||
typedef struct {
|
||||
|
||||
} battlestateexecuting_t;
|
||||
|
||||
#define BATTLE_STATE_EXECUTING_ITEMS_MAX 64
|
||||
|
||||
/**
|
||||
* Called when the battle enters BATTLE_STATE_EXECUTING.
|
||||
*/
|
||||
void battleStateExecutingInit(void);
|
||||
|
||||
/**
|
||||
* Resolves a BATTLE_FIGHTER_POS_TARGET/BATTLE_FIGHTER_POS_USER sentinel
|
||||
* (as parsed onto a BATTLE_FIGHTER_DAMAGE/BATTLE_FIGHTER_HEAL item inside
|
||||
* a move's own cutscene) against the action currently being resolved
|
||||
* into cutscene items. Leaves fighterIndex untouched if it's already a
|
||||
* concrete BATTLE.fighters index.
|
||||
*
|
||||
* @param fighterIndex The field to resolve in place.
|
||||
* @param userIndex Index into BATTLE.fighters of the fighter performing
|
||||
* the action.
|
||||
* @param action The action being resolved.
|
||||
*/
|
||||
void battleStateExecutingResolveFighterIndex(
|
||||
battlefighterpos_t *fighterIndex,
|
||||
const uint8_t userIndex,
|
||||
const battlemoveaction_t *action
|
||||
);
|
||||
|
||||
/**
|
||||
* Updates BATTLE_STATE_EXECUTING for one frame.
|
||||
*/
|
||||
void battleStateExecutingUpdate(void);
|
||||
|
||||
/**
|
||||
* Called when the battle leaves BATTLE_STATE_EXECUTING.
|
||||
*/
|
||||
void battleStateExecutingDispose(void);
|
||||
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "battlestatefled.h"
|
||||
#include "rpg/cutscene/item/cutsceneitem.h"
|
||||
#include "rpg/cutscene/cutscenesystem.h"
|
||||
|
||||
void battleStateFledInit(void) {
|
||||
const cutsceneitem_t items[] = {
|
||||
{
|
||||
.type = CUTSCENE_ITEM_TYPE_TEXT,
|
||||
.text = { .text = "Got away safely!" }
|
||||
},
|
||||
{
|
||||
.type = CUTSCENE_ITEM_TYPE_BATTLE_SET_STATE,
|
||||
.battleSetState = { .state = BATTLE_STATE_ENDING }
|
||||
}
|
||||
};
|
||||
cutsceneSystemInsertItemsNext(items, sizeof(items) / sizeof(cutsceneitem_t));
|
||||
}
|
||||
|
||||
void battleStateFledUpdate(void) {
|
||||
|
||||
}
|
||||
|
||||
void battleStateFledDispose(void) {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "dusk.h"
|
||||
|
||||
typedef struct {
|
||||
|
||||
} battlestatefled_t;
|
||||
|
||||
/**
|
||||
* Called when the battle enters BATTLE_STATE_FLED. Queues a textbox
|
||||
* announcing the escape, then transitions to BATTLE_STATE_ENDING once
|
||||
* dismissed.
|
||||
*/
|
||||
void battleStateFledInit(void);
|
||||
|
||||
/**
|
||||
* Updates BATTLE_STATE_FLED for one frame.
|
||||
*/
|
||||
void battleStateFledUpdate(void);
|
||||
|
||||
/**
|
||||
* Called when the battle leaves BATTLE_STATE_FLED.
|
||||
*/
|
||||
void battleStateFledDispose(void);
|
||||
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "battlestatelost.h"
|
||||
#include "rpg/cutscene/item/cutsceneitem.h"
|
||||
#include "rpg/cutscene/cutscenesystem.h"
|
||||
|
||||
void battleStateLostInit(void) {
|
||||
const cutsceneitem_t items[] = {
|
||||
{
|
||||
.type = CUTSCENE_ITEM_TYPE_TEXT,
|
||||
.text = { .text = "You were defeated..." }
|
||||
},
|
||||
{
|
||||
.type = CUTSCENE_ITEM_TYPE_BATTLE_SET_STATE,
|
||||
.battleSetState = { .state = BATTLE_STATE_ENDING }
|
||||
}
|
||||
};
|
||||
cutsceneSystemInsertItemsNext(items, sizeof(items) / sizeof(cutsceneitem_t));
|
||||
}
|
||||
|
||||
void battleStateLostUpdate(void) {
|
||||
|
||||
}
|
||||
|
||||
void battleStateLostDispose(void) {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "dusk.h"
|
||||
|
||||
typedef struct {
|
||||
|
||||
} battlestatelost_t;
|
||||
|
||||
/**
|
||||
* Called when the battle enters BATTLE_STATE_LOST. Queues a textbox
|
||||
* announcing the loss, then transitions to BATTLE_STATE_ENDING once
|
||||
* dismissed.
|
||||
*/
|
||||
void battleStateLostInit(void);
|
||||
|
||||
/**
|
||||
* Updates BATTLE_STATE_LOST for one frame.
|
||||
*/
|
||||
void battleStateLostUpdate(void);
|
||||
|
||||
/**
|
||||
* Called when the battle leaves BATTLE_STATE_LOST.
|
||||
*/
|
||||
void battleStateLostDispose(void);
|
||||
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#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) {
|
||||
|
||||
}
|
||||
|
||||
void battleStateOpeningDispose(void) {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "dusk.h"
|
||||
|
||||
typedef struct {
|
||||
|
||||
} battlestateopening_t;
|
||||
|
||||
/**
|
||||
* Called when the battle enters BATTLE_STATE_OPENING.
|
||||
*/
|
||||
void battleStateOpeningInit(void);
|
||||
|
||||
/**
|
||||
* Updates BATTLE_STATE_OPENING for one frame.
|
||||
*/
|
||||
void battleStateOpeningUpdate(void);
|
||||
|
||||
/**
|
||||
* Called when the battle leaves BATTLE_STATE_OPENING.
|
||||
*/
|
||||
void battleStateOpeningDispose(void);
|
||||
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "battlestatepostround.h"
|
||||
|
||||
void battleStatePostRoundInit(void) {
|
||||
|
||||
}
|
||||
|
||||
void battleStatePostRoundUpdate(void) {
|
||||
|
||||
}
|
||||
|
||||
void battleStatePostRoundDispose(void) {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "dusk.h"
|
||||
|
||||
typedef struct {
|
||||
|
||||
} battlestatepostround_t;
|
||||
|
||||
/**
|
||||
* Called when the battle enters BATTLE_STATE_POST_ROUND.
|
||||
*/
|
||||
void battleStatePostRoundInit(void);
|
||||
|
||||
/**
|
||||
* Updates BATTLE_STATE_POST_ROUND for one frame.
|
||||
*/
|
||||
void battleStatePostRoundUpdate(void);
|
||||
|
||||
/**
|
||||
* Called when the battle leaves BATTLE_STATE_POST_ROUND.
|
||||
*/
|
||||
void battleStatePostRoundDispose(void);
|
||||
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "battlestatepreround.h"
|
||||
#include "rpg/battle/battle.h"
|
||||
#include "rpg/cutscene/item/cutsceneitem.h"
|
||||
#include "rpg/cutscene/cutscenesystem.h"
|
||||
|
||||
void battleStatePreRoundInit(void) {
|
||||
// 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 }
|
||||
},
|
||||
{
|
||||
.type = CUTSCENE_ITEM_TYPE_BATTLE_WAIT_SELECTION
|
||||
}
|
||||
};
|
||||
cutsceneSystemInsertItemsNext(items, sizeof(items) / sizeof(cutsceneitem_t));
|
||||
}
|
||||
|
||||
void battleStatePreRoundUpdate(void) {
|
||||
|
||||
}
|
||||
|
||||
void battleStatePreRoundDispose(void) {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "dusk.h"
|
||||
|
||||
typedef struct {
|
||||
|
||||
} battlestatepreround_t;
|
||||
|
||||
/**
|
||||
* Called when the battle enters BATTLE_STATE_PRE_ROUND.
|
||||
*/
|
||||
void battleStatePreRoundInit(void);
|
||||
|
||||
/**
|
||||
* Updates BATTLE_STATE_PRE_ROUND for one frame.
|
||||
*/
|
||||
void battleStatePreRoundUpdate(void);
|
||||
|
||||
/**
|
||||
* Called when the battle leaves BATTLE_STATE_PRE_ROUND.
|
||||
*/
|
||||
void battleStatePreRoundDispose(void);
|
||||
@@ -0,0 +1,124 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "battlestateselection.h"
|
||||
#include "rpg/battle/battle.h"
|
||||
#include "assert/assert.h"
|
||||
|
||||
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 battleStateSelectionAiDecide(const uint8_t fighterIndex) {
|
||||
battlefighter_t *fighter = BATTLE.fighters[fighterIndex];
|
||||
const battlefighterteam_t enemyTeam =
|
||||
fighter->team == BATTLE_FIGHTER_TEAM_ALLY ?
|
||||
BATTLE_FIGHTER_TEAM_ENEMY : BATTLE_FIGHTER_TEAM_ALLY;
|
||||
|
||||
// Simple placeholder AI: attack the first living fighter on the
|
||||
// opposing team. No move variety or target scoring yet.
|
||||
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;
|
||||
|
||||
battleStateSelectionQueueAttack(fighterIndex, i);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void battleStateSelectionUpdate(void) {
|
||||
battlestateselection_t *state = &BATTLE.stateData.selection;
|
||||
|
||||
// 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) return;
|
||||
|
||||
battleStateSelectionAiDecide(fighterIndex);
|
||||
state->selectionIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
void battleStateSelectionDispose(void) {
|
||||
|
||||
}
|
||||
|
||||
void battleStateSelectionQueueAttack(
|
||||
const uint8_t fighterIndex,
|
||||
const uint8_t targetIndex
|
||||
) {
|
||||
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];
|
||||
if(action->move == BATTLE_MOVE_ID_NULL) BATTLE.actionsPendingCount++;
|
||||
action->move = move;
|
||||
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.fighterOrderCount) return NULL;
|
||||
|
||||
return BATTLE.fighters[BATTLE.fighterOrder[index]];
|
||||
}
|
||||
|
||||
void battleStateSelectionPlayerAttack(const uint8_t targetIndex) {
|
||||
battlefighter_t *fighter = battleStateSelectionGetCurrentFighter();
|
||||
if(fighter == NULL) return;
|
||||
|
||||
const uint8_t selectionIndex = BATTLE.stateData.selection.selectionIndex;
|
||||
const uint8_t fighterIndex = BATTLE.fighterOrder[selectionIndex];
|
||||
battleStateSelectionQueueAttack(fighterIndex, targetIndex);
|
||||
BATTLE.stateData.selection.selectionIndex++;
|
||||
}
|
||||
|
||||
void battleStateSelectionPlayerFlee(void) {
|
||||
battlefighter_t *fighter = battleStateSelectionGetCurrentFighter();
|
||||
if(fighter == NULL) return;
|
||||
|
||||
BATTLE.result = BATTLE_RESULT_FLED;
|
||||
battleSetState(BATTLE_STATE_FLED);
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
/**
|
||||
* 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"
|
||||
|
||||
typedef struct {
|
||||
// Cursor into BATTLE.fighters (by position index) for the fighter
|
||||
// currently deciding a move this round.
|
||||
uint8_t selectionIndex;
|
||||
} battlestateselection_t;
|
||||
|
||||
/**
|
||||
* Called when the battle enters BATTLE_STATE_SELECTION.
|
||||
*/
|
||||
void battleStateSelectionInit(void);
|
||||
|
||||
/**
|
||||
* Updates BATTLE_STATE_SELECTION for one frame: auto-decides (see
|
||||
* battleStateSelectionAiDecide) for every AI-controlled fighter the
|
||||
* cursor passes, stopping once it reaches a player-controlled fighter
|
||||
* awaiting input (see battleStateSelectionGetCurrentFighter). Once every
|
||||
* position has decided, BATTLE_WAIT_SELECTION (see
|
||||
* cutsceneBattleWaitSelectionUpdate) queues the move to
|
||||
* BATTLE_STATE_EXECUTING.
|
||||
*/
|
||||
void battleStateSelectionUpdate(void);
|
||||
|
||||
/**
|
||||
* Called when the battle leaves BATTLE_STATE_SELECTION.
|
||||
*/
|
||||
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
|
||||
* 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);
|
||||
|
||||
/**
|
||||
* Decides and queues a move for an AI-controlled fighter: currently
|
||||
* always a basic attack against the first living fighter on the
|
||||
* opposing team (see battleStateSelectionQueueAttack). No-ops if there's
|
||||
* no living target (the battle should already be over by then via
|
||||
* BATTLE_POST_MOVE).
|
||||
*
|
||||
* @param fighterIndex Index into BATTLE.fighters of the deciding fighter.
|
||||
*/
|
||||
void battleStateSelectionAiDecide(const uint8_t fighterIndex);
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
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);
|
||||
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "battlestatewon.h"
|
||||
#include "rpg/cutscene/item/cutsceneitem.h"
|
||||
#include "rpg/cutscene/cutscenesystem.h"
|
||||
|
||||
void battleStateWonInit(void) {
|
||||
const cutsceneitem_t items[] = {
|
||||
{
|
||||
.type = CUTSCENE_ITEM_TYPE_TEXT,
|
||||
.text = { .text = "You won the battle!" }
|
||||
},
|
||||
{
|
||||
.type = CUTSCENE_ITEM_TYPE_BATTLE_SET_STATE,
|
||||
.battleSetState = { .state = BATTLE_STATE_ENDING }
|
||||
}
|
||||
};
|
||||
cutsceneSystemInsertItemsNext(items, sizeof(items) / sizeof(cutsceneitem_t));
|
||||
}
|
||||
|
||||
void battleStateWonUpdate(void) {
|
||||
|
||||
}
|
||||
|
||||
void battleStateWonDispose(void) {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "dusk.h"
|
||||
|
||||
typedef struct {
|
||||
|
||||
} battlestatewon_t;
|
||||
|
||||
/**
|
||||
* Called when the battle enters BATTLE_STATE_WON. Queues a textbox
|
||||
* announcing the win, then transitions to BATTLE_STATE_ENDING once
|
||||
* dismissed.
|
||||
*/
|
||||
void battleStateWonInit(void);
|
||||
|
||||
/**
|
||||
* Updates BATTLE_STATE_WON for one frame.
|
||||
*/
|
||||
void battleStateWonUpdate(void);
|
||||
|
||||
/**
|
||||
* Called when the battle leaves BATTLE_STATE_WON.
|
||||
*/
|
||||
void battleStateWonDispose(void);
|
||||
@@ -9,6 +9,8 @@
|
||||
#include "item/json/cutscenejsonpauseflags.h"
|
||||
#include "util/memory.h"
|
||||
#include "assert/assert.h"
|
||||
#include "asset/asset.h"
|
||||
#include "asset/loader/assetloader.h"
|
||||
|
||||
errorret_t cutsceneParseItem(
|
||||
yyjson_val *itemObj,
|
||||
@@ -145,3 +147,51 @@ void cutsceneAppendNext(
|
||||
) {
|
||||
cutsceneAppendAt(scene, (uint8_t)(currentIndex + 1), items, count);
|
||||
}
|
||||
|
||||
void cutsceneRemoveFront(
|
||||
cutscene_t *scene,
|
||||
const uint8_t count
|
||||
) {
|
||||
assertNotNull(scene, "Scene cannot be NULL");
|
||||
assertTrue(count <= scene->itemCount, "count exceeds itemCount");
|
||||
|
||||
const size_t tailCount = (size_t)scene->itemCount - count;
|
||||
// count == 0 has nothing to shift (and would pass dest == src into
|
||||
// memoryMove, which asserts against that as a no-op call).
|
||||
if(count > 0 && tailCount > 0) {
|
||||
memoryMove(
|
||||
&scene->items[0],
|
||||
&scene->items[count],
|
||||
tailCount * sizeof(cutsceneitem_t)
|
||||
);
|
||||
}
|
||||
scene->itemCount -= count;
|
||||
}
|
||||
|
||||
errorret_t cutsceneLoadParse(
|
||||
const char_t *path,
|
||||
cutscene_t *scene,
|
||||
cutsceneitem_t *items,
|
||||
const size_t itemsMax
|
||||
) {
|
||||
assertNotNull(path, "Path cannot be NULL");
|
||||
assertNotNull(scene, "Scene cannot be NULL");
|
||||
assertNotNull(items, "Items cannot be NULL");
|
||||
|
||||
assetentry_t *entry = assetLock(path, ASSET_LOADER_TYPE_JSON, NULL);
|
||||
errorret_t result = assetRequireLoaded(entry);
|
||||
if(errorIsNotOk(result)) {
|
||||
assetUnlockEntry(entry);
|
||||
errorChain(result);
|
||||
}
|
||||
|
||||
// Parse straight out of the entry's own doc while still locked - every
|
||||
// cutsceneitem_t field that could reference it owns its string data by
|
||||
// value, so nothing needs to survive past this call and the entry can
|
||||
// just be unlocked like any other asset once parsing finishes.
|
||||
errorret_t parseResult = cutsceneParseDoc(entry->data.json, scene, items, itemsMax);
|
||||
assetUnlockEntry(entry);
|
||||
errorChain(parseResult);
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
@@ -127,3 +127,45 @@ void cutsceneAppendNext(
|
||||
const cutsceneitem_t *items,
|
||||
const uint8_t count
|
||||
);
|
||||
|
||||
/**
|
||||
* Removes count items from the front of scene->items, shifting the
|
||||
* remainder down to index 0 and shrinking scene->itemCount to match. Used
|
||||
* by cutsceneSystemNext/cutsceneGoTo to reclaim already-run items' slots
|
||||
* instead of leaving them as dead weight forever - see
|
||||
* CUTSCENE_SYSTEM.currentItem's doc comment, since the currently-running
|
||||
* item is always scene->items[0] once a cutscene has started.
|
||||
*
|
||||
* @param scene Cutscene to remove from.
|
||||
* @param count Number of items to remove from the front - must be
|
||||
* <= scene->itemCount.
|
||||
*/
|
||||
void cutsceneRemoveFront(
|
||||
cutscene_t *scene,
|
||||
const uint8_t count
|
||||
);
|
||||
|
||||
/**
|
||||
* Loads a cutscene JSON asset by path and parses it into scene/items (see
|
||||
* cutsceneParseDoc) - the common "lock the asset, require it loaded,
|
||||
* parse its 'items' array, unlock it again" sequence shared by every
|
||||
* place that resolves a cutscene by file path (cutsceneSystemLoad,
|
||||
* cutsceneCutsceneResolve, cutsceneInsertResolveAndSplice,
|
||||
* battleStateExecutingInit, cutsceneGoTo's reload). Purely propagates the
|
||||
* error on failure (like cutsceneParseDoc) - this is core cutscene logic,
|
||||
* not UI, so it doesn't reach for uiFatalErrorOpen itself; each caller
|
||||
* decides how to surface the failure (typically errorCatch(errorPrint())
|
||||
* + uiFatalErrorOpen(NULL), same as any other asset load failure).
|
||||
*
|
||||
* @param path Asset path of the cutscene JSON file to load.
|
||||
* @param scene Destination scene - itemCount/itemsMax/pause overwritten.
|
||||
* @param items Destination item array, capacity itemsMax.
|
||||
* @param itemsMax Capacity of items.
|
||||
* @return Error code indicating success or failure of the load/parse.
|
||||
*/
|
||||
errorret_t cutsceneLoadParse(
|
||||
const char_t *path,
|
||||
cutscene_t *scene,
|
||||
cutsceneitem_t *items,
|
||||
const size_t itemsMax
|
||||
);
|
||||
|
||||
@@ -10,8 +10,6 @@
|
||||
#include "util/memory.h"
|
||||
#include "util/string.h"
|
||||
#include "assert/assert.h"
|
||||
#include "asset/asset.h"
|
||||
#include "asset/loader/assetloader.h"
|
||||
#include "ui/overlay/uifatalerror.h"
|
||||
|
||||
cutscenesystem_t CUTSCENE_SYSTEM;
|
||||
@@ -55,7 +53,19 @@ void cutsceneSystemPrepare(
|
||||
CUTSCENE_SYSTEM.textCache[0] = '\0';
|
||||
CUTSCENE_SYSTEM.currentItem = 0xFF;// Set to 0xFF so Next wraps to 0.
|
||||
CUTSCENE_SYSTEM.onComplete = NULL;
|
||||
CUTSCENE_SYSTEM.loadedFile[0] = '\0';
|
||||
|
||||
// Only invalidate the known source file when switching away from
|
||||
// loadedScene entirely. Starting loadedScene itself means
|
||||
// cutsceneSystemLoad/cutsceneCutsceneResolve just populated it - for
|
||||
// cutsceneCutsceneResolve (see cutsceneCutsceneStart), that already
|
||||
// happened before this call, so clearing loadedFile here would
|
||||
// immediately undo it. cutsceneGoTo needs loadedFile intact to reload
|
||||
// a marker behind the current position; cutsceneSystemLoad re-stamps
|
||||
// it itself right after this call anyway, so it's unaffected either
|
||||
// way.
|
||||
if(cutscene != &CUTSCENE_SYSTEM.loadedScene) {
|
||||
CUTSCENE_SYSTEM.loadedFile[0] = '\0';
|
||||
}
|
||||
}
|
||||
|
||||
void cutsceneSystemStartCutscene(cutscene_t *cutscene) {
|
||||
@@ -87,44 +97,43 @@ 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");
|
||||
|
||||
assetentry_t *entry = assetLock(file, ASSET_LOADER_TYPE_JSON, NULL);
|
||||
errorret_t result = assetRequireLoaded(entry);
|
||||
if(errorIsNotOk(result)) {
|
||||
assetUnlockEntry(entry);
|
||||
errorCatch(errorPrint(result));
|
||||
uiFatalErrorOpen(NULL);
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse straight out of the entry's own doc while still locked - every
|
||||
// cutsceneitem_t field that could reference it owns its string data by
|
||||
// value (see cutscenecutsceneref_t/cutscenemarker_t's doc comments), so
|
||||
// nothing needs to survive past this call and the entry can just be
|
||||
// unlocked like any other asset once parsing finishes.
|
||||
errorret_t parseResult = cutsceneParseDoc(
|
||||
entry->data.json, &CUTSCENE_SYSTEM.loadedScene, CUTSCENE_SYSTEM.loadedItems,
|
||||
errorret_t result = cutsceneLoadParse(
|
||||
file, &CUTSCENE_SYSTEM.loadedScene, CUTSCENE_SYSTEM.loadedItems,
|
||||
CUTSCENE_LOADED_ITEMS_MAX
|
||||
);
|
||||
assetUnlockEntry(entry);
|
||||
if(errorIsNotOk(parseResult)) {
|
||||
errorCatch(errorPrint(parseResult));
|
||||
if(errorIsNotOk(result)) {
|
||||
errorCatch(errorPrint(result));
|
||||
uiFatalErrorOpen(NULL);
|
||||
return;
|
||||
}
|
||||
@@ -242,15 +251,21 @@ void cutsceneSystemSetTextCache(const char_t *text) {
|
||||
void cutsceneSystemNext() {
|
||||
if(CUTSCENE_SYSTEM.scene == NULL) return;
|
||||
|
||||
CUTSCENE_SYSTEM.currentItem++;
|
||||
// Pop the just-finished item off the front rather than advancing an
|
||||
// index further into the array - see CUTSCENE_SYSTEM.currentItem's doc
|
||||
// comment. The very first call (currentItem still 0xFF) has nothing to
|
||||
// pop yet; it just marks the cutscene as started.
|
||||
if(CUTSCENE_SYSTEM.currentItem == 0xFF) {
|
||||
CUTSCENE_SYSTEM.currentItem = 0;
|
||||
} else {
|
||||
cutsceneRemoveFront(CUTSCENE_SYSTEM.scene, 1);
|
||||
}
|
||||
|
||||
// End of the cutscene? Note that a CUTSCENE_ITEM_TYPE_INSERT item's
|
||||
// spliced-in items are physically part of this same array (see
|
||||
// cutsceneSystemInsertCutscene), so there's no separate "parent scene"
|
||||
// to fall back into here - itemCount already accounts for them.
|
||||
if(
|
||||
CUTSCENE_SYSTEM.currentItem >= CUTSCENE_SYSTEM.scene->itemCount
|
||||
) {
|
||||
if(CUTSCENE_SYSTEM.scene->itemCount == 0) {
|
||||
// Saved and cleared before firing so a callback that immediately
|
||||
// starts another cutscene (or sets its own onComplete) isn't clobbered
|
||||
// by this function's own cleanup running after it - same reentrancy
|
||||
@@ -284,18 +299,57 @@ void cutsceneGoTo(const char_t *name) {
|
||||
assertNotNull(
|
||||
CUTSCENE_SYSTEM.scene, "cutsceneGoTo called with no cutscene running"
|
||||
);
|
||||
// A marker behind the current position can't be found in the live
|
||||
// queue - already-run items are evicted from its front as the
|
||||
// cutscene advances (see cutsceneSystemNext/cutsceneRemoveFront), so
|
||||
// the untouched original has to be re-parsed from source instead. Only
|
||||
// supported when the running cutscene is the one cutsceneSystemLoad/
|
||||
// cutsceneCutsceneResolve populated - a C-authored cutscene_t, or
|
||||
// content spliced in via INSERT/battleStateExecutingInit, has no
|
||||
// source to reload from, so a backward jump into those isn't supported
|
||||
// yet.
|
||||
assertTrue(
|
||||
CUTSCENE_SYSTEM.scene == &CUTSCENE_SYSTEM.loadedScene &&
|
||||
CUTSCENE_SYSTEM.loadedFile[0] != '\0',
|
||||
"cutsceneGoTo requires the running cutscene to have come from "
|
||||
"cutsceneSystemLoad/cutsceneCutsceneResolve"
|
||||
);
|
||||
|
||||
for(uint8_t i = 0; i < CUTSCENE_SYSTEM.scene->itemCount; i++) {
|
||||
const cutsceneitem_t *item = &CUTSCENE_SYSTEM.scene->items[i];
|
||||
cutscene_t freshScene = { .items = CUTSCENE_SYSTEM.gotoScratchItems };
|
||||
errorret_t result = cutsceneLoadParse(
|
||||
CUTSCENE_SYSTEM.loadedFile, &freshScene, CUTSCENE_SYSTEM.gotoScratchItems,
|
||||
CUTSCENE_LOADED_ITEMS_MAX
|
||||
);
|
||||
if(errorIsNotOk(result)) {
|
||||
errorCatch(errorPrint(result));
|
||||
uiFatalErrorOpen(NULL);
|
||||
return;
|
||||
}
|
||||
|
||||
for(uint8_t i = 0; i < freshScene.itemCount; i++) {
|
||||
const cutsceneitem_t *item = &freshScene.items[i];
|
||||
if(
|
||||
item->type == CUTSCENE_ITEM_TYPE_MARKER &&
|
||||
stringEquals(item->marker.name, name)
|
||||
) {
|
||||
CUTSCENE_SYSTEM.currentItem = i;
|
||||
memoryZero(&CUTSCENE_SYSTEM.data, sizeof(CUTSCENE_SYSTEM.data));
|
||||
cutsceneItemStart(item, &CUTSCENE_SYSTEM.data);
|
||||
return;
|
||||
}
|
||||
item->type != CUTSCENE_ITEM_TYPE_MARKER ||
|
||||
!stringEquals(item->marker.name, name)
|
||||
) continue;
|
||||
|
||||
// Commit: replace the live queue outright with marker-onward from
|
||||
// the fresh parse. Nothing before the marker (including whatever was
|
||||
// running) gets Started - only the marker item itself does, same as
|
||||
// the old in-place jump's semantics.
|
||||
const uint8_t remaining = freshScene.itemCount - i;
|
||||
memoryCopy(
|
||||
CUTSCENE_SYSTEM.loadedItems, &freshScene.items[i],
|
||||
remaining * sizeof(cutsceneitem_t)
|
||||
);
|
||||
CUTSCENE_SYSTEM.loadedScene.itemCount = remaining;
|
||||
CUTSCENE_SYSTEM.currentItem = 0;
|
||||
|
||||
memoryZero(&CUTSCENE_SYSTEM.data, sizeof(CUTSCENE_SYSTEM.data));
|
||||
cutsceneItemStart(
|
||||
&CUTSCENE_SYSTEM.loadedItems[0], &CUTSCENE_SYSTEM.data
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
assertTrue(false, "cutsceneGoTo: no marker found with that name");
|
||||
@@ -311,5 +365,6 @@ void cutsceneSystemUpdate() {
|
||||
const cutsceneitem_t * cutsceneSystemGetCurrentItem() {
|
||||
if(CUTSCENE_SYSTEM.scene == NULL) return NULL;
|
||||
|
||||
return &CUTSCENE_SYSTEM.scene->items[CUTSCENE_SYSTEM.currentItem];
|
||||
// Always index 0 - see CUTSCENE_SYSTEM.currentItem's doc comment.
|
||||
return &CUTSCENE_SYSTEM.scene->items[0];
|
||||
}
|
||||
|
||||
@@ -23,6 +23,13 @@ typedef struct entity_s entity_t;
|
||||
|
||||
typedef struct {
|
||||
cutscene_t *scene;
|
||||
|
||||
// 0xFF if nothing has started running yet, otherwise always 0 - the
|
||||
// currently-running item is always scene->items[0]; cutsceneSystemNext
|
||||
// removes it from the front (see cutsceneRemoveFront) rather than
|
||||
// advancing an index further into the array, so an item's slot is
|
||||
// reclaimed the moment it finishes instead of sitting there as dead
|
||||
// weight for the rest of the cutscene's run.
|
||||
uint8_t currentItem;
|
||||
cutscenepause_t pause;
|
||||
entity_t *entityInteract;
|
||||
@@ -47,6 +54,13 @@ typedef struct {
|
||||
|
||||
// Filename last passed to cutsceneSystemLoad - see cutsceneRestart.
|
||||
char_t loadedFile[ASSET_FILE_NAME_MAX];
|
||||
|
||||
// Scratch space for cutsceneGoTo's re-parse of loadedFile - a fixed,
|
||||
// persistent buffer rather than a local one since cutsceneitem_t is
|
||||
// large (e.g. cutscenemodal_t alone carries a 256-byte message buffer),
|
||||
// making a CUTSCENE_LOADED_ITEMS_MAX-sized array too big to put on the
|
||||
// stack on constrained platforms (PSP/GameCube/Wii).
|
||||
cutsceneitem_t gotoScratchItems[CUTSCENE_LOADED_ITEMS_MAX];
|
||||
} cutscenesystem_t;
|
||||
|
||||
extern cutscenesystem_t CUTSCENE_SYSTEM;
|
||||
@@ -109,6 +123,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.
|
||||
*
|
||||
@@ -185,8 +225,20 @@ void cutsceneSystemNext();
|
||||
|
||||
/**
|
||||
* Jumps the running cutscene directly to the marker with the given name
|
||||
* and starts it immediately. Asserts if no cutscene is running or no
|
||||
* marker with that name exists.
|
||||
* and starts it immediately. Re-parses the cutscene fresh from its
|
||||
* source file (see cutsceneLoadParse) and replaces the live queue with
|
||||
* marker-onward from that fresh parse - items already run may have been
|
||||
* evicted from the front of the live queue (see
|
||||
* cutsceneSystemNext/cutsceneRemoveFront), so a marker behind the
|
||||
* current position can only be found in the untouched original. Nothing
|
||||
* before the marker gets Started, only the marker item itself.
|
||||
*
|
||||
* Requires the running cutscene to be the one cutsceneSystemLoad/
|
||||
* cutsceneCutsceneResolve populated (i.e. CUTSCENE_SYSTEM.scene ==
|
||||
* &CUTSCENE_SYSTEM.loadedScene) - asserts otherwise. A C-authored
|
||||
* cutscene_t, or content spliced in via INSERT/battleStateExecutingInit,
|
||||
* has no source file to reload from, so backward jumps into those
|
||||
* aren't supported yet.
|
||||
*
|
||||
* @param name Marker name to search for.
|
||||
*/
|
||||
|
||||
@@ -5,8 +5,10 @@
|
||||
|
||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
PUBLIC
|
||||
cutscenestartbattle.c
|
||||
cutscenebattlewaitstate.c
|
||||
cutscenebattleforceaction.c
|
||||
cutsceneregularbattle.c
|
||||
cutscenebattlestart.c
|
||||
cutscenebattlesetstate.c
|
||||
cutscenebattlewaitselection.c
|
||||
cutscenebattlepostmove.c
|
||||
cutscenebattlefighterdamage.c
|
||||
cutscenebattlefighterheal.c
|
||||
)
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* 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/cutscenejsonfield.h"
|
||||
#include "assert/assert.h"
|
||||
#include "util/string.h"
|
||||
|
||||
#define CUTSCENE_BATTLE_FIGHTER_REF_MAX 16
|
||||
|
||||
void cutsceneBattleFighterDamageStart(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
) {
|
||||
const cutscenebattlefighterdamage_t *damage = &item->battleFighterDamage;
|
||||
assertTrue(
|
||||
damage->fighterIndex < BATTLE_FIGHTER_POS_COUNT,
|
||||
"fighterIndex sentinel was never resolved"
|
||||
);
|
||||
battlefighter_t *fighter = BATTLE.fighters[damage->fighterIndex];
|
||||
assertNotNull(fighter, "Fighter cannot be NULL");
|
||||
|
||||
battleFighterDamage(fighter, damage->amount);
|
||||
}
|
||||
|
||||
bool_t cutsceneBattleFighterDamageUpdate(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
errorret_t cutsceneBattleFighterDamageLoad(
|
||||
yyjson_val *itemObj,
|
||||
cutsceneitem_t *item
|
||||
) {
|
||||
char_t fighterRef[CUTSCENE_BATTLE_FIGHTER_REF_MAX];
|
||||
if(!cutsceneJsonCopyString(
|
||||
itemObj, "fighter", NULL, fighterRef, CUTSCENE_BATTLE_FIGHTER_REF_MAX
|
||||
)) {
|
||||
errorThrow("Cutscene item (BATTLE_FIGHTER_DAMAGE): missing 'fighter'");
|
||||
}
|
||||
if(stringEquals(fighterRef, "target")) {
|
||||
item->battleFighterDamage.fighterIndex = BATTLE_FIGHTER_POS_TARGET;
|
||||
} else if(stringEquals(fighterRef, "user")) {
|
||||
item->battleFighterDamage.fighterIndex = BATTLE_FIGHTER_POS_USER;
|
||||
} else {
|
||||
errorThrow(
|
||||
"Cutscene item (BATTLE_FIGHTER_DAMAGE): 'fighter' must be "
|
||||
"'target' or 'user'"
|
||||
);
|
||||
}
|
||||
|
||||
const int32_t amount = cutsceneJsonGetInt(itemObj, "amount", -1);
|
||||
if(amount < 0 || amount > UINT16_MAX) {
|
||||
errorThrow(
|
||||
"Cutscene item (BATTLE_FIGHTER_DAMAGE): invalid/missing 'amount'"
|
||||
);
|
||||
}
|
||||
item->battleFighterDamage.amount = (uint16_t)amount;
|
||||
|
||||
errorOk();
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* 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"
|
||||
#include "rpg/battle/fighter/battlefighterpos.h"
|
||||
|
||||
typedef struct {
|
||||
// Index into BATTLE.fighters of the fighter to damage, or one of
|
||||
// BATTLE_FIGHTER_POS_TARGET/BATTLE_FIGHTER_POS_USER, resolved against
|
||||
// the currently-executing action by battleStateExecutingInit before
|
||||
// this item ever runs.
|
||||
battlefighterpos_t fighterIndex;
|
||||
uint16_t amount;
|
||||
} cutscenebattlefighterdamage_t;
|
||||
|
||||
/**
|
||||
* Starts a BATTLE_FIGHTER_DAMAGE item, applying the configured damage to
|
||||
* BATTLE.fighters[fighterIndex] immediately.
|
||||
*
|
||||
* @param item The cutscene item.
|
||||
* @param data Runtime data storage.
|
||||
*/
|
||||
void cutsceneBattleFighterDamageStart(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
);
|
||||
|
||||
/**
|
||||
* Updates a BATTLE_FIGHTER_DAMAGE item (always completes immediately).
|
||||
*
|
||||
* @param item The cutscene item.
|
||||
* @param data Runtime data storage.
|
||||
* @returns true always.
|
||||
*/
|
||||
bool_t cutsceneBattleFighterDamageUpdate(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
);
|
||||
|
||||
/**
|
||||
* Parses a BATTLE_FIGHTER_DAMAGE 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 cutsceneBattleFighterDamageLoad(
|
||||
yyjson_val *itemObj,
|
||||
cutsceneitem_t *item
|
||||
);
|
||||
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* 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/cutscenejsonfield.h"
|
||||
#include "assert/assert.h"
|
||||
#include "util/string.h"
|
||||
|
||||
#define CUTSCENE_BATTLE_FIGHTER_REF_MAX 16
|
||||
|
||||
void cutsceneBattleFighterHealStart(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
) {
|
||||
const cutscenebattlefighterheal_t *heal = &item->battleFighterHeal;
|
||||
assertTrue(
|
||||
heal->fighterIndex < BATTLE_FIGHTER_POS_COUNT,
|
||||
"fighterIndex sentinel was never resolved"
|
||||
);
|
||||
battlefighter_t *fighter = BATTLE.fighters[heal->fighterIndex];
|
||||
assertNotNull(fighter, "Fighter cannot be NULL");
|
||||
|
||||
battleFighterHeal(fighter, heal->amount);
|
||||
}
|
||||
|
||||
bool_t cutsceneBattleFighterHealUpdate(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
errorret_t cutsceneBattleFighterHealLoad(
|
||||
yyjson_val *itemObj,
|
||||
cutsceneitem_t *item
|
||||
) {
|
||||
char_t fighterRef[CUTSCENE_BATTLE_FIGHTER_REF_MAX];
|
||||
if(!cutsceneJsonCopyString(
|
||||
itemObj, "fighter", NULL, fighterRef, CUTSCENE_BATTLE_FIGHTER_REF_MAX
|
||||
)) {
|
||||
errorThrow("Cutscene item (BATTLE_FIGHTER_HEAL): missing 'fighter'");
|
||||
}
|
||||
if(stringEquals(fighterRef, "target")) {
|
||||
item->battleFighterHeal.fighterIndex = BATTLE_FIGHTER_POS_TARGET;
|
||||
} else if(stringEquals(fighterRef, "user")) {
|
||||
item->battleFighterHeal.fighterIndex = BATTLE_FIGHTER_POS_USER;
|
||||
} else {
|
||||
errorThrow(
|
||||
"Cutscene item (BATTLE_FIGHTER_HEAL): 'fighter' must be "
|
||||
"'target' or 'user'"
|
||||
);
|
||||
}
|
||||
|
||||
const int32_t amount = cutsceneJsonGetInt(itemObj, "amount", -1);
|
||||
if(amount < 0 || amount > UINT16_MAX) {
|
||||
errorThrow(
|
||||
"Cutscene item (BATTLE_FIGHTER_HEAL): invalid/missing 'amount'"
|
||||
);
|
||||
}
|
||||
item->battleFighterHeal.amount = (uint16_t)amount;
|
||||
|
||||
errorOk();
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* 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"
|
||||
#include "rpg/battle/fighter/battlefighterpos.h"
|
||||
|
||||
typedef struct {
|
||||
// Index into BATTLE.fighters of the fighter to heal, or one of
|
||||
// BATTLE_FIGHTER_POS_TARGET/BATTLE_FIGHTER_POS_USER, resolved against
|
||||
// the currently-executing action by battleStateExecutingInit before
|
||||
// this item ever runs.
|
||||
battlefighterpos_t fighterIndex;
|
||||
uint16_t amount;
|
||||
} cutscenebattlefighterheal_t;
|
||||
|
||||
/**
|
||||
* Starts a BATTLE_FIGHTER_HEAL item, applying the configured heal to
|
||||
* BATTLE.fighters[fighterIndex] immediately.
|
||||
*
|
||||
* @param item The cutscene item.
|
||||
* @param data Runtime data storage.
|
||||
*/
|
||||
void cutsceneBattleFighterHealStart(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
);
|
||||
|
||||
/**
|
||||
* Updates a BATTLE_FIGHTER_HEAL item (always completes immediately).
|
||||
*
|
||||
* @param item The cutscene item.
|
||||
* @param data Runtime data storage.
|
||||
* @returns true always.
|
||||
*/
|
||||
bool_t cutsceneBattleFighterHealUpdate(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
);
|
||||
|
||||
/**
|
||||
* Parses a BATTLE_FIGHTER_HEAL 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 cutsceneBattleFighterHealLoad(
|
||||
yyjson_val *itemObj,
|
||||
cutsceneitem_t *item
|
||||
);
|
||||
@@ -1,37 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "rpg/cutscene/item/cutsceneitem.h"
|
||||
|
||||
void cutsceneBattleForceActionStart(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
) {
|
||||
battleQueueAction(
|
||||
item->battleForceAction.fighterIndex, BATTLE_ACTION_ATTACK,
|
||||
item->battleForceAction.targetIndex
|
||||
);
|
||||
}
|
||||
|
||||
bool_t cutsceneBattleForceActionUpdate(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
errorret_t cutsceneBattleForceActionLoad(
|
||||
yyjson_val *itemObj,
|
||||
cutsceneitem_t *item
|
||||
) {
|
||||
item->battleForceAction.fighterIndex =
|
||||
(uint8_t)yyjson_get_int(yyjson_obj_get(itemObj, "fighterIndex"));
|
||||
item->battleForceAction.targetIndex =
|
||||
(uint8_t)yyjson_get_int(yyjson_obj_get(itemObj, "targetIndex"));
|
||||
|
||||
errorOk();
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* 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/cutscenesystem.h"
|
||||
#include "console/console.h"
|
||||
|
||||
void cutsceneBattlePostMoveStart(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
) {
|
||||
consolePrint("BATTLE_POST_MOVE\n");
|
||||
|
||||
// Once a result has already been decided (e.g. the player fled), there
|
||||
// is nothing left to check.
|
||||
if(BATTLE.result != BATTLE_RESULT_NONE) return;
|
||||
|
||||
bool_t allyAlive = false;
|
||||
bool_t enemyAlive = false;
|
||||
for(uint8_t i = 0; i < BATTLE_FIGHTER_POS_COUNT; i++) {
|
||||
battlefighter_t *fighter = BATTLE.fighters[i];
|
||||
if(fighter == NULL) continue;
|
||||
if(!battleFighterIsAlive(fighter)) continue;
|
||||
|
||||
if(fighter->team == BATTLE_FIGHTER_TEAM_ALLY) allyAlive = true;
|
||||
else enemyAlive = true;
|
||||
}
|
||||
|
||||
if(allyAlive && enemyAlive) return;
|
||||
|
||||
BATTLE.result = allyAlive ? BATTLE_RESULT_WIN : BATTLE_RESULT_LOSS;
|
||||
|
||||
const cutsceneitem_t items[] = {
|
||||
{
|
||||
.type = CUTSCENE_ITEM_TYPE_BATTLE_SET_STATE,
|
||||
.battleSetState = {
|
||||
.state = allyAlive ? BATTLE_STATE_WON : BATTLE_STATE_LOST
|
||||
}
|
||||
}
|
||||
};
|
||||
cutsceneSystemInsertItemsNext(items, sizeof(items) / sizeof(cutsceneitem_t));
|
||||
}
|
||||
|
||||
bool_t cutsceneBattlePostMoveUpdate(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* 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: checks whether every ally or every enemy fighter has been
|
||||
* knocked out and, if so, sets BATTLE.result and splices in a
|
||||
* BATTLE_SET_STATE to BATTLE_STATE_WON/LOST. No-ops the health check if
|
||||
* BATTLE.result is already set (e.g. the player fled). 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
|
||||
);
|
||||
@@ -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();
|
||||
}
|
||||
+10
-12
@@ -6,47 +6,45 @@
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "rpg/battle/battle.h"
|
||||
#include "rpg/battle/state/battlestate.h"
|
||||
#include "rpg/cutscene/item/cutsceneitembase.h"
|
||||
|
||||
typedef struct {
|
||||
uint8_t fighterIndex;
|
||||
uint8_t targetIndex;
|
||||
} cutscenebattleforceaction_t;
|
||||
battlestate_t state;
|
||||
} cutscenebattlesetstate_t;
|
||||
|
||||
/**
|
||||
* Starts a battle force-action step: immediately queues an attack for the
|
||||
* given fighter against the given target, bypassing normal player/AI
|
||||
* selection for that fighter this round.
|
||||
* 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 cutsceneBattleForceActionStart(
|
||||
void cutsceneBattleSetStateStart(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
);
|
||||
|
||||
/**
|
||||
* Updates a battle force-action step (always completes immediately).
|
||||
* Updates a BATTLE_SET_STATE item (always completes immediately).
|
||||
*
|
||||
* @param item The cutscene item.
|
||||
* @param data Runtime data storage.
|
||||
* @returns true always.
|
||||
*/
|
||||
bool_t cutsceneBattleForceActionUpdate(
|
||||
bool_t cutsceneBattleSetStateUpdate(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
);
|
||||
|
||||
/**
|
||||
* Parses a BATTLE_FORCE_ACTION cutscene item's JSON fields into item.
|
||||
* 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 cutsceneBattleForceActionLoad(
|
||||
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,29 @@
|
||||
/**
|
||||
* 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/cutscene/cutscenesystem.h"
|
||||
#include "rpg/battle/battle.h"
|
||||
#include "rpg/battle/state/battlestateselection.h"
|
||||
|
||||
bool_t cutsceneBattleWaitSelectionUpdate(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
) {
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* 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 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 selection has completed or the battle has left
|
||||
* BATTLE_STATE_SELECTION.
|
||||
*/
|
||||
bool_t cutsceneBattleWaitSelectionUpdate(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
);
|
||||
@@ -1,31 +0,0 @@
|
||||
/**
|
||||
* 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/cutscene/item/json/cutscenejsonbattlestate.h"
|
||||
|
||||
bool_t cutsceneBattleWaitStateUpdate(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
) {
|
||||
return BATTLE.state == item->battleWaitState.state;
|
||||
}
|
||||
|
||||
errorret_t cutsceneBattleWaitStateLoad(
|
||||
yyjson_val *itemObj,
|
||||
cutsceneitem_t *item
|
||||
) {
|
||||
battlestate_t stateValue;
|
||||
if(!cutsceneJsonLookupBattleState(
|
||||
yyjson_get_str(yyjson_obj_get(itemObj, "state")), &stateValue
|
||||
)) {
|
||||
errorThrow("Cutscene item (BATTLE_WAIT_STATE): invalid 'state'");
|
||||
}
|
||||
item->battleWaitState.state = stateValue;
|
||||
|
||||
errorOk();
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "rpg/battle/battle.h"
|
||||
#include "rpg/cutscene/item/cutsceneitembase.h"
|
||||
|
||||
typedef struct {
|
||||
battlestate_t state;
|
||||
} cutscenebattlewaitstate_t;
|
||||
|
||||
/**
|
||||
* Updates a battle wait-state step, completing once BATTLE.state reaches
|
||||
* the watched state. Has no Start callback -- there's nothing to do until
|
||||
* the state is actually reached.
|
||||
*
|
||||
* @param item The cutscene item.
|
||||
* @param data Runtime data storage.
|
||||
* @returns true once BATTLE.state equals the watched state.
|
||||
*/
|
||||
bool_t cutsceneBattleWaitStateUpdate(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
);
|
||||
|
||||
/**
|
||||
* Parses a BATTLE_WAIT_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 cutsceneBattleWaitStateLoad(
|
||||
yyjson_val *itemObj,
|
||||
cutsceneitem_t *item
|
||||
);
|
||||
@@ -1,15 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "rpg/cutscene/item/cutsceneitem.h"
|
||||
|
||||
bool_t cutsceneRegularBattleUpdate(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
/**
|
||||
* 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 regular-battle step. By the time this item runs, the preceding
|
||||
* START_BATTLE item has already waited out the whole encounter, synced
|
||||
* party HP/MP, and returned to the overworld scene - so this is currently
|
||||
* just a completes-immediately hook point for whatever regular-battle-
|
||||
* specific follow-up (loot, post-battle dialogue, etc.) gets added later.
|
||||
*
|
||||
* @param item The cutscene item.
|
||||
* @param data Runtime data storage.
|
||||
* @returns true always.
|
||||
*/
|
||||
bool_t cutsceneRegularBattleUpdate(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
);
|
||||
|
||||
// REGULAR_BATTLE has no fields of its own to parse yet - see
|
||||
// cutsceneItemLoadNoop (cutsceneitembase.h), used directly as its .load
|
||||
// callback.
|
||||
@@ -1,204 +0,0 @@
|
||||
/**
|
||||
* 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/party.h"
|
||||
#include "scene/scene.h"
|
||||
#include "save/save.h"
|
||||
#include "rpg/cutscene/item/json/cutscenejsonbattleencounter.h"
|
||||
#include "rpg/cutscene/item/json/cutscenejsonbattleability.h"
|
||||
#include "rpg/cutscene/item/json/cutscenejsonfield.h"
|
||||
#include "util/memory.h"
|
||||
|
||||
void cutsceneStartBattleStart(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
) {
|
||||
const cutscenestartbattle_t *config = &item->startBattle;
|
||||
|
||||
battleInit();
|
||||
|
||||
for(uint8_t i = 0; i < config->playerCount; i++) {
|
||||
battlefighter_t *member =
|
||||
partyGetOrderMember(config->players[i]);
|
||||
if(member == NULL) continue;
|
||||
|
||||
battlefighter_t *fighter = battleAddFighter(
|
||||
BATTLE_FIGHTER_TEAM_ALLY, BATTLE_FIGHTER_CONTROLLER_PLAYER,
|
||||
member->stats, member->healthMax, member->mpMax
|
||||
);
|
||||
if(fighter == NULL) continue;
|
||||
|
||||
fighter->health = member->health;
|
||||
fighter->mp = member->mp;
|
||||
fighter->status = member->status;
|
||||
memoryCopy(fighter->abilities, member->abilities, sizeof(fighter->abilities));
|
||||
}
|
||||
|
||||
for(uint8_t i = 0; i < config->enemyCount; i++) {
|
||||
const cutscenestartbattleenemy_t *enemy = &config->enemies[i];
|
||||
battlefighter_t *fighter = battleAddFighter(
|
||||
BATTLE_FIGHTER_TEAM_ENEMY, BATTLE_FIGHTER_CONTROLLER_AI,
|
||||
enemy->stats, enemy->healthMax, enemy->mpMax
|
||||
);
|
||||
if(fighter == NULL) continue;
|
||||
|
||||
memoryCopy(fighter->abilities, enemy->abilities, sizeof(fighter->abilities));
|
||||
}
|
||||
|
||||
battleStart(config->encounterType, config->fleeAvailable);
|
||||
sceneSet(SCENE_TYPE_BATTLE);
|
||||
}
|
||||
|
||||
bool_t cutsceneStartBattleUpdate(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
) {
|
||||
if(BATTLE.result == BATTLE_RESULT_NONE) return false;
|
||||
|
||||
const cutscenestartbattle_t *config = &item->startBattle;
|
||||
|
||||
// Sync ally HP/MP back to the persistent party roster. Relies on
|
||||
// ally fighters having been added to BATTLE.fighters in the same
|
||||
// order config->players iterates (see cutsceneStartBattleStart).
|
||||
uint8_t allySlot = 0;
|
||||
for(uint8_t i = 0; i < config->playerCount; i++) {
|
||||
battlefighter_t *member =
|
||||
partyGetOrderMember(config->players[i]);
|
||||
if(member == NULL) continue;
|
||||
|
||||
battlefighter_t *fighter = &BATTLE.fighters[allySlot++];
|
||||
member->health = fighter->health;
|
||||
member->mp = fighter->mp;
|
||||
member->status = fighter->status;
|
||||
}
|
||||
|
||||
sceneSet(SCENE_TYPE_OVERWORLD);
|
||||
battleDispose();
|
||||
return true;
|
||||
}
|
||||
|
||||
errorret_t cutsceneStartBattleLoad(
|
||||
yyjson_val *itemObj,
|
||||
cutsceneitem_t *item
|
||||
) {
|
||||
battleencountertype_t encounterValue;
|
||||
if(!cutsceneJsonLookupBattleEncounter(
|
||||
yyjson_get_str(yyjson_obj_get(itemObj, "encounterType")), &encounterValue
|
||||
)) {
|
||||
errorThrow("Cutscene item (START_BATTLE): invalid 'encounterType'");
|
||||
}
|
||||
item->startBattle.encounterType = encounterValue;
|
||||
|
||||
item->startBattle.fleeAvailable =
|
||||
cutsceneJsonGetBool(itemObj, "fleeAvailable", true);
|
||||
|
||||
yyjson_val *players = yyjson_obj_get(itemObj, "players");
|
||||
if(players == NULL) {
|
||||
item->startBattle.playerCount = PARTY_ACTIVE_SIZE_MAX;
|
||||
for(uint8_t i = 0; i < PARTY_ACTIVE_SIZE_MAX; i++) {
|
||||
item->startBattle.players[i] = i;
|
||||
}
|
||||
} else {
|
||||
size_t playerCount = yyjson_arr_size(players);
|
||||
if(!yyjson_is_arr(players) || playerCount > PARTY_ACTIVE_SIZE_MAX) {
|
||||
errorThrow(
|
||||
"Cutscene item (START_BATTLE): 'players' must have 0-%u entries",
|
||||
(uint32_t)PARTY_ACTIVE_SIZE_MAX
|
||||
);
|
||||
}
|
||||
item->startBattle.playerCount = (uint8_t)playerCount;
|
||||
|
||||
yyjson_arr_iter playerIter = yyjson_arr_iter_with(players);
|
||||
yyjson_val *playerVal;
|
||||
size_t p = 0;
|
||||
while((playerVal = yyjson_arr_iter_next(&playerIter)) != NULL) {
|
||||
int64_t index = yyjson_get_int(playerVal);
|
||||
if(index < 0 || index >= PARTY_ACTIVE_SIZE_MAX) {
|
||||
errorThrow(
|
||||
"Cutscene item (START_BATTLE): players[%u] must be 0-%u",
|
||||
(uint32_t)p, (uint32_t)(PARTY_ACTIVE_SIZE_MAX - 1)
|
||||
);
|
||||
}
|
||||
item->startBattle.players[p] = (uint8_t)index;
|
||||
p++;
|
||||
}
|
||||
}
|
||||
|
||||
yyjson_val *enemies = yyjson_obj_get(itemObj, "enemies");
|
||||
size_t enemyCount = yyjson_arr_size(enemies);
|
||||
if(!yyjson_is_arr(enemies) || enemyCount == 0 ||
|
||||
enemyCount > CUTSCENE_START_BATTLE_ENEMY_COUNT_MAX) {
|
||||
errorThrow(
|
||||
"Cutscene item (START_BATTLE): 'enemies' must have 1-%u entries",
|
||||
(uint32_t)CUTSCENE_START_BATTLE_ENEMY_COUNT_MAX
|
||||
);
|
||||
}
|
||||
item->startBattle.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) {
|
||||
yyjson_val *stats = yyjson_obj_get(enemyVal, "stats");
|
||||
if(!yyjson_is_obj(stats)) {
|
||||
errorThrow(
|
||||
"Cutscene item (START_BATTLE): enemies[%u] is missing 'stats'",
|
||||
(uint32_t)e
|
||||
);
|
||||
}
|
||||
cutscenestartbattleenemy_t *enemy = &item->startBattle.enemies[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->healthMax =
|
||||
(uint16_t)yyjson_get_int(yyjson_obj_get(enemyVal, "healthMax"));
|
||||
enemy->mpMax =
|
||||
(uint16_t)yyjson_get_int(yyjson_obj_get(enemyVal, "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 (START_BATTLE): 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) {
|
||||
battleability_t ability;
|
||||
if(!cutsceneJsonLookupBattleAbility(
|
||||
yyjson_get_str(abilityVal), &ability
|
||||
)) {
|
||||
errorThrow(
|
||||
"Cutscene item (START_BATTLE): enemies[%u].abilities[%u] is "
|
||||
"invalid",
|
||||
(uint32_t)e, (uint32_t)a
|
||||
);
|
||||
}
|
||||
enemy->abilities[a] = ability;
|
||||
a++;
|
||||
}
|
||||
}
|
||||
e++;
|
||||
}
|
||||
|
||||
errorOk();
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "rpg/battle/battle.h"
|
||||
#include "rpg/battle/party.h"
|
||||
#include "rpg/cutscene/item/cutsceneitembase.h"
|
||||
|
||||
#define CUTSCENE_START_BATTLE_ENEMY_COUNT_MAX 4
|
||||
|
||||
typedef struct {
|
||||
battlefighterstats_t stats;
|
||||
uint16_t healthMax;
|
||||
uint16_t mpMax;
|
||||
battleability_t abilities[BATTLE_FIGHTER_ABILITY_COUNT_MAX];
|
||||
} cutscenestartbattleenemy_t;
|
||||
|
||||
typedef struct {
|
||||
battleencountertype_t encounterType;
|
||||
bool_t fleeAvailable;
|
||||
uint8_t enemyCount;
|
||||
cutscenestartbattleenemy_t enemies[CUTSCENE_START_BATTLE_ENEMY_COUNT_MAX];
|
||||
|
||||
// Active party order slots (see party.h) to bring into the battle, in
|
||||
// the order they should be added. Defaults to every active slot, 0
|
||||
// through PARTY_ACTIVE_SIZE_MAX - 1 in order, when the item's JSON
|
||||
// omits "players" - see cutsceneStartBattleLoad.
|
||||
uint8_t players[PARTY_ACTIVE_SIZE_MAX];
|
||||
uint8_t playerCount;
|
||||
} cutscenestartbattle_t;
|
||||
|
||||
/**
|
||||
* Starts a battle: seeds BATTLE with the party's active order members
|
||||
* and the item's configured enemies, then switches to the battle
|
||||
* scene.
|
||||
*
|
||||
* @param item The cutscene item.
|
||||
* @param data Runtime data storage.
|
||||
*/
|
||||
void cutsceneStartBattleStart(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
);
|
||||
|
||||
/**
|
||||
* Waits for the battle to produce a result, syncs ally HP/MP back to
|
||||
* the party roster, then returns to the overworld scene.
|
||||
*
|
||||
* @param item The cutscene item.
|
||||
* @param data Runtime data storage.
|
||||
* @returns true once the battle has ended.
|
||||
*/
|
||||
bool_t cutsceneStartBattleUpdate(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
);
|
||||
|
||||
/**
|
||||
* Parses a START_BATTLE 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 cutsceneStartBattleLoad(
|
||||
yyjson_val *itemObj,
|
||||
cutsceneitem_t *item
|
||||
);
|
||||
@@ -7,9 +7,6 @@
|
||||
|
||||
#include "rpg/cutscene/cutscenesystem.h"
|
||||
#include "rpg/cutscene/item/cutsceneitembase.h"
|
||||
#include "asset/asset.h"
|
||||
#include "asset/assetfile.h"
|
||||
#include "asset/loader/assetloader.h"
|
||||
#include "util/memory.h"
|
||||
#include "util/string.h"
|
||||
#include "ui/overlay/uifatalerror.h"
|
||||
@@ -17,27 +14,7 @@
|
||||
cutsceneitemcallbacks_t CUTSCENE_ITEM_CALLBACKS[CUTSCENE_ITEM_TYPE_COUNT] = {
|
||||
[CUTSCENE_ITEM_TYPE_NULL] = { 0 },
|
||||
|
||||
[CUTSCENE_ITEM_TYPE_TEXT] = {
|
||||
.init = cutsceneTextStart,
|
||||
.update = cutsceneTextUpdate,
|
||||
.jsonName = "TEXT",
|
||||
.load = cutsceneTextLoad
|
||||
},
|
||||
|
||||
[CUTSCENE_ITEM_TYPE_TEXT_MINI] = {
|
||||
.init = cutsceneTextMiniStart,
|
||||
.update = cutsceneTextMiniUpdate,
|
||||
.jsonName = "TEXT_MINI",
|
||||
.load = cutsceneTextMiniLoad
|
||||
},
|
||||
|
||||
[CUTSCENE_ITEM_TYPE_TEXT_MINI_HIDE] = {
|
||||
.init = cutsceneTextMiniHideStart,
|
||||
.update = cutsceneTextMiniHideUpdate,
|
||||
.jsonName = "TEXT_MINI_HIDE",
|
||||
.load = cutsceneTextMiniHideLoad
|
||||
},
|
||||
|
||||
// Control
|
||||
[CUTSCENE_ITEM_TYPE_CALLBACK] = {
|
||||
.init = cutsceneCallbackStart,
|
||||
.update = cutsceneCallbackUpdate
|
||||
@@ -57,25 +34,11 @@ cutsceneitemcallbacks_t CUTSCENE_ITEM_CALLBACKS[CUTSCENE_ITEM_TYPE_COUNT] = {
|
||||
.load = cutsceneCutsceneLoad
|
||||
},
|
||||
|
||||
[CUTSCENE_ITEM_TYPE_ENTITY_TELEPORT] = {
|
||||
.init = cutsceneEntityTeleportStart,
|
||||
.update = cutsceneEntityTeleportUpdate,
|
||||
.jsonName = "ENTITY_TELEPORT",
|
||||
.load = cutsceneEntityTeleportLoad
|
||||
},
|
||||
|
||||
[CUTSCENE_ITEM_TYPE_ENTITY_WALK_TO] = {
|
||||
.init = cutsceneEntityWalkToStart,
|
||||
.update = cutsceneEntityWalkToUpdate,
|
||||
.jsonName = "ENTITY_WALK_TO",
|
||||
.load = cutsceneEntityWalkToLoad
|
||||
},
|
||||
|
||||
[CUTSCENE_ITEM_TYPE_FADE] = {
|
||||
.init = cutsceneFadeStart,
|
||||
.update = cutsceneFadeUpdate,
|
||||
.jsonName = "FADE",
|
||||
.load = cutsceneFadeLoad
|
||||
[CUTSCENE_ITEM_TYPE_INSERT] = {
|
||||
.init = cutsceneInsertStart,
|
||||
.update = cutsceneInsertUpdate,
|
||||
.jsonName = "INSERT",
|
||||
.load = cutsceneInsertLoad
|
||||
},
|
||||
|
||||
[CUTSCENE_ITEM_TYPE_SET_PAUSE] = {
|
||||
@@ -90,122 +53,6 @@ cutsceneitemcallbacks_t CUTSCENE_ITEM_CALLBACKS[CUTSCENE_ITEM_TYPE_COUNT] = {
|
||||
.update = cutsceneConcurrentUpdate
|
||||
},
|
||||
|
||||
[CUTSCENE_ITEM_TYPE_ITEM_GIVE] = {
|
||||
.init = cutsceneItemGiveStart,
|
||||
.update = cutsceneItemGiveUpdate,
|
||||
.jsonName = "ITEM_GIVE",
|
||||
.load = cutsceneItemGiveLoad
|
||||
},
|
||||
|
||||
[CUTSCENE_ITEM_TYPE_ENTITY_REMOVE] = {
|
||||
.init = cutsceneEntityRemoveStart,
|
||||
.update = cutsceneEntityRemoveUpdate,
|
||||
.jsonName = "ENTITY_REMOVE",
|
||||
.load = cutsceneEntityRemoveLoad
|
||||
},
|
||||
|
||||
[CUTSCENE_ITEM_TYPE_ENTITY_ADD] = {
|
||||
.init = cutsceneEntityAddStart,
|
||||
.update = cutsceneEntityAddUpdate,
|
||||
.jsonName = "ENTITY_ADD",
|
||||
.load = cutsceneEntityAddLoad
|
||||
},
|
||||
|
||||
[CUTSCENE_ITEM_TYPE_ENTITY_TURN] = {
|
||||
.init = cutsceneEntityTurnStart,
|
||||
.update = cutsceneEntityTurnUpdate,
|
||||
.jsonName = "ENTITY_TURN",
|
||||
.load = cutsceneEntityTurnLoad
|
||||
},
|
||||
|
||||
[CUTSCENE_ITEM_TYPE_ENTITY_WALK_TO_ENTITY] = {
|
||||
.init = cutsceneEntityWalkToEntityStart,
|
||||
.update = cutsceneEntityWalkToEntityUpdate,
|
||||
.jsonName = "ENTITY_WALK_TO_ENTITY",
|
||||
.load = cutsceneEntityWalkToEntityLoad
|
||||
},
|
||||
|
||||
[CUTSCENE_ITEM_TYPE_MAP_AREA_ADD] = {
|
||||
.init = cutsceneMapAreaAddStart,
|
||||
.update = cutsceneMapAreaAddUpdate
|
||||
},
|
||||
|
||||
[CUTSCENE_ITEM_TYPE_MAP_AREA_REMOVE] = {
|
||||
.init = cutsceneMapAreaRemoveStart,
|
||||
.update = cutsceneMapAreaRemoveUpdate,
|
||||
.jsonName = "MAP_AREA_REMOVE",
|
||||
.load = cutsceneMapAreaRemoveLoad
|
||||
},
|
||||
|
||||
[CUTSCENE_ITEM_TYPE_MAP_AREA_WAIT] = {
|
||||
.init = cutsceneMapAreaWaitStart,
|
||||
.update = cutsceneMapAreaWaitUpdate,
|
||||
.jsonName = "MAP_AREA_WAIT",
|
||||
.load = cutsceneMapAreaWaitLoad
|
||||
},
|
||||
|
||||
[CUTSCENE_ITEM_TYPE_START_BATTLE] = {
|
||||
.init = cutsceneStartBattleStart,
|
||||
.update = cutsceneStartBattleUpdate,
|
||||
.jsonName = "START_BATTLE",
|
||||
.load = cutsceneStartBattleLoad
|
||||
},
|
||||
|
||||
[CUTSCENE_ITEM_TYPE_EMOJI] = {
|
||||
.init = cutsceneEmojiStart,
|
||||
.update = cutsceneEmojiUpdate,
|
||||
.jsonName = "EMOJI",
|
||||
.load = cutsceneEmojiLoad
|
||||
},
|
||||
|
||||
[CUTSCENE_ITEM_TYPE_SHAKE] = {
|
||||
.init = cutsceneShakeStart,
|
||||
.update = cutsceneShakeUpdate,
|
||||
.jsonName = "SHAKE",
|
||||
.load = cutsceneShakeLoad
|
||||
},
|
||||
|
||||
[CUTSCENE_ITEM_TYPE_BATTLE_WAIT_STATE] = {
|
||||
.update = cutsceneBattleWaitStateUpdate,
|
||||
.jsonName = "BATTLE_WAIT_STATE",
|
||||
.load = cutsceneBattleWaitStateLoad
|
||||
},
|
||||
|
||||
[CUTSCENE_ITEM_TYPE_BATTLE_FORCE_ACTION] = {
|
||||
.init = cutsceneBattleForceActionStart,
|
||||
.update = cutsceneBattleForceActionUpdate,
|
||||
.jsonName = "BATTLE_FORCE_ACTION",
|
||||
.load = cutsceneBattleForceActionLoad
|
||||
},
|
||||
|
||||
[CUTSCENE_ITEM_TYPE_MODAL] = {
|
||||
.init = cutsceneModalStart,
|
||||
.update = cutsceneModalUpdate,
|
||||
.jsonName = "MODAL",
|
||||
.load = cutsceneModalLoad
|
||||
},
|
||||
|
||||
[CUTSCENE_ITEM_TYPE_MODAL_OPTIONS_MARKERS] = {
|
||||
.init = cutsceneModalOptionsMarkersStart,
|
||||
.update = cutsceneModalOptionsMarkersUpdate,
|
||||
.jsonName = "MODAL_OPTIONS_MARKERS",
|
||||
.load = cutsceneModalOptionsMarkersLoad
|
||||
},
|
||||
|
||||
[CUTSCENE_ITEM_TYPE_MODAL_CLOSE] = {
|
||||
.init = cutsceneModalCloseStart,
|
||||
.update = cutsceneModalCloseUpdate,
|
||||
.jsonName = "MODAL_CLOSE",
|
||||
.load = cutsceneItemLoadNoop
|
||||
},
|
||||
|
||||
[CUTSCENE_ITEM_TYPE_PRINT] = {
|
||||
.init = cutscenePrintStart,
|
||||
.update = cutscenePrintUpdate,
|
||||
.jsonName = "PRINT",
|
||||
.load = cutscenePrintLoad
|
||||
},
|
||||
|
||||
[CUTSCENE_ITEM_TYPE_MARKER] = {
|
||||
.init = cutsceneMarkerStart,
|
||||
.update = cutsceneMarkerUpdate,
|
||||
@@ -234,6 +81,207 @@ cutsceneitemcallbacks_t CUTSCENE_ITEM_CALLBACKS[CUTSCENE_ITEM_TYPE_COUNT] = {
|
||||
.load = cutsceneSceneLoad
|
||||
},
|
||||
|
||||
[CUTSCENE_ITEM_TYPE_IDLE] = {
|
||||
.update = cutsceneIdleUpdate,
|
||||
.jsonName = "IDLE",
|
||||
.load = cutsceneItemLoadNoop
|
||||
},
|
||||
|
||||
// Entity
|
||||
[CUTSCENE_ITEM_TYPE_ENTITY_TELEPORT] = {
|
||||
.init = cutsceneEntityTeleportStart,
|
||||
.update = cutsceneEntityTeleportUpdate,
|
||||
.jsonName = "ENTITY_TELEPORT",
|
||||
.load = cutsceneEntityTeleportLoad
|
||||
},
|
||||
|
||||
[CUTSCENE_ITEM_TYPE_ENTITY_WALK_TO] = {
|
||||
.init = cutsceneEntityWalkToStart,
|
||||
.update = cutsceneEntityWalkToUpdate,
|
||||
.jsonName = "ENTITY_WALK_TO",
|
||||
.load = cutsceneEntityWalkToLoad
|
||||
},
|
||||
|
||||
[CUTSCENE_ITEM_TYPE_ENTITY_WALK_TO_ENTITY] = {
|
||||
.init = cutsceneEntityWalkToEntityStart,
|
||||
.update = cutsceneEntityWalkToEntityUpdate,
|
||||
.jsonName = "ENTITY_WALK_TO_ENTITY",
|
||||
.load = cutsceneEntityWalkToEntityLoad
|
||||
},
|
||||
|
||||
[CUTSCENE_ITEM_TYPE_ENTITY_REMOVE] = {
|
||||
.init = cutsceneEntityRemoveStart,
|
||||
.update = cutsceneEntityRemoveUpdate,
|
||||
.jsonName = "ENTITY_REMOVE",
|
||||
.load = cutsceneEntityRemoveLoad
|
||||
},
|
||||
|
||||
[CUTSCENE_ITEM_TYPE_ENTITY_ADD] = {
|
||||
.init = cutsceneEntityAddStart,
|
||||
.update = cutsceneEntityAddUpdate,
|
||||
.jsonName = "ENTITY_ADD",
|
||||
.load = cutsceneEntityAddLoad
|
||||
},
|
||||
|
||||
[CUTSCENE_ITEM_TYPE_ENTITY_TURN] = {
|
||||
.init = cutsceneEntityTurnStart,
|
||||
.update = cutsceneEntityTurnUpdate,
|
||||
.jsonName = "ENTITY_TURN",
|
||||
.load = cutsceneEntityTurnLoad
|
||||
},
|
||||
|
||||
// UI
|
||||
[CUTSCENE_ITEM_TYPE_TEXT] = {
|
||||
.init = cutsceneTextStart,
|
||||
.update = cutsceneTextUpdate,
|
||||
.jsonName = "TEXT",
|
||||
.load = cutsceneTextLoad
|
||||
},
|
||||
|
||||
[CUTSCENE_ITEM_TYPE_TEXT_MINI] = {
|
||||
.init = cutsceneTextMiniStart,
|
||||
.update = cutsceneTextMiniUpdate,
|
||||
.jsonName = "TEXT_MINI",
|
||||
.load = cutsceneTextMiniLoad
|
||||
},
|
||||
|
||||
[CUTSCENE_ITEM_TYPE_TEXT_MINI_HIDE] = {
|
||||
.init = cutsceneTextMiniHideStart,
|
||||
.update = cutsceneTextMiniHideUpdate,
|
||||
.jsonName = "TEXT_MINI_HIDE",
|
||||
.load = cutsceneTextMiniHideLoad
|
||||
},
|
||||
|
||||
[CUTSCENE_ITEM_TYPE_PRINT] = {
|
||||
.init = cutscenePrintStart,
|
||||
.update = cutscenePrintUpdate,
|
||||
.jsonName = "PRINT",
|
||||
.load = cutscenePrintLoad
|
||||
},
|
||||
|
||||
[CUTSCENE_ITEM_TYPE_FADE] = {
|
||||
.init = cutsceneFadeStart,
|
||||
.update = cutsceneFadeUpdate,
|
||||
.jsonName = "FADE",
|
||||
.load = cutsceneFadeLoad
|
||||
},
|
||||
|
||||
[CUTSCENE_ITEM_TYPE_EMOJI] = {
|
||||
.init = cutsceneEmojiStart,
|
||||
.update = cutsceneEmojiUpdate,
|
||||
.jsonName = "EMOJI",
|
||||
.load = cutsceneEmojiLoad
|
||||
},
|
||||
|
||||
[CUTSCENE_ITEM_TYPE_SHAKE] = {
|
||||
.init = cutsceneShakeStart,
|
||||
.update = cutsceneShakeUpdate,
|
||||
.jsonName = "SHAKE",
|
||||
.load = cutsceneShakeLoad
|
||||
},
|
||||
|
||||
[CUTSCENE_ITEM_TYPE_MODAL] = {
|
||||
.init = cutsceneModalStart,
|
||||
.update = cutsceneModalUpdate,
|
||||
.jsonName = "MODAL",
|
||||
.load = cutsceneModalLoad
|
||||
},
|
||||
|
||||
[CUTSCENE_ITEM_TYPE_MODAL_OPTIONS_MARKERS] = {
|
||||
.init = cutsceneModalOptionsMarkersStart,
|
||||
.update = cutsceneModalOptionsMarkersUpdate,
|
||||
.jsonName = "MODAL_OPTIONS_MARKERS",
|
||||
.load = cutsceneModalOptionsMarkersLoad
|
||||
},
|
||||
|
||||
[CUTSCENE_ITEM_TYPE_MODAL_CLOSE] = {
|
||||
.init = cutsceneModalCloseStart,
|
||||
.update = cutsceneModalCloseUpdate,
|
||||
.jsonName = "MODAL_CLOSE",
|
||||
.load = cutsceneItemLoadNoop
|
||||
},
|
||||
|
||||
[CUTSCENE_ITEM_TYPE_KEYBOARD] = {
|
||||
.init = cutsceneKeyboardStart,
|
||||
.update = cutsceneKeyboardUpdate
|
||||
},
|
||||
|
||||
[CUTSCENE_ITEM_TYPE_UI_SHOW] = {
|
||||
.init = cutsceneUIShowStart,
|
||||
.update = cutsceneUIShowUpdate,
|
||||
.jsonName = "UI_SHOW",
|
||||
.load = cutsceneUIShowLoad
|
||||
},
|
||||
|
||||
// Item
|
||||
[CUTSCENE_ITEM_TYPE_ITEM_GIVE] = {
|
||||
.init = cutsceneItemGiveStart,
|
||||
.update = cutsceneItemGiveUpdate,
|
||||
.jsonName = "ITEM_GIVE",
|
||||
.load = cutsceneItemGiveLoad
|
||||
},
|
||||
|
||||
// Map area
|
||||
[CUTSCENE_ITEM_TYPE_MAP_AREA_ADD] = {
|
||||
.init = cutsceneMapAreaAddStart,
|
||||
.update = cutsceneMapAreaAddUpdate
|
||||
},
|
||||
|
||||
[CUTSCENE_ITEM_TYPE_MAP_AREA_REMOVE] = {
|
||||
.init = cutsceneMapAreaRemoveStart,
|
||||
.update = cutsceneMapAreaRemoveUpdate,
|
||||
.jsonName = "MAP_AREA_REMOVE",
|
||||
.load = cutsceneMapAreaRemoveLoad
|
||||
},
|
||||
|
||||
[CUTSCENE_ITEM_TYPE_MAP_AREA_WAIT] = {
|
||||
.init = cutsceneMapAreaWaitStart,
|
||||
.update = cutsceneMapAreaWaitUpdate,
|
||||
.jsonName = "MAP_AREA_WAIT",
|
||||
.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
|
||||
},
|
||||
|
||||
[CUTSCENE_ITEM_TYPE_BATTLE_POST_MOVE] = {
|
||||
.init = cutsceneBattlePostMoveStart,
|
||||
.update = cutsceneBattlePostMoveUpdate
|
||||
},
|
||||
|
||||
[CUTSCENE_ITEM_TYPE_BATTLE_FIGHTER_DAMAGE] = {
|
||||
.init = cutsceneBattleFighterDamageStart,
|
||||
.update = cutsceneBattleFighterDamageUpdate,
|
||||
.jsonName = "BATTLE_FIGHTER_DAMAGE",
|
||||
.load = cutsceneBattleFighterDamageLoad
|
||||
},
|
||||
|
||||
[CUTSCENE_ITEM_TYPE_BATTLE_FIGHTER_HEAL] = {
|
||||
.init = cutsceneBattleFighterHealStart,
|
||||
.update = cutsceneBattleFighterHealUpdate,
|
||||
.jsonName = "BATTLE_FIGHTER_HEAL",
|
||||
.load = cutsceneBattleFighterHealLoad
|
||||
},
|
||||
|
||||
// Save
|
||||
[CUTSCENE_ITEM_TYPE_SAVE_DEVICE_CHECK] = {
|
||||
.init = cutsceneSaveDeviceCheckStart,
|
||||
.update = cutsceneSaveDeviceCheckUpdate,
|
||||
@@ -248,11 +296,7 @@ cutsceneitemcallbacks_t CUTSCENE_ITEM_CALLBACKS[CUTSCENE_ITEM_TYPE_COUNT] = {
|
||||
.load = cutsceneSaveLoadAllSlotsLoad
|
||||
},
|
||||
|
||||
[CUTSCENE_ITEM_TYPE_KEYBOARD] = {
|
||||
.init = cutsceneKeyboardStart,
|
||||
.update = cutsceneKeyboardUpdate
|
||||
},
|
||||
|
||||
// Audio
|
||||
[CUTSCENE_ITEM_TYPE_AUDIO_PLAY] = {
|
||||
.init = cutsceneAudioPlayStart,
|
||||
.update = cutsceneAudioPlayUpdate,
|
||||
@@ -315,32 +359,7 @@ cutsceneitemcallbacks_t CUTSCENE_ITEM_CALLBACKS[CUTSCENE_ITEM_TYPE_COUNT] = {
|
||||
.load = cutsceneAudioSetLoad
|
||||
},
|
||||
|
||||
[CUTSCENE_ITEM_TYPE_IDLE] = {
|
||||
.update = cutsceneIdleUpdate,
|
||||
.jsonName = "IDLE",
|
||||
.load = cutsceneItemLoadNoop
|
||||
},
|
||||
|
||||
[CUTSCENE_ITEM_TYPE_UI_SHOW] = {
|
||||
.init = cutsceneUIShowStart,
|
||||
.update = cutsceneUIShowUpdate,
|
||||
.jsonName = "UI_SHOW",
|
||||
.load = cutsceneUIShowLoad
|
||||
},
|
||||
|
||||
[CUTSCENE_ITEM_TYPE_REGULAR_BATTLE] = {
|
||||
.update = cutsceneRegularBattleUpdate,
|
||||
.jsonName = "REGULAR_BATTLE",
|
||||
.load = cutsceneItemLoadNoop
|
||||
},
|
||||
|
||||
[CUTSCENE_ITEM_TYPE_INSERT] = {
|
||||
.init = cutsceneInsertStart,
|
||||
.update = cutsceneInsertUpdate,
|
||||
.jsonName = "INSERT",
|
||||
.load = cutsceneInsertLoad
|
||||
},
|
||||
|
||||
// Story
|
||||
[CUTSCENE_ITEM_TYPE_STORY_SET_FLAG] = {
|
||||
.init = cutsceneStorySetFlagStart,
|
||||
.update = cutsceneStorySetFlagUpdate,
|
||||
@@ -411,30 +430,21 @@ bool_t cutsceneCutsceneUpdate(
|
||||
}
|
||||
|
||||
cutscene_t * cutsceneCutsceneResolve(const char_t *name) {
|
||||
assetentry_t *entry = assetLock(name, ASSET_LOADER_TYPE_JSON, NULL);
|
||||
errorret_t result = assetRequireLoaded(entry);
|
||||
errorret_t result = cutsceneLoadParse(
|
||||
name, &CUTSCENE_SYSTEM.loadedScene, CUTSCENE_SYSTEM.loadedItems,
|
||||
CUTSCENE_LOADED_ITEMS_MAX
|
||||
);
|
||||
if(errorIsNotOk(result)) {
|
||||
assetUnlockEntry(entry);
|
||||
errorCatch(errorPrint(result));
|
||||
uiFatalErrorOpen(NULL);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Parse straight out of the entry's own doc while still locked - every
|
||||
// cutsceneitem_t field that could reference it owns its string data by
|
||||
// value (see cutscenecutsceneref_t/cutscenemarker_t's doc comments), so
|
||||
// nothing needs to survive past this call and the entry can just be
|
||||
// unlocked like any other asset once parsing finishes.
|
||||
errorret_t parseResult = cutsceneParseDoc(
|
||||
entry->data.json, &CUTSCENE_SYSTEM.loadedScene, CUTSCENE_SYSTEM.loadedItems,
|
||||
CUTSCENE_LOADED_ITEMS_MAX
|
||||
);
|
||||
assetUnlockEntry(entry);
|
||||
if(errorIsNotOk(parseResult)) {
|
||||
errorCatch(errorPrint(parseResult));
|
||||
uiFatalErrorOpen(NULL);
|
||||
return NULL;
|
||||
}
|
||||
// Shares loadedScene/loadedItems with cutsceneSystemLoad, so track the
|
||||
// source the same way it does - cutsceneGoTo needs this to reload the
|
||||
// untouched original when jumping to a marker behind the current
|
||||
// position (see cutsceneGoTo's doc comment).
|
||||
stringCopy(CUTSCENE_SYSTEM.loadedFile, name, ASSET_FILE_NAME_MAX - 1);
|
||||
|
||||
return &CUTSCENE_SYSTEM.loadedScene;
|
||||
}
|
||||
@@ -489,28 +499,14 @@ bool_t cutsceneInsertUpdate(
|
||||
}
|
||||
|
||||
void cutsceneInsertResolveAndSplice(const char_t *name) {
|
||||
assetentry_t *entry = assetLock(name, ASSET_LOADER_TYPE_JSON, NULL);
|
||||
errorret_t result = assetRequireLoaded(entry);
|
||||
if(errorIsNotOk(result)) {
|
||||
assetUnlockEntry(entry);
|
||||
errorCatch(errorPrint(result));
|
||||
uiFatalErrorOpen(NULL);
|
||||
return;
|
||||
}
|
||||
|
||||
// Local scratch, not a shared buffer - this target is spliced straight
|
||||
// into the running scene below and never referenced again afterward, so
|
||||
// it only needs to live for the rest of this call. Parsed straight out
|
||||
// of the entry's own doc while still locked, then unlocked like any
|
||||
// other asset - see cutsceneCutsceneResolve's matching comment for why
|
||||
// nothing needs to keep the doc itself resident past the parse.
|
||||
// it only needs to live for the rest of this call.
|
||||
cutsceneitem_t items[CUTSCENE_INSERT_ITEMS_MAX];
|
||||
cutscene_t scene = { .items = items };
|
||||
errorret_t parseResult =
|
||||
cutsceneParseDoc(entry->data.json, &scene, items, CUTSCENE_INSERT_ITEMS_MAX);
|
||||
assetUnlockEntry(entry);
|
||||
if(errorIsNotOk(parseResult)) {
|
||||
errorCatch(errorPrint(parseResult));
|
||||
errorret_t result = cutsceneLoadParse(name, &scene, items, CUTSCENE_INSERT_ITEMS_MAX);
|
||||
if(errorIsNotOk(result)) {
|
||||
errorCatch(errorPrint(result));
|
||||
uiFatalErrorOpen(NULL);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -40,10 +40,12 @@
|
||||
#include "maparea/cutscenemapareaadd.h"
|
||||
#include "maparea/cutscenemaparearemove.h"
|
||||
#include "maparea/cutscenemapareawait.h"
|
||||
#include "battle/cutscenestartbattle.h"
|
||||
#include "battle/cutscenebattlewaitstate.h"
|
||||
#include "battle/cutscenebattleforceaction.h"
|
||||
#include "battle/cutsceneregularbattle.h"
|
||||
#include "battle/cutscenebattlestart.h"
|
||||
#include "battle/cutscenebattlesetstate.h"
|
||||
#include "battle/cutscenebattlewaitselection.h"
|
||||
#include "battle/cutscenebattlepostmove.h"
|
||||
#include "battle/cutscenebattlefighterdamage.h"
|
||||
#include "battle/cutscenebattlefighterheal.h"
|
||||
#include "save/cutscenesavedevicecheck.h"
|
||||
#include "save/cutscenesaveloadallslots.h"
|
||||
#include "audio/cutsceneaudioplay.h"
|
||||
@@ -116,11 +118,12 @@ struct cutsceneitem_s {
|
||||
cutscenemapareaadd_t mapAreaAdd;
|
||||
cutscenemaparearemove_t mapAreaRemove;
|
||||
cutscenemapareawait_t mapAreaWait;
|
||||
cutscenestartbattle_t startBattle;
|
||||
cutscenebattlestart_t battleStart;
|
||||
cutscenebattlesetstate_t battleSetState;
|
||||
cutscenebattlefighterdamage_t battleFighterDamage;
|
||||
cutscenebattlefighterheal_t battleFighterHeal;
|
||||
cutsceneemoji_t emoji;
|
||||
cutsceneshake_t shake;
|
||||
cutscenebattlewaitstate_t battleWaitState;
|
||||
cutscenebattleforceaction_t battleForceAction;
|
||||
cutscenemodal_t modal;
|
||||
cutscenemodaloptionsmarkers_t modalOptionsMarkers;
|
||||
cutsceneprint_t print;
|
||||
|
||||
@@ -16,41 +16,62 @@ typedef union cutsceneitemdata_u cutsceneitemdata_t;
|
||||
typedef enum {
|
||||
CUTSCENE_ITEM_TYPE_NULL,
|
||||
|
||||
CUTSCENE_ITEM_TYPE_TEXT,
|
||||
CUTSCENE_ITEM_TYPE_TEXT_MINI,
|
||||
CUTSCENE_ITEM_TYPE_TEXT_MINI_HIDE,
|
||||
// Control
|
||||
CUTSCENE_ITEM_TYPE_CALLBACK,
|
||||
CUTSCENE_ITEM_TYPE_WAIT,
|
||||
CUTSCENE_ITEM_TYPE_CUTSCENE,
|
||||
CUTSCENE_ITEM_TYPE_ENTITY_TELEPORT,
|
||||
CUTSCENE_ITEM_TYPE_ENTITY_WALK_TO,
|
||||
CUTSCENE_ITEM_TYPE_FADE,
|
||||
CUTSCENE_ITEM_TYPE_INSERT,
|
||||
CUTSCENE_ITEM_TYPE_SET_PAUSE,
|
||||
CUTSCENE_ITEM_TYPE_CONCURRENT,
|
||||
CUTSCENE_ITEM_TYPE_ITEM_GIVE,
|
||||
CUTSCENE_ITEM_TYPE_ENTITY_REMOVE,
|
||||
CUTSCENE_ITEM_TYPE_ENTITY_ADD,
|
||||
CUTSCENE_ITEM_TYPE_ENTITY_TURN,
|
||||
CUTSCENE_ITEM_TYPE_ENTITY_WALK_TO_ENTITY,
|
||||
CUTSCENE_ITEM_TYPE_MAP_AREA_ADD,
|
||||
CUTSCENE_ITEM_TYPE_MAP_AREA_REMOVE,
|
||||
CUTSCENE_ITEM_TYPE_MAP_AREA_WAIT,
|
||||
CUTSCENE_ITEM_TYPE_START_BATTLE,
|
||||
CUTSCENE_ITEM_TYPE_EMOJI,
|
||||
CUTSCENE_ITEM_TYPE_SHAKE,
|
||||
CUTSCENE_ITEM_TYPE_BATTLE_WAIT_STATE,
|
||||
CUTSCENE_ITEM_TYPE_BATTLE_FORCE_ACTION,
|
||||
CUTSCENE_ITEM_TYPE_MODAL,
|
||||
CUTSCENE_ITEM_TYPE_MODAL_OPTIONS_MARKERS,
|
||||
CUTSCENE_ITEM_TYPE_MODAL_CLOSE,
|
||||
CUTSCENE_ITEM_TYPE_PRINT,
|
||||
CUTSCENE_ITEM_TYPE_MARKER,
|
||||
CUTSCENE_ITEM_TYPE_RESTART,
|
||||
CUTSCENE_ITEM_TYPE_QUIT_GAME,
|
||||
CUTSCENE_ITEM_TYPE_SCENE,
|
||||
CUTSCENE_ITEM_TYPE_IDLE,
|
||||
|
||||
// Entity
|
||||
CUTSCENE_ITEM_TYPE_ENTITY_TELEPORT,
|
||||
CUTSCENE_ITEM_TYPE_ENTITY_WALK_TO,
|
||||
CUTSCENE_ITEM_TYPE_ENTITY_WALK_TO_ENTITY,
|
||||
CUTSCENE_ITEM_TYPE_ENTITY_REMOVE,
|
||||
CUTSCENE_ITEM_TYPE_ENTITY_ADD,
|
||||
CUTSCENE_ITEM_TYPE_ENTITY_TURN,
|
||||
|
||||
// UI
|
||||
CUTSCENE_ITEM_TYPE_TEXT,
|
||||
CUTSCENE_ITEM_TYPE_TEXT_MINI,
|
||||
CUTSCENE_ITEM_TYPE_TEXT_MINI_HIDE,
|
||||
CUTSCENE_ITEM_TYPE_PRINT,
|
||||
CUTSCENE_ITEM_TYPE_FADE,
|
||||
CUTSCENE_ITEM_TYPE_EMOJI,
|
||||
CUTSCENE_ITEM_TYPE_SHAKE,
|
||||
CUTSCENE_ITEM_TYPE_MODAL,
|
||||
CUTSCENE_ITEM_TYPE_MODAL_OPTIONS_MARKERS,
|
||||
CUTSCENE_ITEM_TYPE_MODAL_CLOSE,
|
||||
CUTSCENE_ITEM_TYPE_KEYBOARD,
|
||||
CUTSCENE_ITEM_TYPE_UI_SHOW,
|
||||
|
||||
// Item
|
||||
CUTSCENE_ITEM_TYPE_ITEM_GIVE,
|
||||
|
||||
// Map area
|
||||
CUTSCENE_ITEM_TYPE_MAP_AREA_ADD,
|
||||
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,
|
||||
CUTSCENE_ITEM_TYPE_BATTLE_POST_MOVE,
|
||||
CUTSCENE_ITEM_TYPE_BATTLE_FIGHTER_DAMAGE,
|
||||
CUTSCENE_ITEM_TYPE_BATTLE_FIGHTER_HEAL,
|
||||
|
||||
// Save
|
||||
CUTSCENE_ITEM_TYPE_SAVE_DEVICE_CHECK,
|
||||
CUTSCENE_ITEM_TYPE_SAVE_LOAD_ALL_SLOTS,
|
||||
CUTSCENE_ITEM_TYPE_KEYBOARD,
|
||||
|
||||
// Audio
|
||||
CUTSCENE_ITEM_TYPE_AUDIO_PLAY,
|
||||
CUTSCENE_ITEM_TYPE_AUDIO_STOP,
|
||||
CUTSCENE_ITEM_TYPE_AUDIO_PAUSE,
|
||||
@@ -60,10 +81,8 @@ typedef enum {
|
||||
CUTSCENE_ITEM_TYPE_AUDIO_SET_PAN,
|
||||
CUTSCENE_ITEM_TYPE_AUDIO_SET_LOOP,
|
||||
CUTSCENE_ITEM_TYPE_AUDIO_SET,
|
||||
CUTSCENE_ITEM_TYPE_IDLE,
|
||||
CUTSCENE_ITEM_TYPE_UI_SHOW,
|
||||
CUTSCENE_ITEM_TYPE_REGULAR_BATTLE,
|
||||
CUTSCENE_ITEM_TYPE_INSERT,
|
||||
|
||||
// Story
|
||||
CUTSCENE_ITEM_TYPE_STORY_SET_FLAG,
|
||||
CUTSCENE_ITEM_TYPE_STORY_WAIT_FLAG,
|
||||
|
||||
|
||||
@@ -14,8 +14,6 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
cutscenejsonemoji.c
|
||||
cutscenejsonentitytype.c
|
||||
cutscenejsonentitydir.c
|
||||
cutscenejsonbattleencounter.c
|
||||
cutscenejsonbattleability.c
|
||||
cutscenejsonbattlestate.c
|
||||
cutscenejsonscenetype.c
|
||||
cutscenejsonuiscreentype.c
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "cutscenejsonbattleability.h"
|
||||
#include "cutscenejsonenum.h"
|
||||
|
||||
static const cutscenejsonenumentry_t CUTSCENE_JSON_BATTLE_ABILITY_TABLE[] = {
|
||||
{ "FIRE", BATTLE_ABILITY_FIRE },
|
||||
};
|
||||
#define CUTSCENE_JSON_BATTLE_ABILITY_TABLE_COUNT \
|
||||
(sizeof(CUTSCENE_JSON_BATTLE_ABILITY_TABLE) / \
|
||||
sizeof(cutscenejsonenumentry_t))
|
||||
|
||||
bool_t cutsceneJsonLookupBattleAbility(
|
||||
const char_t *name,
|
||||
battleability_t *out
|
||||
) {
|
||||
int32_t value;
|
||||
if(!cutsceneJsonLookupEnumInsensitive(
|
||||
CUTSCENE_JSON_BATTLE_ABILITY_TABLE,
|
||||
CUTSCENE_JSON_BATTLE_ABILITY_TABLE_COUNT, name, &value
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
*out = (battleability_t)value;
|
||||
return true;
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "dusk.h"
|
||||
#include "rpg/battle/battleability.h"
|
||||
|
||||
/**
|
||||
* Looks up name (case-insensitively) against the battle ability table.
|
||||
*
|
||||
* @param name Name to search for.
|
||||
* @param out Set to the matching ability on success.
|
||||
* @return true if a matching entry was found, false otherwise.
|
||||
*/
|
||||
bool_t cutsceneJsonLookupBattleAbility(
|
||||
const char_t *name,
|
||||
battleability_t *out
|
||||
);
|
||||
@@ -1,34 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "cutscenejsonbattleencounter.h"
|
||||
#include "cutscenejsonenum.h"
|
||||
|
||||
static const cutscenejsonenumentry_t
|
||||
CUTSCENE_JSON_BATTLE_ENCOUNTER_TABLE[] = {
|
||||
{ "REGULAR", BATTLE_ENCOUNTER_REGULAR },
|
||||
{ "PLAYER_ADVANTAGE", BATTLE_ENCOUNTER_PLAYER_ADVANTAGE },
|
||||
{ "BACK_ATTACK", BATTLE_ENCOUNTER_BACK_ATTACK },
|
||||
};
|
||||
#define CUTSCENE_JSON_BATTLE_ENCOUNTER_TABLE_COUNT \
|
||||
(sizeof(CUTSCENE_JSON_BATTLE_ENCOUNTER_TABLE) / \
|
||||
sizeof(cutscenejsonenumentry_t))
|
||||
|
||||
bool_t cutsceneJsonLookupBattleEncounter(
|
||||
const char_t *name,
|
||||
battleencountertype_t *out
|
||||
) {
|
||||
int32_t value;
|
||||
if(!cutsceneJsonLookupEnumInsensitive(
|
||||
CUTSCENE_JSON_BATTLE_ENCOUNTER_TABLE,
|
||||
CUTSCENE_JSON_BATTLE_ENCOUNTER_TABLE_COUNT, name, &value
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
*out = (battleencountertype_t)value;
|
||||
return true;
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "dusk.h"
|
||||
#include "rpg/battle/battle.h"
|
||||
|
||||
/**
|
||||
* Looks up name (case-insensitively) against the battle encounter type
|
||||
* table.
|
||||
*
|
||||
* @param name Name to search for.
|
||||
* @param out Set to the matching encounter type on success.
|
||||
* @return true if a matching entry was found, false otherwise.
|
||||
*/
|
||||
bool_t cutsceneJsonLookupBattleEncounter(
|
||||
const char_t *name,
|
||||
battleencountertype_t *out
|
||||
);
|
||||
@@ -9,23 +9,27 @@
|
||||
#include "cutscenejsonenum.h"
|
||||
|
||||
static const cutscenejsonenumentry_t CUTSCENE_JSON_BATTLE_STATE_TABLE[] = {
|
||||
{ "NONE", BATTLE_STATE_NONE },
|
||||
{ "OPENING", BATTLE_STATE_OPENING },
|
||||
{ "PRE_ROUND", BATTLE_STATE_PRE_ROUND },
|
||||
{ "PLAYER_SELECTION", BATTLE_STATE_PLAYER_SELECTION },
|
||||
{ "AI_SELECTION", BATTLE_STATE_AI_SELECTION },
|
||||
{ "MOVES_EXECUTING", BATTLE_STATE_MOVES_EXECUTING },
|
||||
{ "SELECTION", BATTLE_STATE_SELECTION },
|
||||
{ "EXECUTING", BATTLE_STATE_EXECUTING },
|
||||
{ "POST_ROUND", BATTLE_STATE_POST_ROUND },
|
||||
{ "ENDED", BATTLE_STATE_ENDED },
|
||||
{ "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) {
|
||||
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
|
||||
CUTSCENE_JSON_BATTLE_STATE_TABLE,
|
||||
CUTSCENE_JSON_BATTLE_STATE_TABLE_COUNT, name, &value
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -6,14 +6,16 @@
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "dusk.h"
|
||||
#include "rpg/battle/battle.h"
|
||||
#include "rpg/battle/state/battlestate.h"
|
||||
|
||||
/**
|
||||
* Looks up name (case-insensitively) against the battle state table.
|
||||
* Looks up name (case-insensitive) against every battlestate_t name.
|
||||
*
|
||||
* @param name Name to search for.
|
||||
* @param out Set to the matching battle state on success.
|
||||
* @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);
|
||||
bool_t cutsceneJsonLookupBattleState(
|
||||
const char_t *name,
|
||||
battlestate_t *out
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -19,7 +19,7 @@ ENTITY_GLOBAL_CALLBACK(3) {
|
||||
|
||||
ENTITY_GLOBAL_CALLBACK(4) {
|
||||
create->entity->interact.type = ENTITY_INTERACT_CUTSCENE_NAME;
|
||||
create->entity->interact.data.cutsceneName = "battletest";
|
||||
create->entity->interact.data.cutsceneName = "cutscenes/battletest.jsonc";
|
||||
}
|
||||
|
||||
static const entityglobaldef_t ENTITY_GLOBAL_LIST[] = {
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#include "rpg/item/item.h"
|
||||
#include "rpg/item/backpack.h"
|
||||
#include "rpg/battle/party.h"
|
||||
#include "rpg/battle/move/battlemove.h"
|
||||
#include "save/save.h"
|
||||
#include "ui/rpg/textbox/uitextboxminilist.h"
|
||||
#include "time/time.h"
|
||||
@@ -30,6 +31,7 @@ errorret_t rpgInit(void) {
|
||||
|
||||
errorChain(storyInit());
|
||||
errorChain(itemInit());
|
||||
errorChain(battleMoveInit());
|
||||
backpackInit();
|
||||
partyInit();
|
||||
cutsceneSystemInit();
|
||||
|
||||
@@ -34,9 +34,11 @@ void saveSlotNewGame(void) {
|
||||
|
||||
// TEMPORARY: placeholder starting party stats - replace once there's a
|
||||
// real starting-party/character-creation flow.
|
||||
const battlefighterstats_t startingStats =
|
||||
{ .attack = 10, .defense = 5, .magic = 0, .speed = 10, .luck = 0 };
|
||||
partyAddMember(startingStats, 30, 10);
|
||||
const battlefighterstats_t startingStats = {
|
||||
.attack = 10, .defense = 5, .magic = 0, .speed = 10, .luck = 0,
|
||||
.healthMax = 30, .mpMax = 10
|
||||
};
|
||||
partyAddMember(startingStats);
|
||||
}
|
||||
|
||||
bool_t saveSlotInUse(saveslotcache_t *slot) {
|
||||
|
||||
@@ -32,10 +32,10 @@ errorret_t saveSlotVer1WriteJSON(
|
||||
writeUInt32("status", member->status);
|
||||
writeUInt32("team", member->team);
|
||||
writeUInt32("controller", member->controller);
|
||||
writeUInt32("health", member->health);
|
||||
writeUInt32("healthMax", member->healthMax);
|
||||
writeUInt32("mp", member->mp);
|
||||
writeUInt32("mpMax", member->mpMax);
|
||||
writeUInt32("health", member->stats.health);
|
||||
writeUInt32("healthMax", member->stats.healthMax);
|
||||
writeUInt32("mp", member->stats.mp);
|
||||
writeUInt32("mpMax", member->stats.mpMax);
|
||||
writeUInt32("statAttack", member->stats.attack);
|
||||
writeUInt32("statDefense", member->stats.defense);
|
||||
writeUInt32("statMagic", member->stats.magic);
|
||||
@@ -93,10 +93,10 @@ errorret_t saveSlotVer1ReadJSON(saveslotver1_t *slot, yyjson_val *object) {
|
||||
readUInt32(
|
||||
"controller", member->controller, BATTLE_FIGHTER_CONTROLLER_PLAYER
|
||||
);
|
||||
readUInt32("health", member->health, 0);
|
||||
readUInt32("healthMax", member->healthMax, 0);
|
||||
readUInt32("mp", member->mp, 0);
|
||||
readUInt32("mpMax", member->mpMax, 0);
|
||||
readUInt32("health", member->stats.health, 0);
|
||||
readUInt32("healthMax", member->stats.healthMax, 0);
|
||||
readUInt32("mp", member->stats.mp, 0);
|
||||
readUInt32("mpMax", member->stats.mpMax, 0);
|
||||
readUInt32("statAttack", member->stats.attack, 0);
|
||||
readUInt32("statDefense", member->stats.defense, 0);
|
||||
readUInt32("statMagic", member->stats.magic, 0);
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
*/
|
||||
|
||||
#include "scenebattle.h"
|
||||
#include "assert/assert.h"
|
||||
#include "display/display.h"
|
||||
#include "display/displaystate.h"
|
||||
#include "display/shader/shader.h"
|
||||
@@ -45,7 +46,7 @@ errorret_t sceneBattleRender(scenedata_t *sceneData) {
|
||||
.flags = DISPLAY_STATE_FLAG_DEPTH_TEST | DISPLAY_STATE_FLAG_CULL
|
||||
}));
|
||||
|
||||
for(uint8_t i = 0; i < BATTLE_FIGHTER_COUNT_MAX; i++) {
|
||||
for(uint8_t i = 0; i < BATTLE_FIGHTER_POS_COUNT; i++) {
|
||||
errorChain(sceneBattleDrawFighter(i));
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
const battlefighter_t *fighter = &BATTLE.fighters[fighterIndex];
|
||||
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_COUNT_MAX; i++) {
|
||||
if(BATTLE.fighters[i].status == BATTLE_FIGHTER_STATUS_NULL) continue;
|
||||
if(BATTLE.fighters[i].team != fighter->team) continue;
|
||||
for(uint8_t i = 0; i < BATTLE_FIGHTER_POS_COUNT; i++) {
|
||||
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) {
|
||||
const battlefighter_t *fighter = &BATTLE.fighters[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;
|
||||
|
||||
@@ -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_COUNT_MAX; i++) {
|
||||
const battlefighter_t *fighter = &BATTLE.fighters[i];
|
||||
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;
|
||||
@@ -60,14 +61,14 @@ errorret_t uiBattleHudDraw(void) {
|
||||
char_t hpText[UI_BATTLE_HUD_TEXT_MAX];
|
||||
stringFormat(
|
||||
hpText, UI_BATTLE_HUD_TEXT_MAX - 1, UI_BATTLE_HUD.hpFormat,
|
||||
fighter->health, fighter->healthMax
|
||||
fighter->stats.health, fighter->stats.healthMax
|
||||
);
|
||||
errorChain(textDraw(screenPos[0], screenPos[1], hpText, COLOR_WHITE, NULL));
|
||||
|
||||
char_t mpText[UI_BATTLE_HUD_TEXT_MAX];
|
||||
stringFormat(
|
||||
mpText, UI_BATTLE_HUD_TEXT_MAX - 1, UI_BATTLE_HUD.mpFormat,
|
||||
fighter->mp, fighter->mpMax
|
||||
fighter->stats.mp, fighter->stats.mpMax
|
||||
);
|
||||
errorChain(textDraw(
|
||||
screenPos[0], screenPos[1] + UI_BATTLE_HUD_LINE_HEIGHT,
|
||||
|
||||
@@ -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 =
|
||||
@@ -40,8 +41,9 @@ void uiBattleMenuOpenTargets(void) {
|
||||
uiBattleMenuTargetSelected, NULL, NULL
|
||||
);
|
||||
|
||||
for(uint8_t i = 0; i < BATTLE_FIGHTER_COUNT_MAX; i++) {
|
||||
battlefighter_t *candidate = &BATTLE.fighters[i];
|
||||
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;
|
||||
|
||||
@@ -49,7 +51,7 @@ void uiBattleMenuOpenTargets(void) {
|
||||
UI_BATTLE_MENU.targetLabels[menuIndex],
|
||||
UI_BATTLE_MENU_TARGET_LABEL_MAX - 1,
|
||||
UI_BATTLE_MENU.targetFormat,
|
||||
candidate->id, candidate->health, candidate->healthMax
|
||||
candidate->id, candidate->stats.health, candidate->stats.healthMax
|
||||
);
|
||||
UI_BATTLE_MENU.targetFighterIndex[menuIndex] = i;
|
||||
MENU_BUTTON(UI_BATTLE_MENU.targetLabels[menuIndex]);
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
#define UI_BATTLE_MENU_ACTION_INDEX_FLEE 1
|
||||
#define UI_BATTLE_MENU_ACTION_LABEL_MAX 32
|
||||
|
||||
#define UI_BATTLE_MENU_TARGET_ITEM_COUNT BATTLE_FIGHTER_COUNT_MAX
|
||||
#define UI_BATTLE_MENU_TARGET_ITEM_COUNT BATTLE_FIGHTER_POS_COUNT
|
||||
#define UI_BATTLE_MENU_TARGET_LABEL_MAX 32
|
||||
#define UI_BATTLE_MENU_TARGET_FORMAT_MAX 48
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -7,32 +7,9 @@
|
||||
|
||||
#include "dusktest.h"
|
||||
#include "rpg/battle/battle.h"
|
||||
#include "rpg/cutscene/cutscenesystem.h"
|
||||
|
||||
// Fighter slots as added by test_battleCutsceneForceActionOverridesTarget:
|
||||
// 0 = allyA, 1 = allyB, 2 = enemy.
|
||||
static cutsceneitem_t CUTSCENE_TEST_SCRATCH_ITEMS[] = {
|
||||
// Waiting BEFORE pausing is the correct order: pausing first would
|
||||
// freeze BATTLE.state wherever it happened to be and it would never
|
||||
// reach PRE_ROUND on its own to satisfy the wait.
|
||||
{
|
||||
.type = CUTSCENE_ITEM_TYPE_BATTLE_WAIT_STATE,
|
||||
.battleWaitState = { .state = BATTLE_STATE_PRE_ROUND }
|
||||
},
|
||||
{ .type = CUTSCENE_ITEM_TYPE_SET_PAUSE, .setPause = CUTSCENE_PAUSE_BATTLE },
|
||||
// enemy (slot 2) forced onto allyA (slot 0)
|
||||
{
|
||||
.type = CUTSCENE_ITEM_TYPE_BATTLE_FORCE_ACTION,
|
||||
.battleForceAction = { .fighterIndex = 2, .targetIndex = 0 }
|
||||
},
|
||||
{ .type = CUTSCENE_ITEM_TYPE_SET_PAUSE, .setPause = CUTSCENE_PAUSE_NONE }
|
||||
};
|
||||
static cutscene_t CUTSCENE_TEST_SCRATCH = {
|
||||
.items = CUTSCENE_TEST_SCRATCH_ITEMS,
|
||||
.itemCount = sizeof(CUTSCENE_TEST_SCRATCH_ITEMS) / sizeof(cutsceneitem_t),
|
||||
.itemsMax = sizeof(CUTSCENE_TEST_SCRATCH_ITEMS) / sizeof(cutsceneitem_t),
|
||||
.pause = CUTSCENE_PAUSE_NONE
|
||||
};
|
||||
// TEMPORARY: hardcoded pending a real moves.jsonc-backed registry lookup.
|
||||
#define BATTLE_MOVE_ID_ATTACK ((battlemoveid_t)1)
|
||||
|
||||
static battlefighter_t *addFighter(
|
||||
const battlefighterteam_t team,
|
||||
@@ -101,13 +78,13 @@ static void test_battlePlayerSelectionAdvancesAndSkipsDecidedFighters(
|
||||
assert_int_equal(BATTLE.state, BATTLE_STATE_PLAYER_SELECTION);
|
||||
assert_ptr_equal(battleGetCurrentFighter(), ally1);
|
||||
|
||||
// A forced action (as a scripted-battle cutscene item would push) marks
|
||||
// A forced move (as a scripted-battle cutscene item would push) marks
|
||||
// ally2 as already decided, so PLAYER_SELECTION must skip it.
|
||||
battleQueueAction(ally2->id, BATTLE_ACTION_ATTACK, enemy->id);
|
||||
battleQueueMove(ally2->id, BATTLE_MOVE_ID_ATTACK, enemy->id);
|
||||
|
||||
battlePlayerAttack(enemy->id);
|
||||
assert_int_equal(BATTLE.actions[ally1->id].type, BATTLE_ACTION_ATTACK);
|
||||
assert_int_equal(BATTLE.actions[ally1->id].targetIndex, enemy->id);
|
||||
assert_int_equal(BATTLE.moves[ally1->id].move, BATTLE_MOVE_ID_ATTACK);
|
||||
assert_int_equal(BATTLE.moves[ally1->id].targets[0], enemy->id);
|
||||
|
||||
// ally2 already had a decision queued, and enemy is AI-controlled, so
|
||||
// there's nothing left for PLAYER_SELECTION -- it should have moved on.
|
||||
@@ -134,8 +111,8 @@ static void test_battleAiSelectionAutoQueuesUndecidedAiFighters(void **state) {
|
||||
|
||||
battleUpdate();// AI_SELECTION auto-decides for enemy
|
||||
assert_int_equal(BATTLE.state, BATTLE_STATE_MOVES_EXECUTING);
|
||||
assert_int_equal(BATTLE.actions[enemy->id].type, BATTLE_ACTION_ATTACK);
|
||||
assert_int_equal(BATTLE.actions[enemy->id].targetIndex, ally->id);
|
||||
assert_int_equal(BATTLE.moves[enemy->id].move, BATTLE_MOVE_ID_ATTACK);
|
||||
assert_int_equal(BATTLE.moves[enemy->id].targets[0], ally->id);
|
||||
}
|
||||
|
||||
static void test_battleMovesExecutingResolvesInSpeedOrderAndFizzles(
|
||||
@@ -278,176 +255,6 @@ static void test_battleFleeUnavailableIsIgnored(void **state) {
|
||||
assert_int_equal(BATTLE.result, BATTLE_RESULT_NONE);
|
||||
}
|
||||
|
||||
static uint8_t stateChangedCount;
|
||||
static battlestate_t stateChangedLog[32][2];
|
||||
|
||||
static void recordStateChanged(
|
||||
const battlestate_t previous,
|
||||
const battlestate_t next
|
||||
) {
|
||||
if(stateChangedCount >= 32) return;
|
||||
stateChangedLog[stateChangedCount][0] = previous;
|
||||
stateChangedLog[stateChangedCount][1] = next;
|
||||
stateChangedCount++;
|
||||
}
|
||||
|
||||
static void test_battleOnStateChangedFiresAcrossFullRound(void **state) {
|
||||
|
||||
|
||||
battleInit();
|
||||
stateChangedCount = 0;
|
||||
BATTLE.onStateChanged = recordStateChanged;
|
||||
|
||||
addFighter(
|
||||
BATTLE_FIGHTER_TEAM_ALLY, BATTLE_FIGHTER_CONTROLLER_PLAYER, 1, 0, 10, 100
|
||||
);
|
||||
battlefighter_t *enemy = addFighter(
|
||||
BATTLE_FIGHTER_TEAM_ENEMY, BATTLE_FIGHTER_CONTROLLER_AI, 1, 0, 5, 100
|
||||
);
|
||||
|
||||
battleStart(BATTLE_ENCOUNTER_REGULAR, true);// fires NONE -> OPENING
|
||||
battleUpdate();// OPENING -> PRE_ROUND
|
||||
battleUpdate();// PRE_ROUND -> PLAYER_SELECTION
|
||||
battlePlayerAttack(enemy->id);// PLAYER_SELECTION -> AI_SELECTION
|
||||
battleUpdate();// AI_SELECTION -> MOVES_EXECUTING
|
||||
battleUpdate();// MOVES_EXECUTING: ally's attack resolves (still executing)
|
||||
battleUpdate();// MOVES_EXECUTING: enemy's attack resolves (still executing)
|
||||
battleUpdate();// both slots processed -> POST_ROUND
|
||||
battleUpdate();// POST_ROUND -> PRE_ROUND, round advances
|
||||
|
||||
assert_int_equal(stateChangedLog[0][0], BATTLE_STATE_NONE);
|
||||
assert_int_equal(stateChangedLog[0][1], BATTLE_STATE_OPENING);
|
||||
assert_int_equal(stateChangedLog[1][1], BATTLE_STATE_PRE_ROUND);
|
||||
assert_int_equal(stateChangedLog[2][1], BATTLE_STATE_PLAYER_SELECTION);
|
||||
assert_int_equal(stateChangedLog[3][1], BATTLE_STATE_AI_SELECTION);
|
||||
assert_int_equal(stateChangedLog[4][1], BATTLE_STATE_MOVES_EXECUTING);
|
||||
assert_int_equal(stateChangedLog[5][1], BATTLE_STATE_POST_ROUND);
|
||||
assert_int_equal(stateChangedLog[6][1], BATTLE_STATE_PRE_ROUND);
|
||||
|
||||
BATTLE.onStateChanged = NULL;
|
||||
}
|
||||
|
||||
static uint8_t actionDecidedCount;
|
||||
|
||||
static void recordActionDecided(
|
||||
const battlefighter_t *fighter,
|
||||
const battleaction_t *action
|
||||
) {
|
||||
actionDecidedCount++;
|
||||
}
|
||||
|
||||
static void test_battleOnActionDecidedFiresPerFighterRegardlessOfSource(
|
||||
void **state
|
||||
) {
|
||||
|
||||
|
||||
battleInit();
|
||||
actionDecidedCount = 0;
|
||||
BATTLE.onActionDecided = recordActionDecided;
|
||||
|
||||
battlefighter_t *ally1 = addFighter(
|
||||
BATTLE_FIGHTER_TEAM_ALLY, BATTLE_FIGHTER_CONTROLLER_PLAYER, 1, 0, 10, 100
|
||||
);
|
||||
battlefighter_t *ally2 = addFighter(
|
||||
BATTLE_FIGHTER_TEAM_ALLY, BATTLE_FIGHTER_CONTROLLER_PLAYER, 1, 0, 9, 100
|
||||
);
|
||||
battlefighter_t *enemy = addFighter(
|
||||
BATTLE_FIGHTER_TEAM_ENEMY, BATTLE_FIGHTER_CONTROLLER_AI, 1, 0, 5, 100
|
||||
);
|
||||
|
||||
battleStart(BATTLE_ENCOUNTER_REGULAR, true);
|
||||
battleUpdate();// OPENING -> PRE_ROUND
|
||||
battleUpdate();// PRE_ROUND -> PLAYER_SELECTION
|
||||
|
||||
// A forced action, as CUTSCENE_BATTLE_FORCE_ACTION would push, counts too.
|
||||
battleQueueAction(ally2->id, BATTLE_ACTION_ATTACK, enemy->id);
|
||||
assert_int_equal(actionDecidedCount, 1);
|
||||
|
||||
battlePlayerAttack(enemy->id);// ally1's real decision
|
||||
assert_int_equal(actionDecidedCount, 2);
|
||||
assert_int_equal(BATTLE.state, BATTLE_STATE_AI_SELECTION);
|
||||
|
||||
battleUpdate();// AI_SELECTION decides for enemy
|
||||
assert_int_equal(actionDecidedCount, 3);
|
||||
|
||||
BATTLE.onActionDecided = NULL;
|
||||
}
|
||||
|
||||
// Exercises CUTSCENE_BATTLE_WAIT_STATE + CUTSCENE_SET_PAUSE(BATTLE) +
|
||||
// CUTSCENE_BATTLE_FORCE_ACTION together, driving the cutscene and battle
|
||||
// systems side by side the way rpg.c's main loop does, to prove a
|
||||
// partially-scripted round actually works end-to-end (not just that each
|
||||
// item type compiles).
|
||||
static void test_battleCutsceneForceActionOverridesTarget(void **state) {
|
||||
|
||||
|
||||
battleInit();
|
||||
cutsceneSystemInit();
|
||||
|
||||
battlefighter_t *allyA = addFighter(
|
||||
BATTLE_FIGHTER_TEAM_ALLY, BATTLE_FIGHTER_CONTROLLER_PLAYER, 5, 0, 10, 20
|
||||
);
|
||||
battlefighter_t *allyB = addFighter(
|
||||
BATTLE_FIGHTER_TEAM_ALLY, BATTLE_FIGHTER_CONTROLLER_PLAYER, 5, 0, 8, 20
|
||||
);
|
||||
battlefighter_t *enemy = addFighter(
|
||||
BATTLE_FIGHTER_TEAM_ENEMY, BATTLE_FIGHTER_CONTROLLER_AI, 3, 0, 5, 100
|
||||
);
|
||||
assert_int_equal(allyA->id, 0);
|
||||
assert_int_equal(allyB->id, 1);
|
||||
assert_int_equal(enemy->id, 2);
|
||||
|
||||
battleStart(BATTLE_ENCOUNTER_REGULAR, true);
|
||||
cutsceneSystemStartCutscene(&CUTSCENE_TEST_SCRATCH);
|
||||
|
||||
// Drive both systems together until BATTLE.state first reaches PRE_ROUND:
|
||||
// cutscene sees it via CUTSCENE_BATTLE_WAIT_STATE and immediately pauses
|
||||
// the battle before it can cascade on to PLAYER_SELECTION.
|
||||
cutsceneSystemUpdate();// WAIT_STATE: still OPENING, not satisfied yet
|
||||
battleUpdate();// OPENING -> PRE_ROUND
|
||||
cutsceneSystemUpdate();// WAIT_STATE satisfied -> SET_PAUSE(BATTLE) applied
|
||||
battleUpdate();// paused: no-op
|
||||
|
||||
assert_int_equal(BATTLE.state, BATTLE_STATE_PRE_ROUND);
|
||||
assert_true(CUTSCENE_SYSTEM.pause & CUTSCENE_PAUSE_BATTLE);
|
||||
|
||||
cutsceneSystemUpdate();// SET_PAUSE done -> FORCE_ACTION queues enemy's move
|
||||
battleUpdate();// still paused: no-op
|
||||
|
||||
assert_int_equal(BATTLE.actions[enemy->id].type, BATTLE_ACTION_ATTACK);
|
||||
assert_int_equal(BATTLE.actions[enemy->id].targetIndex, allyA->id);
|
||||
assert_int_equal(BATTLE.state, BATTLE_STATE_PRE_ROUND);// still frozen
|
||||
|
||||
cutsceneSystemUpdate();// FORCE_ACTION done -> SET_PAUSE(NONE) lifts it
|
||||
battleUpdate();// unpaused: PRE_ROUND -> PLAYER_SELECTION, allyA first
|
||||
|
||||
assert_false(CUTSCENE_SYSTEM.pause & CUTSCENE_PAUSE_BATTLE);
|
||||
assert_int_equal(BATTLE.state, BATTLE_STATE_PLAYER_SELECTION);
|
||||
assert_ptr_equal(battleGetCurrentFighter(), allyA);
|
||||
// The forced action from three frames ago survived untouched.
|
||||
assert_int_equal(BATTLE.actions[enemy->id].type, BATTLE_ACTION_ATTACK);
|
||||
assert_int_equal(BATTLE.actions[enemy->id].targetIndex, allyA->id);
|
||||
|
||||
battlePlayerAttack(enemy->id);// allyA -> AI_SELECTION would be next...
|
||||
assert_ptr_equal(battleGetCurrentFighter(), allyB);
|
||||
battlePlayerAttack(enemy->id);// ...but AI_SELECTION must skip the enemy,
|
||||
// since it's decided
|
||||
assert_int_equal(BATTLE.state, BATTLE_STATE_AI_SELECTION);
|
||||
|
||||
// AI_SELECTION: enemy already decided, skipped -> MOVES_EXECUTING
|
||||
battleUpdate();
|
||||
// untouched
|
||||
assert_int_equal(BATTLE.actions[enemy->id].targetIndex, allyA->id);
|
||||
|
||||
battleUpdate();// allyA attacks enemy
|
||||
battleUpdate();// allyB attacks enemy
|
||||
battleUpdate();// enemy attacks its forced target: allyA, not allyB
|
||||
|
||||
assert_int_equal(enemy->health, 90);// 100 - 5 (allyA) - 5 (allyB)
|
||||
assert_int_equal(allyA->health, 17);// 20 - 3 (enemy's forced attack)
|
||||
assert_int_equal(allyB->health, 20);// never targeted -- proves the override
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
const struct CMUnitTest tests[] = {
|
||||
cmocka_unit_test(test_battleStartEntersOpeningThenPreRound),
|
||||
@@ -458,11 +265,6 @@ int main(int argc, char** argv) {
|
||||
cmocka_unit_test(test_battleLossEndsInStateEnded),
|
||||
cmocka_unit_test(test_battleFleeEndsInStateEndedImmediately),
|
||||
cmocka_unit_test(test_battleFleeUnavailableIsIgnored),
|
||||
cmocka_unit_test(test_battleOnStateChangedFiresAcrossFullRound),
|
||||
cmocka_unit_test(
|
||||
test_battleOnActionDecidedFiresPerFighterRegardlessOfSource
|
||||
),
|
||||
cmocka_unit_test(test_battleCutsceneForceActionOverridesTarget),
|
||||
};
|
||||
|
||||
return cmocka_run_group_tests(tests, NULL, NULL);
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
include(dusktest)
|
||||
|
||||
# Tests
|
||||
dusktest(test_cutscene.c)
|
||||
dusktest(test_cutscenesystem.c)
|
||||
dusktest(test_cutscenecontrol.c)
|
||||
dusktest(test_cutscenemaparea.c)
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "dusktest.h"
|
||||
#include "rpg/cutscene/cutscene.h"
|
||||
#include "util/memory.h"
|
||||
#include "util/string.h"
|
||||
|
||||
// ============================================================
|
||||
// cutsceneRemoveFront
|
||||
// ============================================================
|
||||
|
||||
static cutsceneitem_t MARKER_ITEM(const char_t *name) {
|
||||
cutsceneitem_t item;
|
||||
memoryZero(&item, sizeof(item));
|
||||
item.type = CUTSCENE_ITEM_TYPE_MARKER;
|
||||
stringCopy(item.marker.name, name, CUTSCENE_MARKER_NAME_MAX - 1);
|
||||
return item;
|
||||
}
|
||||
|
||||
static void test_cutsceneRemoveFrontShiftsRemainderToIndexZero(
|
||||
void **state
|
||||
) {
|
||||
|
||||
cutsceneitem_t items[4] = {
|
||||
MARKER_ITEM("a"), MARKER_ITEM("b"), MARKER_ITEM("c"), MARKER_ITEM("d")
|
||||
};
|
||||
cutscene_t scene = { .items = items, .itemCount = 4, .itemsMax = 4 };
|
||||
|
||||
cutsceneRemoveFront(&scene, 2);
|
||||
|
||||
assert_int_equal(scene.itemCount, 2);
|
||||
assert_string_equal(scene.items[0].marker.name, "c");
|
||||
assert_string_equal(scene.items[1].marker.name, "d");
|
||||
}
|
||||
|
||||
static void test_cutsceneRemoveFrontAllItemsLeavesEmptyScene(void **state) {
|
||||
|
||||
cutsceneitem_t items[3] = {
|
||||
MARKER_ITEM("a"), MARKER_ITEM("b"), MARKER_ITEM("c")
|
||||
};
|
||||
cutscene_t scene = { .items = items, .itemCount = 3, .itemsMax = 3 };
|
||||
|
||||
cutsceneRemoveFront(&scene, 3);
|
||||
|
||||
assert_int_equal(scene.itemCount, 0);
|
||||
}
|
||||
|
||||
static void test_cutsceneRemoveFrontZeroIsNoop(void **state) {
|
||||
|
||||
cutsceneitem_t items[2] = { MARKER_ITEM("a"), MARKER_ITEM("b") };
|
||||
cutscene_t scene = { .items = items, .itemCount = 2, .itemsMax = 2 };
|
||||
|
||||
cutsceneRemoveFront(&scene, 0);
|
||||
|
||||
assert_int_equal(scene.itemCount, 2);
|
||||
assert_string_equal(scene.items[0].marker.name, "a");
|
||||
assert_string_equal(scene.items[1].marker.name, "b");
|
||||
}
|
||||
|
||||
static void test_cutsceneRemoveFrontAssertsWhenCountExceedsItemCount(
|
||||
void **state
|
||||
) {
|
||||
|
||||
cutsceneitem_t items[2] = { MARKER_ITEM("a"), MARKER_ITEM("b") };
|
||||
cutscene_t scene = { .items = items, .itemCount = 2, .itemsMax = 2 };
|
||||
|
||||
expect_assert_failure(cutsceneRemoveFront(&scene, 3));
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
const struct CMUnitTest tests[] = {
|
||||
cmocka_unit_test(test_cutsceneRemoveFrontShiftsRemainderToIndexZero),
|
||||
cmocka_unit_test(test_cutsceneRemoveFrontAllItemsLeavesEmptyScene),
|
||||
cmocka_unit_test(test_cutsceneRemoveFrontZeroIsNoop),
|
||||
cmocka_unit_test(test_cutsceneRemoveFrontAssertsWhenCountExceedsItemCount),
|
||||
};
|
||||
|
||||
return cmocka_run_group_tests(tests, NULL, NULL);
|
||||
}
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "rpg/cutscene/cutscenesystem.h"
|
||||
#include "rpg/entity/entity.h"
|
||||
#include "time/time.h"
|
||||
#include "util/string.h"
|
||||
|
||||
static uint8_t callbackFired;
|
||||
|
||||
@@ -198,18 +199,72 @@ static void test_cutsceneSystemDisposeResetsState(void **state) {
|
||||
assert_null(CUTSCENE_SYSTEM.entityInteracted);
|
||||
}
|
||||
|
||||
// --- loadedFile survival across cutsceneSystemPrepare -------------------
|
||||
//
|
||||
// cutsceneCutsceneResolve (and cutsceneSystemLoad) stamp
|
||||
// CUTSCENE_SYSTEM.loadedFile with what they just parsed loadedScene from
|
||||
// - cutsceneGoTo needs that to survive so it can reload the same source
|
||||
// later. cutsceneCutsceneResolve's caller stamps it BEFORE calling
|
||||
// cutsceneSystemStartCutscene(&CUTSCENE_SYSTEM.loadedScene), so
|
||||
// cutsceneSystemPrepare must not blindly clear loadedFile on every
|
||||
// start - only when switching away from loadedScene to some other
|
||||
// cutscene_t entirely (see cutsceneSystemPrepare's doc comment). This
|
||||
// regression-tests the exact bug hit via initial.jsonc's CUTSCENE item
|
||||
// into main_menu.jsonc: cutsceneCutsceneResolve stamped loadedFile, then
|
||||
// the immediately following cutsceneSystemStartCutscene call wiped it
|
||||
// straight back out, so a later cutsceneGoTo on the still-running
|
||||
// cutscene had nothing to reload from.
|
||||
static void test_cutsceneSystemPrepareKeepsLoadedFileWhenReenteringLoadedScene(
|
||||
void **state
|
||||
) {
|
||||
|
||||
cutsceneSystemInit();
|
||||
|
||||
// Simulate cutsceneCutsceneResolve: populate loadedScene/loadedItems
|
||||
// and stamp loadedFile with its source, before the cutscene has
|
||||
// actually started running.
|
||||
CUTSCENE_SYSTEM.loadedItems[0] =
|
||||
(cutsceneitem_t){ .type = CUTSCENE_ITEM_TYPE_WAIT, .wait = 1.0f };
|
||||
CUTSCENE_SYSTEM.loadedScene = (cutscene_t){
|
||||
.items = CUTSCENE_SYSTEM.loadedItems,
|
||||
.itemCount = 1,
|
||||
.itemsMax = CUTSCENE_LOADED_ITEMS_MAX,
|
||||
.pause = CUTSCENE_PAUSE_NONE
|
||||
};
|
||||
stringCopy(
|
||||
CUTSCENE_SYSTEM.loadedFile, "cutscenes/main_menu.jsonc",
|
||||
ASSET_FILE_NAME_MAX - 1
|
||||
);
|
||||
|
||||
cutsceneSystemStartCutscene(&CUTSCENE_SYSTEM.loadedScene);
|
||||
|
||||
assert_string_equal(
|
||||
CUTSCENE_SYSTEM.loadedFile, "cutscenes/main_menu.jsonc"
|
||||
);
|
||||
|
||||
// Starting a genuinely different cutscene, though, must still
|
||||
// invalidate it - there's nothing left to reload it from.
|
||||
cutsceneSystemStartCutscene(&CUTSCENE_TEST_SINGLE_WAIT);
|
||||
|
||||
assert_string_equal(CUTSCENE_SYSTEM.loadedFile, "");
|
||||
}
|
||||
|
||||
// --- CUTSCENE_INSERT ---------------------------------------------------
|
||||
//
|
||||
// INSERT now splices its target's items directly into the running
|
||||
// scene's own array (see cutsceneSystemInsertCutscene) rather than
|
||||
// swapping CUTSCENE_SYSTEM.scene to a different cutscene_t and pushing a
|
||||
// return frame - CUTSCENE_SYSTEM.scene therefore stays the same outer
|
||||
// cutscene throughout every test below; itemCount growing is what shows
|
||||
// the splice actually happened. Since the running scene's own array is
|
||||
// what gets appended into now, any outer cutscene used with INSERT must
|
||||
// declare its items array (and .itemsMax) with enough spare capacity for
|
||||
// whatever ends up spliced into it, including transitively through any
|
||||
// nested INSERT.
|
||||
// cutscene throughout every test below. cutsceneSystemNext pops each
|
||||
// already-run item off the front of the array as it advances (see
|
||||
// CUTSCENE_SYSTEM.currentItem's doc comment), so itemCount reflects only
|
||||
// what's left to run at any given point, not a running total of
|
||||
// everything ever spliced in - the itemCount assertions below are sized
|
||||
// accordingly. Since the running scene's own array is what gets appended
|
||||
// into, any outer cutscene used with INSERT must still declare its items
|
||||
// array (and .itemsMax) with enough spare capacity for whatever ends up
|
||||
// spliced into it at once, including transitively through any nested
|
||||
// INSERT.
|
||||
|
||||
static uint8_t insertOrderLog[8];
|
||||
static uint8_t insertOrderLogCount;
|
||||
@@ -263,10 +318,12 @@ static void test_cutsceneInsertSplicesItemsInPlaceThenResumes(void **state) {
|
||||
cutsceneSystemUpdate();// the WAIT elapses -> starts order1
|
||||
cutsceneSystemUpdate();// order1 done -> starts the INSERT, cascading into orderA
|
||||
|
||||
// The splice happened in place - CUTSCENE_SYSTEM.scene never changed,
|
||||
// but it grew by the snippet's 3 items.
|
||||
// The splice happened in place - CUTSCENE_SYSTEM.scene never changed.
|
||||
// order0/the WAIT/order1/the INSERT item itself have all since been
|
||||
// popped off the front, leaving the snippet's 3 items (orderA now
|
||||
// running, its WAIT, orderB) plus order2.
|
||||
assert_ptr_equal(CUTSCENE_SYSTEM.scene, &CUTSCENE_TEST_INSERT_OUTER);
|
||||
assert_int_equal(CUTSCENE_SYSTEM.scene->itemCount, 8);
|
||||
assert_int_equal(CUTSCENE_SYSTEM.scene->itemCount, 4);
|
||||
|
||||
TIME.delta = 2.5f;// elapses the snippet's WAIT(2.0)
|
||||
cutsceneSystemUpdate();// orderA done -> starts snippet's WAIT
|
||||
@@ -323,7 +380,9 @@ static void test_cutsceneInsertAsLastItemEndsCutsceneNaturally(
|
||||
|
||||
cutsceneSystemUpdate();// order0 done -> starts INSERT, cascades to orderA
|
||||
assert_ptr_equal(CUTSCENE_SYSTEM.scene, &CUTSCENE_TEST_INSERT_AS_LAST_ITEM_OUTER);
|
||||
assert_int_equal(CUTSCENE_SYSTEM.scene->itemCount, 3);
|
||||
// order0 and the INSERT item itself have both been popped off the
|
||||
// front by now, leaving just orderA (currently running).
|
||||
assert_int_equal(CUTSCENE_SYSTEM.scene->itemCount, 1);
|
||||
|
||||
// orderA is the snippet's only item, and the snippet was spliced in as
|
||||
// the outer's last item -- finishing it should reach the natural end
|
||||
@@ -398,7 +457,9 @@ static void test_cutsceneInsertNestsThroughMultipleLevels(void **state) {
|
||||
// land in OUTER's own array.
|
||||
cutsceneSystemUpdate();
|
||||
assert_ptr_equal(CUTSCENE_SYSTEM.scene, &CUTSCENE_TEST_INSERT_NESTED_OUTER);
|
||||
assert_int_equal(CUTSCENE_SYSTEM.scene->itemCount, 6);
|
||||
// order0 and both INSERT items themselves have all been popped off the
|
||||
// front by now, leaving orderA (currently running), orderB, order2.
|
||||
assert_int_equal(CUTSCENE_SYSTEM.scene->itemCount, 3);
|
||||
|
||||
cutsceneSystemUpdate();// orderA done -> continues on to orderB
|
||||
cutsceneSystemUpdate();// orderB done -> continues on to order2
|
||||
@@ -486,9 +547,10 @@ static void test_cutsceneInsertDoesNotResetPauseOrInteractEntities(
|
||||
);
|
||||
|
||||
// The INSERT item's Start already spliced the snippet into place and
|
||||
// cascaded into it by now.
|
||||
// cascaded into it by now - and since the INSERT item itself has since
|
||||
// been popped off the front, only the snippet's one item is left.
|
||||
assert_ptr_equal(CUTSCENE_SYSTEM.scene, &CUTSCENE_TEST_INSERT_PERSIST_OUTER);
|
||||
assert_int_equal(CUTSCENE_SYSTEM.scene->itemCount, 2);
|
||||
assert_int_equal(CUTSCENE_SYSTEM.scene->itemCount, 1);
|
||||
assert_ptr_equal(CUTSCENE_SYSTEM.entityInteract, &ENTITIES[3]);
|
||||
assert_ptr_equal(CUTSCENE_SYSTEM.entityInteracted, &ENTITIES[4]);
|
||||
// The snippet declares ALL, but INSERT doesn't apply it -- the outer
|
||||
@@ -507,6 +569,9 @@ int main(int argc, char** argv) {
|
||||
cmocka_unit_test(test_cutsceneSystemGetAreaId),
|
||||
cmocka_unit_test(test_cutsceneSystemGetTextMiniId),
|
||||
cmocka_unit_test(test_cutsceneSystemDisposeResetsState),
|
||||
cmocka_unit_test(
|
||||
test_cutsceneSystemPrepareKeepsLoadedFileWhenReenteringLoadedScene
|
||||
),
|
||||
cmocka_unit_test(test_cutsceneInsertSplicesItemsInPlaceThenResumes),
|
||||
cmocka_unit_test(test_cutsceneInsertAsLastItemEndsCutsceneNaturally),
|
||||
cmocka_unit_test(test_cutsceneInsertNestsThroughMultipleLevels),
|
||||
|
||||
@@ -49,7 +49,7 @@ import zipfile
|
||||
import zlib
|
||||
import io
|
||||
|
||||
MAGIC = b'DSK2'
|
||||
MAGIC = b'DSK'
|
||||
VERSION = 2
|
||||
HEADER_FORMAT = '<4sIII'
|
||||
HEADER_SIZE = struct.calcsize(HEADER_FORMAT)
|
||||
|
||||
Reference in New Issue
Block a user