Chapter
Save model impact (`Velan.Save`)
(Idea) A short spec for a later pure-C# ticket. Velan.Save stores what the player holds; it doesn't know the catalogue, so it checks shapes, not rules. Caps, prices, slot kinds and standing belong to the item rules (VEL-78 decides where they live).
What to add
Bump SaveSchema.CurrentVersion to 2. New fields on SaveGame, all required, in this JSON shape (camelCase, like the rest of the save):
"purse": 20,
"inventory": [
{ "itemId": "homespun_tunic", "count": 1 },
{ "itemId": "warden_band", "count": 2 },
{ "itemId": "mend_tonic", "count": 3 }
],
"equipment": { "leftBand": "warden_band", "rightBand": "warden_band", "charm": "", "coat": "homespun_tunic" },
"belt": ["mend_tonic", "", ""],
"prepared": { "meal": "eel_pie", "ward": "" }
public int Purse;
public List<ItemStack> Inventory = new List<ItemStack>();
public Equipment Equipment = new Equipment();
public List<string> Belt = new List<string> { "", "", "" };
public PreparedItems Prepared = new PreparedItems();
public sealed class ItemStack { public string ItemId = ""; public int Count; }
public sealed class Equipment { public string LeftBand = ""; public string RightBand = ""; public string Charm = ""; public string Coat = ""; }
public sealed class PreparedItems { public string Meal = ""; public string Ward = ""; }
""means an empty slot, asSaveLocationdoes for its ids.- Inventory order is the player's order (the satchel is sorted in the UI however the player chose), so it is written in list order, not sorted. Everything else stays deterministic, as the serializer already promises.
- Key items add nothing to the save. Their state is story flags, which
Flagsalready holds:letter.twinholdandletter.lanternfall(1 carried, 2 delivered; the send-off already sets 1),letter.twinhold.openedandletter.lanternfall.opened(1 once the seal is broken),item.<itemId>for every other key item (1 held, 0 gone, e.g.item.fragment_coil,item.truce_token), andunique.<itemId>for one-off gear already given. Learned spells stay inKnownSpellIds. - Not saved: the buy-back list (cleared on leaving town) and Market Board listings (server-side, later).
Rules for the serializer
Reject, with SaveFormatException naming the field, as the existing readers do:
pursenot a whole number, or below 0.inventorynot an array; an entry without a non-empty stringitemId;countnot a whole number of at least 1; the sameitemIdtwice (one stack per item).equipmentmissing any of its four keys, or a value that isn't a string.beltnot an array of exactly three strings.preparedmissingmealorward, or a value that isn't a string.
Don't check that equipped, belt or prepared ids are in the inventory, or that counts are under the caps: the catalogue can change between builds, and the item rules fix that up on load (an equipped item no longer held is unequipped; an unknown id is dropped and logged).
New game and migration
SaveGame.NewGame:Purse0, inventory[{ homespun_tunic, 1 }],Coat=homespun_tunic, everything else empty. The send-off's gifts arrive through dialogue, not the new-game defaults.- Migration from version 1 adds exactly those defaults to the JSON.
Tests
- Round trip of a save with every new field filled, and with every field empty.
- Loading a version 1 save gives the new-game defaults above.
- One rejection test per rule in "Rules for the serializer".
- The same save always writes the same text (inventory order kept).
For the item rules and dialogue (not Velan.Save)
Listed so VEL-78 and the dialogue tickets have them in one place: item data files in Assets/StreamingAssets/Data/Items/<id>.json with an index.json (id, name, category, slot, tier, faction, price, stats, pair bonus, flavour, icon), shop files in Data/Shops/<id>.json (stock with optional price overrides, flag conditions and commission materials); add and remove with the stack caps and overflow sale; buy and sell with the standing modifier; equip checks for slot kind and stack count; stat totals with the caps from section 2; and dialogue commands <<give_item id count>>, <<take_item id count>>, <<give_copper n>>, <<open_shop id>> with functions item_count("id") and copper().