Add shared battleFighterParse/Serialize, use it everywhere a fighter is (de)serialized
Replaces three independently hand-rolled battlefighter_t JSON readers/ writers (party.jsonc roster loading, BATTLE_START enemy definitions, and the v1 save-file party member format) with one generic pair in battlefighter.c: battleFighterParse(fighter, obj) and its inverse battleFighterSerialize(fighter, doc). Covers team/controller/status (optional, default ally/player/normal), the 7 base stats (required), current health/mp (optional, default to max), and an optional abilities name array. - party.c: partyInit now parses each roster member via battleFighterParse directly, dropping the old partyParseStats/partyParseStatField. - cutscenebattlestart.c/.h: cutscenebattlestartenemy_t now embeds a full battlefighter_t, parsed once at Load time; Start just copies it into the enemy pool instead of re-initializing. battletest.jsonc's enemies are now flat (dropped the "stats" wrapper) to match. - saveslotver1.c/.h: rewritten in place to use the shared parser/ serializer, changing the v1 on-disk field names/shape (no migration - acceptable pre-release, documented in saveslotver1.h). Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
@@ -4,17 +4,13 @@
|
|||||||
"type": "BATTLE_START",
|
"type": "BATTLE_START",
|
||||||
"enemies": [
|
"enemies": [
|
||||||
{
|
{
|
||||||
"stats": {
|
"attack": 8, "defense": 4, "magic": 0, "speed": 5, "luck": 2,
|
||||||
"attack": 8, "defense": 4, "magic": 0, "speed": 5, "luck": 2,
|
"healthMax": 30, "mpMax": 0,
|
||||||
"healthMax": 30, "mpMax": 0
|
|
||||||
},
|
|
||||||
"abilities": ["fire"]
|
"abilities": ["fire"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"stats": {
|
"attack": 6, "defense": 3, "magic": 0, "speed": 7, "luck": 2,
|
||||||
"attack": 6, "defense": 3, "magic": 0, "speed": 7, "luck": 2,
|
"healthMax": 20, "mpMax": 0
|
||||||
"healthMax": 20, "mpMax": 0
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,161 @@
|
|||||||
#include "assert/assert.h"
|
#include "assert/assert.h"
|
||||||
#include "util/memory.h"
|
#include "util/memory.h"
|
||||||
|
|
||||||
|
errorret_t battleFighterParseStatField(
|
||||||
|
yyjson_val *obj,
|
||||||
|
const char_t *key,
|
||||||
|
uint16_t *out
|
||||||
|
) {
|
||||||
|
yyjson_val *val = yyjson_obj_get(obj, key);
|
||||||
|
if(!yyjson_is_int(val)) {
|
||||||
|
errorThrow("Fighter: missing/invalid '%s'", key);
|
||||||
|
}
|
||||||
|
|
||||||
|
const int64_t num = yyjson_get_sint(val);
|
||||||
|
if(num < 0 || num > UINT16_MAX) {
|
||||||
|
errorThrow("Fighter: '%s' must be between 0 and %u", key, UINT16_MAX);
|
||||||
|
}
|
||||||
|
|
||||||
|
*out = (uint16_t)num;
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t battleFighterParseOptionalIntField(
|
||||||
|
yyjson_val *obj,
|
||||||
|
const char_t *key,
|
||||||
|
const int32_t defaultValue,
|
||||||
|
int32_t *out
|
||||||
|
) {
|
||||||
|
yyjson_val *val = yyjson_obj_get(obj, key);
|
||||||
|
if(val == NULL) {
|
||||||
|
*out = defaultValue;
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!yyjson_is_int(val)) errorThrow("Fighter: invalid '%s'", key);
|
||||||
|
*out = (int32_t)yyjson_get_sint(val);
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t battleFighterParse(battlefighter_t *fighter, yyjson_val *obj) {
|
||||||
|
assertNotNull(fighter, "Fighter cannot be NULL");
|
||||||
|
assertNotNull(obj, "JSON object cannot be NULL");
|
||||||
|
|
||||||
|
int32_t teamNum;
|
||||||
|
errorChain(battleFighterParseOptionalIntField(
|
||||||
|
obj, "team", BATTLE_FIGHTER_TEAM_ALLY, &teamNum
|
||||||
|
));
|
||||||
|
if(teamNum < 0 || teamNum >= BATTLE_FIGHTER_TEAM_COUNT) {
|
||||||
|
errorThrow("Fighter: invalid 'team'");
|
||||||
|
}
|
||||||
|
|
||||||
|
int32_t controllerNum;
|
||||||
|
errorChain(battleFighterParseOptionalIntField(
|
||||||
|
obj, "controller", BATTLE_FIGHTER_CONTROLLER_PLAYER, &controllerNum
|
||||||
|
));
|
||||||
|
if(controllerNum < 0 || controllerNum >= BATTLE_FIGHTER_CONTROLLER_COUNT) {
|
||||||
|
errorThrow("Fighter: invalid 'controller'");
|
||||||
|
}
|
||||||
|
|
||||||
|
battlefighterstats_t stats;
|
||||||
|
errorChain(battleFighterParseStatField(obj, "attack", &stats.attack));
|
||||||
|
errorChain(battleFighterParseStatField(obj, "defense", &stats.defense));
|
||||||
|
errorChain(battleFighterParseStatField(obj, "magic", &stats.magic));
|
||||||
|
errorChain(battleFighterParseStatField(obj, "speed", &stats.speed));
|
||||||
|
errorChain(battleFighterParseStatField(obj, "luck", &stats.luck));
|
||||||
|
errorChain(battleFighterParseStatField(obj, "healthMax", &stats.healthMax));
|
||||||
|
errorChain(battleFighterParseStatField(obj, "mpMax", &stats.mpMax));
|
||||||
|
|
||||||
|
battleFighterInit(
|
||||||
|
fighter, (battlefighterteam_t)teamNum,
|
||||||
|
(battlefightercontroller_t)controllerNum, stats
|
||||||
|
);
|
||||||
|
|
||||||
|
int32_t statusNum;
|
||||||
|
errorChain(battleFighterParseOptionalIntField(
|
||||||
|
obj, "status", BATTLE_FIGHTER_STATUS_NORMAL, &statusNum
|
||||||
|
));
|
||||||
|
if(statusNum < 0 || statusNum >= BATTLE_FIGHTER_STATUS_COUNT) {
|
||||||
|
errorThrow("Fighter: invalid 'status'");
|
||||||
|
}
|
||||||
|
fighter->status = (battlefighterstatus_t)statusNum;
|
||||||
|
|
||||||
|
int32_t health;
|
||||||
|
errorChain(
|
||||||
|
battleFighterParseOptionalIntField(obj, "health", stats.healthMax, &health)
|
||||||
|
);
|
||||||
|
if(health < 0 || health > stats.healthMax) {
|
||||||
|
errorThrow("Fighter: 'health' out of range");
|
||||||
|
}
|
||||||
|
fighter->stats.health = (uint16_t)health;
|
||||||
|
|
||||||
|
int32_t mp;
|
||||||
|
errorChain(battleFighterParseOptionalIntField(obj, "mp", stats.mpMax, &mp));
|
||||||
|
if(mp < 0 || mp > stats.mpMax) {
|
||||||
|
errorThrow("Fighter: 'mp' out of range");
|
||||||
|
}
|
||||||
|
fighter->stats.mp = (uint16_t)mp;
|
||||||
|
|
||||||
|
yyjson_val *abilities = yyjson_obj_get(obj, "abilities");
|
||||||
|
if(abilities != NULL) {
|
||||||
|
if(
|
||||||
|
!yyjson_is_arr(abilities) ||
|
||||||
|
yyjson_arr_size(abilities) > BATTLE_FIGHTER_ABILITY_COUNT_MAX
|
||||||
|
) {
|
||||||
|
errorThrow(
|
||||||
|
"Fighter: 'abilities' must have 0-%u entries",
|
||||||
|
BATTLE_FIGHTER_ABILITY_COUNT_MAX
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
yyjson_arr_iter iter = yyjson_arr_iter_with(abilities);
|
||||||
|
yyjson_val *abilityVal;
|
||||||
|
while((abilityVal = yyjson_arr_iter_next(&iter)) != 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("Fighter: invalid entry in 'abilities'");
|
||||||
|
}
|
||||||
|
battleFighterAbilityAdd(fighter, ability);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
yyjson_mut_val *battleFighterSerialize(
|
||||||
|
const battlefighter_t *fighter,
|
||||||
|
yyjson_mut_doc *doc
|
||||||
|
) {
|
||||||
|
assertNotNull(fighter, "Fighter cannot be NULL");
|
||||||
|
assertNotNull(doc, "Doc cannot be NULL");
|
||||||
|
|
||||||
|
yyjson_mut_val *obj = yyjson_mut_obj(doc);
|
||||||
|
|
||||||
|
yyjson_mut_obj_add_uint(doc, obj, "team", fighter->team);
|
||||||
|
yyjson_mut_obj_add_uint(doc, obj, "controller", fighter->controller);
|
||||||
|
yyjson_mut_obj_add_uint(doc, obj, "status", fighter->status);
|
||||||
|
|
||||||
|
yyjson_mut_obj_add_uint(doc, obj, "attack", fighter->stats.attack);
|
||||||
|
yyjson_mut_obj_add_uint(doc, obj, "defense", fighter->stats.defense);
|
||||||
|
yyjson_mut_obj_add_uint(doc, obj, "magic", fighter->stats.magic);
|
||||||
|
yyjson_mut_obj_add_uint(doc, obj, "speed", fighter->stats.speed);
|
||||||
|
yyjson_mut_obj_add_uint(doc, obj, "luck", fighter->stats.luck);
|
||||||
|
yyjson_mut_obj_add_uint(doc, obj, "healthMax", fighter->stats.healthMax);
|
||||||
|
yyjson_mut_obj_add_uint(doc, obj, "mpMax", fighter->stats.mpMax);
|
||||||
|
yyjson_mut_obj_add_uint(doc, obj, "health", fighter->stats.health);
|
||||||
|
yyjson_mut_obj_add_uint(doc, obj, "mp", fighter->stats.mp);
|
||||||
|
|
||||||
|
yyjson_mut_val *abilities = yyjson_mut_arr(doc);
|
||||||
|
for(uint8_t i = 0; i < fighter->abilityCount; i++) {
|
||||||
|
yyjson_mut_arr_add_str(doc, abilities, BATTLE_MOVES[fighter->abilities[i]].name);
|
||||||
|
}
|
||||||
|
yyjson_mut_obj_add_val(doc, obj, "abilities", abilities);
|
||||||
|
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
void battleFighterInit(
|
void battleFighterInit(
|
||||||
battlefighter_t *fighter,
|
battlefighter_t *fighter,
|
||||||
const battlefighterteam_t team,
|
const battlefighterteam_t team,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
#include "error/error.h"
|
#include "error/error.h"
|
||||||
|
#include "yyjson.h"
|
||||||
#include "rpg/battle/move/battlemove.h"
|
#include "rpg/battle/move/battlemove.h"
|
||||||
#include "rpg/battle/fighter/battlefighterstatus.h"
|
#include "rpg/battle/fighter/battlefighterstatus.h"
|
||||||
#include "rpg/battle/fighter/battlefighterteam.h"
|
#include "rpg/battle/fighter/battlefighterteam.h"
|
||||||
@@ -45,6 +46,42 @@ void battleFighterInit(
|
|||||||
const battlefighterstats_t stats
|
const battlefighterstats_t stats
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generic battlefighter_t parser, shared by every place that builds a
|
||||||
|
* fighter from JSON (party.jsonc roster members, BATTLE_START enemy
|
||||||
|
* definitions, save-file party members) - see battleFighterSerialize for
|
||||||
|
* the inverse. Reads 'team'/'controller'/'status' as ints (each optional,
|
||||||
|
* defaulting to ALLY/PLAYER/NORMAL respectively - a caller that needs a
|
||||||
|
* different team/controller, e.g. BATTLE_START's enemies, sets those
|
||||||
|
* fields on fighter after this returns), 'attack'/'defense'/'magic'/
|
||||||
|
* 'speed'/'luck'/'healthMax'/'mpMax' as required ints in [0, UINT16_MAX],
|
||||||
|
* 'health'/'mp' as optional ints defaulting to healthMax/mpMax (a fresh
|
||||||
|
* fighter), and an optional 'abilities' array of move name strings. Does
|
||||||
|
* not touch fighter->id - set it (or leave it) before calling, same as
|
||||||
|
* battleFighterInit.
|
||||||
|
*
|
||||||
|
* @param fighter Pointer to the fighter to fill in.
|
||||||
|
* @param obj The JSON object to read fields from.
|
||||||
|
* @return Error if a required field is missing/invalid, or an abilities
|
||||||
|
* entry doesn't name a real move.
|
||||||
|
*/
|
||||||
|
errorret_t battleFighterParse(battlefighter_t *fighter, yyjson_val *obj);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inverse of battleFighterParse: builds a new JSON object (not yet
|
||||||
|
* attached anywhere - the caller attaches it, e.g. via
|
||||||
|
* yyjson_mut_arr_add_val/yyjson_mut_obj_add_val) with every field
|
||||||
|
* battleFighterParse reads except id (see its doc comment for why).
|
||||||
|
*
|
||||||
|
* @param fighter The fighter to serialize.
|
||||||
|
* @param doc The mutable document to allocate the object in.
|
||||||
|
* @return The newly built object.
|
||||||
|
*/
|
||||||
|
yyjson_mut_val *battleFighterSerialize(
|
||||||
|
const battlefighter_t *fighter,
|
||||||
|
yyjson_mut_doc *doc
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if the fighter is in a state where it can still act (i.e.
|
* Returns true if the fighter is in a state where it can still act (i.e.
|
||||||
* is not dead).
|
* is not dead).
|
||||||
|
|||||||
@@ -13,43 +13,6 @@
|
|||||||
#include "asset/asset.h"
|
#include "asset/asset.h"
|
||||||
#include "asset/loader/assetloader.h"
|
#include "asset/loader/assetloader.h"
|
||||||
|
|
||||||
errorret_t partyParseStatField(
|
|
||||||
yyjson_val *obj,
|
|
||||||
const char_t *key,
|
|
||||||
const uint16_t min,
|
|
||||||
uint16_t *out
|
|
||||||
) {
|
|
||||||
yyjson_val *val = yyjson_obj_get(obj, key);
|
|
||||||
if(!yyjson_is_int(val)) {
|
|
||||||
errorThrow("Party stats: missing/invalid '%s'", key);
|
|
||||||
}
|
|
||||||
|
|
||||||
const int64_t num = yyjson_get_sint(val);
|
|
||||||
if(num < min || num > UINT16_MAX) {
|
|
||||||
errorThrow(
|
|
||||||
"Party stats: '%s' must be between %u and %u", key, min, UINT16_MAX
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
*out = (uint16_t)num;
|
|
||||||
errorOk();
|
|
||||||
}
|
|
||||||
|
|
||||||
errorret_t partyParseStats(yyjson_val *obj, battlefighterstats_t *out) {
|
|
||||||
assertNotNull(obj, "JSON object cannot be NULL");
|
|
||||||
assertNotNull(out, "Destination stats cannot be NULL");
|
|
||||||
|
|
||||||
errorChain(partyParseStatField(obj, "attack", 0, &out->attack));
|
|
||||||
errorChain(partyParseStatField(obj, "defense", 0, &out->defense));
|
|
||||||
errorChain(partyParseStatField(obj, "magic", 0, &out->magic));
|
|
||||||
errorChain(partyParseStatField(obj, "speed", 0, &out->speed));
|
|
||||||
errorChain(partyParseStatField(obj, "luck", 0, &out->luck));
|
|
||||||
errorChain(partyParseStatField(obj, "healthMax", 1, &out->healthMax));
|
|
||||||
errorChain(partyParseStatField(obj, "mpMax", 0, &out->mpMax));
|
|
||||||
|
|
||||||
errorOk();
|
|
||||||
}
|
|
||||||
|
|
||||||
errorret_t partyInit(party_t *party) {
|
errorret_t partyInit(party_t *party) {
|
||||||
assertNotNull(party, "Party cannot be NULL");
|
assertNotNull(party, "Party cannot be NULL");
|
||||||
|
|
||||||
@@ -96,21 +59,15 @@ errorret_t partyInit(party_t *party) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
battlefighterstats_t stats;
|
|
||||||
errorret_t statsResult = partyParseStats(memberObj, &stats);
|
|
||||||
if(errorIsNotOk(statsResult)) {
|
|
||||||
assetUnlockEntry(entry);
|
|
||||||
errorChain(statsResult);
|
|
||||||
}
|
|
||||||
|
|
||||||
stringCopy(party->keys[idx], yyjson_get_str(key), PARTY_KEY_LENGTH_MAX - 1);
|
stringCopy(party->keys[idx], yyjson_get_str(key), PARTY_KEY_LENGTH_MAX - 1);
|
||||||
|
|
||||||
battlefighter_t *member = &party->members[idx];
|
battlefighter_t *member = &party->members[idx];
|
||||||
member->id = idx;
|
member->id = idx;
|
||||||
battleFighterInit(
|
errorret_t parseResult = battleFighterParse(member, memberObj);
|
||||||
member, BATTLE_FIGHTER_TEAM_ALLY, BATTLE_FIGHTER_CONTROLLER_PLAYER,
|
if(errorIsNotOk(parseResult)) {
|
||||||
stats
|
assetUnlockEntry(entry);
|
||||||
);
|
errorChain(parseResult);
|
||||||
|
}
|
||||||
|
|
||||||
idx++;
|
idx++;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,12 +25,12 @@ typedef struct {
|
|||||||
* Initializes party with an empty order, then seeds every roster slot
|
* Initializes party with an empty order, then seeds every roster slot
|
||||||
* directly from party.jsonc's "members" object (each key/value pair fills
|
* directly from party.jsonc's "members" object (each key/value pair fills
|
||||||
* the next members[]/keys[] slot in the object's iteration order, key
|
* the next members[]/keys[] slot in the object's iteration order, key
|
||||||
* copied into keys[] and value parsed via partyParseStats/
|
* copied into keys[] and value parsed via battleFighterParse) - unlike
|
||||||
* battleFighterInit) - unlike partyAddMember, this never touches order
|
* partyAddMember, this never touches order itself except for one
|
||||||
* itself except for one placeholder default: if party.jsonc defines at
|
* placeholder default: if party.jsonc defines at least one member,
|
||||||
* least one member, order[0] is set to 0 (the first member starts active
|
* order[0] is set to 0 (the first member starts active in the first party
|
||||||
* in the first party position). Replace this default once there's a real
|
* position). Replace this default once there's a real starting-party/
|
||||||
* starting-party/character-creation flow.
|
* character-creation flow.
|
||||||
*
|
*
|
||||||
* @param party The party to initialize.
|
* @param party The party to initialize.
|
||||||
* @return Error if party.jsonc fails to load/parse, defines more members
|
* @return Error if party.jsonc fails to load/parse, defines more members
|
||||||
@@ -58,19 +58,6 @@ uint8_t partyGetMemberIndexByKey(const char_t *key);
|
|||||||
*/
|
*/
|
||||||
const char_t *partyGetMemberKey(const uint8_t memberIndex);
|
const char_t *partyGetMemberKey(const uint8_t memberIndex);
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses a set of base combat stats (attack/defense/magic/speed/luck/
|
|
||||||
* healthMax/mpMax) out of a JSON object. Every field is required - there
|
|
||||||
* is no implicit default - and must be an integer in [0, UINT16_MAX],
|
|
||||||
* except healthMax which must be at least 1 (a fighter with 0 max health
|
|
||||||
* makes no sense).
|
|
||||||
*
|
|
||||||
* @param obj The JSON object to read fields from.
|
|
||||||
* @param out Destination stats, filled in on success.
|
|
||||||
* @return Error if a field is missing, not an integer, or out of range.
|
|
||||||
*/
|
|
||||||
errorret_t partyParseStats(yyjson_val *obj, battlefighterstats_t *out);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks whether memberIndex currently occupies a slot in the current save
|
* Checks whether memberIndex currently occupies a slot in the current save
|
||||||
* slot's party order (i.e. is actively in the party right now).
|
* slot's party order (i.e. is actively in the party right now).
|
||||||
|
|||||||
@@ -34,17 +34,8 @@ void cutsceneBattleStartStart(
|
|||||||
sizeof(CUTSCENE_BATTLE_START_ENEMY_POOL)
|
sizeof(CUTSCENE_BATTLE_START_ENEMY_POOL)
|
||||||
);
|
);
|
||||||
for(uint8_t i = 0; i < config->enemyCount; i++) {
|
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];
|
battlefighter_t *enemy = &CUTSCENE_BATTLE_START_ENEMY_POOL[i];
|
||||||
|
*enemy = config->enemies[i].fighter;
|
||||||
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;
|
fighters[i] = enemy;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,54 +79,14 @@ errorret_t cutsceneBattleStartLoad(
|
|||||||
while((enemyVal = yyjson_arr_iter_next(&iter)) != NULL) {
|
while((enemyVal = yyjson_arr_iter_next(&iter)) != NULL) {
|
||||||
cutscenebattlestartenemy_t *enemy = &item->battleStart.enemies[e];
|
cutscenebattlestartenemy_t *enemy = &item->battleStart.enemies[e];
|
||||||
|
|
||||||
yyjson_val *stats = yyjson_obj_get(enemyVal, "stats");
|
errorret_t parseResult = battleFighterParse(&enemy->fighter, enemyVal);
|
||||||
if(!yyjson_is_obj(stats)) {
|
if(errorIsNotOk(parseResult)) errorChain(parseResult);
|
||||||
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");
|
// Enemy JSON never specifies team/controller (battleFighterParse just
|
||||||
if(abilities != NULL) {
|
// defaulted them to ALLY/PLAYER) - BATTLE_START enemies are always
|
||||||
size_t abilityCount = yyjson_arr_size(abilities);
|
// ENEMY/AI.
|
||||||
if(
|
enemy->fighter.team = BATTLE_FIGHTER_TEAM_ENEMY;
|
||||||
!yyjson_is_arr(abilities) ||
|
enemy->fighter.controller = BATTLE_FIGHTER_CONTROLLER_AI;
|
||||||
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++;
|
e++;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,9 +13,11 @@
|
|||||||
#define CUTSCENE_BATTLE_START_ENEMY_COUNT_MAX 4
|
#define CUTSCENE_BATTLE_START_ENEMY_COUNT_MAX 4
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
battlefighterstats_t stats;
|
// Fully parsed at Load time via battleFighterParse (team/controller
|
||||||
battlemoveid_t abilities[BATTLE_FIGHTER_ABILITY_COUNT_MAX];
|
// forced to ENEMY/AI right after, since enemy JSON only ever supplies
|
||||||
uint8_t abilityCount;
|
// stats/abilities) - Start splices this directly into
|
||||||
|
// CUTSCENE_BATTLE_START_ENEMY_POOL, no further init needed.
|
||||||
|
battlefighter_t fighter;
|
||||||
} cutscenebattlestartenemy_t;
|
} cutscenebattlestartenemy_t;
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
|
|||||||
@@ -26,21 +26,9 @@ errorret_t saveSlotVer1WriteJSON(
|
|||||||
|
|
||||||
yyjson_mut_val *partyMembersArr = yyjson_mut_arr(doc);
|
yyjson_mut_val *partyMembersArr = yyjson_mut_arr(doc);
|
||||||
for(uint8_t i = 0; i < PARTY_MEMBER_COUNT_MAX; i++) {
|
for(uint8_t i = 0; i < PARTY_MEMBER_COUNT_MAX; i++) {
|
||||||
battlefighter_t *member = &slot->partyMembers[i];
|
yyjson_mut_val *memberObj =
|
||||||
writeArrayObject(partyMembersArr);
|
battleFighterSerialize(&slot->partyMembers[i], doc);
|
||||||
writeUInt32("id", member->id);
|
yyjson_mut_arr_add_val(partyMembersArr, memberObj);
|
||||||
writeUInt32("status", member->status);
|
|
||||||
writeUInt32("team", member->team);
|
|
||||||
writeUInt32("controller", member->controller);
|
|
||||||
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);
|
|
||||||
writeUInt32("statSpeed", member->stats.speed);
|
|
||||||
writeUInt32("statLuck", member->stats.luck);
|
|
||||||
}
|
}
|
||||||
writeValue("partyMembers", partyMembersArr);
|
writeValue("partyMembers", partyMembersArr);
|
||||||
|
|
||||||
@@ -84,24 +72,8 @@ errorret_t saveSlotVer1ReadJSON(saveslotver1_t *slot, yyjson_val *object) {
|
|||||||
partyMembersArr, memberIdx, memberLen, memberObj
|
partyMembersArr, memberIdx, memberLen, memberObj
|
||||||
) {
|
) {
|
||||||
battlefighter_t *member = &slot->partyMembers[memberIdx];
|
battlefighter_t *member = &slot->partyMembers[memberIdx];
|
||||||
// Shadows the outer `object` so readUInt32() below (which hardcodes
|
member->id = (uint8_t)memberIdx;
|
||||||
// that name) reads from this member's object instead.
|
errorChain(battleFighterParse(member, memberObj));
|
||||||
yyjson_val *object = memberObj;
|
|
||||||
readUInt32("id", member->id, 0);
|
|
||||||
readUInt32("status", member->status, BATTLE_FIGHTER_STATUS_NULL);
|
|
||||||
readUInt32("team", member->team, BATTLE_FIGHTER_TEAM_ALLY);
|
|
||||||
readUInt32(
|
|
||||||
"controller", member->controller, BATTLE_FIGHTER_CONTROLLER_PLAYER
|
|
||||||
);
|
|
||||||
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);
|
|
||||||
readUInt32("statSpeed", member->stats.speed, 0);
|
|
||||||
readUInt32("statLuck", member->stats.luck, 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
readUInt8Array(
|
readUInt8Array(
|
||||||
|
|||||||
@@ -24,6 +24,16 @@
|
|||||||
// keyed by flag name (not position), so its on-disk shape is decoupled from
|
// keyed by flag name (not position), so its on-disk shape is decoupled from
|
||||||
// STORY_FLAG_COUNT_MAX entirely; a save simply loses whatever flags no
|
// STORY_FLAG_COUNT_MAX entirely; a save simply loses whatever flags no
|
||||||
// longer resolve by name (see storyGetFlagByName) if that cap ever shrinks.
|
// longer resolve by name (see storyGetFlagByName) if that cap ever shrinks.
|
||||||
|
//
|
||||||
|
// partyMembers is a THIRD exception, deliberately broken pre-release: its
|
||||||
|
// per-member shape now comes from battleFighterSerialize/battleFighterParse
|
||||||
|
// (rpg/battle/fighter/battlefighter.h) - the shared fighter JSON format
|
||||||
|
// used everywhere else (party.jsonc, BATTLE_START enemies) - instead of its
|
||||||
|
// own hand-rolled field set. This is a real on-disk break (different key
|
||||||
|
// names, no more "statAttack" etc.) with no migration - an old v1 save
|
||||||
|
// simply reads back as corrupt. Acceptable only because nothing has
|
||||||
|
// shipped yet; don't repeat this once real saves exist without a version
|
||||||
|
// bump instead.
|
||||||
#define SAVE_SLOT_VER1_NAME_LENGTH 8
|
#define SAVE_SLOT_VER1_NAME_LENGTH 8
|
||||||
#define SAVE_SLOT_VER1_MAP_NAME_MAX 10
|
#define SAVE_SLOT_VER1_MAP_NAME_MAX 10
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user