From e3f56c4d1275015fbba8e433af785b4e3224f7a2 Mon Sep 17 00:00:00 2001 From: Dominic Masters Date: Sun, 20 Sep 2026 13:32:52 -0500 Subject: [PATCH] Drive battle rounds through cutscene items, fix render/init bugs Adds BATTLE_START, BATTLE_SET_STATE and BATTLE_WAIT_SELECTION cutscene items so a battle's state transitions (opening -> pre-round -> selection -> executing) are chained through the running cutscene, with cutsceneSystemInsertItemsNext/InsertItems splitting "splice" from "splice and advance immediately". Adds battlestateselection's get-current-fighter/player-attack/player-flee API backing the battle UI, and battleMoveGetIdByName for JSON-authored ability lookups. Fixes: off-by-one in battleMoveInit that let the first-defined move alias BATTLE_MOVE_ID_NULL; a segfault in scenebattle.c from rendering unfilled (NULL) fighter slots; stale battleGetCurrentFighter/ battlePlayerAttack/battlePlayerFlee references in the battle UI. Adds assertions across the battle/scene/UI code for fighter-index bounds and non-null invariants. Updates battletest.jsonc for the new BATTLE_START item schema. Co-Authored-By: Claude Sonnet 5 --- assets/cutscenes/battletest.jsonc | 22 ++- src/dusk/rpg/battle/battle.c | 28 +++- src/dusk/rpg/battle/battle.h | 8 + src/dusk/rpg/battle/move/battlemove.c | 13 +- src/dusk/rpg/battle/move/battlemove.h | 8 + .../rpg/battle/state/battlestateopening.c | 10 +- .../rpg/battle/state/battlestatepreround.c | 13 +- .../rpg/battle/state/battlestateselection.c | 101 +++++++++++- .../rpg/battle/state/battlestateselection.h | 58 ++++++- src/dusk/rpg/cutscene/cutscenesystem.c | 24 ++- src/dusk/rpg/cutscene/cutscenesystem.h | 26 ++++ src/dusk/rpg/cutscene/item/CMakeLists.txt | 1 + .../rpg/cutscene/item/battle/CMakeLists.txt | 11 ++ .../item/battle/cutscenebattlesetstate.c | 41 +++++ .../item/battle/cutscenebattlesetstate.h | 50 ++++++ .../item/battle/cutscenebattlestart.c | 144 ++++++++++++++++++ .../item/battle/cutscenebattlestart.h | 62 ++++++++ .../item/battle/cutscenebattlewaitselection.c | 16 ++ .../item/battle/cutscenebattlewaitselection.h | 25 +++ src/dusk/rpg/cutscene/item/cutsceneitem.c | 21 +++ src/dusk/rpg/cutscene/item/cutsceneitem.h | 5 + src/dusk/rpg/cutscene/item/cutsceneitembase.h | 5 + .../rpg/cutscene/item/json/CMakeLists.txt | 1 + .../item/json/cutscenejsonbattlestate.c | 38 +++++ .../item/json/cutscenejsonbattlestate.h | 21 +++ src/dusk/scene/battle/scenebattle.c | 16 +- src/dusk/ui/screen/battle/uibattlehud.c | 3 +- src/dusk/ui/screen/battle/uibattlemenu.c | 10 +- 28 files changed, 747 insertions(+), 34 deletions(-) create mode 100644 src/dusk/rpg/cutscene/item/battle/CMakeLists.txt create mode 100644 src/dusk/rpg/cutscene/item/battle/cutscenebattlesetstate.c create mode 100644 src/dusk/rpg/cutscene/item/battle/cutscenebattlesetstate.h create mode 100644 src/dusk/rpg/cutscene/item/battle/cutscenebattlestart.c create mode 100644 src/dusk/rpg/cutscene/item/battle/cutscenebattlestart.h create mode 100644 src/dusk/rpg/cutscene/item/battle/cutscenebattlewaitselection.c create mode 100644 src/dusk/rpg/cutscene/item/battle/cutscenebattlewaitselection.h create mode 100644 src/dusk/rpg/cutscene/item/json/cutscenejsonbattlestate.c create mode 100644 src/dusk/rpg/cutscene/item/json/cutscenejsonbattlestate.h diff --git a/assets/cutscenes/battletest.jsonc b/assets/cutscenes/battletest.jsonc index 6039ec6b..22b5de51 100644 --- a/assets/cutscenes/battletest.jsonc +++ b/assets/cutscenes/battletest.jsonc @@ -1,24 +1,22 @@ { "items": [ { - "type": "START_BATTLE", - "encounterType": "REGULAR", - "fleeAvailable": true, + "type": "BATTLE_START", "enemies": [ { - "stats": { "attack": 8, "defense": 4, "magic": 0, "speed": 5, "luck": 2 }, - "healthMax": 30, - "mpMax": 0 + "stats": { + "attack": 8, "defense": 4, "magic": 0, "speed": 5, "luck": 2, + "healthMax": 30, "mpMax": 0 + }, + "abilities": ["fire"] }, { - "stats": { "attack": 6, "defense": 3, "magic": 0, "speed": 7, "luck": 2 }, - "healthMax": 20, - "mpMax": 0 + "stats": { + "attack": 6, "defense": 3, "magic": 0, "speed": 7, "luck": 2, + "healthMax": 20, "mpMax": 0 + } } ] - }, - { - "type": "REGULAR_BATTLE" } ] } diff --git a/src/dusk/rpg/battle/battle.c b/src/dusk/rpg/battle/battle.c index ac6a40be..f9e2d47a 100644 --- a/src/dusk/rpg/battle/battle.c +++ b/src/dusk/rpg/battle/battle.c @@ -9,6 +9,7 @@ #include "assert/assert.h" #include "util/memory.h" #include "rpg/cutscene/cutscenesystem.h" +#include "scene/scene.h" battle_t BATTLE; @@ -22,7 +23,8 @@ void battleStart( assertNotNull(fighters, "Fighters cannot be NULL"); #if DUSK_ASSERTIONS - // Must be at least two non null fighter present. + // Must be at least two non null fighter present, may need to ensure they + // are from seperate teams also. uint8_t nonNullCount = 0; for(uint8_t i = 0; i < BATTLE_FIGHTER_POS_COUNT; i++) { if(fighters[i] != NULL) nonNullCount++; @@ -39,18 +41,38 @@ void battleStart( sizeof(battlefighter_t *) * BATTLE_FIGHTER_POS_COUNT ); - // Call init on opening state. - BATTLE.state = BATTLE_STATE_OPENING; + // Enter the opening state. + battleSetState(BATTLE_STATE_OPENING); + + // Change scene + sceneSet(SCENE_TYPE_BATTLE); +} + +void battleSetState(const battlestate_t next) { + assertTrue(next < BATTLE_STATE_COUNT, "Invalid battle state"); + assertNotNull(BATTLE_STATE_FUNCTIONS[next].init, "Missing state init function"); + assertNotNull(BATTLE_STATE_FUNCTIONS[next].update, "Missing state update function"); + assertNotNull(BATTLE_STATE_FUNCTIONS[next].dispose, "Missing state dispose function"); + + if(BATTLE.state != BATTLE_STATE_NULL) { + BATTLE_STATE_FUNCTIONS[BATTLE.state].dispose(); + } + BATTLE.state = next; BATTLE_STATE_FUNCTIONS[BATTLE.state].init(); } void battleUpdate(void) { if(BATTLE.state == BATTLE_STATE_NULL) return; + assertTrue(BATTLE.state < BATTLE_STATE_COUNT, "Invalid battle state"); + assertNotNull(BATTLE_STATE_FUNCTIONS[BATTLE.state].update, "Missing state update function"); + BATTLE_STATE_FUNCTIONS[BATTLE.state].update(); } void battleDispose(void) { if(BATTLE.state == BATTLE_STATE_NULL) return; + assertTrue(BATTLE.state < BATTLE_STATE_COUNT, "Invalid battle state"); + assertNotNull(BATTLE_STATE_FUNCTIONS[BATTLE.state].dispose, "Missing state dispose function"); BATTLE_STATE_FUNCTIONS[BATTLE.state].dispose(); memoryZero(&BATTLE, sizeof(battle_t)); diff --git a/src/dusk/rpg/battle/battle.h b/src/dusk/rpg/battle/battle.h index 1d071b09..2c86fa37 100644 --- a/src/dusk/rpg/battle/battle.h +++ b/src/dusk/rpg/battle/battle.h @@ -49,6 +49,14 @@ void battleStart( battlefighter_t *fighters[BATTLE_FIGHTER_POS_COUNT] ); +/** + * Transitions the battle to a new state: disposes the current state (if + * any) and initializes next. + * + * @param next The state to transition to. + */ +void battleSetState(const battlestate_t next); + /** * Updates the battle system. */ diff --git a/src/dusk/rpg/battle/move/battlemove.c b/src/dusk/rpg/battle/move/battlemove.c index 45b57269..389c5ee0 100644 --- a/src/dusk/rpg/battle/move/battlemove.c +++ b/src/dusk/rpg/battle/move/battlemove.c @@ -8,6 +8,7 @@ #include "battlemove.h" #include "asset/asset.h" #include "asset/loader/assetloader.h" +#include "assert/assert.h" #include "util/memory.h" #include "util/string.h" #include @@ -17,7 +18,7 @@ uint8_t BATTLE_MOVE_COUNT; errorret_t battleMoveInit(void) { memoryZero(BATTLE_MOVES, sizeof(BATTLE_MOVES)); - BATTLE_MOVE_COUNT = BATTLE_MOVE_ID_NULL; + BATTLE_MOVE_COUNT = BATTLE_MOVE_ID_NULL + 1;// Reserve NULL. // Get moves JSON. assetentry_t *entry = assetLock( @@ -126,3 +127,13 @@ errorret_t battleMoveInit(void) { assetUnlockEntry(entry); errorOk(); } + +battlemoveid_t battleMoveGetIdByName(const char_t *name) { + assertNotNull(name, "name must not be NULL"); + + for(battlemoveid_t id = BATTLE_MOVE_ID_NULL + 1; id < BATTLE_MOVE_COUNT; id++) { + if(stringEquals(BATTLE_MOVES[id].name, name)) return id; + } + + return BATTLE_MOVE_ID_NULL; +} diff --git a/src/dusk/rpg/battle/move/battlemove.h b/src/dusk/rpg/battle/move/battlemove.h index 538306d6..3c11b2d2 100644 --- a/src/dusk/rpg/battle/move/battlemove.h +++ b/src/dusk/rpg/battle/move/battlemove.h @@ -32,3 +32,11 @@ extern battleability_t BATTLE_MOVES[BATTLE_MOVE_COUNT_MAX]; * @return Any error that occurs. */ errorret_t battleMoveInit(void); + +/** + * Finds a move's id by name (exact match). + * + * @param name The move name to search for. + * @return The matching move's id, or BATTLE_MOVE_ID_NULL if not found. + */ +battlemoveid_t battleMoveGetIdByName(const char_t *name); diff --git a/src/dusk/rpg/battle/state/battlestateopening.c b/src/dusk/rpg/battle/state/battlestateopening.c index d39afe17..eb2a2752 100644 --- a/src/dusk/rpg/battle/state/battlestateopening.c +++ b/src/dusk/rpg/battle/state/battlestateopening.c @@ -6,9 +6,17 @@ */ #include "battlestateopening.h" +#include "rpg/cutscene/item/cutsceneitem.h" +#include "rpg/cutscene/cutscenesystem.h" void battleStateOpeningInit(void) { - + const cutsceneitem_t items[] = { + { + .type = CUTSCENE_ITEM_TYPE_BATTLE_SET_STATE, + .battleSetState = { .state = BATTLE_STATE_PRE_ROUND } + } + }; + cutsceneSystemInsertItemsNext(items, sizeof(items) / sizeof(cutsceneitem_t)); } void battleStateOpeningUpdate(void) { diff --git a/src/dusk/rpg/battle/state/battlestatepreround.c b/src/dusk/rpg/battle/state/battlestatepreround.c index 297ae979..28f1d2be 100644 --- a/src/dusk/rpg/battle/state/battlestatepreround.c +++ b/src/dusk/rpg/battle/state/battlestatepreround.c @@ -6,9 +6,20 @@ */ #include "battlestatepreround.h" +#include "rpg/cutscene/item/cutsceneitem.h" +#include "rpg/cutscene/cutscenesystem.h" void battleStatePreRoundInit(void) { - + const cutsceneitem_t items[] = { + { + .type = CUTSCENE_ITEM_TYPE_BATTLE_SET_STATE, + .battleSetState = { .state = BATTLE_STATE_SELECTION } + }, + { + .type = CUTSCENE_ITEM_TYPE_BATTLE_WAIT_SELECTION + } + }; + cutsceneSystemInsertItemsNext(items, sizeof(items) / sizeof(cutsceneitem_t)); } void battleStatePreRoundUpdate(void) { diff --git a/src/dusk/rpg/battle/state/battlestateselection.c b/src/dusk/rpg/battle/state/battlestateselection.c index a753b8df..3d45a11b 100644 --- a/src/dusk/rpg/battle/state/battlestateselection.c +++ b/src/dusk/rpg/battle/state/battlestateselection.c @@ -6,15 +6,114 @@ */ #include "battlestateselection.h" +#include "rpg/battle/battle.h" +#include "rpg/cutscene/item/cutsceneitem.h" +#include "rpg/cutscene/cutscenesystem.h" +#include "assert/assert.h" + +// TEMPORARY: hardcoded pending a real moves.jsonc-backed registry lookup. +#define BATTLE_MOVE_ID_ATTACK ((battlemoveid_t)1) void battleStateSelectionInit(void) { - + BATTLE.stateData.selection.selectionIndex = 0; } void battleStateSelectionUpdate(void) { + battlestateselection_t *state = &BATTLE.stateData.selection; + while(state->selectionIndex < BATTLE_FIGHTER_POS_COUNT) { + const uint8_t fighterIndex = state->selectionIndex; + if(!battleStateSelectionFighterNeedsDecision(fighterIndex)) { + state->selectionIndex++; + continue; + } + + battlefighter_t *fighter = BATTLE.fighters[fighterIndex]; + if(fighter->controller != BATTLE_FIGHTER_CONTROLLER_AI) { + // Player-controlled: wait for battleStateSelectionPlayerAttack/Flee. + return; + } + + const battlefighterteam_t enemyTeam = + fighter->team == BATTLE_FIGHTER_TEAM_ALLY ? + BATTLE_FIGHTER_TEAM_ENEMY : BATTLE_FIGHTER_TEAM_ALLY; + + uint8_t targetIndex = 0xFF; + for(uint8_t i = 0; i < BATTLE_FIGHTER_POS_COUNT; i++) { + battlefighter_t *candidate = BATTLE.fighters[i]; + if(candidate == NULL) continue; + if(candidate->team != enemyTeam) continue; + if(!battleFighterIsAlive(candidate)) continue; + targetIndex = i; + break; + } + + if(targetIndex != 0xFF) { + battleStateSelectionQueueAttack(fighterIndex, targetIndex); + } + state->selectionIndex++; + } + + const cutsceneitem_t items[] = { + { + .type = CUTSCENE_ITEM_TYPE_BATTLE_SET_STATE, + .battleSetState = { .state = BATTLE_STATE_EXECUTING } + } + }; + cutsceneSystemInsertItems(items, sizeof(items) / sizeof(cutsceneitem_t)); } void battleStateSelectionDispose(void) { } + +bool_t battleStateSelectionFighterNeedsDecision(const uint8_t fighterIndex) { + assertTrue(fighterIndex < BATTLE_FIGHTER_POS_COUNT, "fighterIndex out of bounds"); + + battlefighter_t *fighter = BATTLE.fighters[fighterIndex]; + if(fighter == NULL) return false; + if(!battleFighterIsAlive(fighter)) return false; + + return BATTLE.actionsPending[fighterIndex].move == BATTLE_MOVE_ID_NULL; +} + +void battleStateSelectionQueueAttack( + const uint8_t fighterIndex, + const uint8_t targetIndex +) { + assertTrue(fighterIndex < BATTLE_FIGHTER_POS_COUNT, "fighterIndex out of bounds"); + assertTrue(targetIndex < BATTLE_FIGHTER_POS_COUNT, "targetIndex out of bounds"); + assertNotNull(BATTLE.fighters[fighterIndex], "Fighter cannot be NULL"); + assertNotNull(BATTLE.fighters[targetIndex], "Target cannot be NULL"); + + battlemoveaction_t *action = &BATTLE.actionsPending[fighterIndex]; + action->move = BATTLE_MOVE_ID_ATTACK; + action->targets[0] = targetIndex; + action->user = BATTLE.fighters[fighterIndex]; +} + +battlefighter_t *battleStateSelectionGetCurrentFighter(void) { + if(BATTLE.state != BATTLE_STATE_SELECTION) return NULL; + + const uint8_t index = BATTLE.stateData.selection.selectionIndex; + if(index >= BATTLE_FIGHTER_POS_COUNT) return NULL; + + return BATTLE.fighters[index]; +} + +void battleStateSelectionPlayerAttack(const uint8_t targetIndex) { + battlefighter_t *fighter = battleStateSelectionGetCurrentFighter(); + if(fighter == NULL) return; + + battleStateSelectionQueueAttack( + BATTLE.stateData.selection.selectionIndex, targetIndex + ); + BATTLE.stateData.selection.selectionIndex++; +} + +void battleStateSelectionPlayerFlee(void) { + battlefighter_t *fighter = battleStateSelectionGetCurrentFighter(); + if(fighter == NULL) return; + + battleSetState(BATTLE_STATE_FLED); +} diff --git a/src/dusk/rpg/battle/state/battlestateselection.h b/src/dusk/rpg/battle/state/battlestateselection.h index 4fad166f..ad93cfb2 100644 --- a/src/dusk/rpg/battle/state/battlestateselection.h +++ b/src/dusk/rpg/battle/state/battlestateselection.h @@ -6,10 +6,12 @@ */ #pragma once -#include "dusk.h" +#include "rpg/battle/fighter/battlefighter.h" typedef struct { - + // Cursor into BATTLE.fighters (by position index) for the fighter + // currently deciding a move this round. + uint8_t selectionIndex; } battlestateselection_t; /** @@ -18,7 +20,11 @@ typedef struct { void battleStateSelectionInit(void); /** - * Updates BATTLE_STATE_SELECTION for one frame. + * Updates BATTLE_STATE_SELECTION for one frame: auto-decides for every + * AI-controlled fighter the cursor passes, stopping once it reaches a + * player-controlled fighter awaiting input (see + * battleStateSelectionGetCurrentFighter), or transitions to + * BATTLE_STATE_EXECUTING once every position has decided. */ void battleStateSelectionUpdate(void); @@ -26,3 +32,49 @@ void battleStateSelectionUpdate(void); * Called when the battle leaves BATTLE_STATE_SELECTION. */ void battleStateSelectionDispose(void); + +/** + * Checks whether the fighter at fighterIndex is a live, undecided + * candidate that BATTLE_STATE_SELECTION should still be deciding a move + * for. + * + * @param fighterIndex Index into BATTLE.fighters to check. + * @return True if the fighter is alive and has no move queued yet. + */ +bool_t battleStateSelectionFighterNeedsDecision(const uint8_t fighterIndex); + +/** + * Queues a basic attack from fighterIndex against targetIndex. + * + * @param fighterIndex Index into BATTLE.fighters of the deciding fighter. + * @param targetIndex Index into BATTLE.fighters of the target. + */ +void battleStateSelectionQueueAttack( + const uint8_t fighterIndex, + const uint8_t targetIndex +); + +/** + * Returns the fighter currently awaiting a decision in + * BATTLE_STATE_SELECTION. + * + * @return Pointer to the fighter awaiting a decision, or NULL if the + * battle isn't in BATTLE_STATE_SELECTION or every position has + * already decided. + */ +battlefighter_t *battleStateSelectionGetCurrentFighter(void); + +/** + * Submits the currently-selecting fighter's attack against a target, if + * the battle is in BATTLE_STATE_SELECTION and awaiting a decision. Queues + * the move and advances the selection cursor. + * + * @param targetIndex Index into BATTLE.fighters of the target. + */ +void battleStateSelectionPlayerAttack(const uint8_t targetIndex); + +/** + * Ends the battle immediately with BATTLE_STATE_FLED, if the battle is in + * BATTLE_STATE_SELECTION and awaiting a decision. + */ +void battleStateSelectionPlayerFlee(void); diff --git a/src/dusk/rpg/cutscene/cutscenesystem.c b/src/dusk/rpg/cutscene/cutscenesystem.c index f7d6cfba..fe9a48c9 100644 --- a/src/dusk/rpg/cutscene/cutscenesystem.c +++ b/src/dusk/rpg/cutscene/cutscenesystem.c @@ -87,20 +87,34 @@ void cutsceneSystemStartCutsceneAndGoToMarker( cutsceneGoTo(marker); } -void cutsceneSystemInsertCutscene(const cutscene_t *cutscene) { - assertNotNull(cutscene, "Cutscene cannot be NULL"); +void cutsceneSystemInsertItemsNext( + const cutsceneitem_t *items, + const size_t itemCount +) { + assertNotNull(items, "Items cannot be NULL"); assertNotNull( CUTSCENE_SYSTEM.scene, - "cutsceneSystemInsertCutscene called with no cutscene running" + "cutsceneSystemInsertItemsNext called with no cutscene running" ); cutsceneAppendNext( - CUTSCENE_SYSTEM.scene, CUTSCENE_SYSTEM.currentItem, - cutscene->items, cutscene->itemCount + CUTSCENE_SYSTEM.scene, CUTSCENE_SYSTEM.currentItem, items, itemCount ); +} + +void cutsceneSystemInsertItems( + const cutsceneitem_t *items, + const size_t itemCount +) { + cutsceneSystemInsertItemsNext(items, itemCount); cutsceneSystemNext(); } +void cutsceneSystemInsertCutscene(const cutscene_t *cutscene) { + assertNotNull(cutscene, "Cutscene cannot be NULL"); + cutsceneSystemInsertItems(cutscene->items, cutscene->itemCount); +} + void cutsceneSystemLoad(const char_t *file) { assertNotNull(file, "File cannot be NULL"); diff --git a/src/dusk/rpg/cutscene/cutscenesystem.h b/src/dusk/rpg/cutscene/cutscenesystem.h index 76dd79f7..eb988a49 100644 --- a/src/dusk/rpg/cutscene/cutscenesystem.h +++ b/src/dusk/rpg/cutscene/cutscenesystem.h @@ -109,6 +109,32 @@ void cutsceneSystemStartCutsceneAndGoToMarker( const char_t *marker ); +/** + * Splices items into the running cutscene so that items[0] becomes the + * very next item, without otherwise disturbing the currently-running + * item's progress. + * + * @param items Items to splice in. + * @param itemCount Number of items in items. + */ +void cutsceneSystemInsertItemsNext( + const cutsceneitem_t *items, + const size_t itemCount +); + +/** + * Splices items into the running cutscene right after the current item + * (see cutsceneSystemInsertItemsNext), then immediately advances into the + * first spliced item. + * + * @param items Items to splice in. + * @param itemCount Number of items in items. + */ +void cutsceneSystemInsertItems( + const cutsceneitem_t *items, + const size_t itemCount +); + /** * Splices cutscene's items into the running cutscene. * diff --git a/src/dusk/rpg/cutscene/item/CMakeLists.txt b/src/dusk/rpg/cutscene/item/CMakeLists.txt index 1915c7e2..42868c1b 100755 --- a/src/dusk/rpg/cutscene/item/CMakeLists.txt +++ b/src/dusk/rpg/cutscene/item/CMakeLists.txt @@ -16,6 +16,7 @@ add_subdirectory(entity) add_subdirectory(item) add_subdirectory(maparea) add_subdirectory(ui) +add_subdirectory(battle) add_subdirectory(save) add_subdirectory(audio) add_subdirectory(json) diff --git a/src/dusk/rpg/cutscene/item/battle/CMakeLists.txt b/src/dusk/rpg/cutscene/item/battle/CMakeLists.txt new file mode 100644 index 00000000..2faba70b --- /dev/null +++ b/src/dusk/rpg/cutscene/item/battle/CMakeLists.txt @@ -0,0 +1,11 @@ +# Copyright (c) 2026 Dominic Masters +# +# This software is released under the MIT License. +# https://opensource.org/licenses/MIT + +target_sources(${DUSK_LIBRARY_TARGET_NAME} + PUBLIC + cutscenebattlestart.c + cutscenebattlesetstate.c + cutscenebattlewaitselection.c +) diff --git a/src/dusk/rpg/cutscene/item/battle/cutscenebattlesetstate.c b/src/dusk/rpg/cutscene/item/battle/cutscenebattlesetstate.c new file mode 100644 index 00000000..5b715c4c --- /dev/null +++ b/src/dusk/rpg/cutscene/item/battle/cutscenebattlesetstate.c @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "rpg/cutscene/item/cutsceneitem.h" +#include "rpg/battle/battle.h" +#include "rpg/cutscene/item/json/cutscenejsonbattlestate.h" +#include "assert/assert.h" + +void cutsceneBattleSetStateStart( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +) { + assertNotNull(item, "item cannot be NULL"); + battleSetState(item->battleSetState.state); +} + +bool_t cutsceneBattleSetStateUpdate( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +) { + return true; +} + +errorret_t cutsceneBattleSetStateLoad( + yyjson_val *itemObj, + cutsceneitem_t *item +) { + battlestate_t state; + if(!cutsceneJsonLookupBattleState( + yyjson_get_str(yyjson_obj_get(itemObj, "state")), &state + )) { + errorThrow("Cutscene item (BATTLE_SET_STATE): invalid 'state'"); + } + item->battleSetState.state = state; + + errorOk(); +} diff --git a/src/dusk/rpg/cutscene/item/battle/cutscenebattlesetstate.h b/src/dusk/rpg/cutscene/item/battle/cutscenebattlesetstate.h new file mode 100644 index 00000000..2d4753bf --- /dev/null +++ b/src/dusk/rpg/cutscene/item/battle/cutscenebattlesetstate.h @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "rpg/battle/state/battlestate.h" +#include "rpg/cutscene/item/cutsceneitembase.h" + +typedef struct { + battlestate_t state; +} cutscenebattlesetstate_t; + +/** + * Starts a BATTLE_SET_STATE item, transitioning the battle to the + * configured state immediately. + * + * @param item The cutscene item. + * @param data Runtime data storage. + */ +void cutsceneBattleSetStateStart( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +); + +/** + * Updates a BATTLE_SET_STATE item (always completes immediately). + * + * @param item The cutscene item. + * @param data Runtime data storage. + * @returns true always. + */ +bool_t cutsceneBattleSetStateUpdate( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +); + +/** + * Parses a BATTLE_SET_STATE cutscene item's JSON fields into item. + * + * @param itemObj The item's JSON object. + * @param item Destination item, already zeroed by the caller. + * @return Error code indicating success or failure of the parse. + */ +errorret_t cutsceneBattleSetStateLoad( + yyjson_val *itemObj, + cutsceneitem_t *item +); diff --git a/src/dusk/rpg/cutscene/item/battle/cutscenebattlestart.c b/src/dusk/rpg/cutscene/item/battle/cutscenebattlestart.c new file mode 100644 index 00000000..439f20b8 --- /dev/null +++ b/src/dusk/rpg/cutscene/item/battle/cutscenebattlestart.c @@ -0,0 +1,144 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "rpg/cutscene/item/cutsceneitem.h" +#include "rpg/battle/battle.h" +#include "rpg/battle/party.h" +#include "assert/assert.h" +#include "util/memory.h" + +battlefighter_t CUTSCENE_BATTLE_START_ENEMY_POOL[ + CUTSCENE_BATTLE_START_ENEMY_COUNT_MAX +]; + +void cutsceneBattleStartStart( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +) { + assertNotNull(item, "item cannot be NULL"); + + const cutscenebattlestart_t *config = &item->battleStart; + assertTrue( + config->enemyCount <= CUTSCENE_BATTLE_START_ENEMY_COUNT_MAX, + "enemyCount out of bounds" + ); + + battlefighter_t *fighters[BATTLE_FIGHTER_POS_COUNT] = { 0 }; + + memoryZero( + CUTSCENE_BATTLE_START_ENEMY_POOL, + sizeof(CUTSCENE_BATTLE_START_ENEMY_POOL) + ); + for(uint8_t i = 0; i < config->enemyCount; i++) { + const cutscenebattlestartenemy_t *enemyConfig = &config->enemies[i]; + battlefighter_t *enemy = &CUTSCENE_BATTLE_START_ENEMY_POOL[i]; + + battleFighterInit( + enemy, BATTLE_FIGHTER_TEAM_ENEMY, BATTLE_FIGHTER_CONTROLLER_AI, + enemyConfig->stats + ); + for(uint8_t a = 0; a < enemyConfig->abilityCount; a++) { + battleFighterAbilityAdd(enemy, enemyConfig->abilities[a]); + } + + fighters[i] = enemy; + } + + for(uint8_t i = 0; i < PARTY_ACTIVE_SIZE_MAX; i++) { + battlefighter_t *member = partyGetOrderMember(i); + if(member == NULL) continue; + + fighters[BATTLE_FIGHTER_POS_4 + i] = member; + } + + battleStart(fighters); +} + +bool_t cutsceneBattleStartUpdate( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +) { + return true; +} + +errorret_t cutsceneBattleStartLoad( + yyjson_val *itemObj, + cutsceneitem_t *item +) { + yyjson_val *enemies = yyjson_obj_get(itemObj, "enemies"); + size_t enemyCount = yyjson_arr_size(enemies); + if( + !yyjson_is_arr(enemies) || enemyCount == 0 || + enemyCount > CUTSCENE_BATTLE_START_ENEMY_COUNT_MAX + ) { + errorThrow( + "Cutscene item (BATTLE_START): 'enemies' must have 1-%u entries", + (uint32_t)CUTSCENE_BATTLE_START_ENEMY_COUNT_MAX + ); + } + item->battleStart.enemyCount = (uint8_t)enemyCount; + + yyjson_arr_iter iter = yyjson_arr_iter_with(enemies); + yyjson_val *enemyVal; + size_t e = 0; + while((enemyVal = yyjson_arr_iter_next(&iter)) != NULL) { + cutscenebattlestartenemy_t *enemy = &item->battleStart.enemies[e]; + + yyjson_val *stats = yyjson_obj_get(enemyVal, "stats"); + if(!yyjson_is_obj(stats)) { + errorThrow( + "Cutscene item (BATTLE_START): enemies[%u] is missing 'stats'", + (uint32_t)e + ); + } + enemy->stats.attack = (uint16_t)yyjson_get_int(yyjson_obj_get(stats, "attack")); + enemy->stats.defense = (uint16_t)yyjson_get_int(yyjson_obj_get(stats, "defense")); + enemy->stats.magic = (uint16_t)yyjson_get_int(yyjson_obj_get(stats, "magic")); + enemy->stats.speed = (uint16_t)yyjson_get_int(yyjson_obj_get(stats, "speed")); + enemy->stats.luck = (uint16_t)yyjson_get_int(yyjson_obj_get(stats, "luck")); + enemy->stats.healthMax = (uint16_t)yyjson_get_int(yyjson_obj_get(stats, "healthMax")); + enemy->stats.mpMax = (uint16_t)yyjson_get_int(yyjson_obj_get(stats, "mpMax")); + + yyjson_val *abilities = yyjson_obj_get(enemyVal, "abilities"); + if(abilities != NULL) { + size_t abilityCount = yyjson_arr_size(abilities); + if( + !yyjson_is_arr(abilities) || + abilityCount > BATTLE_FIGHTER_ABILITY_COUNT_MAX + ) { + errorThrow( + "Cutscene item (BATTLE_START): enemies[%u].abilities must have " + "0-%u entries", + (uint32_t)e, (uint32_t)BATTLE_FIGHTER_ABILITY_COUNT_MAX + ); + } + + yyjson_arr_iter abilityIter = yyjson_arr_iter_with(abilities); + yyjson_val *abilityVal; + size_t a = 0; + while((abilityVal = yyjson_arr_iter_next(&abilityIter)) != NULL) { + const char_t *name = yyjson_get_str(abilityVal); + battlemoveid_t ability = name == NULL + ? BATTLE_MOVE_ID_NULL : battleMoveGetIdByName(name); + if(ability == BATTLE_MOVE_ID_NULL) { + errorThrow( + "Cutscene item (BATTLE_START): enemies[%u].abilities[%u] is " + "invalid", + (uint32_t)e, (uint32_t)a + ); + } + enemy->abilities[a] = ability; + a++; + } + enemy->abilityCount = (uint8_t)abilityCount; + } + + e++; + } + + errorOk(); +} diff --git a/src/dusk/rpg/cutscene/item/battle/cutscenebattlestart.h b/src/dusk/rpg/cutscene/item/battle/cutscenebattlestart.h new file mode 100644 index 00000000..e4e2926c --- /dev/null +++ b/src/dusk/rpg/cutscene/item/battle/cutscenebattlestart.h @@ -0,0 +1,62 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "rpg/battle/fighter/battlefighter.h" +#include "rpg/cutscene/item/cutsceneitembase.h" + +// Enemies always occupy the left-side positions (BATTLE_FIGHTER_POS_0-3). +#define CUTSCENE_BATTLE_START_ENEMY_COUNT_MAX 4 + +typedef struct { + battlefighterstats_t stats; + battlemoveid_t abilities[BATTLE_FIGHTER_ABILITY_COUNT_MAX]; + uint8_t abilityCount; +} cutscenebattlestartenemy_t; + +typedef struct { + cutscenebattlestartenemy_t enemies[CUTSCENE_BATTLE_START_ENEMY_COUNT_MAX]; + uint8_t enemyCount; +} cutscenebattlestart_t; + +/** + * Starts a BATTLE_START item: spawns the configured enemies, fills the + * remaining positions with the active party (via partyGetOrderMember), and + * calls battleStart. Always completes immediately - the battle scene takes + * over from here. + * + * @param item The cutscene item. + * @param data Runtime data storage. + */ +void cutsceneBattleStartStart( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +); + +/** + * Updates a BATTLE_START item (always completes immediately). + * + * @param item The cutscene item. + * @param data Runtime data storage. + * @returns true always. + */ +bool_t cutsceneBattleStartUpdate( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +); + +/** + * Parses a BATTLE_START cutscene item's JSON fields into item. + * + * @param itemObj The item's JSON object. + * @param item Destination item, already zeroed by the caller. + * @return Error code indicating success or failure of the parse. + */ +errorret_t cutsceneBattleStartLoad( + yyjson_val *itemObj, + cutsceneitem_t *item +); diff --git a/src/dusk/rpg/cutscene/item/battle/cutscenebattlewaitselection.c b/src/dusk/rpg/cutscene/item/battle/cutscenebattlewaitselection.c new file mode 100644 index 00000000..70f3582d --- /dev/null +++ b/src/dusk/rpg/cutscene/item/battle/cutscenebattlewaitselection.c @@ -0,0 +1,16 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "rpg/cutscene/item/cutsceneitem.h" +#include "rpg/battle/battle.h" + +bool_t cutsceneBattleWaitSelectionUpdate( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +) { + return BATTLE.state != BATTLE_STATE_SELECTION; +} diff --git a/src/dusk/rpg/cutscene/item/battle/cutscenebattlewaitselection.h b/src/dusk/rpg/cutscene/item/battle/cutscenebattlewaitselection.h new file mode 100644 index 00000000..ba66dc24 --- /dev/null +++ b/src/dusk/rpg/cutscene/item/battle/cutscenebattlewaitselection.h @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "rpg/cutscene/item/cutsceneitembase.h" + +/** + * Updates a BATTLE_WAIT_SELECTION step, completing once the battle is no + * longer in BATTLE_STATE_SELECTION -- i.e. every fighter has decided a + * move (moving on to BATTLE_STATE_EXECUTING) or the selecting fighter has + * fled (moving on to BATTLE_STATE_FLED). Has no Start callback -- there's + * nothing to do until selection actually finishes. + * + * @param item The cutscene item. + * @param data Runtime data storage. + * @returns true once BATTLE.state is no longer BATTLE_STATE_SELECTION. + */ +bool_t cutsceneBattleWaitSelectionUpdate( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +); diff --git a/src/dusk/rpg/cutscene/item/cutsceneitem.c b/src/dusk/rpg/cutscene/item/cutsceneitem.c index e1b31a1d..dd126730 100644 --- a/src/dusk/rpg/cutscene/item/cutsceneitem.c +++ b/src/dusk/rpg/cutscene/item/cutsceneitem.c @@ -244,6 +244,27 @@ cutsceneitemcallbacks_t CUTSCENE_ITEM_CALLBACKS[CUTSCENE_ITEM_TYPE_COUNT] = { .load = cutsceneMapAreaWaitLoad }, + // Battle + [CUTSCENE_ITEM_TYPE_BATTLE_START] = { + .init = cutsceneBattleStartStart, + .update = cutsceneBattleStartUpdate, + .jsonName = "BATTLE_START", + .load = cutsceneBattleStartLoad + }, + + [CUTSCENE_ITEM_TYPE_BATTLE_SET_STATE] = { + .init = cutsceneBattleSetStateStart, + .update = cutsceneBattleSetStateUpdate, + .jsonName = "BATTLE_SET_STATE", + .load = cutsceneBattleSetStateLoad + }, + + [CUTSCENE_ITEM_TYPE_BATTLE_WAIT_SELECTION] = { + .update = cutsceneBattleWaitSelectionUpdate, + .jsonName = "BATTLE_WAIT_SELECTION", + .load = cutsceneItemLoadNoop + }, + // Save [CUTSCENE_ITEM_TYPE_SAVE_DEVICE_CHECK] = { .init = cutsceneSaveDeviceCheckStart, diff --git a/src/dusk/rpg/cutscene/item/cutsceneitem.h b/src/dusk/rpg/cutscene/item/cutsceneitem.h index 3847fc36..e4e4745f 100644 --- a/src/dusk/rpg/cutscene/item/cutsceneitem.h +++ b/src/dusk/rpg/cutscene/item/cutsceneitem.h @@ -40,6 +40,9 @@ #include "maparea/cutscenemapareaadd.h" #include "maparea/cutscenemaparearemove.h" #include "maparea/cutscenemapareawait.h" +#include "battle/cutscenebattlestart.h" +#include "battle/cutscenebattlesetstate.h" +#include "battle/cutscenebattlewaitselection.h" #include "save/cutscenesavedevicecheck.h" #include "save/cutscenesaveloadallslots.h" #include "audio/cutsceneaudioplay.h" @@ -112,6 +115,8 @@ struct cutsceneitem_s { cutscenemapareaadd_t mapAreaAdd; cutscenemaparearemove_t mapAreaRemove; cutscenemapareawait_t mapAreaWait; + cutscenebattlestart_t battleStart; + cutscenebattlesetstate_t battleSetState; cutsceneemoji_t emoji; cutsceneshake_t shake; cutscenemodal_t modal; diff --git a/src/dusk/rpg/cutscene/item/cutsceneitembase.h b/src/dusk/rpg/cutscene/item/cutsceneitembase.h index e738e082..be187c26 100644 --- a/src/dusk/rpg/cutscene/item/cutsceneitembase.h +++ b/src/dusk/rpg/cutscene/item/cutsceneitembase.h @@ -59,6 +59,11 @@ typedef enum { CUTSCENE_ITEM_TYPE_MAP_AREA_REMOVE, CUTSCENE_ITEM_TYPE_MAP_AREA_WAIT, + // Battle + CUTSCENE_ITEM_TYPE_BATTLE_START, + CUTSCENE_ITEM_TYPE_BATTLE_SET_STATE, + CUTSCENE_ITEM_TYPE_BATTLE_WAIT_SELECTION, + // Save CUTSCENE_ITEM_TYPE_SAVE_DEVICE_CHECK, CUTSCENE_ITEM_TYPE_SAVE_LOAD_ALL_SLOTS, diff --git a/src/dusk/rpg/cutscene/item/json/CMakeLists.txt b/src/dusk/rpg/cutscene/item/json/CMakeLists.txt index 57b7e422..60a8f286 100644 --- a/src/dusk/rpg/cutscene/item/json/CMakeLists.txt +++ b/src/dusk/rpg/cutscene/item/json/CMakeLists.txt @@ -14,6 +14,7 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME} cutscenejsonemoji.c cutscenejsonentitytype.c cutscenejsonentitydir.c + cutscenejsonbattlestate.c cutscenejsonscenetype.c cutscenejsonuiscreentype.c cutscenejsonaudiochannel.c diff --git a/src/dusk/rpg/cutscene/item/json/cutscenejsonbattlestate.c b/src/dusk/rpg/cutscene/item/json/cutscenejsonbattlestate.c new file mode 100644 index 00000000..60993646 --- /dev/null +++ b/src/dusk/rpg/cutscene/item/json/cutscenejsonbattlestate.c @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "cutscenejsonbattlestate.h" +#include "cutscenejsonenum.h" + +static const cutscenejsonenumentry_t CUTSCENE_JSON_BATTLE_STATE_TABLE[] = { + { "OPENING", BATTLE_STATE_OPENING }, + { "PRE_ROUND", BATTLE_STATE_PRE_ROUND }, + { "SELECTION", BATTLE_STATE_SELECTION }, + { "EXECUTING", BATTLE_STATE_EXECUTING }, + { "POST_ROUND", BATTLE_STATE_POST_ROUND }, + { "LOST", BATTLE_STATE_LOST }, + { "WON", BATTLE_STATE_WON }, + { "FLED", BATTLE_STATE_FLED }, + { "ENDING", BATTLE_STATE_ENDING }, +}; +#define CUTSCENE_JSON_BATTLE_STATE_TABLE_COUNT \ + (sizeof(CUTSCENE_JSON_BATTLE_STATE_TABLE) / sizeof(cutscenejsonenumentry_t)) + +bool_t cutsceneJsonLookupBattleState( + const char_t *name, + battlestate_t *out +) { + int32_t value; + if(!cutsceneJsonLookupEnumInsensitive( + CUTSCENE_JSON_BATTLE_STATE_TABLE, + CUTSCENE_JSON_BATTLE_STATE_TABLE_COUNT, name, &value + )) { + return false; + } + *out = (battlestate_t)value; + return true; +} diff --git a/src/dusk/rpg/cutscene/item/json/cutscenejsonbattlestate.h b/src/dusk/rpg/cutscene/item/json/cutscenejsonbattlestate.h new file mode 100644 index 00000000..1cf302f2 --- /dev/null +++ b/src/dusk/rpg/cutscene/item/json/cutscenejsonbattlestate.h @@ -0,0 +1,21 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "rpg/battle/state/battlestate.h" + +/** + * Looks up name (case-insensitive) against every battlestate_t name. + * + * @param name Name to search for. + * @param out Set to the matching state on success. + * @return true if a matching entry was found, false otherwise. + */ +bool_t cutsceneJsonLookupBattleState( + const char_t *name, + battlestate_t *out +); diff --git a/src/dusk/scene/battle/scenebattle.c b/src/dusk/scene/battle/scenebattle.c index 7aa3c7c2..190db10a 100644 --- a/src/dusk/scene/battle/scenebattle.c +++ b/src/dusk/scene/battle/scenebattle.c @@ -6,6 +6,7 @@ */ #include "scenebattle.h" +#include "assert/assert.h" #include "display/display.h" #include "display/displaystate.h" #include "display/shader/shader.h" @@ -80,6 +81,10 @@ void sceneBattleWorldToScreen( vec3 renderPos, vec2 out ) { + assertNotNull(battle, "battle cannot be NULL"); + assertNotNull(renderPos, "renderPos cannot be NULL"); + assertNotNull(out, "out cannot be NULL"); + mat4 viewProj; glm_mat4_mul(battle->projection, battle->eye, viewProj); @@ -94,15 +99,20 @@ void sceneBattleWorldToScreen( } void sceneBattleGetFighterPosition(const uint8_t fighterIndex, vec3 out) { + assertTrue(fighterIndex < BATTLE_FIGHTER_POS_COUNT, "fighterIndex out of bounds"); const battlefighter_t *fighter = BATTLE.fighters[fighterIndex]; + assertNotNull(fighter, "Fighter cannot be NULL"); + assertNotNull(out, "out cannot be NULL"); // Ordinal (and count) of this fighter among its living-slot teammates, // in slot order, so each team lays out as an evenly spaced row. uint8_t ordinal = 0; uint8_t teamCount = 0; for(uint8_t i = 0; i < BATTLE_FIGHTER_POS_COUNT; i++) { - if(BATTLE.fighters[i]->status == BATTLE_FIGHTER_STATUS_NULL) continue; - if(BATTLE.fighters[i]->team != fighter->team) continue; + const battlefighter_t *candidate = BATTLE.fighters[i]; + if(candidate == NULL) continue; + if(candidate->status == BATTLE_FIGHTER_STATUS_NULL) continue; + if(candidate->team != fighter->team) continue; if(i < fighterIndex) ordinal++; teamCount++; } @@ -119,7 +129,9 @@ void sceneBattleGetFighterPosition(const uint8_t fighterIndex, vec3 out) { } errorret_t sceneBattleDrawFighter(const uint8_t fighterIndex) { + assertTrue(fighterIndex < BATTLE_FIGHTER_POS_COUNT, "fighterIndex out of bounds"); const battlefighter_t *fighter = BATTLE.fighters[fighterIndex]; + if(fighter == NULL) errorOk(); if(fighter->status == BATTLE_FIGHTER_STATUS_NULL) errorOk(); vec3 position; diff --git a/src/dusk/ui/screen/battle/uibattlehud.c b/src/dusk/ui/screen/battle/uibattlehud.c index 085b0f21..9126c1d8 100644 --- a/src/dusk/ui/screen/battle/uibattlehud.c +++ b/src/dusk/ui/screen/battle/uibattlehud.c @@ -42,12 +42,13 @@ errorret_t uiBattleHudInit(void) { } errorret_t uiBattleHudDraw(void) { - if(BATTLE.state == BATTLE_STATE_NONE) errorOk(); + if(BATTLE.state == BATTLE_STATE_NULL) errorOk(); scenebattle_t *battle = &SCENE.data.battle; for(uint8_t i = 0; i < BATTLE_FIGHTER_POS_COUNT; i++) { const battlefighter_t *fighter = BATTLE.fighters[i]; + if(fighter == NULL) continue; if(fighter->status == BATTLE_FIGHTER_STATUS_NULL) continue; vec3 position; diff --git a/src/dusk/ui/screen/battle/uibattlemenu.c b/src/dusk/ui/screen/battle/uibattlemenu.c index de209629..2b88343c 100644 --- a/src/dusk/ui/screen/battle/uibattlemenu.c +++ b/src/dusk/ui/screen/battle/uibattlemenu.c @@ -22,13 +22,14 @@ void uiBattleMenuTargetSelected( const uint8_t index, const uimenuitem_t *item ) { - battlePlayerAttack(UI_BATTLE_MENU.targetFighterIndex[index]); + assertTrue(index < UI_BATTLE_MENU_TARGET_ITEM_COUNT, "index out of bounds"); + battleStateSelectionPlayerAttack(UI_BATTLE_MENU.targetFighterIndex[index]); uiMenuClose(&UI_BATTLE_MENU.targetMenu); uiMenuClose(&UI_BATTLE_MENU.actionMenu); } void uiBattleMenuOpenTargets(void) { - battlefighter_t *current = battleGetCurrentFighter(); + battlefighter_t *current = battleStateSelectionGetCurrentFighter(); if(current == NULL) return; const battlefighterteam_t enemyTeam = @@ -42,6 +43,7 @@ void uiBattleMenuOpenTargets(void) { for(uint8_t i = 0; i < BATTLE_FIGHTER_POS_COUNT; i++) { battlefighter_t *candidate = BATTLE.fighters[i]; + if(candidate == NULL) continue; if(candidate->team != enemyTeam) continue; if(!battleFighterIsAlive(candidate)) continue; @@ -71,7 +73,7 @@ void uiBattleMenuActionSelected( break; case UI_BATTLE_MENU_ACTION_INDEX_FLEE: - battlePlayerFlee(); + battleStateSelectionPlayerFlee(); uiMenuClose(&UI_BATTLE_MENU.actionMenu); break; @@ -116,7 +118,7 @@ errorret_t uiBattleMenuInit(void) { errorret_t uiBattleMenuUpdate(void) { if(uiMenuIsActive(&UI_BATTLE_MENU.actionMenu)) errorOk(); - battlefighter_t *current = battleGetCurrentFighter(); + battlefighter_t *current = battleStateSelectionGetCurrentFighter(); if(current == NULL) errorOk(); if(current->controller != BATTLE_FIGHTER_CONTROLLER_PLAYER) errorOk();