diff --git a/assets/battle/moves.jsonc b/assets/battle/moves.jsonc index f6870e77..1ab6a329 100644 --- a/assets/battle/moves.jsonc +++ b/assets/battle/moves.jsonc @@ -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" } } } diff --git a/assets/cutscenes/battle/moves/fire.jsonc b/assets/cutscenes/battle/moves/fire.jsonc new file mode 100644 index 00000000..8c88770a --- /dev/null +++ b/assets/cutscenes/battle/moves/fire.jsonc @@ -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 + } + ] +} diff --git a/assets/cutscenes/battle/moves/slash.jsonc b/assets/cutscenes/battle/moves/slash.jsonc new file mode 100644 index 00000000..0e80637f --- /dev/null +++ b/assets/cutscenes/battle/moves/slash.jsonc @@ -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 + } + ] +} diff --git a/assets/cutscenes/battletest.jsonc b/assets/cutscenes/battletest.jsonc index 6039ec6b..22b5de51 100644 --- a/assets/cutscenes/battletest.jsonc +++ b/assets/cutscenes/battletest.jsonc @@ -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" } ] } diff --git a/assets/locale/en_US.po b/assets/locale/en_US.po index d45ecd33..9f1242b0 100644 --- a/assets/locale/en_US.po +++ b/assets/locale/en_US.po @@ -266,6 +266,10 @@ msgstr "loading" msgid "ui.autosave.saving" msgstr "SAVING" + +# +# Items +# msgid "item.potion.name" msgstr "Potion" @@ -273,4 +277,13 @@ msgid "item.potato.name" msgstr "Potato" msgid "item.apple.name" -msgstr "Apple" \ No newline at end of file +msgstr "Apple" + +# +# Moves +# +msgid "move.fire.name" +msgstr "Fire" + +msgid "move.slash.name" +msgstr "Slash" \ No newline at end of file diff --git a/src/dusk/asset/assetdsk.h b/src/dusk/asset/assetdsk.h index 6be8644a..efb28800 100644 --- a/src/dusk/asset/assetdsk.h +++ b/src/dusk/asset/assetdsk.h @@ -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 diff --git a/src/dusk/rpg/battle/CMakeLists.txt b/src/dusk/rpg/battle/CMakeLists.txt index 5b0a546c..49363600 100644 --- a/src/dusk/rpg/battle/CMakeLists.txt +++ b/src/dusk/rpg/battle/CMakeLists.txt @@ -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) diff --git a/src/dusk/rpg/battle/battle.c b/src/dusk/rpg/battle/battle.c index 0a3ec94f..f9e2d47a 100644 --- a/src/dusk/rpg/battle/battle.c +++ b/src/dusk/rpg/battle/battle.c @@ -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)); +} \ No newline at end of file diff --git a/src/dusk/rpg/battle/battle.h b/src/dusk/rpg/battle/battle.h index 6bdfee25..108a0742 100644 --- a/src/dusk/rpg/battle/battle.h +++ b/src/dusk/rpg/battle/battle.h @@ -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. - * - * @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. + * Starts a 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); \ No newline at end of file diff --git a/src/dusk/rpg/battle/battleaction.h b/src/dusk/rpg/battle/battleaction.h deleted file mode 100644 index c786bcfd..00000000 --- a/src/dusk/rpg/battle/battleaction.h +++ /dev/null @@ -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 -); diff --git a/src/dusk/rpg/battle/battlefighter.c b/src/dusk/rpg/battle/battlefighter.c deleted file mode 100644 index 8d72d552..00000000 --- a/src/dusk/rpg/battle/battlefighter.c +++ /dev/null @@ -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; -} diff --git a/src/dusk/rpg/battle/battlefighter.h b/src/dusk/rpg/battle/battlefighter.h deleted file mode 100644 index 1b3674b1..00000000 --- a/src/dusk/rpg/battle/battlefighter.h +++ /dev/null @@ -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); diff --git a/src/dusk/rpg/battle/fighter/CMakeLists.txt b/src/dusk/rpg/battle/fighter/CMakeLists.txt new file mode 100644 index 00000000..bc721bd2 --- /dev/null +++ b/src/dusk/rpg/battle/fighter/CMakeLists.txt @@ -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 +) diff --git a/src/dusk/rpg/battle/fighter/battlefighter.c b/src/dusk/rpg/battle/fighter/battlefighter.c new file mode 100644 index 00000000..d2b9e385 --- /dev/null +++ b/src/dusk/rpg/battle/fighter/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; +} diff --git a/src/dusk/rpg/battle/fighter/battlefighter.h b/src/dusk/rpg/battle/fighter/battlefighter.h new file mode 100644 index 00000000..46482373 --- /dev/null +++ b/src/dusk/rpg/battle/fighter/battlefighter.h @@ -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 +); diff --git a/src/dusk/rpg/battle/fighter/battlefightercontroller.h b/src/dusk/rpg/battle/fighter/battlefightercontroller.h new file mode 100644 index 00000000..5e0389d0 --- /dev/null +++ b/src/dusk/rpg/battle/fighter/battlefightercontroller.h @@ -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; diff --git a/src/dusk/rpg/battle/fighter/battlefighterpos.h b/src/dusk/rpg/battle/fighter/battlefighterpos.h new file mode 100644 index 00000000..e7f237b0 --- /dev/null +++ b/src/dusk/rpg/battle/fighter/battlefighterpos.h @@ -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) \ No newline at end of file diff --git a/src/dusk/rpg/battle/fighter/battlefighterstats.h b/src/dusk/rpg/battle/fighter/battlefighterstats.h new file mode 100644 index 00000000..f4b731bb --- /dev/null +++ b/src/dusk/rpg/battle/fighter/battlefighterstats.h @@ -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; diff --git a/src/dusk/rpg/battle/fighter/battlefighterstatus.h b/src/dusk/rpg/battle/fighter/battlefighterstatus.h new file mode 100644 index 00000000..3c73af2d --- /dev/null +++ b/src/dusk/rpg/battle/fighter/battlefighterstatus.h @@ -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; diff --git a/src/dusk/rpg/battle/battleability.h b/src/dusk/rpg/battle/fighter/battlefighterteam.h similarity index 57% rename from src/dusk/rpg/battle/battleability.h rename to src/dusk/rpg/battle/fighter/battlefighterteam.h index 6b2297e4..85abda07 100644 --- a/src/dusk/rpg/battle/battleability.h +++ b/src/dusk/rpg/battle/fighter/battlefighterteam.h @@ -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; diff --git a/src/dusk/rpg/battle/move/CMakeLists.txt b/src/dusk/rpg/battle/move/CMakeLists.txt new file mode 100644 index 00000000..cdbe40d2 --- /dev/null +++ b/src/dusk/rpg/battle/move/CMakeLists.txt @@ -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 +) diff --git a/src/dusk/rpg/battle/move/battlemove.c b/src/dusk/rpg/battle/move/battlemove.c new file mode 100644 index 00000000..389c5ee0 --- /dev/null +++ b/src/dusk/rpg/battle/move/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 + +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; +} diff --git a/src/dusk/rpg/battle/move/battlemove.h b/src/dusk/rpg/battle/move/battlemove.h new file mode 100644 index 00000000..3c11b2d2 --- /dev/null +++ b/src/dusk/rpg/battle/move/battlemove.h @@ -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); diff --git a/src/dusk/rpg/battle/move/battlemoveaction.h b/src/dusk/rpg/battle/move/battlemoveaction.h new file mode 100644 index 00000000..f39f8852 --- /dev/null +++ b/src/dusk/rpg/battle/move/battlemoveaction.h @@ -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; diff --git a/src/dusk/rpg/battle/move/battletarget.h b/src/dusk/rpg/battle/move/battletarget.h new file mode 100644 index 00000000..73abfb38 --- /dev/null +++ b/src/dusk/rpg/battle/move/battletarget.h @@ -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) \ No newline at end of file diff --git a/src/dusk/rpg/battle/party.c b/src/dusk/rpg/battle/party.c index c5971f73..a6568fb0 100644 --- a/src/dusk/rpg/battle/party.c +++ b/src/dusk/rpg/battle/party.c @@ -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++) { diff --git a/src/dusk/rpg/battle/party.h b/src/dusk/rpg/battle/party.h index 3e8f4e8f..367d01d7 100644 --- a/src/dusk/rpg/battle/party.h +++ b/src/dusk/rpg/battle/party.h @@ -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 ); /** diff --git a/src/dusk/rpg/battle/state/CMakeLists.txt b/src/dusk/rpg/battle/state/CMakeLists.txt new file mode 100644 index 00000000..603227d0 --- /dev/null +++ b/src/dusk/rpg/battle/state/CMakeLists.txt @@ -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 +) diff --git a/src/dusk/rpg/battle/state/battlestate.c b/src/dusk/rpg/battle/state/battlestate.c new file mode 100644 index 00000000..d5387658 --- /dev/null +++ b/src/dusk/rpg/battle/state/battlestate.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 + } +}; diff --git a/src/dusk/rpg/battle/state/battlestate.h b/src/dusk/rpg/battle/state/battlestate.h new file mode 100644 index 00000000..00eddd5e --- /dev/null +++ b/src/dusk/rpg/battle/state/battlestate.h @@ -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]; \ No newline at end of file diff --git a/src/dusk/rpg/battle/state/battlestateending.c b/src/dusk/rpg/battle/state/battlestateending.c new file mode 100644 index 00000000..2a195cbc --- /dev/null +++ b/src/dusk/rpg/battle/state/battlestateending.c @@ -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) { + +} diff --git a/src/dusk/rpg/battle/state/battlestateending.h b/src/dusk/rpg/battle/state/battlestateending.h new file mode 100644 index 00000000..7b4fc581 --- /dev/null +++ b/src/dusk/rpg/battle/state/battlestateending.h @@ -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); diff --git a/src/dusk/rpg/battle/state/battlestateexecuting.c b/src/dusk/rpg/battle/state/battlestateexecuting.c new file mode 100644 index 00000000..d0045a38 --- /dev/null +++ b/src/dusk/rpg/battle/state/battlestateexecuting.c @@ -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) { + +} diff --git a/src/dusk/rpg/battle/state/battlestateexecuting.h b/src/dusk/rpg/battle/state/battlestateexecuting.h new file mode 100644 index 00000000..8e51d3e9 --- /dev/null +++ b/src/dusk/rpg/battle/state/battlestateexecuting.h @@ -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); diff --git a/src/dusk/rpg/battle/state/battlestatefled.c b/src/dusk/rpg/battle/state/battlestatefled.c new file mode 100644 index 00000000..441072a2 --- /dev/null +++ b/src/dusk/rpg/battle/state/battlestatefled.c @@ -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) { + +} diff --git a/src/dusk/rpg/battle/state/battlestatefled.h b/src/dusk/rpg/battle/state/battlestatefled.h new file mode 100644 index 00000000..f482b6be --- /dev/null +++ b/src/dusk/rpg/battle/state/battlestatefled.h @@ -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); diff --git a/src/dusk/rpg/battle/state/battlestatelost.c b/src/dusk/rpg/battle/state/battlestatelost.c new file mode 100644 index 00000000..334d8d87 --- /dev/null +++ b/src/dusk/rpg/battle/state/battlestatelost.c @@ -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) { + +} diff --git a/src/dusk/rpg/battle/state/battlestatelost.h b/src/dusk/rpg/battle/state/battlestatelost.h new file mode 100644 index 00000000..7a20dca7 --- /dev/null +++ b/src/dusk/rpg/battle/state/battlestatelost.h @@ -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); diff --git a/src/dusk/rpg/battle/state/battlestateopening.c b/src/dusk/rpg/battle/state/battlestateopening.c new file mode 100644 index 00000000..eb2a2752 --- /dev/null +++ b/src/dusk/rpg/battle/state/battlestateopening.c @@ -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) { + +} diff --git a/src/dusk/rpg/battle/state/battlestateopening.h b/src/dusk/rpg/battle/state/battlestateopening.h new file mode 100644 index 00000000..946c4d06 --- /dev/null +++ b/src/dusk/rpg/battle/state/battlestateopening.h @@ -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); diff --git a/src/dusk/rpg/battle/state/battlestatepostround.c b/src/dusk/rpg/battle/state/battlestatepostround.c new file mode 100644 index 00000000..2e713610 --- /dev/null +++ b/src/dusk/rpg/battle/state/battlestatepostround.c @@ -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) { + +} diff --git a/src/dusk/rpg/battle/state/battlestatepostround.h b/src/dusk/rpg/battle/state/battlestatepostround.h new file mode 100644 index 00000000..cba3a38f --- /dev/null +++ b/src/dusk/rpg/battle/state/battlestatepostround.h @@ -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); diff --git a/src/dusk/rpg/battle/state/battlestatepreround.c b/src/dusk/rpg/battle/state/battlestatepreround.c new file mode 100644 index 00000000..7b85ece7 --- /dev/null +++ b/src/dusk/rpg/battle/state/battlestatepreround.c @@ -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) { + +} diff --git a/src/dusk/rpg/battle/state/battlestatepreround.h b/src/dusk/rpg/battle/state/battlestatepreround.h new file mode 100644 index 00000000..0f26afe9 --- /dev/null +++ b/src/dusk/rpg/battle/state/battlestatepreround.h @@ -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); diff --git a/src/dusk/rpg/battle/state/battlestateselection.c b/src/dusk/rpg/battle/state/battlestateselection.c new file mode 100644 index 00000000..22d08c55 --- /dev/null +++ b/src/dusk/rpg/battle/state/battlestateselection.c @@ -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); +} \ No newline at end of file diff --git a/src/dusk/rpg/battle/state/battlestateselection.h b/src/dusk/rpg/battle/state/battlestateselection.h new file mode 100644 index 00000000..c4a7d1ba --- /dev/null +++ b/src/dusk/rpg/battle/state/battlestateselection.h @@ -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); \ No newline at end of file diff --git a/src/dusk/rpg/battle/state/battlestatewon.c b/src/dusk/rpg/battle/state/battlestatewon.c new file mode 100644 index 00000000..95d09f06 --- /dev/null +++ b/src/dusk/rpg/battle/state/battlestatewon.c @@ -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) { + +} diff --git a/src/dusk/rpg/battle/state/battlestatewon.h b/src/dusk/rpg/battle/state/battlestatewon.h new file mode 100644 index 00000000..d9c72dec --- /dev/null +++ b/src/dusk/rpg/battle/state/battlestatewon.h @@ -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); diff --git a/src/dusk/rpg/cutscene/cutscene.c b/src/dusk/rpg/cutscene/cutscene.c index 710d188e..7e5c16bf 100644 --- a/src/dusk/rpg/cutscene/cutscene.c +++ b/src/dusk/rpg/cutscene/cutscene.c @@ -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(); +} diff --git a/src/dusk/rpg/cutscene/cutscene.h b/src/dusk/rpg/cutscene/cutscene.h index 2da53b0c..b6263a9d 100644 --- a/src/dusk/rpg/cutscene/cutscene.h +++ b/src/dusk/rpg/cutscene/cutscene.h @@ -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 +); diff --git a/src/dusk/rpg/cutscene/cutscenesystem.c b/src/dusk/rpg/cutscene/cutscenesystem.c index f7d6cfba..415e917d 100644 --- a/src/dusk/rpg/cutscene/cutscenesystem.c +++ b/src/dusk/rpg/cutscene/cutscenesystem.c @@ -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]; } diff --git a/src/dusk/rpg/cutscene/cutscenesystem.h b/src/dusk/rpg/cutscene/cutscenesystem.h index 76dd79f7..3ccdf3fd 100644 --- a/src/dusk/rpg/cutscene/cutscenesystem.h +++ b/src/dusk/rpg/cutscene/cutscenesystem.h @@ -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. */ diff --git a/src/dusk/rpg/cutscene/item/battle/CMakeLists.txt b/src/dusk/rpg/cutscene/item/battle/CMakeLists.txt index 60a5f108..55c2fdd3 100644 --- a/src/dusk/rpg/cutscene/item/battle/CMakeLists.txt +++ b/src/dusk/rpg/cutscene/item/battle/CMakeLists.txt @@ -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 ) diff --git a/src/dusk/rpg/cutscene/item/battle/cutscenebattlefighterdamage.c b/src/dusk/rpg/cutscene/item/battle/cutscenebattlefighterdamage.c new file mode 100644 index 00000000..36a9bdbf --- /dev/null +++ b/src/dusk/rpg/cutscene/item/battle/cutscenebattlefighterdamage.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(); +} diff --git a/src/dusk/rpg/cutscene/item/battle/cutscenebattlefighterdamage.h b/src/dusk/rpg/cutscene/item/battle/cutscenebattlefighterdamage.h new file mode 100644 index 00000000..7d592732 --- /dev/null +++ b/src/dusk/rpg/cutscene/item/battle/cutscenebattlefighterdamage.h @@ -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 +); diff --git a/src/dusk/rpg/cutscene/item/battle/cutscenebattlefighterheal.c b/src/dusk/rpg/cutscene/item/battle/cutscenebattlefighterheal.c new file mode 100644 index 00000000..ee288541 --- /dev/null +++ b/src/dusk/rpg/cutscene/item/battle/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 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(); +} diff --git a/src/dusk/rpg/cutscene/item/battle/cutscenebattlefighterheal.h b/src/dusk/rpg/cutscene/item/battle/cutscenebattlefighterheal.h new file mode 100644 index 00000000..ae414b9d --- /dev/null +++ b/src/dusk/rpg/cutscene/item/battle/cutscenebattlefighterheal.h @@ -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 +); diff --git a/src/dusk/rpg/cutscene/item/battle/cutscenebattleforceaction.c b/src/dusk/rpg/cutscene/item/battle/cutscenebattleforceaction.c deleted file mode 100644 index 34a497fe..00000000 --- a/src/dusk/rpg/cutscene/item/battle/cutscenebattleforceaction.c +++ /dev/null @@ -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(); -} diff --git a/src/dusk/rpg/cutscene/item/battle/cutscenebattlepostmove.c b/src/dusk/rpg/cutscene/item/battle/cutscenebattlepostmove.c new file mode 100644 index 00000000..9c1136dc --- /dev/null +++ b/src/dusk/rpg/cutscene/item/battle/cutscenebattlepostmove.c @@ -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; +} diff --git a/src/dusk/rpg/cutscene/item/battle/cutscenebattlepostmove.h b/src/dusk/rpg/cutscene/item/battle/cutscenebattlepostmove.h new file mode 100644 index 00000000..58b62943 --- /dev/null +++ b/src/dusk/rpg/cutscene/item/battle/cutscenebattlepostmove.h @@ -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 +); diff --git a/src/dusk/rpg/cutscene/item/battle/cutscenebattlesetstate.c b/src/dusk/rpg/cutscene/item/battle/cutscenebattlesetstate.c new file mode 100644 index 00000000..5b715c4c --- /dev/null +++ b/src/dusk/rpg/cutscene/item/battle/cutscenebattlesetstate.c @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "rpg/cutscene/item/cutsceneitem.h" +#include "rpg/battle/battle.h" +#include "rpg/cutscene/item/json/cutscenejsonbattlestate.h" +#include "assert/assert.h" + +void cutsceneBattleSetStateStart( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +) { + assertNotNull(item, "item cannot be NULL"); + battleSetState(item->battleSetState.state); +} + +bool_t cutsceneBattleSetStateUpdate( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +) { + return true; +} + +errorret_t cutsceneBattleSetStateLoad( + yyjson_val *itemObj, + cutsceneitem_t *item +) { + battlestate_t state; + if(!cutsceneJsonLookupBattleState( + yyjson_get_str(yyjson_obj_get(itemObj, "state")), &state + )) { + errorThrow("Cutscene item (BATTLE_SET_STATE): invalid 'state'"); + } + item->battleSetState.state = state; + + errorOk(); +} diff --git a/src/dusk/rpg/cutscene/item/battle/cutscenebattleforceaction.h b/src/dusk/rpg/cutscene/item/battle/cutscenebattlesetstate.h similarity index 57% rename from src/dusk/rpg/cutscene/item/battle/cutscenebattleforceaction.h rename to src/dusk/rpg/cutscene/item/battle/cutscenebattlesetstate.h index 4acb73dc..2d4753bf 100644 --- a/src/dusk/rpg/cutscene/item/battle/cutscenebattleforceaction.h +++ b/src/dusk/rpg/cutscene/item/battle/cutscenebattlesetstate.h @@ -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 ); diff --git a/src/dusk/rpg/cutscene/item/battle/cutscenebattlestart.c b/src/dusk/rpg/cutscene/item/battle/cutscenebattlestart.c new file mode 100644 index 00000000..439f20b8 --- /dev/null +++ b/src/dusk/rpg/cutscene/item/battle/cutscenebattlestart.c @@ -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(); +} diff --git a/src/dusk/rpg/cutscene/item/battle/cutscenebattlestart.h b/src/dusk/rpg/cutscene/item/battle/cutscenebattlestart.h new file mode 100644 index 00000000..e4e2926c --- /dev/null +++ b/src/dusk/rpg/cutscene/item/battle/cutscenebattlestart.h @@ -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 +); diff --git a/src/dusk/rpg/cutscene/item/battle/cutscenebattlewaitselection.c b/src/dusk/rpg/cutscene/item/battle/cutscenebattlewaitselection.c new file mode 100644 index 00000000..763dbbff --- /dev/null +++ b/src/dusk/rpg/cutscene/item/battle/cutscenebattlewaitselection.c @@ -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; +} diff --git a/src/dusk/rpg/cutscene/item/battle/cutscenebattlewaitselection.h b/src/dusk/rpg/cutscene/item/battle/cutscenebattlewaitselection.h new file mode 100644 index 00000000..c5235734 --- /dev/null +++ b/src/dusk/rpg/cutscene/item/battle/cutscenebattlewaitselection.h @@ -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 +); diff --git a/src/dusk/rpg/cutscene/item/battle/cutscenebattlewaitstate.c b/src/dusk/rpg/cutscene/item/battle/cutscenebattlewaitstate.c deleted file mode 100644 index 77b29035..00000000 --- a/src/dusk/rpg/cutscene/item/battle/cutscenebattlewaitstate.c +++ /dev/null @@ -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(); -} diff --git a/src/dusk/rpg/cutscene/item/battle/cutscenebattlewaitstate.h b/src/dusk/rpg/cutscene/item/battle/cutscenebattlewaitstate.h deleted file mode 100644 index 705c1714..00000000 --- a/src/dusk/rpg/cutscene/item/battle/cutscenebattlewaitstate.h +++ /dev/null @@ -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 -); diff --git a/src/dusk/rpg/cutscene/item/battle/cutsceneregularbattle.c b/src/dusk/rpg/cutscene/item/battle/cutsceneregularbattle.c deleted file mode 100644 index 24cad590..00000000 --- a/src/dusk/rpg/cutscene/item/battle/cutsceneregularbattle.c +++ /dev/null @@ -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; -} diff --git a/src/dusk/rpg/cutscene/item/battle/cutsceneregularbattle.h b/src/dusk/rpg/cutscene/item/battle/cutsceneregularbattle.h deleted file mode 100644 index d8f631bd..00000000 --- a/src/dusk/rpg/cutscene/item/battle/cutsceneregularbattle.h +++ /dev/null @@ -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. diff --git a/src/dusk/rpg/cutscene/item/battle/cutscenestartbattle.c b/src/dusk/rpg/cutscene/item/battle/cutscenestartbattle.c deleted file mode 100644 index 3cd12e7b..00000000 --- a/src/dusk/rpg/cutscene/item/battle/cutscenestartbattle.c +++ /dev/null @@ -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(); -} diff --git a/src/dusk/rpg/cutscene/item/battle/cutscenestartbattle.h b/src/dusk/rpg/cutscene/item/battle/cutscenestartbattle.h deleted file mode 100644 index 405cbfbd..00000000 --- a/src/dusk/rpg/cutscene/item/battle/cutscenestartbattle.h +++ /dev/null @@ -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 -); diff --git a/src/dusk/rpg/cutscene/item/cutsceneitem.c b/src/dusk/rpg/cutscene/item/cutsceneitem.c index 149b9c04..53b39fa3 100644 --- a/src/dusk/rpg/cutscene/item/cutsceneitem.c +++ b/src/dusk/rpg/cutscene/item/cutsceneitem.c @@ -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; } diff --git a/src/dusk/rpg/cutscene/item/cutsceneitem.h b/src/dusk/rpg/cutscene/item/cutsceneitem.h index 006b79a0..5d53190a 100644 --- a/src/dusk/rpg/cutscene/item/cutsceneitem.h +++ b/src/dusk/rpg/cutscene/item/cutsceneitem.h @@ -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; diff --git a/src/dusk/rpg/cutscene/item/cutsceneitembase.h b/src/dusk/rpg/cutscene/item/cutsceneitembase.h index c19815a4..107523d8 100644 --- a/src/dusk/rpg/cutscene/item/cutsceneitembase.h +++ b/src/dusk/rpg/cutscene/item/cutsceneitembase.h @@ -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, diff --git a/src/dusk/rpg/cutscene/item/json/CMakeLists.txt b/src/dusk/rpg/cutscene/item/json/CMakeLists.txt index a6a8e175..60a8f286 100644 --- a/src/dusk/rpg/cutscene/item/json/CMakeLists.txt +++ b/src/dusk/rpg/cutscene/item/json/CMakeLists.txt @@ -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 diff --git a/src/dusk/rpg/cutscene/item/json/cutscenejsonbattleability.c b/src/dusk/rpg/cutscene/item/json/cutscenejsonbattleability.c deleted file mode 100644 index adb9dc39..00000000 --- a/src/dusk/rpg/cutscene/item/json/cutscenejsonbattleability.c +++ /dev/null @@ -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; -} diff --git a/src/dusk/rpg/cutscene/item/json/cutscenejsonbattleability.h b/src/dusk/rpg/cutscene/item/json/cutscenejsonbattleability.h deleted file mode 100644 index bc6abd95..00000000 --- a/src/dusk/rpg/cutscene/item/json/cutscenejsonbattleability.h +++ /dev/null @@ -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 -); diff --git a/src/dusk/rpg/cutscene/item/json/cutscenejsonbattleencounter.c b/src/dusk/rpg/cutscene/item/json/cutscenejsonbattleencounter.c deleted file mode 100644 index 8cd28e63..00000000 --- a/src/dusk/rpg/cutscene/item/json/cutscenejsonbattleencounter.c +++ /dev/null @@ -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; -} diff --git a/src/dusk/rpg/cutscene/item/json/cutscenejsonbattleencounter.h b/src/dusk/rpg/cutscene/item/json/cutscenejsonbattleencounter.h deleted file mode 100644 index 20e8f76e..00000000 --- a/src/dusk/rpg/cutscene/item/json/cutscenejsonbattleencounter.h +++ /dev/null @@ -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 -); diff --git a/src/dusk/rpg/cutscene/item/json/cutscenejsonbattlestate.c b/src/dusk/rpg/cutscene/item/json/cutscenejsonbattlestate.c index 1e9762aa..60993646 100644 --- a/src/dusk/rpg/cutscene/item/json/cutscenejsonbattlestate.c +++ b/src/dusk/rpg/cutscene/item/json/cutscenejsonbattlestate.c @@ -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; } diff --git a/src/dusk/rpg/cutscene/item/json/cutscenejsonbattlestate.h b/src/dusk/rpg/cutscene/item/json/cutscenejsonbattlestate.h index 1122430e..1cf302f2 100644 --- a/src/dusk/rpg/cutscene/item/json/cutscenejsonbattlestate.h +++ b/src/dusk/rpg/cutscene/item/json/cutscenejsonbattlestate.h @@ -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 +); diff --git a/src/dusk/rpg/cutscene/item/ui/cutscenemodal.c b/src/dusk/rpg/cutscene/item/ui/cutscenemodal.c index e4c23db3..bad23afd 100644 --- a/src/dusk/rpg/cutscene/item/ui/cutscenemodal.c +++ b/src/dusk/rpg/cutscene/item/ui/cutscenemodal.c @@ -36,7 +36,8 @@ bool_t cutsceneModalUpdate( const cutsceneitem_t *item, cutsceneitemdata_t *data ) { - return item->modal.optionCount == 0; + if(item->modal.optionCount == 0 && !item->modal.blocking) return true; + return !uiModalIsOpen(); } void cutsceneModalCloseStart( @@ -72,6 +73,7 @@ errorret_t cutsceneModalLoad( )) { errorThrow("Cutscene item (MODAL): missing/too-long 'message'"); } + item->modal.blocking = cutsceneJsonGetBool(itemObj, "blocking", false); errorOk(); } diff --git a/src/dusk/rpg/cutscene/item/ui/cutscenemodal.h b/src/dusk/rpg/cutscene/item/ui/cutscenemodal.h index ff7c73f5..d7f74c19 100644 --- a/src/dusk/rpg/cutscene/item/ui/cutscenemodal.h +++ b/src/dusk/rpg/cutscene/item/ui/cutscenemodal.h @@ -40,6 +40,12 @@ typedef struct { uint8_t optionCount; cutscenemodaloptioncallback_t callback; + + // Whether the cutscene should wait for this dialog to close before + // continuing. Always treated as true when optionCount > 0 (there's no + // other way to move on). Ignored/false by default for a message-only + // dialog, e.g. from CUTSCENE_MODAL - see cutsceneModalUpdate. + bool_t blocking; } cutscenemodal_t; /** @@ -57,23 +63,24 @@ void cutsceneModalStart( ); /** - * Updates a modal item. A message-only modal (optionCount == 0, e.g. - * from CUTSCENE_MODAL) always completes immediately - the cutscene - * continues on to whatever follows this item right away, it does not - * wait for the dialog to be dismissed. Script what should happen while - * it's up, and how it gets closed, as later items in the same cutscene - * (e.g. CUTSCENE_CALLBACK, CUTSCENE_WAIT, CUTSCENE_MODAL_CLOSE). + * Updates a modal item. + * + * A message-only modal (optionCount == 0, e.g. from CUTSCENE_MODAL) + * completes immediately unless item->modal.blocking is set, in which + * case it waits for the dialog to close - either the player pressing + * accept/cancel (see uiModalMessageFocusSelected) or an explicit + * CUTSCENE_MODAL_CLOSE item elsewhere in the cutscene. * * A modal with options (optionCount > 0, e.g. from - * CUTSCENE_MODAL_OPTIONS) never completes on its own - the cutscene - * blocks here indefinitely. The option callback fires once the user - * picks something, and it alone is responsible for moving the cutscene - * on from there (typically via cutsceneGoTo or cutsceneRestart). + * CUTSCENE_MODAL_OPTIONS) always blocks regardless of the blocking + * field - it's dismissed by the player picking one of them, and the + * option callback is responsible for moving the cutscene on from there + * (typically via cutsceneGoTo or cutsceneRestart). * * @param item The cutscene item. * @param data Runtime data storage. - * @returns true once immediately for a message-only modal; false - * always for a modal with options. + * @returns true once the modal has closed, or immediately for a + * non-blocking message-only modal. */ bool_t cutsceneModalUpdate( const cutsceneitem_t *item, diff --git a/src/dusk/rpg/entity/global/entitygloballist.h b/src/dusk/rpg/entity/global/entitygloballist.h index c1437ce4..c2a83e79 100644 --- a/src/dusk/rpg/entity/global/entitygloballist.h +++ b/src/dusk/rpg/entity/global/entitygloballist.h @@ -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[] = { diff --git a/src/dusk/rpg/rpg.c b/src/dusk/rpg/rpg.c index d500dcda..56115801 100644 --- a/src/dusk/rpg/rpg.c +++ b/src/dusk/rpg/rpg.c @@ -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(); diff --git a/src/dusk/save/slot/saveslot.c b/src/dusk/save/slot/saveslot.c index 9051883e..f739e3d1 100644 --- a/src/dusk/save/slot/saveslot.c +++ b/src/dusk/save/slot/saveslot.c @@ -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) { diff --git a/src/dusk/save/slot/saveslotver1.c b/src/dusk/save/slot/saveslotver1.c index 15b0b912..753e36df 100644 --- a/src/dusk/save/slot/saveslotver1.c +++ b/src/dusk/save/slot/saveslotver1.c @@ -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); diff --git a/src/dusk/scene/battle/scenebattle.c b/src/dusk/scene/battle/scenebattle.c index 5c08319e..190db10a 100644 --- a/src/dusk/scene/battle/scenebattle.c +++ b/src/dusk/scene/battle/scenebattle.c @@ -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; diff --git a/src/dusk/ui/screen/battle/uibattlehud.c b/src/dusk/ui/screen/battle/uibattlehud.c index e4c7b54f..9126c1d8 100644 --- a/src/dusk/ui/screen/battle/uibattlehud.c +++ b/src/dusk/ui/screen/battle/uibattlehud.c @@ -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, diff --git a/src/dusk/ui/screen/battle/uibattlemenu.c b/src/dusk/ui/screen/battle/uibattlemenu.c index a71f33c1..2b88343c 100644 --- a/src/dusk/ui/screen/battle/uibattlemenu.c +++ b/src/dusk/ui/screen/battle/uibattlemenu.c @@ -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(); diff --git a/src/dusk/ui/screen/battle/uibattlemenu.h b/src/dusk/ui/screen/battle/uibattlemenu.h index 1e837355..8849fdaf 100644 --- a/src/dusk/ui/screen/battle/uibattlemenu.h +++ b/src/dusk/ui/screen/battle/uibattlemenu.h @@ -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 diff --git a/src/dusk/ui/widget/uimodal.c b/src/dusk/ui/widget/uimodal.c index 37e0eac6..9276f26b 100644 --- a/src/dusk/ui/widget/uimodal.c +++ b/src/dusk/ui/widget/uimodal.c @@ -7,6 +7,7 @@ #include "uimodal.h" #include "ui/widget/uiframe.h" +#include "ui/focus/uifocus.h" #include "assert/assert.h" #include "util/memory.h" #include "util/string.h" @@ -24,6 +25,11 @@ uimodal_t UI_MODAL; +// Focus item backing a message-only modal (no options) - lets a plain +// MODAL still block on player confirmation. NULL whenever a +// message-only modal isn't open. Mirrors uitextboxmain.c's pattern. +static uifocusitem_t *messageFocus = NULL; + void uiModalSelected( const uimenu_t *menu, const uint8_t index, @@ -39,6 +45,18 @@ void uiModalClosed(const uimenu_t *menu) { } } +bool_t uiModalMessageFocusSelected(const uifocusitem_t *item) { + uiFocusPopItem(messageFocus); + return true; +} + +bool_t uiModalMessageFocusClosed(const uifocusitem_t *item) { + messageFocus = NULL; + UI_MODAL.open = false; + uiModalClosed(NULL); + return true; +} + errorret_t uiModalInit(void) { memoryZero(&UI_MODAL, sizeof(uimodal_t)); @@ -212,6 +230,11 @@ void uiModalOpen( uiMenuOpen(&UI_MODAL.menu); } else { uiMenuInit(&UI_MODAL.menu, uiModalSelected, uiModalClosed, NULL); + messageFocus = uiFocusPush( + 1, 1, + uiModalMessageFocusSelected, NULL, uiModalMessageFocusClosed, + NULL, NULL + ); } if(UI_MODAL.onOpen != NULL) { @@ -225,6 +248,8 @@ void uiModalClose(uimodalclosedcallback_t callback) { if(uiMenuIsActive(&UI_MODAL.menu)) { uiMenuClose(&UI_MODAL.menu); + } else if(messageFocus != NULL) { + uiFocusPopItem(messageFocus); } else { uiModalClosed(NULL); } diff --git a/test/rpg/battle/test_battle.c b/test/rpg/battle/test_battle.c index abf19f10..c2112ba1 100644 --- a/test/rpg/battle/test_battle.c +++ b/test/rpg/battle/test_battle.c @@ -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); diff --git a/test/rpg/cutscene/CMakeLists.txt b/test/rpg/cutscene/CMakeLists.txt index d6f0804e..52464928 100644 --- a/test/rpg/cutscene/CMakeLists.txt +++ b/test/rpg/cutscene/CMakeLists.txt @@ -6,6 +6,7 @@ include(dusktest) # Tests +dusktest(test_cutscene.c) dusktest(test_cutscenesystem.c) dusktest(test_cutscenecontrol.c) dusktest(test_cutscenemaparea.c) diff --git a/test/rpg/cutscene/test_cutscene.c b/test/rpg/cutscene/test_cutscene.c new file mode 100644 index 00000000..2f0457cf --- /dev/null +++ b/test/rpg/cutscene/test_cutscene.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); +} diff --git a/test/rpg/cutscene/test_cutscenesystem.c b/test/rpg/cutscene/test_cutscenesystem.c index eaf02f32..c8dbf5ea 100644 --- a/test/rpg/cutscene/test_cutscenesystem.c +++ b/test/rpg/cutscene/test_cutscenesystem.c @@ -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), diff --git a/tools/asset/pack/__main__.py b/tools/asset/pack/__main__.py index c549ea0e..932346c8 100644 --- a/tools/asset/pack/__main__.py +++ b/tools/asset/pack/__main__.py @@ -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)