From d8273a225c313579f2886c95afd1d845dfd9b492 Mon Sep 17 00:00:00 2001 From: Dominic Masters Date: Sat, 19 Sep 2026 22:30:01 -0500 Subject: [PATCH 1/6] Battle changes, first pass. --- assets/battle/moves.jsonc | 20 +- assets/cutscenes/battle/moves/fire.jsonc | 9 + assets/cutscenes/battle/moves/slash.jsonc | 9 + assets/locale/en_US.po | 15 +- src/dusk/asset/assetdsk.h | 13 +- src/dusk/rpg/battle/CMakeLists.txt | 4 +- src/dusk/rpg/battle/battle.c | 42 +- src/dusk/rpg/battle/battle.h | 40 +- src/dusk/rpg/battle/battleaction.h | 27 -- src/dusk/rpg/battle/fighter/CMakeLists.txt | 10 + .../rpg/battle/{ => fighter}/battlefighter.c | 0 .../rpg/battle/{ => fighter}/battlefighter.h | 45 +- .../battle/fighter/battlefightercontroller.h | 18 + .../rpg/battle/fighter/battlefighterstats.h | 17 + .../rpg/battle/fighter/battlefighterstatus.h | 18 + .../battlefighterteam.h} | 11 +- src/dusk/rpg/battle/move/CMakeLists.txt | 10 + src/dusk/rpg/battle/move/battlemove.c | 79 ++++ src/dusk/rpg/battle/move/battlemove.h | 38 ++ src/dusk/rpg/battle/move/battletarget.h | 16 + src/dusk/rpg/battle/party.h | 8 +- .../rpg/cutscene/item/battle/CMakeLists.txt | 2 +- ...orceaction.c => cutscenebattleforcemove.c} | 19 +- ...orceaction.h => cutscenebattleforcemove.h} | 14 +- .../item/battle/cutscenestartbattle.c | 2 +- .../item/battle/cutscenestartbattle.h | 2 +- src/dusk/rpg/cutscene/item/cutsceneitem.c | 388 +++++++++--------- src/dusk/rpg/cutscene/item/cutsceneitem.h | 4 +- src/dusk/rpg/cutscene/item/cutsceneitembase.h | 73 ++-- .../item/json/cutscenejsonbattleability.c | 10 +- .../item/json/cutscenejsonbattleability.h | 6 +- src/dusk/rpg/entity/global/entitygloballist.h | 2 +- src/dusk/rpg/rpg.c | 2 + test/rpg/battle/test_battle.c | 142 ++----- tools/asset/pack/__main__.py | 2 +- 35 files changed, 618 insertions(+), 499 deletions(-) create mode 100644 assets/cutscenes/battle/moves/fire.jsonc create mode 100644 assets/cutscenes/battle/moves/slash.jsonc delete mode 100644 src/dusk/rpg/battle/battleaction.h create mode 100644 src/dusk/rpg/battle/fighter/CMakeLists.txt rename src/dusk/rpg/battle/{ => fighter}/battlefighter.c (100%) rename src/dusk/rpg/battle/{ => fighter}/battlefighter.h (59%) create mode 100644 src/dusk/rpg/battle/fighter/battlefightercontroller.h create mode 100644 src/dusk/rpg/battle/fighter/battlefighterstats.h create mode 100644 src/dusk/rpg/battle/fighter/battlefighterstatus.h rename src/dusk/rpg/battle/{battleability.h => fighter/battlefighterteam.h} (57%) create mode 100644 src/dusk/rpg/battle/move/CMakeLists.txt create mode 100644 src/dusk/rpg/battle/move/battlemove.c create mode 100644 src/dusk/rpg/battle/move/battlemove.h create mode 100644 src/dusk/rpg/battle/move/battletarget.h rename src/dusk/rpg/cutscene/item/battle/{cutscenebattleforceaction.c => cutscenebattleforcemove.c} (55%) rename src/dusk/rpg/cutscene/item/battle/{cutscenebattleforceaction.h => cutscenebattleforcemove.h} (72%) 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..0b455d28 --- /dev/null +++ b/assets/cutscenes/battle/moves/fire.jsonc @@ -0,0 +1,9 @@ +{ + "items": [ + { + "type": "MODAL", + "title": "BATTLE TEST", + "message": "BATTLE TEST" + } + ] +} \ No newline at end of file diff --git a/assets/cutscenes/battle/moves/slash.jsonc b/assets/cutscenes/battle/moves/slash.jsonc new file mode 100644 index 00000000..0b455d28 --- /dev/null +++ b/assets/cutscenes/battle/moves/slash.jsonc @@ -0,0 +1,9 @@ +{ + "items": [ + { + "type": "MODAL", + "title": "BATTLE TEST", + "message": "BATTLE TEST" + } + ] +} \ No newline at end of file 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..dc397b16 100644 --- a/src/dusk/rpg/battle/CMakeLists.txt +++ b/src/dusk/rpg/battle/CMakeLists.txt @@ -7,6 +7,8 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME} PUBLIC battle.c - battlefighter.c party.c ) + +add_subdirectory(fighter) +add_subdirectory(move) diff --git a/src/dusk/rpg/battle/battle.c b/src/dusk/rpg/battle/battle.c index 0a3ec94f..0d9ad040 100644 --- a/src/dusk/rpg/battle/battle.c +++ b/src/dusk/rpg/battle/battle.c @@ -10,10 +10,14 @@ #include "util/memory.h" #include "rpg/cutscene/cutscenesystem.h" +// TEMPORARY: hardcoded pending a real moves.jsonc-backed registry lookup. +#define BATTLE_MOVE_ID_ATTACK ((battlemoveid_t)1) + 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; } @@ -103,18 +107,14 @@ battleresult_t battleCheckResult(void) { return BATTLE.result; } -void battleQueueAction( +void battleQueueMove( const uint8_t fighterIndex, - const battleactiontype_t type, + const battlemoveid_t move, const uint8_t targetIndex ) { - battleaction_t *action = &BATTLE.actions[fighterIndex]; - action->type = type; + battlemoveaction_t *action = &BATTLE.moves[fighterIndex]; + action->move = move; action->targetIndex = targetIndex; - - if(BATTLE.onActionDecided != NULL) { - BATTLE.onActionDecided(&BATTLE.fighters[fighterIndex], action); - } } void battlePlayerAttack(const uint8_t targetIndex) { @@ -123,7 +123,7 @@ void battlePlayerAttack(const uint8_t targetIndex) { if(targetIndex >= BATTLE_FIGHTER_COUNT_MAX) return; if(!battleFighterIsAlive(&BATTLE.fighters[targetIndex])) return; - battleQueueAction(fighter->id, BATTLE_ACTION_ATTACK, targetIndex); + battleQueueMove(fighter->id, BATTLE_MOVE_ID_ATTACK, targetIndex); BATTLE.selectionIndex++; battleAdvanceSelection(); } @@ -220,9 +220,11 @@ void battleMoveTeamFirst(const battlefighterteam_t team) { } battlefighter_t *battleAIChooseTarget(const battlefighter_t *fighter) { - const battlefighterteam_t enemyTeam = + const battlefighterteam_t enemyTeam = ( fighter->team == BATTLE_FIGHTER_TEAM_ALLY ? - BATTLE_FIGHTER_TEAM_ENEMY : BATTLE_FIGHTER_TEAM_ALLY; + BATTLE_FIGHTER_TEAM_ENEMY : + BATTLE_FIGHTER_TEAM_ALLY + ); battlefighter_t *weakest = NULL; for(uint8_t i = 0; i < BATTLE_FIGHTER_COUNT_MAX; i++) { @@ -238,9 +240,7 @@ battlefighter_t *battleAIChooseTarget(const battlefighter_t *fighter) { } void battleSetState(const battlestate_t next) { - const battlestate_t previous = BATTLE.state; BATTLE.state = next; - if(BATTLE.onStateChanged != NULL) BATTLE.onStateChanged(previous, next); } void battleSetResult(const battleresult_t result) { @@ -254,7 +254,7 @@ bool_t battleFighterNeedsDecision( ) { return battleFighterIsAlive(&BATTLE.fighters[fighterIndex]) && BATTLE.fighters[fighterIndex].controller == controller - && BATTLE.actions[fighterIndex].type == BATTLE_ACTION_NONE; + && BATTLE.moves[fighterIndex].move == BATTLE_MOVE_ID_NULL; } void battleAdvanceSelection(void) { @@ -272,10 +272,10 @@ void battleAdvanceSelection(void) { } 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 + // No need to clear BATTLE.moves here: every living fighter's move is + // unconditionally reset to BATTLE_MOVE_ID_NULL as it's processed in // battleUpdateMovesExecuting, and round 1 starts pre-zeroed by - // battleInit(). Clearing it here would also wipe out any action a + // battleInit(). Clearing it here would also wipe out any move a // cutscene force-queued while parked at BATTLE_STATE_PRE_ROUND. battleBuildExecutionOrder(BATTLE.round == 1); @@ -295,7 +295,7 @@ void battleUpdateAiSelection(void) { battleAIChooseTarget(&BATTLE.fighters[fighterIndex]); if(target == NULL) continue; - battleQueueAction(fighterIndex, BATTLE_ACTION_ATTACK, target->id); + battleQueueMove(fighterIndex, BATTLE_MOVE_ID_ATTACK, target->id); } BATTLE.executionIndex = 0; @@ -312,12 +312,12 @@ void battleUpdateMovesExecuting(void) { battlefighter_t *fighter = &BATTLE.fighters[fighterIndex]; if(!battleFighterIsAlive(fighter)) return; - battleaction_t *action = &BATTLE.actions[fighterIndex]; - if(action->type == BATTLE_ACTION_ATTACK) { + battlemoveaction_t *action = &BATTLE.moves[fighterIndex]; + if(action->move == BATTLE_MOVE_ID_ATTACK) { battlefighter_t *target = &BATTLE.fighters[action->targetIndex]; if(battleFighterIsAlive(target)) battleResolveAttack(fighter, target); } - action->type = BATTLE_ACTION_NONE; + action->move = BATTLE_MOVE_ID_NULL; battleCheckResult(); } diff --git a/src/dusk/rpg/battle/battle.h b/src/dusk/rpg/battle/battle.h index 6bdfee25..306bef7e 100644 --- a/src/dusk/rpg/battle/battle.h +++ b/src/dusk/rpg/battle/battle.h @@ -6,8 +6,8 @@ */ #pragma once -#include "battlefighter.h" -#include "battleaction.h" +#include "rpg/battle/fighter/battlefighter.h" +#include "rpg/battle/move/battlemove.h" #define BATTLE_FIGHTER_COUNT_MAX 8 @@ -43,15 +43,10 @@ typedef enum { BATTLE_STATE_COUNT } battlestate_t; -typedef void (*battlestatechangedcallback_t)( - const battlestate_t previous, - const battlestate_t next -); - typedef struct { battlestate_t state; battlefighter_t fighters[BATTLE_FIGHTER_COUNT_MAX]; - battleaction_t actions[BATTLE_FIGHTER_COUNT_MAX]; + battlemoveaction_t moves[BATTLE_FIGHTER_COUNT_MAX]; battleencountertype_t encounterType; bool_t fleeAvailable; @@ -66,9 +61,6 @@ typedef struct { // 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; @@ -163,25 +155,24 @@ void battleResolveAttack( 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. + * Queues a move for a fighter to perform once BATTLE_STATE_MOVES_EXECUTING + * reaches them this round, overwriting any move already queued for that + * fighter. * * @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. + * @param move The move to perform. + * @param targetIndex Index into BATTLE.fighters of the target. */ -void battleQueueAction( +void battleQueueMove( const uint8_t fighterIndex, - const battleactiontype_t type, + const battlemoveid_t move, 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. + * Queues the move and advances the selection cursor. * * @param targetIndex Index into BATTLE.fighters of the target. */ @@ -230,8 +221,7 @@ void battleMoveTeamFirst(const battlefighterteam_t team); battlefighter_t *battleAIChooseTarget(const battlefighter_t *fighter); /** - * Sets BATTLE.state and fires BATTLE.onStateChanged with the previous and - * new state. + * Sets BATTLE.state. * * @param next The state to transition to. */ @@ -247,13 +237,13 @@ void battleSetResult(const battleresult_t result); /** * 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. + * be deciding a move 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. + * move queued yet. */ bool_t battleFighterNeedsDecision( const uint8_t fighterIndex, @@ -280,7 +270,7 @@ void battleUpdatePreRound(void); void battleUpdateAiSelection(void); /** - * Handles BATTLE_STATE_MOVES_EXECUTING: resolves one queued action from + * Handles BATTLE_STATE_MOVES_EXECUTING: resolves one queued move from * BATTLE.executionOrder per call, or transitions to BATTLE_STATE_POST_ROUND * once every fighter this round has been processed. */ 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/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/battlefighter.c b/src/dusk/rpg/battle/fighter/battlefighter.c similarity index 100% rename from src/dusk/rpg/battle/battlefighter.c rename to src/dusk/rpg/battle/fighter/battlefighter.c diff --git a/src/dusk/rpg/battle/battlefighter.h b/src/dusk/rpg/battle/fighter/battlefighter.h similarity index 59% rename from src/dusk/rpg/battle/battlefighter.h rename to src/dusk/rpg/battle/fighter/battlefighter.h index 1b3674b1..fb24db73 100644 --- a/src/dusk/rpg/battle/battlefighter.h +++ b/src/dusk/rpg/battle/fighter/battlefighter.h @@ -7,46 +7,15 @@ #pragma once #include "error/error.h" -#include "battleability.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" #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 { +typedef struct battlefighter_s { uint8_t id; battlefighterstatus_t status; battlefighterteam_t team; @@ -58,7 +27,7 @@ typedef struct { uint16_t mpMax; battlefighterstats_t stats; - battleability_t abilities[BATTLE_FIGHTER_ABILITY_COUNT_MAX]; + battlemoveid_t abilities[BATTLE_FIGHTER_ABILITY_COUNT_MAX]; } battlefighter_t; /** 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/battlefighterstats.h b/src/dusk/rpg/battle/fighter/battlefighterstats.h new file mode 100644 index 00000000..1d9820e1 --- /dev/null +++ b/src/dusk/rpg/battle/fighter/battlefighterstats.h @@ -0,0 +1,17 @@ +/** + * 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 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..c24e496d --- /dev/null +++ b/src/dusk/rpg/battle/fighter/battlefighterstatus.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 "dusk.h" + +typedef enum { + BATTLE_FIGHTER_STATUS_NULL, + + BATTLE_FIGHTER_STATUS_NORMAL, + BATTLE_FIGHTER_STATUS_DEAD, + + 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..1c33cb96 --- /dev/null +++ b/src/dusk/rpg/battle/move/battlemove.c @@ -0,0 +1,79 @@ +/** + * 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" + +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; + + // 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))) { + assertTrue( + BATTLE_MOVE_COUNT < BATTLE_MOVE_COUNT_MAX, + "Too many battle moves defined." + ); + + yyjson_val *val = yyjson_obj_iter_get_val(key); + battlemoveid_t id = BATTLE_MOVE_COUNT++; + battleability_t *move = &BATTLE_MOVES[id]; + + stringCopy(move->name, yyjson_get_str(key), BATTLE_MOVE_NAME_MAX); + + battletarget_t target = 0; + if(yyjson_get_bool(yyjson_obj_get(val, "enemies"))) { + target |= BATTLE_TARGET_ENEMIES; + } + if(yyjson_get_bool(yyjson_obj_get(val, "allies"))) { + target |= BATTLE_TARGET_ALLIES; + } + if(yyjson_get_bool(yyjson_obj_get(val, "multiple"))) { + target |= BATTLE_TARGET_MULTIPLE; + } + if(yyjson_get_bool(yyjson_obj_get(val, "self"))) { + target |= BATTLE_TARGET_SELF; + } + move->target = target; + + const char_t *defaultTarget = yyjson_get_str( + yyjson_obj_get(val, "default") + ); + if(defaultTarget != NULL && stringEquals(defaultTarget, "enemy")) { + move->targetDefault = BATTLE_TARGET_ENEMIES; + } else if(defaultTarget != NULL && stringEquals(defaultTarget, "ally")) { + move->targetDefault = BATTLE_TARGET_ALLIES; + } else if(defaultTarget != NULL && stringEquals(defaultTarget, "self")) { + move->targetDefault = BATTLE_TARGET_SELF; + } else { + move->targetDefault = 0; + } + } + + assetUnlockEntry(entry); + errorOk(); +} diff --git a/src/dusk/rpg/battle/move/battlemove.h b/src/dusk/rpg/battle/move/battlemove.h new file mode 100644 index 00000000..46b23a5d --- /dev/null +++ b/src/dusk/rpg/battle/move/battlemove.h @@ -0,0 +1,38 @@ +/** + * 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" + +#define BATTLE_MOVE_NAME_MAX 16 +#define BATTLE_MOVE_COUNT_MAX 250 + +typedef uint8_t battlemoveid_t; +#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; + +typedef struct { + battlemoveid_t move; + uint8_t targetIndex; +} battlemoveaction_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); 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.h b/src/dusk/rpg/battle/party.h index 3e8f4e8f..9e83485e 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; /** diff --git a/src/dusk/rpg/cutscene/item/battle/CMakeLists.txt b/src/dusk/rpg/cutscene/item/battle/CMakeLists.txt index 60a5f108..715334ca 100644 --- a/src/dusk/rpg/cutscene/item/battle/CMakeLists.txt +++ b/src/dusk/rpg/cutscene/item/battle/CMakeLists.txt @@ -7,6 +7,6 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME} PUBLIC cutscenestartbattle.c cutscenebattlewaitstate.c - cutscenebattleforceaction.c + cutscenebattleforcemove.c cutsceneregularbattle.c ) diff --git a/src/dusk/rpg/cutscene/item/battle/cutscenebattleforceaction.c b/src/dusk/rpg/cutscene/item/battle/cutscenebattleforcemove.c similarity index 55% rename from src/dusk/rpg/cutscene/item/battle/cutscenebattleforceaction.c rename to src/dusk/rpg/cutscene/item/battle/cutscenebattleforcemove.c index 34a497fe..dca48c23 100644 --- a/src/dusk/rpg/cutscene/item/battle/cutscenebattleforceaction.c +++ b/src/dusk/rpg/cutscene/item/battle/cutscenebattleforcemove.c @@ -7,30 +7,33 @@ #include "rpg/cutscene/item/cutsceneitem.h" -void cutsceneBattleForceActionStart( +// TEMPORARY: hardcoded pending a real moves.jsonc-backed registry lookup. +#define BATTLE_MOVE_ID_ATTACK ((battlemoveid_t)1) + +void cutsceneBattleForceMoveStart( const cutsceneitem_t *item, cutsceneitemdata_t *data ) { - battleQueueAction( - item->battleForceAction.fighterIndex, BATTLE_ACTION_ATTACK, - item->battleForceAction.targetIndex + battleQueueMove( + item->battleForceMove.fighterIndex, BATTLE_MOVE_ID_ATTACK, + item->battleForceMove.targetIndex ); } -bool_t cutsceneBattleForceActionUpdate( +bool_t cutsceneBattleForceMoveUpdate( const cutsceneitem_t *item, cutsceneitemdata_t *data ) { return true; } -errorret_t cutsceneBattleForceActionLoad( +errorret_t cutsceneBattleForceMoveLoad( yyjson_val *itemObj, cutsceneitem_t *item ) { - item->battleForceAction.fighterIndex = + item->battleForceMove.fighterIndex = (uint8_t)yyjson_get_int(yyjson_obj_get(itemObj, "fighterIndex")); - item->battleForceAction.targetIndex = + item->battleForceMove.targetIndex = (uint8_t)yyjson_get_int(yyjson_obj_get(itemObj, "targetIndex")); errorOk(); diff --git a/src/dusk/rpg/cutscene/item/battle/cutscenebattleforceaction.h b/src/dusk/rpg/cutscene/item/battle/cutscenebattleforcemove.h similarity index 72% rename from src/dusk/rpg/cutscene/item/battle/cutscenebattleforceaction.h rename to src/dusk/rpg/cutscene/item/battle/cutscenebattleforcemove.h index 4acb73dc..1889b126 100644 --- a/src/dusk/rpg/cutscene/item/battle/cutscenebattleforceaction.h +++ b/src/dusk/rpg/cutscene/item/battle/cutscenebattleforcemove.h @@ -12,41 +12,41 @@ typedef struct { uint8_t fighterIndex; uint8_t targetIndex; -} cutscenebattleforceaction_t; +} cutscenebattleforcemove_t; /** - * Starts a battle force-action step: immediately queues an attack for the + * Starts a battle force-move step: immediately queues an attack for the * given fighter against the given target, bypassing normal player/AI * selection for that fighter this round. * * @param item The cutscene item. * @param data Runtime data storage. */ -void cutsceneBattleForceActionStart( +void cutsceneBattleForceMoveStart( const cutsceneitem_t *item, cutsceneitemdata_t *data ); /** - * Updates a battle force-action step (always completes immediately). + * Updates a battle force-move step (always completes immediately). * * @param item The cutscene item. * @param data Runtime data storage. * @returns true always. */ -bool_t cutsceneBattleForceActionUpdate( +bool_t cutsceneBattleForceMoveUpdate( const cutsceneitem_t *item, cutsceneitemdata_t *data ); /** - * Parses a BATTLE_FORCE_ACTION cutscene item's JSON fields into item. + * Parses a BATTLE_FORCE_MOVE 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 cutsceneBattleForceMoveLoad( yyjson_val *itemObj, cutsceneitem_t *item ); diff --git a/src/dusk/rpg/cutscene/item/battle/cutscenestartbattle.c b/src/dusk/rpg/cutscene/item/battle/cutscenestartbattle.c index 3cd12e7b..38abb5cb 100644 --- a/src/dusk/rpg/cutscene/item/battle/cutscenestartbattle.c +++ b/src/dusk/rpg/cutscene/item/battle/cutscenestartbattle.c @@ -183,7 +183,7 @@ errorret_t cutsceneStartBattleLoad( yyjson_val *abilityVal; size_t a = 0; while((abilityVal = yyjson_arr_iter_next(&abilityIter)) != NULL) { - battleability_t ability; + battlemoveid_t ability; if(!cutsceneJsonLookupBattleAbility( yyjson_get_str(abilityVal), &ability )) { diff --git a/src/dusk/rpg/cutscene/item/battle/cutscenestartbattle.h b/src/dusk/rpg/cutscene/item/battle/cutscenestartbattle.h index 405cbfbd..dab96d8b 100644 --- a/src/dusk/rpg/cutscene/item/battle/cutscenestartbattle.h +++ b/src/dusk/rpg/cutscene/item/battle/cutscenestartbattle.h @@ -16,7 +16,7 @@ typedef struct { battlefighterstats_t stats; uint16_t healthMax; uint16_t mpMax; - battleability_t abilities[BATTLE_FIGHTER_ABILITY_COUNT_MAX]; + battlemoveid_t abilities[BATTLE_FIGHTER_ABILITY_COUNT_MAX]; } cutscenestartbattleenemy_t; typedef struct { diff --git a/src/dusk/rpg/cutscene/item/cutsceneitem.c b/src/dusk/rpg/cutscene/item/cutsceneitem.c index 149b9c04..98fcad25 100644 --- a/src/dusk/rpg/cutscene/item/cutsceneitem.c +++ b/src/dusk/rpg/cutscene/item/cutsceneitem.c @@ -17,27 +17,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 +37,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 +56,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 +84,194 @@ 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_START_BATTLE] = { + .init = cutsceneStartBattleStart, + .update = cutsceneStartBattleUpdate, + .jsonName = "START_BATTLE", + .load = cutsceneStartBattleLoad + }, + + [CUTSCENE_ITEM_TYPE_BATTLE_WAIT_STATE] = { + .update = cutsceneBattleWaitStateUpdate, + .jsonName = "BATTLE_WAIT_STATE", + .load = cutsceneBattleWaitStateLoad + }, + + [CUTSCENE_ITEM_TYPE_BATTLE_FORCE_MOVE] = { + .init = cutsceneBattleForceMoveStart, + .update = cutsceneBattleForceMoveUpdate, + .jsonName = "BATTLE_FORCE_MOVE", + .load = cutsceneBattleForceMoveLoad + }, + + [CUTSCENE_ITEM_TYPE_REGULAR_BATTLE] = { + .update = cutsceneRegularBattleUpdate, + .jsonName = "REGULAR_BATTLE", + .load = cutsceneItemLoadNoop + }, + + // Save [CUTSCENE_ITEM_TYPE_SAVE_DEVICE_CHECK] = { .init = cutsceneSaveDeviceCheckStart, .update = cutsceneSaveDeviceCheckUpdate, @@ -248,11 +286,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 +349,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, @@ -506,8 +515,9 @@ void cutsceneInsertResolveAndSplice(const char_t *name) { // nothing needs to keep the doc itself resident past the parse. 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); + errorret_t parseResult = cutsceneParseDoc( + entry->data.json, &scene, items, CUTSCENE_INSERT_ITEMS_MAX + ); assetUnlockEntry(entry); if(errorIsNotOk(parseResult)) { errorCatch(errorPrint(parseResult)); diff --git a/src/dusk/rpg/cutscene/item/cutsceneitem.h b/src/dusk/rpg/cutscene/item/cutsceneitem.h index 006b79a0..872271a6 100644 --- a/src/dusk/rpg/cutscene/item/cutsceneitem.h +++ b/src/dusk/rpg/cutscene/item/cutsceneitem.h @@ -42,7 +42,7 @@ #include "maparea/cutscenemapareawait.h" #include "battle/cutscenestartbattle.h" #include "battle/cutscenebattlewaitstate.h" -#include "battle/cutscenebattleforceaction.h" +#include "battle/cutscenebattleforcemove.h" #include "battle/cutsceneregularbattle.h" #include "save/cutscenesavedevicecheck.h" #include "save/cutscenesaveloadallslots.h" @@ -120,7 +120,7 @@ struct cutsceneitem_s { cutsceneemoji_t emoji; cutsceneshake_t shake; cutscenebattlewaitstate_t battleWaitState; - cutscenebattleforceaction_t battleForceAction; + cutscenebattleforcemove_t battleForceMove; 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..f2cd7518 100644 --- a/src/dusk/rpg/cutscene/item/cutsceneitembase.h +++ b/src/dusk/rpg/cutscene/item/cutsceneitembase.h @@ -16,41 +16,60 @@ 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_START_BATTLE, + CUTSCENE_ITEM_TYPE_BATTLE_WAIT_STATE, + CUTSCENE_ITEM_TYPE_BATTLE_FORCE_MOVE, + CUTSCENE_ITEM_TYPE_REGULAR_BATTLE, + + // 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 +79,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/cutscenejsonbattleability.c b/src/dusk/rpg/cutscene/item/json/cutscenejsonbattleability.c index adb9dc39..01c64077 100644 --- a/src/dusk/rpg/cutscene/item/json/cutscenejsonbattleability.c +++ b/src/dusk/rpg/cutscene/item/json/cutscenejsonbattleability.c @@ -8,8 +8,12 @@ #include "cutscenejsonbattleability.h" #include "cutscenejsonenum.h" +// TEMPORARY: hardcoded pending a real moves.jsonc-backed registry lookup +// (see BATTLE_MOVE_COUNT). +#define BATTLE_MOVE_ID_FIRE ((battlemoveid_t)2) + static const cutscenejsonenumentry_t CUTSCENE_JSON_BATTLE_ABILITY_TABLE[] = { - { "FIRE", BATTLE_ABILITY_FIRE }, + { "FIRE", BATTLE_MOVE_ID_FIRE }, }; #define CUTSCENE_JSON_BATTLE_ABILITY_TABLE_COUNT \ (sizeof(CUTSCENE_JSON_BATTLE_ABILITY_TABLE) / \ @@ -17,7 +21,7 @@ static const cutscenejsonenumentry_t CUTSCENE_JSON_BATTLE_ABILITY_TABLE[] = { bool_t cutsceneJsonLookupBattleAbility( const char_t *name, - battleability_t *out + battlemoveid_t *out ) { int32_t value; if(!cutsceneJsonLookupEnumInsensitive( @@ -26,6 +30,6 @@ bool_t cutsceneJsonLookupBattleAbility( )) { return false; } - *out = (battleability_t)value; + *out = (battlemoveid_t)value; return true; } diff --git a/src/dusk/rpg/cutscene/item/json/cutscenejsonbattleability.h b/src/dusk/rpg/cutscene/item/json/cutscenejsonbattleability.h index bc6abd95..ec2d92c7 100644 --- a/src/dusk/rpg/cutscene/item/json/cutscenejsonbattleability.h +++ b/src/dusk/rpg/cutscene/item/json/cutscenejsonbattleability.h @@ -7,16 +7,16 @@ #pragma once #include "dusk.h" -#include "rpg/battle/battleability.h" +#include "rpg/battle/move/battlemove.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. + * @param out Set to the matching move id on success. * @return true if a matching entry was found, false otherwise. */ bool_t cutsceneJsonLookupBattleAbility( const char_t *name, - battleability_t *out + battlemoveid_t *out ); 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/test/rpg/battle/test_battle.c b/test/rpg/battle/test_battle.c index abf19f10..30956300 100644 --- a/test/rpg/battle/test_battle.c +++ b/test/rpg/battle/test_battle.c @@ -9,7 +9,10 @@ #include "rpg/battle/battle.h" #include "rpg/cutscene/cutscenesystem.h" -// Fighter slots as added by test_battleCutsceneForceActionOverridesTarget: +// TEMPORARY: hardcoded pending a real moves.jsonc-backed registry lookup. +#define BATTLE_MOVE_ID_ATTACK ((battlemoveid_t)1) + +// Fighter slots as added by test_battleCutsceneForceMoveOverridesTarget: // 0 = allyA, 1 = allyB, 2 = enemy. static cutsceneitem_t CUTSCENE_TEST_SCRATCH_ITEMS[] = { // Waiting BEFORE pausing is the correct order: pausing first would @@ -22,8 +25,8 @@ static cutsceneitem_t CUTSCENE_TEST_SCRATCH_ITEMS[] = { { .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_BATTLE_FORCE_MOVE, + .battleForceMove = { .fighterIndex = 2, .targetIndex = 0 } }, { .type = CUTSCENE_ITEM_TYPE_SET_PAUSE, .setPause = CUTSCENE_PAUSE_NONE } }; @@ -101,13 +104,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].targetIndex, 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 +137,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].targetIndex, ally->id); } static void test_battleMovesExecutingResolvesInSpeedOrderAndFizzles( @@ -278,107 +281,12 @@ 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 +// CUTSCENE_BATTLE_FORCE_MOVE 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) { +static void test_battleCutsceneForceMoveOverridesTarget(void **state) { battleInit(); @@ -411,22 +319,22 @@ static void test_battleCutsceneForceActionOverridesTarget(void **state) { 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 + cutsceneSystemUpdate();// SET_PAUSE done -> FORCE_MOVE 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.moves[enemy->id].move, BATTLE_MOVE_ID_ATTACK); + assert_int_equal(BATTLE.moves[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 + cutsceneSystemUpdate();// FORCE_MOVE 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); + // The forced move from three frames ago survived untouched. + assert_int_equal(BATTLE.moves[enemy->id].move, BATTLE_MOVE_ID_ATTACK); + assert_int_equal(BATTLE.moves[enemy->id].targetIndex, allyA->id); battlePlayerAttack(enemy->id);// allyA -> AI_SELECTION would be next... assert_ptr_equal(battleGetCurrentFighter(), allyB); @@ -437,7 +345,7 @@ static void test_battleCutsceneForceActionOverridesTarget(void **state) { // AI_SELECTION: enemy already decided, skipped -> MOVES_EXECUTING battleUpdate(); // untouched - assert_int_equal(BATTLE.actions[enemy->id].targetIndex, allyA->id); + assert_int_equal(BATTLE.moves[enemy->id].targetIndex, allyA->id); battleUpdate();// allyA attacks enemy battleUpdate();// allyB attacks enemy @@ -458,11 +366,7 @@ 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), + cmocka_unit_test(test_battleCutsceneForceMoveOverridesTarget), }; return cmocka_run_group_tests(tests, NULL, NULL); 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) From 3675ec634f5e180ff3f5d46007b875593f4ebc71 Mon Sep 17 00:00:00 2001 From: Dominic Masters Date: Sat, 19 Sep 2026 23:17:27 -0500 Subject: [PATCH 2/6] Rework battle system: pointer-based fighters, per-state files, move split Splits battlemoveaction_t into its own header, adds a per-state struct/init/update/dispose file under rpg/battle/state driven by a dispatch table, switches BATTLE.fighters to battlefighter_t* backed by a pool, folds current/max health and mp into battlefighterstats_t, and adds battleFighterDamage/Heal/AbilityAdd. Removes the now-unused battle cutscene items (START_BATTLE, BATTLE_WAIT_STATE, BATTLE_FORCE_MOVE, REGULAR_BATTLE) and their JSON lookup helpers, and adds JSON validation to battleMoveInit. battle.h/battle.c are mid-rewrite around the new state machine, so some UI consumers (uibattlehud.c, uibattlemenu.c) and test_battle.c are currently out of sync with the new API. Co-Authored-By: Claude Sonnet 5 --- src/dusk/rpg/battle/CMakeLists.txt | 1 + src/dusk/rpg/battle/battle.c | 325 ++---------------- src/dusk/rpg/battle/battle.h | 275 ++------------- src/dusk/rpg/battle/fighter/battlefighter.c | 46 ++- src/dusk/rpg/battle/fighter/battlefighter.h | 54 ++- .../rpg/battle/fighter/battlefighterpos.h | 27 ++ .../rpg/battle/fighter/battlefighterstats.h | 5 + .../rpg/battle/fighter/battlefighterstatus.h | 2 + src/dusk/rpg/battle/move/battlemove.c | 101 ++++-- src/dusk/rpg/battle/move/battlemove.h | 8 +- src/dusk/rpg/battle/move/battlemoveaction.h | 18 + src/dusk/rpg/battle/party.c | 7 +- src/dusk/rpg/battle/party.h | 8 +- src/dusk/rpg/battle/state/CMakeLists.txt | 19 + src/dusk/rpg/battle/state/battlestate.c | 66 ++++ src/dusk/rpg/battle/state/battlestate.h | 64 ++++ src/dusk/rpg/battle/state/battlestateending.c | 20 ++ src/dusk/rpg/battle/state/battlestateending.h | 28 ++ .../rpg/battle/state/battlestateexecuting.c | 20 ++ .../rpg/battle/state/battlestateexecuting.h | 28 ++ src/dusk/rpg/battle/state/battlestatefled.c | 20 ++ src/dusk/rpg/battle/state/battlestatefled.h | 28 ++ src/dusk/rpg/battle/state/battlestatelost.c | 20 ++ src/dusk/rpg/battle/state/battlestatelost.h | 28 ++ .../rpg/battle/state/battlestateopening.c | 20 ++ .../rpg/battle/state/battlestateopening.h | 28 ++ .../rpg/battle/state/battlestatepostround.c | 20 ++ .../rpg/battle/state/battlestatepostround.h | 28 ++ .../rpg/battle/state/battlestatepreround.c | 20 ++ .../rpg/battle/state/battlestatepreround.h | 28 ++ .../rpg/battle/state/battlestateselection.c | 20 ++ .../rpg/battle/state/battlestateselection.h | 28 ++ src/dusk/rpg/battle/state/battlestatewon.c | 20 ++ src/dusk/rpg/battle/state/battlestatewon.h | 28 ++ src/dusk/rpg/cutscene/item/CMakeLists.txt | 1 - .../rpg/cutscene/item/battle/CMakeLists.txt | 12 - .../item/battle/cutscenebattleforcemove.c | 40 --- .../item/battle/cutscenebattleforcemove.h | 52 --- .../item/battle/cutscenebattlewaitstate.c | 31 -- .../item/battle/cutscenebattlewaitstate.h | 40 --- .../item/battle/cutsceneregularbattle.c | 15 - .../item/battle/cutsceneregularbattle.h | 29 -- .../item/battle/cutscenestartbattle.c | 204 ----------- .../item/battle/cutscenestartbattle.h | 72 ---- src/dusk/rpg/cutscene/item/cutsceneitem.c | 27 -- src/dusk/rpg/cutscene/item/cutsceneitem.h | 7 - src/dusk/rpg/cutscene/item/cutsceneitembase.h | 6 - .../rpg/cutscene/item/json/CMakeLists.txt | 3 - .../item/json/cutscenejsonbattleability.c | 35 -- .../item/json/cutscenejsonbattleability.h | 22 -- .../item/json/cutscenejsonbattleencounter.c | 34 -- .../item/json/cutscenejsonbattleencounter.h | 23 -- .../item/json/cutscenejsonbattlestate.c | 34 -- .../item/json/cutscenejsonbattlestate.h | 19 - src/dusk/save/slot/saveslot.c | 8 +- src/dusk/save/slot/saveslotver1.c | 16 +- src/dusk/scene/battle/scenebattle.c | 12 +- src/dusk/ui/screen/battle/uibattlehud.c | 8 +- src/dusk/ui/screen/battle/uibattlemenu.c | 6 +- src/dusk/ui/screen/battle/uibattlemenu.h | 2 +- test/rpg/battle/test_battle.c | 106 +----- 61 files changed, 878 insertions(+), 1444 deletions(-) create mode 100644 src/dusk/rpg/battle/fighter/battlefighterpos.h create mode 100644 src/dusk/rpg/battle/move/battlemoveaction.h create mode 100644 src/dusk/rpg/battle/state/CMakeLists.txt create mode 100644 src/dusk/rpg/battle/state/battlestate.c create mode 100644 src/dusk/rpg/battle/state/battlestate.h create mode 100644 src/dusk/rpg/battle/state/battlestateending.c create mode 100644 src/dusk/rpg/battle/state/battlestateending.h create mode 100644 src/dusk/rpg/battle/state/battlestateexecuting.c create mode 100644 src/dusk/rpg/battle/state/battlestateexecuting.h create mode 100644 src/dusk/rpg/battle/state/battlestatefled.c create mode 100644 src/dusk/rpg/battle/state/battlestatefled.h create mode 100644 src/dusk/rpg/battle/state/battlestatelost.c create mode 100644 src/dusk/rpg/battle/state/battlestatelost.h create mode 100644 src/dusk/rpg/battle/state/battlestateopening.c create mode 100644 src/dusk/rpg/battle/state/battlestateopening.h create mode 100644 src/dusk/rpg/battle/state/battlestatepostround.c create mode 100644 src/dusk/rpg/battle/state/battlestatepostround.h create mode 100644 src/dusk/rpg/battle/state/battlestatepreround.c create mode 100644 src/dusk/rpg/battle/state/battlestatepreround.h create mode 100644 src/dusk/rpg/battle/state/battlestateselection.c create mode 100644 src/dusk/rpg/battle/state/battlestateselection.h create mode 100644 src/dusk/rpg/battle/state/battlestatewon.c create mode 100644 src/dusk/rpg/battle/state/battlestatewon.h delete mode 100644 src/dusk/rpg/cutscene/item/battle/CMakeLists.txt delete mode 100644 src/dusk/rpg/cutscene/item/battle/cutscenebattleforcemove.c delete mode 100644 src/dusk/rpg/cutscene/item/battle/cutscenebattleforcemove.h delete mode 100644 src/dusk/rpg/cutscene/item/battle/cutscenebattlewaitstate.c delete mode 100644 src/dusk/rpg/cutscene/item/battle/cutscenebattlewaitstate.h delete mode 100644 src/dusk/rpg/cutscene/item/battle/cutsceneregularbattle.c delete mode 100644 src/dusk/rpg/cutscene/item/battle/cutsceneregularbattle.h delete mode 100644 src/dusk/rpg/cutscene/item/battle/cutscenestartbattle.c delete mode 100644 src/dusk/rpg/cutscene/item/battle/cutscenestartbattle.h delete mode 100644 src/dusk/rpg/cutscene/item/json/cutscenejsonbattleability.c delete mode 100644 src/dusk/rpg/cutscene/item/json/cutscenejsonbattleability.h delete mode 100644 src/dusk/rpg/cutscene/item/json/cutscenejsonbattleencounter.c delete mode 100644 src/dusk/rpg/cutscene/item/json/cutscenejsonbattleencounter.h delete mode 100644 src/dusk/rpg/cutscene/item/json/cutscenejsonbattlestate.c delete mode 100644 src/dusk/rpg/cutscene/item/json/cutscenejsonbattlestate.h diff --git a/src/dusk/rpg/battle/CMakeLists.txt b/src/dusk/rpg/battle/CMakeLists.txt index dc397b16..49363600 100644 --- a/src/dusk/rpg/battle/CMakeLists.txt +++ b/src/dusk/rpg/battle/CMakeLists.txt @@ -12,3 +12,4 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME} 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 0d9ad040..ac6a40be 100644 --- a/src/dusk/rpg/battle/battle.c +++ b/src/dusk/rpg/battle/battle.c @@ -10,319 +10,48 @@ #include "util/memory.h" #include "rpg/cutscene/cutscenesystem.h" -// TEMPORARY: hardcoded pending a real moves.jsonc-backed registry lookup. -#define BATTLE_MOVE_ID_ATTACK ((battlemoveid_t)1) - 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; + #if DUSK_ASSERTIONS + // Must be at least two non null fighter present. + uint8_t nonNullCount = 0; + for(uint8_t i = 0; i < BATTLE_FIGHTER_POS_COUNT; i++) { + if(fighters[i] != NULL) nonNullCount++; + } + assertTrue(nonNullCount >= 2, "Must have at least two fighters"); + #endif - battleSetState(BATTLE_STATE_OPENING); -} -void battleDispose(void) { - battleInit(); -} + // TODO: I may not need to zero entire battle. + memoryZero(&BATTLE, sizeof(battle_t)); + memoryCopy( + BATTLE.fighters, + fighters, + sizeof(battlefighter_t *) * BATTLE_FIGHTER_POS_COUNT + ); -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 battleQueueMove( - const uint8_t fighterIndex, - const battlemoveid_t move, - const uint8_t targetIndex -) { - battlemoveaction_t *action = &BATTLE.moves[fighterIndex]; - action->move = move; - action->targetIndex = targetIndex; -} - -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; - - battleQueueMove(fighter->id, BATTLE_MOVE_ID_ATTACK, targetIndex); - BATTLE.selectionIndex++; - battleAdvanceSelection(); -} - -void battlePlayerFlee(void) { - battlefighter_t *fighter = battleGetCurrentFighter(); - if(fighter == NULL) return; - if(!BATTLE.fleeAvailable) return; - - battleSetResult(BATTLE_RESULT_FLED); + // Call init on opening state. + BATTLE.state = BATTLE_STATE_OPENING; + BATTLE_STATE_FUNCTIONS[BATTLE.state].init(); } 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; - } + if(BATTLE.state == BATTLE_STATE_NULL) return; + BATTLE_STATE_FUNCTIONS[BATTLE.state].update(); } -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; - } +void battleDispose(void) { + if(BATTLE.state == BATTLE_STATE_NULL) return; - // 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--; - } - BATTLE.executionOrder[j + 1] = key; - } - - 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]; - } - - memoryCopy( - BATTLE.executionOrder, sorted, sizeof(uint8_t) * BATTLE.executionCount - ); -} - -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 - ); - - 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; -} - -void battleSetState(const battlestate_t next) { - BATTLE.state = next; -} - -void battleSetResult(const battleresult_t result) { - BATTLE.result = result; - battleSetState(BATTLE_STATE_ENDED); -} - -bool_t battleFighterNeedsDecision( - const uint8_t fighterIndex, - const battlefightercontroller_t controller -) { - return battleFighterIsAlive(&BATTLE.fighters[fighterIndex]) - && BATTLE.fighters[fighterIndex].controller == controller - && BATTLE.moves[fighterIndex].move == BATTLE_MOVE_ID_NULL; -} - -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.moves here: every living fighter's move is - // unconditionally reset to BATTLE_MOVE_ID_NULL as it's processed in - // battleUpdateMovesExecuting, and round 1 starts pre-zeroed by - // battleInit(). Clearing it here would also wipe out any move 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; - - battleQueueMove(fighterIndex, BATTLE_MOVE_ID_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; - - battlemoveaction_t *action = &BATTLE.moves[fighterIndex]; - if(action->move == BATTLE_MOVE_ID_ATTACK) { - battlefighter_t *target = &BATTLE.fighters[action->targetIndex]; - if(battleFighterIsAlive(target)) battleResolveAttack(fighter, target); - } - action->move = BATTLE_MOVE_ID_NULL; - - 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 306bef7e..1d071b09 100644 --- a/src/dusk/rpg/battle/battle.h +++ b/src/dusk/rpg/battle/battle.h @@ -8,59 +8,29 @@ #pragma once #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" -#define BATTLE_FIGHTER_COUNT_MAX 8 - -typedef enum { - BATTLE_ENCOUNTER_REGULAR, - BATTLE_ENCOUNTER_PLAYER_ADVANTAGE, - BATTLE_ENCOUNTER_BACK_ATTACK, - - BATTLE_ENCOUNTER_COUNT -} battleencountertype_t; - -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 enum { +// BATTLE_RESULT_NONE, +// BATTLE_RESULT_WIN, +// BATTLE_RESULT_LOSS, +// BATTLE_RESULT_FLED, +// BATTLE_RESULT_COUNT +// } battleresult_t; typedef struct { + battlefighter_t *fighters[BATTLE_FIGHTER_POS_COUNT]; + + // Order of who gets to act. + uint8_t fighterOrder[BATTLE_FIGHTER_POS_COUNT]; + + // Actions to be executed. + battlemoveaction_t actionsPending[BATTLE_FIGHTER_POS_COUNT]; + battlestate_t state; - battlefighter_t fighters[BATTLE_FIGHTER_COUNT_MAX]; - battlemoveaction_t moves[BATTLE_FIGHTER_COUNT_MAX]; - - battleencountertype_t encounterType; - bool_t fleeAvailable; - 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; + battlestatedata_t stateData; } battle_t; extern battle_t BATTLE; @@ -71,213 +41,20 @@ 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 a move for a fighter to perform once BATTLE_STATE_MOVES_EXECUTING - * reaches them this round, overwriting any move already queued for that - * fighter. - * - * @param fighterIndex Index into BATTLE.fighters of the deciding fighter. - * @param move The move to perform. - * @param targetIndex Index into BATTLE.fighters of the target. - */ -void battleQueueMove( - const uint8_t fighterIndex, - const battlemoveid_t move, - 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 move 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. + * Updates the battle system. */ 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). + * Disposes of the battle, clearing all fighters and marking it inactive. */ -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. - * - * @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. - */ -void battleSetResult(const battleresult_t result); - -/** - * 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 a move 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 - * move queued yet. - */ -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 move 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/fighter/battlefighter.c b/src/dusk/rpg/battle/fighter/battlefighter.c index 8d72d552..d2b9e385 100644 --- a/src/dusk/rpg/battle/fighter/battlefighter.c +++ b/src/dusk/rpg/battle/fighter/battlefighter.c @@ -13,9 +13,7 @@ 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 + const battlefighterstats_t stats ) { assertNotNull(fighter, "Fighter pointer cannot be NULL"); assertTrue(team < BATTLE_FIGHTER_TEAM_COUNT, "Invalid fighter team"); @@ -31,13 +29,47 @@ void battleFighterInit( fighter->team = team; fighter->controller = controller; fighter->stats = stats; - fighter->healthMax = healthMax; - fighter->health = healthMax; - fighter->mpMax = mpMax; - fighter->mp = mpMax; + 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 index fb24db73..46482373 100644 --- a/src/dusk/rpg/battle/fighter/battlefighter.h +++ b/src/dusk/rpg/battle/fighter/battlefighter.h @@ -12,6 +12,7 @@ #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 @@ -21,33 +22,27 @@ typedef struct battlefighter_s { battlefighterteam_t team; battlefightercontroller_t controller; - uint16_t health; - uint16_t healthMax; - uint16_t mp; - uint16_t mpMax; 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 their maximum - * values and setting its status to normal. + * 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. - * @param healthMax The fighter's maximum health. - * @param mpMax The fighter's maximum mp. + * @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, - const uint16_t healthMax, - const uint16_t mpMax + const battlefighterstats_t stats ); /** @@ -58,3 +53,38 @@ void battleFighterInit( * @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/battlefighterpos.h b/src/dusk/rpg/battle/fighter/battlefighterpos.h new file mode 100644 index 00000000..98389c40 --- /dev/null +++ b/src/dusk/rpg/battle/fighter/battlefighterpos.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 "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) \ No newline at end of file diff --git a/src/dusk/rpg/battle/fighter/battlefighterstats.h b/src/dusk/rpg/battle/fighter/battlefighterstats.h index 1d9820e1..f4b731bb 100644 --- a/src/dusk/rpg/battle/fighter/battlefighterstats.h +++ b/src/dusk/rpg/battle/fighter/battlefighterstats.h @@ -9,6 +9,11 @@ #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; diff --git a/src/dusk/rpg/battle/fighter/battlefighterstatus.h b/src/dusk/rpg/battle/fighter/battlefighterstatus.h index c24e496d..3c73af2d 100644 --- a/src/dusk/rpg/battle/fighter/battlefighterstatus.h +++ b/src/dusk/rpg/battle/fighter/battlefighterstatus.h @@ -14,5 +14,7 @@ typedef enum { 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/move/battlemove.c b/src/dusk/rpg/battle/move/battlemove.c index 1c33cb96..45b57269 100644 --- a/src/dusk/rpg/battle/move/battlemove.c +++ b/src/dusk/rpg/battle/move/battlemove.c @@ -8,9 +8,9 @@ #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; @@ -34,43 +34,92 @@ errorret_t battleMoveInit(void) { yyjson_obj_iter iter = yyjson_obj_iter_with(moves); yyjson_val *key; while((key = yyjson_obj_iter_next(&iter))) { - assertTrue( - BATTLE_MOVE_COUNT < BATTLE_MOVE_COUNT_MAX, - "Too many battle moves defined." - ); + 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 + ); + } - yyjson_val *val = yyjson_obj_iter_get_val(key); battlemoveid_t id = BATTLE_MOVE_COUNT++; battleability_t *move = &BATTLE_MOVES[id]; - stringCopy(move->name, yyjson_get_str(key), BATTLE_MOVE_NAME_MAX); + // 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(yyjson_obj_get(val, "enemies"))) { - target |= BATTLE_TARGET_ENEMIES; - } - if(yyjson_get_bool(yyjson_obj_get(val, "allies"))) { - target |= BATTLE_TARGET_ALLIES; - } - if(yyjson_get_bool(yyjson_obj_get(val, "multiple"))) { - target |= BATTLE_TARGET_MULTIPLE; - } - if(yyjson_get_bool(yyjson_obj_get(val, "self"))) { - target |= BATTLE_TARGET_SELF; - } + 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); + } - const char_t *defaultTarget = yyjson_get_str( - yyjson_obj_get(val, "default") - ); - if(defaultTarget != NULL && stringEquals(defaultTarget, "enemy")) { + // 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(defaultTarget != NULL && stringEquals(defaultTarget, "ally")) { + } else if(stringCompareInsensitive(defaultTarget, "ally") == 0) { move->targetDefault = BATTLE_TARGET_ALLIES; - } else if(defaultTarget != NULL && stringEquals(defaultTarget, "self")) { + } else if(stringCompareInsensitive(defaultTarget, "self") == 0) { move->targetDefault = BATTLE_TARGET_SELF; } else { - move->targetDefault = 0; + assetUnlockEntry(entry); + errorThrow("Battle move %s has invalid default target.", name); } } diff --git a/src/dusk/rpg/battle/move/battlemove.h b/src/dusk/rpg/battle/move/battlemove.h index 46b23a5d..538306d6 100644 --- a/src/dusk/rpg/battle/move/battlemove.h +++ b/src/dusk/rpg/battle/move/battlemove.h @@ -9,10 +9,11 @@ #include "asset/asset.h" #include "battletarget.h" +typedef uint8_t battlemoveid_t; + #define BATTLE_MOVE_NAME_MAX 16 #define BATTLE_MOVE_COUNT_MAX 250 -typedef uint8_t battlemoveid_t; #define BATTLE_MOVE_ID_NULL ((battlemoveid_t)0) extern uint8_t BATTLE_MOVE_COUNT; @@ -23,11 +24,6 @@ typedef struct { battletarget_t targetDefault; } battleability_t; -typedef struct { - battlemoveid_t move; - uint8_t targetIndex; -} battlemoveaction_t; - extern battleability_t BATTLE_MOVES[BATTLE_MOVE_COUNT_MAX]; /** 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/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 9e83485e..367d01d7 100644 --- a/src/dusk/rpg/battle/party.h +++ b/src/dusk/rpg/battle/party.h @@ -38,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..448171d4 --- /dev/null +++ b/src/dusk/rpg/battle/state/battlestateending.c @@ -0,0 +1,20 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "battlestateending.h" + +void battleStateEndingInit(void) { + +} + +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..56721c24 --- /dev/null +++ b/src/dusk/rpg/battle/state/battlestateending.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 { + +} battlestateending_t; + +/** + * Called when the battle enters BATTLE_STATE_ENDING. + */ +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..c2a058a8 --- /dev/null +++ b/src/dusk/rpg/battle/state/battlestateexecuting.c @@ -0,0 +1,20 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "battlestateexecuting.h" + +void battleStateExecutingInit(void) { + +} + +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..1dc49c9d --- /dev/null +++ b/src/dusk/rpg/battle/state/battlestateexecuting.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 { + +} battlestateexecuting_t; + +/** + * Called when the battle enters BATTLE_STATE_EXECUTING. + */ +void battleStateExecutingInit(void); + +/** + * 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..1fbbed4d --- /dev/null +++ b/src/dusk/rpg/battle/state/battlestatefled.c @@ -0,0 +1,20 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "battlestatefled.h" + +void battleStateFledInit(void) { + +} + +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..1cf2ef39 --- /dev/null +++ b/src/dusk/rpg/battle/state/battlestatefled.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 { + +} battlestatefled_t; + +/** + * Called when the battle enters BATTLE_STATE_FLED. + */ +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..e160279b --- /dev/null +++ b/src/dusk/rpg/battle/state/battlestatelost.c @@ -0,0 +1,20 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "battlestatelost.h" + +void battleStateLostInit(void) { + +} + +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..202de391 --- /dev/null +++ b/src/dusk/rpg/battle/state/battlestatelost.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 { + +} battlestatelost_t; + +/** + * Called when the battle enters BATTLE_STATE_LOST. + */ +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..d39afe17 --- /dev/null +++ b/src/dusk/rpg/battle/state/battlestateopening.c @@ -0,0 +1,20 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "battlestateopening.h" + +void battleStateOpeningInit(void) { + +} + +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..297ae979 --- /dev/null +++ b/src/dusk/rpg/battle/state/battlestatepreround.c @@ -0,0 +1,20 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "battlestatepreround.h" + +void battleStatePreRoundInit(void) { + +} + +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..a753b8df --- /dev/null +++ b/src/dusk/rpg/battle/state/battlestateselection.c @@ -0,0 +1,20 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "battlestateselection.h" + +void battleStateSelectionInit(void) { + +} + +void battleStateSelectionUpdate(void) { + +} + +void battleStateSelectionDispose(void) { + +} diff --git a/src/dusk/rpg/battle/state/battlestateselection.h b/src/dusk/rpg/battle/state/battlestateselection.h new file mode 100644 index 00000000..4fad166f --- /dev/null +++ b/src/dusk/rpg/battle/state/battlestateselection.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 { + +} battlestateselection_t; + +/** + * Called when the battle enters BATTLE_STATE_SELECTION. + */ +void battleStateSelectionInit(void); + +/** + * Updates BATTLE_STATE_SELECTION for one frame. + */ +void battleStateSelectionUpdate(void); + +/** + * Called when the battle leaves BATTLE_STATE_SELECTION. + */ +void battleStateSelectionDispose(void); diff --git a/src/dusk/rpg/battle/state/battlestatewon.c b/src/dusk/rpg/battle/state/battlestatewon.c new file mode 100644 index 00000000..bfd92054 --- /dev/null +++ b/src/dusk/rpg/battle/state/battlestatewon.c @@ -0,0 +1,20 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "battlestatewon.h" + +void battleStateWonInit(void) { + +} + +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..e7b920e6 --- /dev/null +++ b/src/dusk/rpg/battle/state/battlestatewon.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 { + +} battlestatewon_t; + +/** + * Called when the battle enters BATTLE_STATE_WON. + */ +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/item/CMakeLists.txt b/src/dusk/rpg/cutscene/item/CMakeLists.txt index 42868c1b..1915c7e2 100755 --- a/src/dusk/rpg/cutscene/item/CMakeLists.txt +++ b/src/dusk/rpg/cutscene/item/CMakeLists.txt @@ -16,7 +16,6 @@ add_subdirectory(entity) add_subdirectory(item) add_subdirectory(maparea) add_subdirectory(ui) -add_subdirectory(battle) add_subdirectory(save) add_subdirectory(audio) add_subdirectory(json) diff --git a/src/dusk/rpg/cutscene/item/battle/CMakeLists.txt b/src/dusk/rpg/cutscene/item/battle/CMakeLists.txt deleted file mode 100644 index 715334ca..00000000 --- a/src/dusk/rpg/cutscene/item/battle/CMakeLists.txt +++ /dev/null @@ -1,12 +0,0 @@ -# Copyright (c) 2026 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -target_sources(${DUSK_LIBRARY_TARGET_NAME} - PUBLIC - cutscenestartbattle.c - cutscenebattlewaitstate.c - cutscenebattleforcemove.c - cutsceneregularbattle.c -) diff --git a/src/dusk/rpg/cutscene/item/battle/cutscenebattleforcemove.c b/src/dusk/rpg/cutscene/item/battle/cutscenebattleforcemove.c deleted file mode 100644 index dca48c23..00000000 --- a/src/dusk/rpg/cutscene/item/battle/cutscenebattleforcemove.c +++ /dev/null @@ -1,40 +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" - -// TEMPORARY: hardcoded pending a real moves.jsonc-backed registry lookup. -#define BATTLE_MOVE_ID_ATTACK ((battlemoveid_t)1) - -void cutsceneBattleForceMoveStart( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { - battleQueueMove( - item->battleForceMove.fighterIndex, BATTLE_MOVE_ID_ATTACK, - item->battleForceMove.targetIndex - ); -} - -bool_t cutsceneBattleForceMoveUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { - return true; -} - -errorret_t cutsceneBattleForceMoveLoad( - yyjson_val *itemObj, - cutsceneitem_t *item -) { - item->battleForceMove.fighterIndex = - (uint8_t)yyjson_get_int(yyjson_obj_get(itemObj, "fighterIndex")); - item->battleForceMove.targetIndex = - (uint8_t)yyjson_get_int(yyjson_obj_get(itemObj, "targetIndex")); - - errorOk(); -} diff --git a/src/dusk/rpg/cutscene/item/battle/cutscenebattleforcemove.h b/src/dusk/rpg/cutscene/item/battle/cutscenebattleforcemove.h deleted file mode 100644 index 1889b126..00000000 --- a/src/dusk/rpg/cutscene/item/battle/cutscenebattleforcemove.h +++ /dev/null @@ -1,52 +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 { - uint8_t fighterIndex; - uint8_t targetIndex; -} cutscenebattleforcemove_t; - -/** - * Starts a battle force-move step: immediately queues an attack for the - * given fighter against the given target, bypassing normal player/AI - * selection for that fighter this round. - * - * @param item The cutscene item. - * @param data Runtime data storage. - */ -void cutsceneBattleForceMoveStart( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); - -/** - * Updates a battle force-move step (always completes immediately). - * - * @param item The cutscene item. - * @param data Runtime data storage. - * @returns true always. - */ -bool_t cutsceneBattleForceMoveUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); - -/** - * Parses a BATTLE_FORCE_MOVE 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 cutsceneBattleForceMoveLoad( - yyjson_val *itemObj, - cutsceneitem_t *item -); 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 38abb5cb..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) { - battlemoveid_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 dab96d8b..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; - battlemoveid_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 98fcad25..e1b31a1d 100644 --- a/src/dusk/rpg/cutscene/item/cutsceneitem.c +++ b/src/dusk/rpg/cutscene/item/cutsceneitem.c @@ -244,33 +244,6 @@ cutsceneitemcallbacks_t CUTSCENE_ITEM_CALLBACKS[CUTSCENE_ITEM_TYPE_COUNT] = { .load = cutsceneMapAreaWaitLoad }, - // Battle - [CUTSCENE_ITEM_TYPE_START_BATTLE] = { - .init = cutsceneStartBattleStart, - .update = cutsceneStartBattleUpdate, - .jsonName = "START_BATTLE", - .load = cutsceneStartBattleLoad - }, - - [CUTSCENE_ITEM_TYPE_BATTLE_WAIT_STATE] = { - .update = cutsceneBattleWaitStateUpdate, - .jsonName = "BATTLE_WAIT_STATE", - .load = cutsceneBattleWaitStateLoad - }, - - [CUTSCENE_ITEM_TYPE_BATTLE_FORCE_MOVE] = { - .init = cutsceneBattleForceMoveStart, - .update = cutsceneBattleForceMoveUpdate, - .jsonName = "BATTLE_FORCE_MOVE", - .load = cutsceneBattleForceMoveLoad - }, - - [CUTSCENE_ITEM_TYPE_REGULAR_BATTLE] = { - .update = cutsceneRegularBattleUpdate, - .jsonName = "REGULAR_BATTLE", - .load = cutsceneItemLoadNoop - }, - // Save [CUTSCENE_ITEM_TYPE_SAVE_DEVICE_CHECK] = { .init = cutsceneSaveDeviceCheckStart, diff --git a/src/dusk/rpg/cutscene/item/cutsceneitem.h b/src/dusk/rpg/cutscene/item/cutsceneitem.h index 872271a6..3847fc36 100644 --- a/src/dusk/rpg/cutscene/item/cutsceneitem.h +++ b/src/dusk/rpg/cutscene/item/cutsceneitem.h @@ -40,10 +40,6 @@ #include "maparea/cutscenemapareaadd.h" #include "maparea/cutscenemaparearemove.h" #include "maparea/cutscenemapareawait.h" -#include "battle/cutscenestartbattle.h" -#include "battle/cutscenebattlewaitstate.h" -#include "battle/cutscenebattleforcemove.h" -#include "battle/cutsceneregularbattle.h" #include "save/cutscenesavedevicecheck.h" #include "save/cutscenesaveloadallslots.h" #include "audio/cutsceneaudioplay.h" @@ -116,11 +112,8 @@ struct cutsceneitem_s { cutscenemapareaadd_t mapAreaAdd; cutscenemaparearemove_t mapAreaRemove; cutscenemapareawait_t mapAreaWait; - cutscenestartbattle_t startBattle; cutsceneemoji_t emoji; cutsceneshake_t shake; - cutscenebattlewaitstate_t battleWaitState; - cutscenebattleforcemove_t battleForceMove; 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 f2cd7518..e738e082 100644 --- a/src/dusk/rpg/cutscene/item/cutsceneitembase.h +++ b/src/dusk/rpg/cutscene/item/cutsceneitembase.h @@ -59,12 +59,6 @@ typedef enum { CUTSCENE_ITEM_TYPE_MAP_AREA_REMOVE, CUTSCENE_ITEM_TYPE_MAP_AREA_WAIT, - // Battle - CUTSCENE_ITEM_TYPE_START_BATTLE, - CUTSCENE_ITEM_TYPE_BATTLE_WAIT_STATE, - CUTSCENE_ITEM_TYPE_BATTLE_FORCE_MOVE, - CUTSCENE_ITEM_TYPE_REGULAR_BATTLE, - // Save CUTSCENE_ITEM_TYPE_SAVE_DEVICE_CHECK, CUTSCENE_ITEM_TYPE_SAVE_LOAD_ALL_SLOTS, diff --git a/src/dusk/rpg/cutscene/item/json/CMakeLists.txt b/src/dusk/rpg/cutscene/item/json/CMakeLists.txt index a6a8e175..57b7e422 100644 --- a/src/dusk/rpg/cutscene/item/json/CMakeLists.txt +++ b/src/dusk/rpg/cutscene/item/json/CMakeLists.txt @@ -14,9 +14,6 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME} cutscenejsonemoji.c cutscenejsonentitytype.c cutscenejsonentitydir.c - cutscenejsonbattleencounter.c - cutscenejsonbattleability.c - cutscenejsonbattlestate.c cutscenejsonscenetype.c cutscenejsonuiscreentype.c cutscenejsonaudiochannel.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 01c64077..00000000 --- a/src/dusk/rpg/cutscene/item/json/cutscenejsonbattleability.c +++ /dev/null @@ -1,35 +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" - -// TEMPORARY: hardcoded pending a real moves.jsonc-backed registry lookup -// (see BATTLE_MOVE_COUNT). -#define BATTLE_MOVE_ID_FIRE ((battlemoveid_t)2) - -static const cutscenejsonenumentry_t CUTSCENE_JSON_BATTLE_ABILITY_TABLE[] = { - { "FIRE", BATTLE_MOVE_ID_FIRE }, -}; -#define CUTSCENE_JSON_BATTLE_ABILITY_TABLE_COUNT \ - (sizeof(CUTSCENE_JSON_BATTLE_ABILITY_TABLE) / \ - sizeof(cutscenejsonenumentry_t)) - -bool_t cutsceneJsonLookupBattleAbility( - const char_t *name, - battlemoveid_t *out -) { - int32_t value; - if(!cutsceneJsonLookupEnumInsensitive( - CUTSCENE_JSON_BATTLE_ABILITY_TABLE, - CUTSCENE_JSON_BATTLE_ABILITY_TABLE_COUNT, name, &value - )) { - return false; - } - *out = (battlemoveid_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 ec2d92c7..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/move/battlemove.h" - -/** - * Looks up name (case-insensitively) against the battle ability table. - * - * @param name Name to search for. - * @param out Set to the matching move id on success. - * @return true if a matching entry was found, false otherwise. - */ -bool_t cutsceneJsonLookupBattleAbility( - const char_t *name, - battlemoveid_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 deleted file mode 100644 index 1e9762aa..00000000 --- a/src/dusk/rpg/cutscene/item/json/cutscenejsonbattlestate.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 "cutscenejsonbattlestate.h" -#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 }, - { "POST_ROUND", BATTLE_STATE_POST_ROUND }, - { "ENDED", BATTLE_STATE_ENDED }, -}; -#define CUTSCENE_JSON_BATTLE_STATE_TABLE_COUNT \ - (sizeof(CUTSCENE_JSON_BATTLE_STATE_TABLE) / sizeof(cutscenejsonenumentry_t)) - -bool_t cutsceneJsonLookupBattleState(const char_t *name, battlestate_t *out) { - int32_t value; - if(!cutsceneJsonLookupEnumInsensitive( - CUTSCENE_JSON_BATTLE_STATE_TABLE, CUTSCENE_JSON_BATTLE_STATE_TABLE_COUNT, - name, &value - )) { - return false; - } - *out = (battlestate_t)value; - return true; -} diff --git a/src/dusk/rpg/cutscene/item/json/cutscenejsonbattlestate.h b/src/dusk/rpg/cutscene/item/json/cutscenejsonbattlestate.h deleted file mode 100644 index 1122430e..00000000 --- a/src/dusk/rpg/cutscene/item/json/cutscenejsonbattlestate.h +++ /dev/null @@ -1,19 +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 state table. - * - * @param name Name to search for. - * @param out Set to the matching battle state on success. - * @return true if a matching entry was found, false otherwise. - */ -bool_t cutsceneJsonLookupBattleState(const char_t *name, battlestate_t *out); 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..7aa3c7c2 100644 --- a/src/dusk/scene/battle/scenebattle.c +++ b/src/dusk/scene/battle/scenebattle.c @@ -45,7 +45,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)); } @@ -94,15 +94,15 @@ void sceneBattleWorldToScreen( } void sceneBattleGetFighterPosition(const uint8_t fighterIndex, vec3 out) { - const battlefighter_t *fighter = &BATTLE.fighters[fighterIndex]; + const battlefighter_t *fighter = BATTLE.fighters[fighterIndex]; // 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++) { + if(BATTLE.fighters[i]->status == BATTLE_FIGHTER_STATUS_NULL) continue; + if(BATTLE.fighters[i]->team != fighter->team) continue; if(i < fighterIndex) ordinal++; teamCount++; } @@ -119,7 +119,7 @@ void sceneBattleGetFighterPosition(const uint8_t fighterIndex, vec3 out) { } errorret_t sceneBattleDrawFighter(const uint8_t fighterIndex) { - const battlefighter_t *fighter = &BATTLE.fighters[fighterIndex]; + const battlefighter_t *fighter = BATTLE.fighters[fighterIndex]; 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..085b0f21 100644 --- a/src/dusk/ui/screen/battle/uibattlehud.c +++ b/src/dusk/ui/screen/battle/uibattlehud.c @@ -46,8 +46,8 @@ errorret_t uiBattleHudDraw(void) { 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->status == BATTLE_FIGHTER_STATUS_NULL) continue; vec3 position; @@ -60,14 +60,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..de209629 100644 --- a/src/dusk/ui/screen/battle/uibattlemenu.c +++ b/src/dusk/ui/screen/battle/uibattlemenu.c @@ -40,8 +40,8 @@ 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->team != enemyTeam) continue; if(!battleFighterIsAlive(candidate)) continue; @@ -49,7 +49,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]); 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/test/rpg/battle/test_battle.c b/test/rpg/battle/test_battle.c index 30956300..c2112ba1 100644 --- a/test/rpg/battle/test_battle.c +++ b/test/rpg/battle/test_battle.c @@ -7,36 +7,10 @@ #include "dusktest.h" #include "rpg/battle/battle.h" -#include "rpg/cutscene/cutscenesystem.h" // TEMPORARY: hardcoded pending a real moves.jsonc-backed registry lookup. #define BATTLE_MOVE_ID_ATTACK ((battlemoveid_t)1) -// Fighter slots as added by test_battleCutsceneForceMoveOverridesTarget: -// 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_MOVE, - .battleForceMove = { .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 -}; - static battlefighter_t *addFighter( const battlefighterteam_t team, const battlefightercontroller_t controller, @@ -110,7 +84,7 @@ static void test_battlePlayerSelectionAdvancesAndSkipsDecidedFighters( battlePlayerAttack(enemy->id); assert_int_equal(BATTLE.moves[ally1->id].move, BATTLE_MOVE_ID_ATTACK); - assert_int_equal(BATTLE.moves[ally1->id].targetIndex, enemy->id); + 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. @@ -138,7 +112,7 @@ 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.moves[enemy->id].move, BATTLE_MOVE_ID_ATTACK); - assert_int_equal(BATTLE.moves[enemy->id].targetIndex, ally->id); + assert_int_equal(BATTLE.moves[enemy->id].targets[0], ally->id); } static void test_battleMovesExecutingResolvesInSpeedOrderAndFizzles( @@ -281,81 +255,6 @@ static void test_battleFleeUnavailableIsIgnored(void **state) { assert_int_equal(BATTLE.result, BATTLE_RESULT_NONE); } -// Exercises CUTSCENE_BATTLE_WAIT_STATE + CUTSCENE_SET_PAUSE(BATTLE) + -// CUTSCENE_BATTLE_FORCE_MOVE 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_battleCutsceneForceMoveOverridesTarget(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_MOVE queues enemy's move - battleUpdate();// still paused: no-op - - assert_int_equal(BATTLE.moves[enemy->id].move, BATTLE_MOVE_ID_ATTACK); - assert_int_equal(BATTLE.moves[enemy->id].targetIndex, allyA->id); - assert_int_equal(BATTLE.state, BATTLE_STATE_PRE_ROUND);// still frozen - - cutsceneSystemUpdate();// FORCE_MOVE 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 move from three frames ago survived untouched. - assert_int_equal(BATTLE.moves[enemy->id].move, BATTLE_MOVE_ID_ATTACK); - assert_int_equal(BATTLE.moves[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.moves[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), @@ -366,7 +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_battleCutsceneForceMoveOverridesTarget), }; return cmocka_run_group_tests(tests, NULL, NULL); From e3f56c4d1275015fbba8e433af785b4e3224f7a2 Mon Sep 17 00:00:00 2001 From: Dominic Masters Date: Sun, 20 Sep 2026 13:32:52 -0500 Subject: [PATCH 3/6] Drive battle rounds through cutscene items, fix render/init bugs Adds BATTLE_START, BATTLE_SET_STATE and BATTLE_WAIT_SELECTION cutscene items so a battle's state transitions (opening -> pre-round -> selection -> executing) are chained through the running cutscene, with cutsceneSystemInsertItemsNext/InsertItems splitting "splice" from "splice and advance immediately". Adds battlestateselection's get-current-fighter/player-attack/player-flee API backing the battle UI, and battleMoveGetIdByName for JSON-authored ability lookups. Fixes: off-by-one in battleMoveInit that let the first-defined move alias BATTLE_MOVE_ID_NULL; a segfault in scenebattle.c from rendering unfilled (NULL) fighter slots; stale battleGetCurrentFighter/ battlePlayerAttack/battlePlayerFlee references in the battle UI. Adds assertions across the battle/scene/UI code for fighter-index bounds and non-null invariants. Updates battletest.jsonc for the new BATTLE_START item schema. Co-Authored-By: Claude Sonnet 5 --- assets/cutscenes/battletest.jsonc | 22 ++- src/dusk/rpg/battle/battle.c | 28 +++- src/dusk/rpg/battle/battle.h | 8 + src/dusk/rpg/battle/move/battlemove.c | 13 +- src/dusk/rpg/battle/move/battlemove.h | 8 + .../rpg/battle/state/battlestateopening.c | 10 +- .../rpg/battle/state/battlestatepreround.c | 13 +- .../rpg/battle/state/battlestateselection.c | 101 +++++++++++- .../rpg/battle/state/battlestateselection.h | 58 ++++++- src/dusk/rpg/cutscene/cutscenesystem.c | 24 ++- src/dusk/rpg/cutscene/cutscenesystem.h | 26 ++++ src/dusk/rpg/cutscene/item/CMakeLists.txt | 1 + .../rpg/cutscene/item/battle/CMakeLists.txt | 11 ++ .../item/battle/cutscenebattlesetstate.c | 41 +++++ .../item/battle/cutscenebattlesetstate.h | 50 ++++++ .../item/battle/cutscenebattlestart.c | 144 ++++++++++++++++++ .../item/battle/cutscenebattlestart.h | 62 ++++++++ .../item/battle/cutscenebattlewaitselection.c | 16 ++ .../item/battle/cutscenebattlewaitselection.h | 25 +++ src/dusk/rpg/cutscene/item/cutsceneitem.c | 21 +++ src/dusk/rpg/cutscene/item/cutsceneitem.h | 5 + src/dusk/rpg/cutscene/item/cutsceneitembase.h | 5 + .../rpg/cutscene/item/json/CMakeLists.txt | 1 + .../item/json/cutscenejsonbattlestate.c | 38 +++++ .../item/json/cutscenejsonbattlestate.h | 21 +++ src/dusk/scene/battle/scenebattle.c | 16 +- src/dusk/ui/screen/battle/uibattlehud.c | 3 +- src/dusk/ui/screen/battle/uibattlemenu.c | 10 +- 28 files changed, 747 insertions(+), 34 deletions(-) create mode 100644 src/dusk/rpg/cutscene/item/battle/CMakeLists.txt create mode 100644 src/dusk/rpg/cutscene/item/battle/cutscenebattlesetstate.c create mode 100644 src/dusk/rpg/cutscene/item/battle/cutscenebattlesetstate.h create mode 100644 src/dusk/rpg/cutscene/item/battle/cutscenebattlestart.c create mode 100644 src/dusk/rpg/cutscene/item/battle/cutscenebattlestart.h create mode 100644 src/dusk/rpg/cutscene/item/battle/cutscenebattlewaitselection.c create mode 100644 src/dusk/rpg/cutscene/item/battle/cutscenebattlewaitselection.h create mode 100644 src/dusk/rpg/cutscene/item/json/cutscenejsonbattlestate.c create mode 100644 src/dusk/rpg/cutscene/item/json/cutscenejsonbattlestate.h 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/src/dusk/rpg/battle/battle.c b/src/dusk/rpg/battle/battle.c index ac6a40be..f9e2d47a 100644 --- a/src/dusk/rpg/battle/battle.c +++ b/src/dusk/rpg/battle/battle.c @@ -9,6 +9,7 @@ #include "assert/assert.h" #include "util/memory.h" #include "rpg/cutscene/cutscenesystem.h" +#include "scene/scene.h" battle_t BATTLE; @@ -22,7 +23,8 @@ void battleStart( assertNotNull(fighters, "Fighters cannot be NULL"); #if DUSK_ASSERTIONS - // Must be at least two non null fighter present. + // Must be at least two non null fighter present, may need to ensure they + // are from seperate teams also. uint8_t nonNullCount = 0; for(uint8_t i = 0; i < BATTLE_FIGHTER_POS_COUNT; i++) { if(fighters[i] != NULL) nonNullCount++; @@ -39,18 +41,38 @@ void battleStart( sizeof(battlefighter_t *) * BATTLE_FIGHTER_POS_COUNT ); - // Call init on opening state. - BATTLE.state = BATTLE_STATE_OPENING; + // Enter the opening state. + battleSetState(BATTLE_STATE_OPENING); + + // Change scene + sceneSet(SCENE_TYPE_BATTLE); +} + +void battleSetState(const battlestate_t next) { + assertTrue(next < BATTLE_STATE_COUNT, "Invalid battle state"); + assertNotNull(BATTLE_STATE_FUNCTIONS[next].init, "Missing state init function"); + assertNotNull(BATTLE_STATE_FUNCTIONS[next].update, "Missing state update function"); + assertNotNull(BATTLE_STATE_FUNCTIONS[next].dispose, "Missing state dispose function"); + + if(BATTLE.state != BATTLE_STATE_NULL) { + BATTLE_STATE_FUNCTIONS[BATTLE.state].dispose(); + } + BATTLE.state = next; BATTLE_STATE_FUNCTIONS[BATTLE.state].init(); } void battleUpdate(void) { if(BATTLE.state == BATTLE_STATE_NULL) return; + assertTrue(BATTLE.state < BATTLE_STATE_COUNT, "Invalid battle state"); + assertNotNull(BATTLE_STATE_FUNCTIONS[BATTLE.state].update, "Missing state update function"); + BATTLE_STATE_FUNCTIONS[BATTLE.state].update(); } void battleDispose(void) { if(BATTLE.state == BATTLE_STATE_NULL) return; + assertTrue(BATTLE.state < BATTLE_STATE_COUNT, "Invalid battle state"); + assertNotNull(BATTLE_STATE_FUNCTIONS[BATTLE.state].dispose, "Missing state dispose function"); BATTLE_STATE_FUNCTIONS[BATTLE.state].dispose(); memoryZero(&BATTLE, sizeof(battle_t)); diff --git a/src/dusk/rpg/battle/battle.h b/src/dusk/rpg/battle/battle.h index 1d071b09..2c86fa37 100644 --- a/src/dusk/rpg/battle/battle.h +++ b/src/dusk/rpg/battle/battle.h @@ -49,6 +49,14 @@ void battleStart( battlefighter_t *fighters[BATTLE_FIGHTER_POS_COUNT] ); +/** + * Transitions the battle to a new state: disposes the current state (if + * any) and initializes next. + * + * @param next The state to transition to. + */ +void battleSetState(const battlestate_t next); + /** * Updates the battle system. */ diff --git a/src/dusk/rpg/battle/move/battlemove.c b/src/dusk/rpg/battle/move/battlemove.c index 45b57269..389c5ee0 100644 --- a/src/dusk/rpg/battle/move/battlemove.c +++ b/src/dusk/rpg/battle/move/battlemove.c @@ -8,6 +8,7 @@ #include "battlemove.h" #include "asset/asset.h" #include "asset/loader/assetloader.h" +#include "assert/assert.h" #include "util/memory.h" #include "util/string.h" #include @@ -17,7 +18,7 @@ uint8_t BATTLE_MOVE_COUNT; errorret_t battleMoveInit(void) { memoryZero(BATTLE_MOVES, sizeof(BATTLE_MOVES)); - BATTLE_MOVE_COUNT = BATTLE_MOVE_ID_NULL; + BATTLE_MOVE_COUNT = BATTLE_MOVE_ID_NULL + 1;// Reserve NULL. // Get moves JSON. assetentry_t *entry = assetLock( @@ -126,3 +127,13 @@ errorret_t battleMoveInit(void) { assetUnlockEntry(entry); errorOk(); } + +battlemoveid_t battleMoveGetIdByName(const char_t *name) { + assertNotNull(name, "name must not be NULL"); + + for(battlemoveid_t id = BATTLE_MOVE_ID_NULL + 1; id < BATTLE_MOVE_COUNT; id++) { + if(stringEquals(BATTLE_MOVES[id].name, name)) return id; + } + + return BATTLE_MOVE_ID_NULL; +} diff --git a/src/dusk/rpg/battle/move/battlemove.h b/src/dusk/rpg/battle/move/battlemove.h index 538306d6..3c11b2d2 100644 --- a/src/dusk/rpg/battle/move/battlemove.h +++ b/src/dusk/rpg/battle/move/battlemove.h @@ -32,3 +32,11 @@ extern battleability_t BATTLE_MOVES[BATTLE_MOVE_COUNT_MAX]; * @return Any error that occurs. */ errorret_t battleMoveInit(void); + +/** + * Finds a move's id by name (exact match). + * + * @param name The move name to search for. + * @return The matching move's id, or BATTLE_MOVE_ID_NULL if not found. + */ +battlemoveid_t battleMoveGetIdByName(const char_t *name); diff --git a/src/dusk/rpg/battle/state/battlestateopening.c b/src/dusk/rpg/battle/state/battlestateopening.c index d39afe17..eb2a2752 100644 --- a/src/dusk/rpg/battle/state/battlestateopening.c +++ b/src/dusk/rpg/battle/state/battlestateopening.c @@ -6,9 +6,17 @@ */ #include "battlestateopening.h" +#include "rpg/cutscene/item/cutsceneitem.h" +#include "rpg/cutscene/cutscenesystem.h" void battleStateOpeningInit(void) { - + const cutsceneitem_t items[] = { + { + .type = CUTSCENE_ITEM_TYPE_BATTLE_SET_STATE, + .battleSetState = { .state = BATTLE_STATE_PRE_ROUND } + } + }; + cutsceneSystemInsertItemsNext(items, sizeof(items) / sizeof(cutsceneitem_t)); } void battleStateOpeningUpdate(void) { diff --git a/src/dusk/rpg/battle/state/battlestatepreround.c b/src/dusk/rpg/battle/state/battlestatepreround.c index 297ae979..28f1d2be 100644 --- a/src/dusk/rpg/battle/state/battlestatepreround.c +++ b/src/dusk/rpg/battle/state/battlestatepreround.c @@ -6,9 +6,20 @@ */ #include "battlestatepreround.h" +#include "rpg/cutscene/item/cutsceneitem.h" +#include "rpg/cutscene/cutscenesystem.h" void battleStatePreRoundInit(void) { - + const cutsceneitem_t items[] = { + { + .type = CUTSCENE_ITEM_TYPE_BATTLE_SET_STATE, + .battleSetState = { .state = BATTLE_STATE_SELECTION } + }, + { + .type = CUTSCENE_ITEM_TYPE_BATTLE_WAIT_SELECTION + } + }; + cutsceneSystemInsertItemsNext(items, sizeof(items) / sizeof(cutsceneitem_t)); } void battleStatePreRoundUpdate(void) { diff --git a/src/dusk/rpg/battle/state/battlestateselection.c b/src/dusk/rpg/battle/state/battlestateselection.c index a753b8df..3d45a11b 100644 --- a/src/dusk/rpg/battle/state/battlestateselection.c +++ b/src/dusk/rpg/battle/state/battlestateselection.c @@ -6,15 +6,114 @@ */ #include "battlestateselection.h" +#include "rpg/battle/battle.h" +#include "rpg/cutscene/item/cutsceneitem.h" +#include "rpg/cutscene/cutscenesystem.h" +#include "assert/assert.h" + +// TEMPORARY: hardcoded pending a real moves.jsonc-backed registry lookup. +#define BATTLE_MOVE_ID_ATTACK ((battlemoveid_t)1) void battleStateSelectionInit(void) { - + BATTLE.stateData.selection.selectionIndex = 0; } void battleStateSelectionUpdate(void) { + battlestateselection_t *state = &BATTLE.stateData.selection; + while(state->selectionIndex < BATTLE_FIGHTER_POS_COUNT) { + const uint8_t fighterIndex = state->selectionIndex; + if(!battleStateSelectionFighterNeedsDecision(fighterIndex)) { + state->selectionIndex++; + continue; + } + + battlefighter_t *fighter = BATTLE.fighters[fighterIndex]; + if(fighter->controller != BATTLE_FIGHTER_CONTROLLER_AI) { + // Player-controlled: wait for battleStateSelectionPlayerAttack/Flee. + return; + } + + const battlefighterteam_t enemyTeam = + fighter->team == BATTLE_FIGHTER_TEAM_ALLY ? + BATTLE_FIGHTER_TEAM_ENEMY : BATTLE_FIGHTER_TEAM_ALLY; + + uint8_t targetIndex = 0xFF; + for(uint8_t i = 0; i < BATTLE_FIGHTER_POS_COUNT; i++) { + battlefighter_t *candidate = BATTLE.fighters[i]; + if(candidate == NULL) continue; + if(candidate->team != enemyTeam) continue; + if(!battleFighterIsAlive(candidate)) continue; + targetIndex = i; + break; + } + + if(targetIndex != 0xFF) { + battleStateSelectionQueueAttack(fighterIndex, targetIndex); + } + state->selectionIndex++; + } + + const cutsceneitem_t items[] = { + { + .type = CUTSCENE_ITEM_TYPE_BATTLE_SET_STATE, + .battleSetState = { .state = BATTLE_STATE_EXECUTING } + } + }; + cutsceneSystemInsertItems(items, sizeof(items) / sizeof(cutsceneitem_t)); } void battleStateSelectionDispose(void) { } + +bool_t battleStateSelectionFighterNeedsDecision(const uint8_t fighterIndex) { + assertTrue(fighterIndex < BATTLE_FIGHTER_POS_COUNT, "fighterIndex out of bounds"); + + battlefighter_t *fighter = BATTLE.fighters[fighterIndex]; + if(fighter == NULL) return false; + if(!battleFighterIsAlive(fighter)) return false; + + return BATTLE.actionsPending[fighterIndex].move == BATTLE_MOVE_ID_NULL; +} + +void battleStateSelectionQueueAttack( + const uint8_t fighterIndex, + const uint8_t targetIndex +) { + assertTrue(fighterIndex < BATTLE_FIGHTER_POS_COUNT, "fighterIndex out of bounds"); + assertTrue(targetIndex < BATTLE_FIGHTER_POS_COUNT, "targetIndex out of bounds"); + assertNotNull(BATTLE.fighters[fighterIndex], "Fighter cannot be NULL"); + assertNotNull(BATTLE.fighters[targetIndex], "Target cannot be NULL"); + + battlemoveaction_t *action = &BATTLE.actionsPending[fighterIndex]; + action->move = BATTLE_MOVE_ID_ATTACK; + action->targets[0] = targetIndex; + action->user = BATTLE.fighters[fighterIndex]; +} + +battlefighter_t *battleStateSelectionGetCurrentFighter(void) { + if(BATTLE.state != BATTLE_STATE_SELECTION) return NULL; + + const uint8_t index = BATTLE.stateData.selection.selectionIndex; + if(index >= BATTLE_FIGHTER_POS_COUNT) return NULL; + + return BATTLE.fighters[index]; +} + +void battleStateSelectionPlayerAttack(const uint8_t targetIndex) { + battlefighter_t *fighter = battleStateSelectionGetCurrentFighter(); + if(fighter == NULL) return; + + battleStateSelectionQueueAttack( + BATTLE.stateData.selection.selectionIndex, targetIndex + ); + BATTLE.stateData.selection.selectionIndex++; +} + +void battleStateSelectionPlayerFlee(void) { + battlefighter_t *fighter = battleStateSelectionGetCurrentFighter(); + if(fighter == NULL) return; + + battleSetState(BATTLE_STATE_FLED); +} diff --git a/src/dusk/rpg/battle/state/battlestateselection.h b/src/dusk/rpg/battle/state/battlestateselection.h index 4fad166f..ad93cfb2 100644 --- a/src/dusk/rpg/battle/state/battlestateselection.h +++ b/src/dusk/rpg/battle/state/battlestateselection.h @@ -6,10 +6,12 @@ */ #pragma once -#include "dusk.h" +#include "rpg/battle/fighter/battlefighter.h" typedef struct { - + // Cursor into BATTLE.fighters (by position index) for the fighter + // currently deciding a move this round. + uint8_t selectionIndex; } battlestateselection_t; /** @@ -18,7 +20,11 @@ typedef struct { void battleStateSelectionInit(void); /** - * Updates BATTLE_STATE_SELECTION for one frame. + * Updates BATTLE_STATE_SELECTION for one frame: auto-decides for every + * AI-controlled fighter the cursor passes, stopping once it reaches a + * player-controlled fighter awaiting input (see + * battleStateSelectionGetCurrentFighter), or transitions to + * BATTLE_STATE_EXECUTING once every position has decided. */ void battleStateSelectionUpdate(void); @@ -26,3 +32,49 @@ void battleStateSelectionUpdate(void); * Called when the battle leaves BATTLE_STATE_SELECTION. */ void battleStateSelectionDispose(void); + +/** + * Checks whether the fighter at fighterIndex is a live, undecided + * candidate that BATTLE_STATE_SELECTION should still be deciding a move + * for. + * + * @param fighterIndex Index into BATTLE.fighters to check. + * @return True if the fighter is alive and has no move queued yet. + */ +bool_t battleStateSelectionFighterNeedsDecision(const uint8_t fighterIndex); + +/** + * Queues a basic attack from fighterIndex against targetIndex. + * + * @param fighterIndex Index into BATTLE.fighters of the deciding fighter. + * @param targetIndex Index into BATTLE.fighters of the target. + */ +void battleStateSelectionQueueAttack( + const uint8_t fighterIndex, + const uint8_t targetIndex +); + +/** + * Returns the fighter currently awaiting a decision in + * BATTLE_STATE_SELECTION. + * + * @return Pointer to the fighter awaiting a decision, or NULL if the + * battle isn't in BATTLE_STATE_SELECTION or every position has + * already decided. + */ +battlefighter_t *battleStateSelectionGetCurrentFighter(void); + +/** + * Submits the currently-selecting fighter's attack against a target, if + * the battle is in BATTLE_STATE_SELECTION and awaiting a decision. Queues + * the move and advances the selection cursor. + * + * @param targetIndex Index into BATTLE.fighters of the target. + */ +void battleStateSelectionPlayerAttack(const uint8_t targetIndex); + +/** + * Ends the battle immediately with BATTLE_STATE_FLED, if the battle is in + * BATTLE_STATE_SELECTION and awaiting a decision. + */ +void battleStateSelectionPlayerFlee(void); diff --git a/src/dusk/rpg/cutscene/cutscenesystem.c b/src/dusk/rpg/cutscene/cutscenesystem.c index f7d6cfba..fe9a48c9 100644 --- a/src/dusk/rpg/cutscene/cutscenesystem.c +++ b/src/dusk/rpg/cutscene/cutscenesystem.c @@ -87,20 +87,34 @@ void cutsceneSystemStartCutsceneAndGoToMarker( cutsceneGoTo(marker); } -void cutsceneSystemInsertCutscene(const cutscene_t *cutscene) { - assertNotNull(cutscene, "Cutscene cannot be NULL"); +void cutsceneSystemInsertItemsNext( + const cutsceneitem_t *items, + const size_t itemCount +) { + assertNotNull(items, "Items cannot be NULL"); assertNotNull( CUTSCENE_SYSTEM.scene, - "cutsceneSystemInsertCutscene called with no cutscene running" + "cutsceneSystemInsertItemsNext called with no cutscene running" ); cutsceneAppendNext( - CUTSCENE_SYSTEM.scene, CUTSCENE_SYSTEM.currentItem, - cutscene->items, cutscene->itemCount + CUTSCENE_SYSTEM.scene, CUTSCENE_SYSTEM.currentItem, items, itemCount ); +} + +void cutsceneSystemInsertItems( + const cutsceneitem_t *items, + const size_t itemCount +) { + cutsceneSystemInsertItemsNext(items, itemCount); cutsceneSystemNext(); } +void cutsceneSystemInsertCutscene(const cutscene_t *cutscene) { + assertNotNull(cutscene, "Cutscene cannot be NULL"); + cutsceneSystemInsertItems(cutscene->items, cutscene->itemCount); +} + void cutsceneSystemLoad(const char_t *file) { assertNotNull(file, "File cannot be NULL"); diff --git a/src/dusk/rpg/cutscene/cutscenesystem.h b/src/dusk/rpg/cutscene/cutscenesystem.h index 76dd79f7..eb988a49 100644 --- a/src/dusk/rpg/cutscene/cutscenesystem.h +++ b/src/dusk/rpg/cutscene/cutscenesystem.h @@ -109,6 +109,32 @@ void cutsceneSystemStartCutsceneAndGoToMarker( const char_t *marker ); +/** + * Splices items into the running cutscene so that items[0] becomes the + * very next item, without otherwise disturbing the currently-running + * item's progress. + * + * @param items Items to splice in. + * @param itemCount Number of items in items. + */ +void cutsceneSystemInsertItemsNext( + const cutsceneitem_t *items, + const size_t itemCount +); + +/** + * Splices items into the running cutscene right after the current item + * (see cutsceneSystemInsertItemsNext), then immediately advances into the + * first spliced item. + * + * @param items Items to splice in. + * @param itemCount Number of items in items. + */ +void cutsceneSystemInsertItems( + const cutsceneitem_t *items, + const size_t itemCount +); + /** * Splices cutscene's items into the running cutscene. * diff --git a/src/dusk/rpg/cutscene/item/CMakeLists.txt b/src/dusk/rpg/cutscene/item/CMakeLists.txt index 1915c7e2..42868c1b 100755 --- a/src/dusk/rpg/cutscene/item/CMakeLists.txt +++ b/src/dusk/rpg/cutscene/item/CMakeLists.txt @@ -16,6 +16,7 @@ add_subdirectory(entity) add_subdirectory(item) add_subdirectory(maparea) add_subdirectory(ui) +add_subdirectory(battle) add_subdirectory(save) add_subdirectory(audio) add_subdirectory(json) diff --git a/src/dusk/rpg/cutscene/item/battle/CMakeLists.txt b/src/dusk/rpg/cutscene/item/battle/CMakeLists.txt new file mode 100644 index 00000000..2faba70b --- /dev/null +++ b/src/dusk/rpg/cutscene/item/battle/CMakeLists.txt @@ -0,0 +1,11 @@ +# Copyright (c) 2026 Dominic Masters +# +# This software is released under the MIT License. +# https://opensource.org/licenses/MIT + +target_sources(${DUSK_LIBRARY_TARGET_NAME} + PUBLIC + cutscenebattlestart.c + cutscenebattlesetstate.c + cutscenebattlewaitselection.c +) 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/cutscenebattlesetstate.h b/src/dusk/rpg/cutscene/item/battle/cutscenebattlesetstate.h new file mode 100644 index 00000000..2d4753bf --- /dev/null +++ b/src/dusk/rpg/cutscene/item/battle/cutscenebattlesetstate.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 "rpg/battle/state/battlestate.h" +#include "rpg/cutscene/item/cutsceneitembase.h" + +typedef struct { + battlestate_t state; +} cutscenebattlesetstate_t; + +/** + * Starts a BATTLE_SET_STATE item, transitioning the battle to the + * configured state immediately. + * + * @param item The cutscene item. + * @param data Runtime data storage. + */ +void cutsceneBattleSetStateStart( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +); + +/** + * Updates a BATTLE_SET_STATE item (always completes immediately). + * + * @param item The cutscene item. + * @param data Runtime data storage. + * @returns true always. + */ +bool_t cutsceneBattleSetStateUpdate( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +); + +/** + * Parses a BATTLE_SET_STATE cutscene item's JSON fields into item. + * + * @param itemObj The item's JSON object. + * @param item Destination item, already zeroed by the caller. + * @return Error code indicating success or failure of the parse. + */ +errorret_t cutsceneBattleSetStateLoad( + yyjson_val *itemObj, + cutsceneitem_t *item +); 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..70f3582d --- /dev/null +++ b/src/dusk/rpg/cutscene/item/battle/cutscenebattlewaitselection.c @@ -0,0 +1,16 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "rpg/cutscene/item/cutsceneitem.h" +#include "rpg/battle/battle.h" + +bool_t cutsceneBattleWaitSelectionUpdate( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +) { + return BATTLE.state != BATTLE_STATE_SELECTION; +} 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..ba66dc24 --- /dev/null +++ b/src/dusk/rpg/cutscene/item/battle/cutscenebattlewaitselection.h @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "rpg/cutscene/item/cutsceneitembase.h" + +/** + * Updates a BATTLE_WAIT_SELECTION step, completing once the battle is no + * longer in BATTLE_STATE_SELECTION -- i.e. every fighter has decided a + * move (moving on to BATTLE_STATE_EXECUTING) or the selecting fighter has + * fled (moving on to BATTLE_STATE_FLED). Has no Start callback -- there's + * nothing to do until selection actually finishes. + * + * @param item The cutscene item. + * @param data Runtime data storage. + * @returns true once BATTLE.state is no longer BATTLE_STATE_SELECTION. + */ +bool_t cutsceneBattleWaitSelectionUpdate( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +); diff --git a/src/dusk/rpg/cutscene/item/cutsceneitem.c b/src/dusk/rpg/cutscene/item/cutsceneitem.c index e1b31a1d..dd126730 100644 --- a/src/dusk/rpg/cutscene/item/cutsceneitem.c +++ b/src/dusk/rpg/cutscene/item/cutsceneitem.c @@ -244,6 +244,27 @@ cutsceneitemcallbacks_t CUTSCENE_ITEM_CALLBACKS[CUTSCENE_ITEM_TYPE_COUNT] = { .load = cutsceneMapAreaWaitLoad }, + // Battle + [CUTSCENE_ITEM_TYPE_BATTLE_START] = { + .init = cutsceneBattleStartStart, + .update = cutsceneBattleStartUpdate, + .jsonName = "BATTLE_START", + .load = cutsceneBattleStartLoad + }, + + [CUTSCENE_ITEM_TYPE_BATTLE_SET_STATE] = { + .init = cutsceneBattleSetStateStart, + .update = cutsceneBattleSetStateUpdate, + .jsonName = "BATTLE_SET_STATE", + .load = cutsceneBattleSetStateLoad + }, + + [CUTSCENE_ITEM_TYPE_BATTLE_WAIT_SELECTION] = { + .update = cutsceneBattleWaitSelectionUpdate, + .jsonName = "BATTLE_WAIT_SELECTION", + .load = cutsceneItemLoadNoop + }, + // Save [CUTSCENE_ITEM_TYPE_SAVE_DEVICE_CHECK] = { .init = cutsceneSaveDeviceCheckStart, diff --git a/src/dusk/rpg/cutscene/item/cutsceneitem.h b/src/dusk/rpg/cutscene/item/cutsceneitem.h index 3847fc36..e4e4745f 100644 --- a/src/dusk/rpg/cutscene/item/cutsceneitem.h +++ b/src/dusk/rpg/cutscene/item/cutsceneitem.h @@ -40,6 +40,9 @@ #include "maparea/cutscenemapareaadd.h" #include "maparea/cutscenemaparearemove.h" #include "maparea/cutscenemapareawait.h" +#include "battle/cutscenebattlestart.h" +#include "battle/cutscenebattlesetstate.h" +#include "battle/cutscenebattlewaitselection.h" #include "save/cutscenesavedevicecheck.h" #include "save/cutscenesaveloadallslots.h" #include "audio/cutsceneaudioplay.h" @@ -112,6 +115,8 @@ struct cutsceneitem_s { cutscenemapareaadd_t mapAreaAdd; cutscenemaparearemove_t mapAreaRemove; cutscenemapareawait_t mapAreaWait; + cutscenebattlestart_t battleStart; + cutscenebattlesetstate_t battleSetState; cutsceneemoji_t emoji; cutsceneshake_t shake; cutscenemodal_t modal; diff --git a/src/dusk/rpg/cutscene/item/cutsceneitembase.h b/src/dusk/rpg/cutscene/item/cutsceneitembase.h index e738e082..be187c26 100644 --- a/src/dusk/rpg/cutscene/item/cutsceneitembase.h +++ b/src/dusk/rpg/cutscene/item/cutsceneitembase.h @@ -59,6 +59,11 @@ typedef enum { CUTSCENE_ITEM_TYPE_MAP_AREA_REMOVE, CUTSCENE_ITEM_TYPE_MAP_AREA_WAIT, + // Battle + CUTSCENE_ITEM_TYPE_BATTLE_START, + CUTSCENE_ITEM_TYPE_BATTLE_SET_STATE, + CUTSCENE_ITEM_TYPE_BATTLE_WAIT_SELECTION, + // Save CUTSCENE_ITEM_TYPE_SAVE_DEVICE_CHECK, CUTSCENE_ITEM_TYPE_SAVE_LOAD_ALL_SLOTS, diff --git a/src/dusk/rpg/cutscene/item/json/CMakeLists.txt b/src/dusk/rpg/cutscene/item/json/CMakeLists.txt index 57b7e422..60a8f286 100644 --- a/src/dusk/rpg/cutscene/item/json/CMakeLists.txt +++ b/src/dusk/rpg/cutscene/item/json/CMakeLists.txt @@ -14,6 +14,7 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME} cutscenejsonemoji.c cutscenejsonentitytype.c cutscenejsonentitydir.c + cutscenejsonbattlestate.c cutscenejsonscenetype.c cutscenejsonuiscreentype.c cutscenejsonaudiochannel.c diff --git a/src/dusk/rpg/cutscene/item/json/cutscenejsonbattlestate.c b/src/dusk/rpg/cutscene/item/json/cutscenejsonbattlestate.c new file mode 100644 index 00000000..60993646 --- /dev/null +++ b/src/dusk/rpg/cutscene/item/json/cutscenejsonbattlestate.c @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "cutscenejsonbattlestate.h" +#include "cutscenejsonenum.h" + +static const cutscenejsonenumentry_t CUTSCENE_JSON_BATTLE_STATE_TABLE[] = { + { "OPENING", BATTLE_STATE_OPENING }, + { "PRE_ROUND", BATTLE_STATE_PRE_ROUND }, + { "SELECTION", BATTLE_STATE_SELECTION }, + { "EXECUTING", BATTLE_STATE_EXECUTING }, + { "POST_ROUND", BATTLE_STATE_POST_ROUND }, + { "LOST", BATTLE_STATE_LOST }, + { "WON", BATTLE_STATE_WON }, + { "FLED", BATTLE_STATE_FLED }, + { "ENDING", BATTLE_STATE_ENDING }, +}; +#define CUTSCENE_JSON_BATTLE_STATE_TABLE_COUNT \ + (sizeof(CUTSCENE_JSON_BATTLE_STATE_TABLE) / sizeof(cutscenejsonenumentry_t)) + +bool_t cutsceneJsonLookupBattleState( + const char_t *name, + battlestate_t *out +) { + int32_t value; + if(!cutsceneJsonLookupEnumInsensitive( + CUTSCENE_JSON_BATTLE_STATE_TABLE, + CUTSCENE_JSON_BATTLE_STATE_TABLE_COUNT, name, &value + )) { + return false; + } + *out = (battlestate_t)value; + return true; +} diff --git a/src/dusk/rpg/cutscene/item/json/cutscenejsonbattlestate.h b/src/dusk/rpg/cutscene/item/json/cutscenejsonbattlestate.h new file mode 100644 index 00000000..1cf302f2 --- /dev/null +++ b/src/dusk/rpg/cutscene/item/json/cutscenejsonbattlestate.h @@ -0,0 +1,21 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "rpg/battle/state/battlestate.h" + +/** + * Looks up name (case-insensitive) against every battlestate_t name. + * + * @param name Name to search for. + * @param out Set to the matching state on success. + * @return true if a matching entry was found, false otherwise. + */ +bool_t cutsceneJsonLookupBattleState( + const char_t *name, + battlestate_t *out +); diff --git a/src/dusk/scene/battle/scenebattle.c b/src/dusk/scene/battle/scenebattle.c index 7aa3c7c2..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" @@ -80,6 +81,10 @@ void sceneBattleWorldToScreen( vec3 renderPos, vec2 out ) { + assertNotNull(battle, "battle cannot be NULL"); + assertNotNull(renderPos, "renderPos cannot be NULL"); + assertNotNull(out, "out cannot be NULL"); + mat4 viewProj; glm_mat4_mul(battle->projection, battle->eye, viewProj); @@ -94,15 +99,20 @@ void sceneBattleWorldToScreen( } void sceneBattleGetFighterPosition(const uint8_t fighterIndex, vec3 out) { + assertTrue(fighterIndex < BATTLE_FIGHTER_POS_COUNT, "fighterIndex out of bounds"); const battlefighter_t *fighter = BATTLE.fighters[fighterIndex]; + assertNotNull(fighter, "Fighter cannot be NULL"); + assertNotNull(out, "out cannot be NULL"); // Ordinal (and count) of this fighter among its living-slot teammates, // in slot order, so each team lays out as an evenly spaced row. uint8_t ordinal = 0; uint8_t teamCount = 0; for(uint8_t i = 0; i < BATTLE_FIGHTER_POS_COUNT; i++) { - if(BATTLE.fighters[i]->status == BATTLE_FIGHTER_STATUS_NULL) continue; - if(BATTLE.fighters[i]->team != fighter->team) continue; + const battlefighter_t *candidate = BATTLE.fighters[i]; + if(candidate == NULL) continue; + if(candidate->status == BATTLE_FIGHTER_STATUS_NULL) continue; + if(candidate->team != fighter->team) continue; if(i < fighterIndex) ordinal++; teamCount++; } @@ -119,7 +129,9 @@ void sceneBattleGetFighterPosition(const uint8_t fighterIndex, vec3 out) { } errorret_t sceneBattleDrawFighter(const uint8_t fighterIndex) { + assertTrue(fighterIndex < BATTLE_FIGHTER_POS_COUNT, "fighterIndex out of bounds"); const battlefighter_t *fighter = BATTLE.fighters[fighterIndex]; + if(fighter == NULL) errorOk(); if(fighter->status == BATTLE_FIGHTER_STATUS_NULL) errorOk(); vec3 position; diff --git a/src/dusk/ui/screen/battle/uibattlehud.c b/src/dusk/ui/screen/battle/uibattlehud.c index 085b0f21..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_POS_COUNT; i++) { const battlefighter_t *fighter = BATTLE.fighters[i]; + if(fighter == NULL) continue; if(fighter->status == BATTLE_FIGHTER_STATUS_NULL) continue; vec3 position; diff --git a/src/dusk/ui/screen/battle/uibattlemenu.c b/src/dusk/ui/screen/battle/uibattlemenu.c index de209629..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 = @@ -42,6 +43,7 @@ void uiBattleMenuOpenTargets(void) { for(uint8_t i = 0; i < BATTLE_FIGHTER_POS_COUNT; i++) { battlefighter_t *candidate = BATTLE.fighters[i]; + if(candidate == NULL) continue; if(candidate->team != enemyTeam) continue; if(!battleFighterIsAlive(candidate)) continue; @@ -71,7 +73,7 @@ void uiBattleMenuActionSelected( break; case UI_BATTLE_MENU_ACTION_INDEX_FLEE: - battlePlayerFlee(); + battleStateSelectionPlayerFlee(); uiMenuClose(&UI_BATTLE_MENU.actionMenu); break; @@ -116,7 +118,7 @@ errorret_t uiBattleMenuInit(void) { errorret_t uiBattleMenuUpdate(void) { if(uiMenuIsActive(&UI_BATTLE_MENU.actionMenu)) errorOk(); - battlefighter_t *current = battleGetCurrentFighter(); + battlefighter_t *current = battleStateSelectionGetCurrentFighter(); if(current == NULL) errorOk(); if(current->controller != BATTLE_FIGHTER_CONTROLLER_PLAYER) errorOk(); From 37e7e95acecb564093d0e69f7fa4097f34aa8d3b Mon Sep 17 00:00:00 2001 From: Dominic Masters Date: Sun, 20 Sep 2026 17:49:06 -0500 Subject: [PATCH 4/6] Resolve moves into per-move cutscenes during battle execution Each queued action now resolves to its move's cutscene (cutscenes/battle/moves/.jsonc), spliced into the executing state with a BATTLE_POST_MOVE checkpoint after each one. Fighter turn order is tracked via fighterOrder/fighterOrderCount, and resolved actions/pending counts are cleared so fighters aren't skipped in later rounds. Also fixes a BATTLE_WAIT_SELECTION deadlock and adds an optional "blocking" flag to MODAL cutscene items. Co-Authored-By: Claude Sonnet 5 --- assets/cutscenes/battle/moves/fire.jsonc | 15 ++- assets/cutscenes/battle/moves/slash.jsonc | 15 ++- src/dusk/rpg/battle/battle.h | 4 +- .../rpg/battle/state/battlestateexecuting.c | 83 ++++++++++++++++ .../rpg/battle/state/battlestateexecuting.h | 2 + .../rpg/battle/state/battlestatepreround.c | 12 ++- .../rpg/battle/state/battlestateselection.c | 96 ++++++++----------- .../rpg/battle/state/battlestateselection.h | 22 ++++- .../rpg/cutscene/item/battle/CMakeLists.txt | 1 + .../item/battle/cutscenebattlepostmove.c | 25 +++++ .../item/battle/cutscenebattlepostmove.h | 36 +++++++ .../item/battle/cutscenebattlewaitselection.c | 15 ++- .../item/battle/cutscenebattlewaitselection.h | 14 +-- src/dusk/rpg/cutscene/item/cutsceneitem.c | 5 + src/dusk/rpg/cutscene/item/cutsceneitem.h | 1 + src/dusk/rpg/cutscene/item/cutsceneitembase.h | 1 + src/dusk/rpg/cutscene/item/ui/cutscenemodal.c | 4 +- src/dusk/rpg/cutscene/item/ui/cutscenemodal.h | 31 +++--- src/dusk/ui/widget/uimodal.c | 25 +++++ 19 files changed, 320 insertions(+), 87 deletions(-) create mode 100644 src/dusk/rpg/cutscene/item/battle/cutscenebattlepostmove.c create mode 100644 src/dusk/rpg/cutscene/item/battle/cutscenebattlepostmove.h diff --git a/assets/cutscenes/battle/moves/fire.jsonc b/assets/cutscenes/battle/moves/fire.jsonc index 0b455d28..b4760f18 100644 --- a/assets/cutscenes/battle/moves/fire.jsonc +++ b/assets/cutscenes/battle/moves/fire.jsonc @@ -1,9 +1,16 @@ { "items": [ - { + { "type": "MODAL", - "title": "BATTLE TEST", - "message": "BATTLE TEST" + "title": "Fire", + "message": "A burst of flame scorches the target." + }, + { + "type": "WAIT", + "seconds": 2 + }, + { + "type": "MODAL_CLOSE" } ] -} \ No newline at end of file +} diff --git a/assets/cutscenes/battle/moves/slash.jsonc b/assets/cutscenes/battle/moves/slash.jsonc index 0b455d28..9726503b 100644 --- a/assets/cutscenes/battle/moves/slash.jsonc +++ b/assets/cutscenes/battle/moves/slash.jsonc @@ -1,9 +1,16 @@ { "items": [ - { + { "type": "MODAL", - "title": "BATTLE TEST", - "message": "BATTLE TEST" + "title": "Slash", + "message": "A quick blade strike cuts across the target." + }, + { + "type": "WAIT", + "seconds": 2 + }, + { + "type": "MODAL_CLOSE" } ] -} \ No newline at end of file +} diff --git a/src/dusk/rpg/battle/battle.h b/src/dusk/rpg/battle/battle.h index 2c86fa37..9c20aacb 100644 --- a/src/dusk/rpg/battle/battle.h +++ b/src/dusk/rpg/battle/battle.h @@ -25,9 +25,11 @@ typedef struct { // Order of who gets to act. uint8_t fighterOrder[BATTLE_FIGHTER_POS_COUNT]; + uint8_t fighterOrderCount; // Actions to be executed. - battlemoveaction_t actionsPending[BATTLE_FIGHTER_POS_COUNT]; + battlemoveaction_t actionsPending[BATTLE_FIGHTER_POS_COUNT]; + uint8_t actionsPendingCount; battlestate_t state; battlestatedata_t stateData; diff --git a/src/dusk/rpg/battle/state/battlestateexecuting.c b/src/dusk/rpg/battle/state/battlestateexecuting.c index c2a058a8..948a217c 100644 --- a/src/dusk/rpg/battle/state/battlestateexecuting.c +++ b/src/dusk/rpg/battle/state/battlestateexecuting.c @@ -6,9 +6,92 @@ */ #include "battlestateexecuting.h" +#include "rpg/battle/battle.h" +#include "rpg/cutscene/item/cutsceneitem.h" +#include "rpg/cutscene/cutscenesystem.h" +#include "rpg/cutscene/cutscene.h" +#include "asset/asset.h" +#include "asset/loader/assetloader.h" +#include "asset/assetfile.h" +#include "ui/overlay/uifatalerror.h" +#include "assert/assert.h" +#include "util/string.h" + void battleStateExecutingInit(void) { + cutsceneitem_t items[BATTLE_STATE_EXECUTING_ITEMS_MAX]; + uint8_t itemsCount = 0; + // For each action (just queued during selection). actionsPending is + // indexed by fighter position, not sequentially, so scan every position + // and stop early once every pending action has been processed. + uint8_t processed = 0; + for( + uint8_t i = 0; + i < BATTLE_FIGHTER_POS_COUNT && processed < BATTLE.actionsPendingCount; + i++ + ) { + // Get action. + battlemoveaction_t *action = &BATTLE.actionsPending[i]; + if(action->move == BATTLE_MOVE_ID_NULL) continue; + processed++; + + assertTrue(action->move < BATTLE_MOVE_COUNT, "Invalid move id"); + + // Get ability. + battleability_t *ability = &BATTLE_MOVES[action->move]; + assertStrLenMin(ability->name, 1, "Invalid move name"); + + // Determine cutscene path + char_t path[ASSET_FILE_NAME_MAX]; + stringFormat( + path, ASSET_FILE_NAME_MAX - 1, + "cutscenes/battle/moves/%s.jsonc", ability->name + ); + + // Load cutscene asset. + assetentry_t *entry = assetLock(path, ASSET_LOADER_TYPE_JSON, NULL); + errorret_t result = assetRequireLoaded(entry); + if(errorIsNotOk(result)) { + assetUnlockEntry(entry); + errorCatch(errorPrint(result)); + uiFatalErrorOpen(NULL); + continue; + } + + // Parse out cutscene items and append to the list. + cutscene_t scene; + errorret_t parseResult = cutsceneParseDoc( + entry->data.json, &scene, &items[itemsCount], + BATTLE_STATE_EXECUTING_ITEMS_MAX - itemsCount + ); + itemsCount += scene.itemCount; + + // Release asset + assetUnlockEntry(entry); + if(errorIsNotOk(parseResult)) { + errorCatch(errorPrint(parseResult)); + uiFatalErrorOpen(NULL); + continue; + } + + // Insert end of attack cutscene item. + items[itemsCount++] = (cutsceneitem_t){ + .type = CUTSCENE_ITEM_TYPE_BATTLE_POST_MOVE + }; + + // This action has now been resolved - clear it so it isn't + // re-executed (and doesn't block re-selection) next round. + action->move = BATTLE_MOVE_ID_NULL; + } + BATTLE.actionsPendingCount = 0; + + // After all moves completed, transition to pre-round for next round. + items[itemsCount++] = (cutsceneitem_t){ + .type = CUTSCENE_ITEM_TYPE_BATTLE_SET_STATE, + .battleSetState = { .state = BATTLE_STATE_PRE_ROUND } + }; + cutsceneSystemInsertItemsNext(items, itemsCount); } void battleStateExecutingUpdate(void) { diff --git a/src/dusk/rpg/battle/state/battlestateexecuting.h b/src/dusk/rpg/battle/state/battlestateexecuting.h index 1dc49c9d..a6f8f75a 100644 --- a/src/dusk/rpg/battle/state/battlestateexecuting.h +++ b/src/dusk/rpg/battle/state/battlestateexecuting.h @@ -12,6 +12,8 @@ typedef struct { } battlestateexecuting_t; +#define BATTLE_STATE_EXECUTING_ITEMS_MAX 64 + /** * Called when the battle enters BATTLE_STATE_EXECUTING. */ diff --git a/src/dusk/rpg/battle/state/battlestatepreround.c b/src/dusk/rpg/battle/state/battlestatepreround.c index 28f1d2be..7b85ece7 100644 --- a/src/dusk/rpg/battle/state/battlestatepreround.c +++ b/src/dusk/rpg/battle/state/battlestatepreround.c @@ -6,11 +6,21 @@ */ #include "battlestatepreround.h" +#include "rpg/battle/battle.h" #include "rpg/cutscene/item/cutsceneitem.h" #include "rpg/cutscene/cutscenesystem.h" void battleStatePreRoundInit(void) { - const cutsceneitem_t items[] = { + // TODO: Determine selection order and who gets to act. + BATTLE.fighterOrderCount = 0; + for(uint8_t i = 0; i < BATTLE_FIGHTER_POS_COUNT; i++) { + if(BATTLE.fighters[i] == NULL) continue; + if(!battleFighterIsAlive(BATTLE.fighters[i])) continue; + BATTLE.fighterOrder[BATTLE.fighterOrderCount++] = i; + } + + // Queue up the set state and wait for selection to stop cutscene ending. + cutsceneitem_t items[] = { { .type = CUTSCENE_ITEM_TYPE_BATTLE_SET_STATE, .battleSetState = { .state = BATTLE_STATE_SELECTION } diff --git a/src/dusk/rpg/battle/state/battlestateselection.c b/src/dusk/rpg/battle/state/battlestateselection.c index 3d45a11b..168e7c2b 100644 --- a/src/dusk/rpg/battle/state/battlestateselection.c +++ b/src/dusk/rpg/battle/state/battlestateselection.c @@ -7,87 +7,73 @@ #include "battlestateselection.h" #include "rpg/battle/battle.h" -#include "rpg/cutscene/item/cutsceneitem.h" -#include "rpg/cutscene/cutscenesystem.h" #include "assert/assert.h" -// TEMPORARY: hardcoded pending a real moves.jsonc-backed registry lookup. -#define BATTLE_MOVE_ID_ATTACK ((battlemoveid_t)1) - void battleStateSelectionInit(void) { BATTLE.stateData.selection.selectionIndex = 0; } +bool_t battleStateSelectionFighterHasDecided(const uint8_t fighterIndex) { + assertTrue(fighterIndex < BATTLE_FIGHTER_POS_COUNT, "Fighter OOB"); + return BATTLE.actionsPending[fighterIndex].move != BATTLE_MOVE_ID_NULL; +} + +bool_t battleStateSelectionFighterNeedsDecision(const uint8_t fighterIndex) { + assertTrue(fighterIndex < BATTLE_FIGHTER_POS_COUNT, "Fighter OOB"); + + battlefighter_t *fighter = BATTLE.fighters[fighterIndex]; + if(fighter == NULL) return false; + if(!battleFighterIsAlive(fighter)) return false; + + return !battleStateSelectionFighterHasDecided(fighterIndex); +} + +bool_t battleStateSelectionIsComplete(void) { + return BATTLE.stateData.selection.selectionIndex >= BATTLE.fighterOrderCount; +} + void battleStateSelectionUpdate(void) { battlestateselection_t *state = &BATTLE.stateData.selection; - while(state->selectionIndex < BATTLE_FIGHTER_POS_COUNT) { - const uint8_t fighterIndex = state->selectionIndex; + // Are we still waiting for a selection? + while(state->selectionIndex < BATTLE.fighterOrderCount) { + const uint8_t fighterIndex = BATTLE.fighterOrder[state->selectionIndex]; + + // Is this fighter still owed a decision? if(!battleStateSelectionFighterNeedsDecision(fighterIndex)) { state->selectionIndex++; continue; } + // Players will act on their own. battlefighter_t *fighter = BATTLE.fighters[fighterIndex]; - if(fighter->controller != BATTLE_FIGHTER_CONTROLLER_AI) { - // Player-controlled: wait for battleStateSelectionPlayerAttack/Flee. - return; - } + if(fighter->controller != BATTLE_FIGHTER_CONTROLLER_AI) return; - const battlefighterteam_t enemyTeam = - fighter->team == BATTLE_FIGHTER_TEAM_ALLY ? - BATTLE_FIGHTER_TEAM_ENEMY : BATTLE_FIGHTER_TEAM_ALLY; - - uint8_t targetIndex = 0xFF; - for(uint8_t i = 0; i < BATTLE_FIGHTER_POS_COUNT; i++) { - battlefighter_t *candidate = BATTLE.fighters[i]; - if(candidate == NULL) continue; - if(candidate->team != enemyTeam) continue; - if(!battleFighterIsAlive(candidate)) continue; - targetIndex = i; - break; - } - - if(targetIndex != 0xFF) { - battleStateSelectionQueueAttack(fighterIndex, targetIndex); - } + // This is AI, in future I'll have proper AI here, for now I'm actually not + // going to do anything. state->selectionIndex++; } - - const cutsceneitem_t items[] = { - { - .type = CUTSCENE_ITEM_TYPE_BATTLE_SET_STATE, - .battleSetState = { .state = BATTLE_STATE_EXECUTING } - } - }; - cutsceneSystemInsertItems(items, sizeof(items) / sizeof(cutsceneitem_t)); } void battleStateSelectionDispose(void) { } -bool_t battleStateSelectionFighterNeedsDecision(const uint8_t fighterIndex) { - assertTrue(fighterIndex < BATTLE_FIGHTER_POS_COUNT, "fighterIndex out of bounds"); - - battlefighter_t *fighter = BATTLE.fighters[fighterIndex]; - if(fighter == NULL) return false; - if(!battleFighterIsAlive(fighter)) return false; - - return BATTLE.actionsPending[fighterIndex].move == BATTLE_MOVE_ID_NULL; -} - void battleStateSelectionQueueAttack( const uint8_t fighterIndex, const uint8_t targetIndex ) { - assertTrue(fighterIndex < BATTLE_FIGHTER_POS_COUNT, "fighterIndex out of bounds"); - assertTrue(targetIndex < BATTLE_FIGHTER_POS_COUNT, "targetIndex out of bounds"); + assertTrue(fighterIndex < BATTLE_FIGHTER_POS_COUNT, "Fighter OOB"); + assertTrue(targetIndex < BATTLE_FIGHTER_POS_COUNT, "Target OOB"); assertNotNull(BATTLE.fighters[fighterIndex], "Fighter cannot be NULL"); assertNotNull(BATTLE.fighters[targetIndex], "Target cannot be NULL"); + const battlemoveid_t move = battleMoveGetIdByName("slash"); + assertTrue(move != BATTLE_MOVE_ID_NULL, "Missing 'slash' battle move"); + battlemoveaction_t *action = &BATTLE.actionsPending[fighterIndex]; - action->move = BATTLE_MOVE_ID_ATTACK; + if(action->move == BATTLE_MOVE_ID_NULL) BATTLE.actionsPendingCount++; + action->move = move; action->targets[0] = targetIndex; action->user = BATTLE.fighters[fighterIndex]; } @@ -96,18 +82,18 @@ battlefighter_t *battleStateSelectionGetCurrentFighter(void) { if(BATTLE.state != BATTLE_STATE_SELECTION) return NULL; const uint8_t index = BATTLE.stateData.selection.selectionIndex; - if(index >= BATTLE_FIGHTER_POS_COUNT) return NULL; + if(index >= BATTLE.fighterOrderCount) return NULL; - return BATTLE.fighters[index]; + return BATTLE.fighters[BATTLE.fighterOrder[index]]; } void battleStateSelectionPlayerAttack(const uint8_t targetIndex) { battlefighter_t *fighter = battleStateSelectionGetCurrentFighter(); if(fighter == NULL) return; - battleStateSelectionQueueAttack( - BATTLE.stateData.selection.selectionIndex, targetIndex - ); + const uint8_t selectionIndex = BATTLE.stateData.selection.selectionIndex; + const uint8_t fighterIndex = BATTLE.fighterOrder[selectionIndex]; + battleStateSelectionQueueAttack(fighterIndex, targetIndex); BATTLE.stateData.selection.selectionIndex++; } @@ -116,4 +102,4 @@ void battleStateSelectionPlayerFlee(void) { if(fighter == NULL) return; battleSetState(BATTLE_STATE_FLED); -} +} \ No newline at end of file diff --git a/src/dusk/rpg/battle/state/battlestateselection.h b/src/dusk/rpg/battle/state/battlestateselection.h index ad93cfb2..71d8ee69 100644 --- a/src/dusk/rpg/battle/state/battlestateselection.h +++ b/src/dusk/rpg/battle/state/battlestateselection.h @@ -33,6 +33,15 @@ void battleStateSelectionUpdate(void); */ void battleStateSelectionDispose(void); +/** + * Checks whether the fighter at fighterIndex has already queued a move + * this round. + * + * @param fighterIndex Index into BATTLE.fighters to check. + * @return True if an action is already pending for this fighter. + */ +bool_t battleStateSelectionFighterHasDecided(const uint8_t fighterIndex); + /** * Checks whether the fighter at fighterIndex is a live, undecided * candidate that BATTLE_STATE_SELECTION should still be deciding a move @@ -44,7 +53,16 @@ void battleStateSelectionDispose(void); bool_t battleStateSelectionFighterNeedsDecision(const uint8_t fighterIndex); /** - * Queues a basic attack from fighterIndex against targetIndex. + * Checks whether every fighter in BATTLE.fighterOrder has decided a move + * this round. + * + * @return True once the selection cursor has passed every fighter. + */ +bool_t battleStateSelectionIsComplete(void); + +/** + * Queues a basic attack (the "slash" move) from fighterIndex against + * targetIndex. * * @param fighterIndex Index into BATTLE.fighters of the deciding fighter. * @param targetIndex Index into BATTLE.fighters of the target. @@ -77,4 +95,4 @@ void battleStateSelectionPlayerAttack(const uint8_t targetIndex); * Ends the battle immediately with BATTLE_STATE_FLED, if the battle is in * BATTLE_STATE_SELECTION and awaiting a decision. */ -void battleStateSelectionPlayerFlee(void); +void battleStateSelectionPlayerFlee(void); \ No newline at end of file diff --git a/src/dusk/rpg/cutscene/item/battle/CMakeLists.txt b/src/dusk/rpg/cutscene/item/battle/CMakeLists.txt index 2faba70b..80b57f57 100644 --- a/src/dusk/rpg/cutscene/item/battle/CMakeLists.txt +++ b/src/dusk/rpg/cutscene/item/battle/CMakeLists.txt @@ -8,4 +8,5 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME} cutscenebattlestart.c cutscenebattlesetstate.c cutscenebattlewaitselection.c + cutscenebattlepostmove.c ) 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..77efff1a --- /dev/null +++ b/src/dusk/rpg/cutscene/item/battle/cutscenebattlepostmove.c @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "rpg/cutscene/item/cutsceneitem.h" +#include "console/console.h" + +void cutsceneBattlePostMoveStart( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +) { + // TODO: check fighter health etc. here to short-circuit into a + // win/loss (BATTLE_STATE_WON/LOST) once a side has been wiped out. + consolePrint("BATTLE_POST_MOVE\n"); +} + +bool_t cutsceneBattlePostMoveUpdate( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +) { + return true; +} 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..6a7ff0ca --- /dev/null +++ b/src/dusk/rpg/cutscene/item/battle/cutscenebattlepostmove.h @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "rpg/cutscene/item/cutsceneitembase.h" + +/** + * Starts a BATTLE_POST_MOVE item. Runs after each queued move's cutscene + * finishes - eventually this is where we'll check things like fighter + * health to short-circuit into a win/loss, but for now it just prints to + * the console. Not JSON-authorable -- constructed dynamically by + * battleStateExecutingInit. + * + * @param item The cutscene item. + * @param data Runtime data storage. + */ +void cutsceneBattlePostMoveStart( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +); + +/** + * Updates a BATTLE_POST_MOVE item (always completes immediately). + * + * @param item The cutscene item. + * @param data Runtime data storage. + * @returns true always. + */ +bool_t cutsceneBattlePostMoveUpdate( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +); diff --git a/src/dusk/rpg/cutscene/item/battle/cutscenebattlewaitselection.c b/src/dusk/rpg/cutscene/item/battle/cutscenebattlewaitselection.c index 70f3582d..763dbbff 100644 --- a/src/dusk/rpg/cutscene/item/battle/cutscenebattlewaitselection.c +++ b/src/dusk/rpg/cutscene/item/battle/cutscenebattlewaitselection.c @@ -6,11 +6,24 @@ */ #include "rpg/cutscene/item/cutsceneitem.h" +#include "rpg/cutscene/cutscenesystem.h" #include "rpg/battle/battle.h" +#include "rpg/battle/state/battlestateselection.h" bool_t cutsceneBattleWaitSelectionUpdate( const cutsceneitem_t *item, cutsceneitemdata_t *data ) { - return BATTLE.state != BATTLE_STATE_SELECTION; + if(BATTLE.state != BATTLE_STATE_SELECTION) return true; + if(!battleStateSelectionIsComplete()) return false; + + // Every fighter has decided - queue the move to BATTLE_STATE_EXECUTING. + const cutsceneitem_t items[] = { + { + .type = CUTSCENE_ITEM_TYPE_BATTLE_SET_STATE, + .battleSetState = { .state = BATTLE_STATE_EXECUTING } + } + }; + cutsceneSystemInsertItemsNext(items, sizeof(items) / sizeof(cutsceneitem_t)); + return true; } diff --git a/src/dusk/rpg/cutscene/item/battle/cutscenebattlewaitselection.h b/src/dusk/rpg/cutscene/item/battle/cutscenebattlewaitselection.h index ba66dc24..c5235734 100644 --- a/src/dusk/rpg/cutscene/item/battle/cutscenebattlewaitselection.h +++ b/src/dusk/rpg/cutscene/item/battle/cutscenebattlewaitselection.h @@ -9,15 +9,17 @@ #include "rpg/cutscene/item/cutsceneitembase.h" /** - * Updates a BATTLE_WAIT_SELECTION step, completing once the battle is no - * longer in BATTLE_STATE_SELECTION -- i.e. every fighter has decided a - * move (moving on to BATTLE_STATE_EXECUTING) or the selecting fighter has - * fled (moving on to BATTLE_STATE_FLED). Has no Start callback -- there's - * nothing to do until selection actually finishes. + * Updates a BATTLE_WAIT_SELECTION step, completing once every fighter has + * decided a move this round (letting the queued BATTLE_SET_STATE step + * that follows move the battle on to BATTLE_STATE_EXECUTING) or once the + * selecting fighter has fled (moving on to BATTLE_STATE_FLED). Has no + * Start callback -- there's nothing to do until selection actually + * finishes. * * @param item The cutscene item. * @param data Runtime data storage. - * @returns true once BATTLE.state is no longer BATTLE_STATE_SELECTION. + * @returns true once selection has completed or the battle has left + * BATTLE_STATE_SELECTION. */ bool_t cutsceneBattleWaitSelectionUpdate( const cutsceneitem_t *item, diff --git a/src/dusk/rpg/cutscene/item/cutsceneitem.c b/src/dusk/rpg/cutscene/item/cutsceneitem.c index dd126730..ba44a1bb 100644 --- a/src/dusk/rpg/cutscene/item/cutsceneitem.c +++ b/src/dusk/rpg/cutscene/item/cutsceneitem.c @@ -265,6 +265,11 @@ cutsceneitemcallbacks_t CUTSCENE_ITEM_CALLBACKS[CUTSCENE_ITEM_TYPE_COUNT] = { .load = cutsceneItemLoadNoop }, + [CUTSCENE_ITEM_TYPE_BATTLE_POST_MOVE] = { + .init = cutsceneBattlePostMoveStart, + .update = cutsceneBattlePostMoveUpdate + }, + // Save [CUTSCENE_ITEM_TYPE_SAVE_DEVICE_CHECK] = { .init = cutsceneSaveDeviceCheckStart, diff --git a/src/dusk/rpg/cutscene/item/cutsceneitem.h b/src/dusk/rpg/cutscene/item/cutsceneitem.h index e4e4745f..3530fa2e 100644 --- a/src/dusk/rpg/cutscene/item/cutsceneitem.h +++ b/src/dusk/rpg/cutscene/item/cutsceneitem.h @@ -43,6 +43,7 @@ #include "battle/cutscenebattlestart.h" #include "battle/cutscenebattlesetstate.h" #include "battle/cutscenebattlewaitselection.h" +#include "battle/cutscenebattlepostmove.h" #include "save/cutscenesavedevicecheck.h" #include "save/cutscenesaveloadallslots.h" #include "audio/cutsceneaudioplay.h" diff --git a/src/dusk/rpg/cutscene/item/cutsceneitembase.h b/src/dusk/rpg/cutscene/item/cutsceneitembase.h index be187c26..b3f7dcc7 100644 --- a/src/dusk/rpg/cutscene/item/cutsceneitembase.h +++ b/src/dusk/rpg/cutscene/item/cutsceneitembase.h @@ -63,6 +63,7 @@ typedef enum { CUTSCENE_ITEM_TYPE_BATTLE_START, CUTSCENE_ITEM_TYPE_BATTLE_SET_STATE, CUTSCENE_ITEM_TYPE_BATTLE_WAIT_SELECTION, + CUTSCENE_ITEM_TYPE_BATTLE_POST_MOVE, // Save CUTSCENE_ITEM_TYPE_SAVE_DEVICE_CHECK, 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/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); } From 7aeffac9fc4cf336933d7a356fde62550cc09798 Mon Sep 17 00:00:00 2001 From: Dominic Masters Date: Sun, 20 Sep 2026 21:14:00 -0500 Subject: [PATCH 5/6] Add battle fighter damage/heal cutscene items, wire up battle results BATTLE_FIGHTER_DAMAGE/BATTLE_FIGHTER_HEAL let a move's own cutscene apply damage/healing to "target" or "user", resolved from BATTLE_FIGHTER_POS_TARGET/USER sentinels against the current action by battleStateExecutingInit. fire/slash now call BATTLE_FIGHTER_DAMAGE on their target. BATTLE_POST_MOVE checks fighter health after each move and sets BATTLE.result (now a real field, previously commented out) to win/loss, transitioning into BATTLE_STATE_WON/LOST; fleeing sets BATTLE_RESULT_FLED. All three end states now show a textbox before moving on to BATTLE_STATE_ENDING. Co-Authored-By: Claude Sonnet 5 --- assets/cutscenes/battle/moves/fire.jsonc | 5 ++ assets/cutscenes/battle/moves/slash.jsonc | 5 ++ src/dusk/rpg/battle/battle.h | 19 ++++-- .../rpg/battle/fighter/battlefighterpos.h | 10 ++- .../rpg/battle/state/battlestateexecuting.c | 28 ++++++++ .../rpg/battle/state/battlestateexecuting.h | 20 ++++++ src/dusk/rpg/battle/state/battlestatefled.c | 14 +++- src/dusk/rpg/battle/state/battlestatefled.h | 4 +- src/dusk/rpg/battle/state/battlestatelost.c | 14 +++- src/dusk/rpg/battle/state/battlestatelost.h | 4 +- .../rpg/battle/state/battlestateselection.c | 1 + src/dusk/rpg/battle/state/battlestatewon.c | 14 +++- src/dusk/rpg/battle/state/battlestatewon.h | 4 +- .../rpg/cutscene/item/battle/CMakeLists.txt | 2 + .../item/battle/cutscenebattlefighterdamage.c | 68 +++++++++++++++++++ .../item/battle/cutscenebattlefighterdamage.h | 55 +++++++++++++++ .../item/battle/cutscenebattlefighterheal.c | 68 +++++++++++++++++++ .../item/battle/cutscenebattlefighterheal.h | 55 +++++++++++++++ .../item/battle/cutscenebattlepostmove.c | 33 ++++++++- .../item/battle/cutscenebattlepostmove.h | 9 +-- src/dusk/rpg/cutscene/item/cutsceneitem.c | 14 ++++ src/dusk/rpg/cutscene/item/cutsceneitem.h | 4 ++ src/dusk/rpg/cutscene/item/cutsceneitembase.h | 2 + 23 files changed, 432 insertions(+), 20 deletions(-) create mode 100644 src/dusk/rpg/cutscene/item/battle/cutscenebattlefighterdamage.c create mode 100644 src/dusk/rpg/cutscene/item/battle/cutscenebattlefighterdamage.h create mode 100644 src/dusk/rpg/cutscene/item/battle/cutscenebattlefighterheal.c create mode 100644 src/dusk/rpg/cutscene/item/battle/cutscenebattlefighterheal.h diff --git a/assets/cutscenes/battle/moves/fire.jsonc b/assets/cutscenes/battle/moves/fire.jsonc index b4760f18..8c88770a 100644 --- a/assets/cutscenes/battle/moves/fire.jsonc +++ b/assets/cutscenes/battle/moves/fire.jsonc @@ -11,6 +11,11 @@ }, { "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 index 9726503b..0e80637f 100644 --- a/assets/cutscenes/battle/moves/slash.jsonc +++ b/assets/cutscenes/battle/moves/slash.jsonc @@ -11,6 +11,11 @@ }, { "type": "MODAL_CLOSE" + }, + { + "type": "BATTLE_FIGHTER_DAMAGE", + "fighter": "target", + "amount": 8 } ] } diff --git a/src/dusk/rpg/battle/battle.h b/src/dusk/rpg/battle/battle.h index 9c20aacb..108a0742 100644 --- a/src/dusk/rpg/battle/battle.h +++ b/src/dusk/rpg/battle/battle.h @@ -12,13 +12,13 @@ #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_RESULT_NONE, + BATTLE_RESULT_WIN, + BATTLE_RESULT_LOSS, + BATTLE_RESULT_FLED, + BATTLE_RESULT_COUNT +} battleresult_t; typedef struct { battlefighter_t *fighters[BATTLE_FIGHTER_POS_COUNT]; @@ -33,6 +33,11 @@ typedef struct { battlestate_t state; battlestatedata_t stateData; + + // 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; } battle_t; extern battle_t BATTLE; diff --git a/src/dusk/rpg/battle/fighter/battlefighterpos.h b/src/dusk/rpg/battle/fighter/battlefighterpos.h index 98389c40..e7f237b0 100644 --- a/src/dusk/rpg/battle/fighter/battlefighterpos.h +++ b/src/dusk/rpg/battle/fighter/battlefighterpos.h @@ -24,4 +24,12 @@ typedef uint8_t battlefighterpos_t; #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) \ No newline at end of file +#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/state/battlestateexecuting.c b/src/dusk/rpg/battle/state/battlestateexecuting.c index 948a217c..ce8bf499 100644 --- a/src/dusk/rpg/battle/state/battlestateexecuting.c +++ b/src/dusk/rpg/battle/state/battlestateexecuting.c @@ -18,6 +18,18 @@ #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; @@ -60,6 +72,7 @@ void battleStateExecutingInit(void) { } // Parse out cutscene items and append to the list. + const uint8_t moveItemsStart = itemsCount; cutscene_t scene; errorret_t parseResult = cutsceneParseDoc( entry->data.json, &scene, &items[itemsCount], @@ -75,6 +88,21 @@ void battleStateExecutingInit(void) { continue; } + // 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 diff --git a/src/dusk/rpg/battle/state/battlestateexecuting.h b/src/dusk/rpg/battle/state/battlestateexecuting.h index a6f8f75a..8e51d3e9 100644 --- a/src/dusk/rpg/battle/state/battlestateexecuting.h +++ b/src/dusk/rpg/battle/state/battlestateexecuting.h @@ -7,6 +7,8 @@ #pragma once #include "dusk.h" +#include "rpg/battle/fighter/battlefighterpos.h" +#include "rpg/battle/move/battlemoveaction.h" typedef struct { @@ -19,6 +21,24 @@ typedef struct { */ 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. */ diff --git a/src/dusk/rpg/battle/state/battlestatefled.c b/src/dusk/rpg/battle/state/battlestatefled.c index 1fbbed4d..441072a2 100644 --- a/src/dusk/rpg/battle/state/battlestatefled.c +++ b/src/dusk/rpg/battle/state/battlestatefled.c @@ -6,9 +6,21 @@ */ #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) { diff --git a/src/dusk/rpg/battle/state/battlestatefled.h b/src/dusk/rpg/battle/state/battlestatefled.h index 1cf2ef39..f482b6be 100644 --- a/src/dusk/rpg/battle/state/battlestatefled.h +++ b/src/dusk/rpg/battle/state/battlestatefled.h @@ -13,7 +13,9 @@ typedef struct { } battlestatefled_t; /** - * Called when the battle enters BATTLE_STATE_FLED. + * 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); diff --git a/src/dusk/rpg/battle/state/battlestatelost.c b/src/dusk/rpg/battle/state/battlestatelost.c index e160279b..334d8d87 100644 --- a/src/dusk/rpg/battle/state/battlestatelost.c +++ b/src/dusk/rpg/battle/state/battlestatelost.c @@ -6,9 +6,21 @@ */ #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) { diff --git a/src/dusk/rpg/battle/state/battlestatelost.h b/src/dusk/rpg/battle/state/battlestatelost.h index 202de391..7a20dca7 100644 --- a/src/dusk/rpg/battle/state/battlestatelost.h +++ b/src/dusk/rpg/battle/state/battlestatelost.h @@ -13,7 +13,9 @@ typedef struct { } battlestatelost_t; /** - * Called when the battle enters BATTLE_STATE_LOST. + * 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); diff --git a/src/dusk/rpg/battle/state/battlestateselection.c b/src/dusk/rpg/battle/state/battlestateselection.c index 168e7c2b..80ce4a31 100644 --- a/src/dusk/rpg/battle/state/battlestateselection.c +++ b/src/dusk/rpg/battle/state/battlestateselection.c @@ -101,5 +101,6 @@ 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/battlestatewon.c b/src/dusk/rpg/battle/state/battlestatewon.c index bfd92054..95d09f06 100644 --- a/src/dusk/rpg/battle/state/battlestatewon.c +++ b/src/dusk/rpg/battle/state/battlestatewon.c @@ -6,9 +6,21 @@ */ #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) { diff --git a/src/dusk/rpg/battle/state/battlestatewon.h b/src/dusk/rpg/battle/state/battlestatewon.h index e7b920e6..d9c72dec 100644 --- a/src/dusk/rpg/battle/state/battlestatewon.h +++ b/src/dusk/rpg/battle/state/battlestatewon.h @@ -13,7 +13,9 @@ typedef struct { } battlestatewon_t; /** - * Called when the battle enters BATTLE_STATE_WON. + * 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); diff --git a/src/dusk/rpg/cutscene/item/battle/CMakeLists.txt b/src/dusk/rpg/cutscene/item/battle/CMakeLists.txt index 80b57f57..55c2fdd3 100644 --- a/src/dusk/rpg/cutscene/item/battle/CMakeLists.txt +++ b/src/dusk/rpg/cutscene/item/battle/CMakeLists.txt @@ -9,4 +9,6 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME} 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/cutscenebattlepostmove.c b/src/dusk/rpg/cutscene/item/battle/cutscenebattlepostmove.c index 77efff1a..9c1136dc 100644 --- a/src/dusk/rpg/cutscene/item/battle/cutscenebattlepostmove.c +++ b/src/dusk/rpg/cutscene/item/battle/cutscenebattlepostmove.c @@ -6,15 +6,44 @@ */ #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 ) { - // TODO: check fighter health etc. here to short-circuit into a - // win/loss (BATTLE_STATE_WON/LOST) once a side has been wiped out. consolePrint("BATTLE_POST_MOVE\n"); + + // 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( diff --git a/src/dusk/rpg/cutscene/item/battle/cutscenebattlepostmove.h b/src/dusk/rpg/cutscene/item/battle/cutscenebattlepostmove.h index 6a7ff0ca..58b62943 100644 --- a/src/dusk/rpg/cutscene/item/battle/cutscenebattlepostmove.h +++ b/src/dusk/rpg/cutscene/item/battle/cutscenebattlepostmove.h @@ -10,10 +10,11 @@ /** * Starts a BATTLE_POST_MOVE item. Runs after each queued move's cutscene - * finishes - eventually this is where we'll check things like fighter - * health to short-circuit into a win/loss, but for now it just prints to - * the console. Not JSON-authorable -- constructed dynamically by - * battleStateExecutingInit. + * 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. diff --git a/src/dusk/rpg/cutscene/item/cutsceneitem.c b/src/dusk/rpg/cutscene/item/cutsceneitem.c index ba44a1bb..3f10f891 100644 --- a/src/dusk/rpg/cutscene/item/cutsceneitem.c +++ b/src/dusk/rpg/cutscene/item/cutsceneitem.c @@ -270,6 +270,20 @@ cutsceneitemcallbacks_t CUTSCENE_ITEM_CALLBACKS[CUTSCENE_ITEM_TYPE_COUNT] = { .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, diff --git a/src/dusk/rpg/cutscene/item/cutsceneitem.h b/src/dusk/rpg/cutscene/item/cutsceneitem.h index 3530fa2e..5d53190a 100644 --- a/src/dusk/rpg/cutscene/item/cutsceneitem.h +++ b/src/dusk/rpg/cutscene/item/cutsceneitem.h @@ -44,6 +44,8 @@ #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" @@ -118,6 +120,8 @@ struct cutsceneitem_s { cutscenemapareawait_t mapAreaWait; cutscenebattlestart_t battleStart; cutscenebattlesetstate_t battleSetState; + cutscenebattlefighterdamage_t battleFighterDamage; + cutscenebattlefighterheal_t battleFighterHeal; cutsceneemoji_t emoji; cutsceneshake_t shake; cutscenemodal_t modal; diff --git a/src/dusk/rpg/cutscene/item/cutsceneitembase.h b/src/dusk/rpg/cutscene/item/cutsceneitembase.h index b3f7dcc7..107523d8 100644 --- a/src/dusk/rpg/cutscene/item/cutsceneitembase.h +++ b/src/dusk/rpg/cutscene/item/cutsceneitembase.h @@ -64,6 +64,8 @@ typedef enum { 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, From c9f098693c6f41f683171e4b586368dd0cf2ff28 Mon Sep 17 00:00:00 2001 From: Dominic Masters Date: Mon, 21 Sep 2026 15:04:41 -0500 Subject: [PATCH 6/6] Wire up battle AI/scene-return, reclaim cutscene item slots, fix GOTO - battleStateSelectionAiDecide: AI-controlled fighters now actually queue a basic attack against the first living enemy instead of doing nothing every round. - battleStateEndingInit: disposes the battle and switches back to SCENE_TYPE_OVERWORLD instead of leaving the game stuck in the battle scene forever. - cutsceneSystemNext now pops each finished item off the front of the running cutscene (cutsceneRemoveFront) instead of leaving it in place forever, so a long-running cutscene (many battle rounds splicing items in repeatedly) no longer grows scene->items unboundedly. - cutsceneGoTo reworked to match: since consumed items are evicted, a marker behind the current position can't be found in the live queue anymore, so it now re-parses the cutscene fresh from its source file (cutsceneLoadParse, into a persistent scratch buffer, not the stack) and replaces the live queue with marker-onward from that fresh parse. Only supported when the running cutscene is the one cutsceneSystemLoad/cutsceneCutsceneResolve populated. - Fixed a real bug this surfaced: cutsceneSystemPrepare unconditionally cleared loadedFile, which ran *after* cutsceneCutsceneResolve had just stamped it but *before* the cutscene started - wiping it out immediately and breaking cutsceneGoTo for any cutscene entered via a CUTSCENE item or cutsceneCutsceneResolve (e.g. the main menu's NEW_GAME/OPTIONS/QUIT navigation). Now only cleared when switching away from loadedScene entirely. - Added cutsceneLoadParse, the shared "lock asset, require loaded, parse items, unlock" sequence, replacing four near-identical copies across cutsceneSystemLoad/cutsceneCutsceneResolve/ cutsceneInsertResolveAndSplice/battleStateExecutingInit. Each call site handles its own fatal-error display; cutsceneLoadParse itself just propagates. - Added test_cutscene.c (cutsceneRemoveFront) and a regression test for the loadedFile bug. Writing the zero-count RemoveFront test caught a real self-move bug (count == 0 passed dest == src into memoryMove). Co-Authored-By: Claude Sonnet 5 --- src/dusk/rpg/battle/state/battlestateending.c | 10 +- src/dusk/rpg/battle/state/battlestateending.h | 3 +- .../rpg/battle/state/battlestateexecuting.c | 29 ++--- .../rpg/battle/state/battlestateselection.c | 22 +++- .../rpg/battle/state/battlestateselection.h | 23 +++- src/dusk/rpg/cutscene/cutscene.c | 50 ++++++++ src/dusk/rpg/cutscene/cutscene.h | 42 +++++++ src/dusk/rpg/cutscene/cutscenesystem.c | 115 ++++++++++++------ src/dusk/rpg/cutscene/cutscenesystem.h | 30 ++++- src/dusk/rpg/cutscene/item/cutsceneitem.c | 53 ++------ test/rpg/cutscene/CMakeLists.txt | 1 + test/rpg/cutscene/test_cutscene.c | 84 +++++++++++++ test/rpg/cutscene/test_cutscenesystem.c | 91 ++++++++++++-- 13 files changed, 430 insertions(+), 123 deletions(-) create mode 100644 test/rpg/cutscene/test_cutscene.c diff --git a/src/dusk/rpg/battle/state/battlestateending.c b/src/dusk/rpg/battle/state/battlestateending.c index 448171d4..2a195cbc 100644 --- a/src/dusk/rpg/battle/state/battlestateending.c +++ b/src/dusk/rpg/battle/state/battlestateending.c @@ -6,9 +6,17 @@ */ #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) { diff --git a/src/dusk/rpg/battle/state/battlestateending.h b/src/dusk/rpg/battle/state/battlestateending.h index 56721c24..7b4fc581 100644 --- a/src/dusk/rpg/battle/state/battlestateending.h +++ b/src/dusk/rpg/battle/state/battlestateending.h @@ -13,7 +13,8 @@ typedef struct { } battlestateending_t; /** - * Called when the battle enters BATTLE_STATE_ENDING. + * Called when the battle enters BATTLE_STATE_ENDING. Disposes the battle + * (see battleDispose) and switches back to SCENE_TYPE_OVERWORLD. */ void battleStateEndingInit(void); diff --git a/src/dusk/rpg/battle/state/battlestateexecuting.c b/src/dusk/rpg/battle/state/battlestateexecuting.c index ce8bf499..d0045a38 100644 --- a/src/dusk/rpg/battle/state/battlestateexecuting.c +++ b/src/dusk/rpg/battle/state/battlestateexecuting.c @@ -10,8 +10,6 @@ #include "rpg/cutscene/item/cutsceneitem.h" #include "rpg/cutscene/cutscenesystem.h" #include "rpg/cutscene/cutscene.h" -#include "asset/asset.h" -#include "asset/loader/assetloader.h" #include "asset/assetfile.h" #include "ui/overlay/uifatalerror.h" #include "assert/assert.h" @@ -61,33 +59,20 @@ void battleStateExecutingInit(void) { "cutscenes/battle/moves/%s.jsonc", ability->name ); - // Load cutscene asset. - assetentry_t *entry = assetLock(path, ASSET_LOADER_TYPE_JSON, NULL); - errorret_t result = assetRequireLoaded(entry); + // 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)) { - assetUnlockEntry(entry); errorCatch(errorPrint(result)); uiFatalErrorOpen(NULL); continue; } - - // Parse out cutscene items and append to the list. - const uint8_t moveItemsStart = itemsCount; - cutscene_t scene; - errorret_t parseResult = cutsceneParseDoc( - entry->data.json, &scene, &items[itemsCount], - BATTLE_STATE_EXECUTING_ITEMS_MAX - itemsCount - ); itemsCount += scene.itemCount; - // Release asset - assetUnlockEntry(entry); - if(errorIsNotOk(parseResult)) { - errorCatch(errorPrint(parseResult)); - uiFatalErrorOpen(NULL); - continue; - } - // 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++) { diff --git a/src/dusk/rpg/battle/state/battlestateselection.c b/src/dusk/rpg/battle/state/battlestateselection.c index 80ce4a31..22d08c55 100644 --- a/src/dusk/rpg/battle/state/battlestateselection.c +++ b/src/dusk/rpg/battle/state/battlestateselection.c @@ -32,6 +32,25 @@ 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; @@ -49,8 +68,7 @@ void battleStateSelectionUpdate(void) { battlefighter_t *fighter = BATTLE.fighters[fighterIndex]; if(fighter->controller != BATTLE_FIGHTER_CONTROLLER_AI) return; - // This is AI, in future I'll have proper AI here, for now I'm actually not - // going to do anything. + battleStateSelectionAiDecide(fighterIndex); state->selectionIndex++; } } diff --git a/src/dusk/rpg/battle/state/battlestateselection.h b/src/dusk/rpg/battle/state/battlestateselection.h index 71d8ee69..c4a7d1ba 100644 --- a/src/dusk/rpg/battle/state/battlestateselection.h +++ b/src/dusk/rpg/battle/state/battlestateselection.h @@ -20,11 +20,13 @@ typedef struct { void battleStateSelectionInit(void); /** - * Updates BATTLE_STATE_SELECTION for one frame: auto-decides for every - * AI-controlled fighter the cursor passes, stopping once it reaches a - * player-controlled fighter awaiting input (see - * battleStateSelectionGetCurrentFighter), or transitions to - * BATTLE_STATE_EXECUTING once every position has decided. + * 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); @@ -52,6 +54,17 @@ bool_t battleStateSelectionFighterHasDecided(const uint8_t fighterIndex); */ 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. 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 fe9a48c9..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) { @@ -118,27 +128,12 @@ void cutsceneSystemInsertCutscene(const cutscene_t *cutscene) { 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; } @@ -256,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 @@ -298,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"); @@ -325,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 eb988a49..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; @@ -211,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/cutsceneitem.c b/src/dusk/rpg/cutscene/item/cutsceneitem.c index 3f10f891..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" @@ -433,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; } @@ -511,29 +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/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),