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 <[email protected]>
This commit is contained in:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user