From 3675ec634f5e180ff3f5d46007b875593f4ebc71 Mon Sep 17 00:00:00 2001 From: Dominic Masters Date: Sat, 19 Sep 2026 23:17:27 -0500 Subject: [PATCH] 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);