derive serde on all persisted entities; add Settings.version and Dashboard::blank

Store and serde derives coexist on all 23 dashboard types (prototype
confirmed). Settings carries the saved-format version (1) as the
migration hook anchor; Dashboard::blank() is the post-Clear empty
campaign. mock() now builds Settings via Default.
This commit is contained in:
yaroslav k 2026-08-15 15:14:43 +03:00
parent a11e86e839
commit f3b0ceb3e7
2 changed files with 83 additions and 31 deletions

View File

@ -45,9 +45,7 @@ impl Dashboard {
quest_book: generate_mock_questbook(), quest_book: generate_mock_questbook(),
notes: generate_mock_notebook(), notes: generate_mock_notebook(),
people: generate_mock_contact_book(), people: generate_mock_contact_book(),
settings: Settings { settings: Settings::default(),
}
} }
} }
} }

View File

@ -9,12 +9,14 @@
//! //!
//! The model is deliberately "wide": it covers the campaign header, the //! The model is deliberately "wide": it covers the campaign header, the
//! player character, the party's common stash, quests, notes, contacts and //! player character, the party's common stash, quests, notes, contacts and
//! settings. All of it currently comes from hardcoded mock data (the `mock` //! settings. `mock` provides demo data, `Dashboard::blank` an empty slate;
//! module); nothing is persisted yet (see ROADMAP phase 1). //! persistence (localStorage autosave, JSON export/import) lives in
//! `crate::state::persist`.
use std::fmt::Display; use std::fmt::Display;
use chrono::{ NaiveDate, NaiveDateTime }; use chrono::{ NaiveDate, NaiveDateTime };
use reactive_stores::Store; use reactive_stores::Store;
use serde::{ Deserialize, Serialize };
use crate::prelude::*; use crate::prelude::*;
@ -66,7 +68,7 @@ pub struct ItemReferenceMap (pub HashMap<String, BookItem>);
/// party's common inventory, quests, notes, contacts and settings. The /// party's common inventory, quests, notes, contacts and settings. The
/// whole tree derives `Store`, so every nested field is independently /// whole tree derives `Store`, so every nested field is independently
/// reactive. `Dashboard::mock()` provides the initial state. /// reactive. `Dashboard::mock()` provides the initial state.
#[derive(Clone, Debug, Store)] #[derive(Clone, Debug, Store, Serialize, Deserialize)]
pub struct Dashboard { pub struct Dashboard {
pub campaign: String, pub campaign: String,
pub campaign_image: String, pub campaign_image: String,
@ -80,10 +82,49 @@ pub struct Dashboard {
pub settings: Settings pub settings: Settings
} }
impl Dashboard {
/// An empty dashboard: the blank slate after Clear.
///
/// No campaign name, no players, empty books, zeroed numbers.
pub fn blank () -> Self {
Self {
campaign: String::new(),
campaign_image: String::new(),
session: 0,
date: chrono::Local::now().date_naive(),
player: PlayerData {
name: Name::default(),
class: String::new(),
image: String::new(),
level: 0,
xp: 0,
temp_hp: 0,
hp: 0,
max_hp: 0,
balance: Balance::default(),
spell_slots: SpellSlots (core::array::from_fn(|_| SpellSlotLevel { used: 0, total: 0 })),
inventory: Inventory::default(),
attunements: Attunements (core::array::from_fn(|_| None)),
prepared: PreparedSpells (Vec::new()),
hit_dice: HitDice { used: 0, kind: 8 },
death_save_throws: DeathSaveThrows { failed: 0, succeeded: 0 },
},
common: CommonData {
balance: Balance::default(),
inventory: Inventory::default(),
},
quest_book: QuestBook (Vec::new()),
notes: NoteBook (Vec::new()),
people: ContactBook (HashMap::new()),
settings: Settings::default(),
}
}
}
/// The player character: identity, class, level, XP, hit points, money, /// The player character: identity, class, level, XP, hit points, money,
/// spell slots, inventory, attunements, prepared spells, hit dice and /// spell slots, inventory, attunements, prepared spells, hit dice and
/// death saves. This is the heart of the character sheet. /// death saves. This is the heart of the character sheet.
#[derive(Clone, Debug, Store)] #[derive(Clone, Debug, Store, Serialize, Deserialize)]
pub struct PlayerData { pub struct PlayerData {
pub name: Name, pub name: Name,
pub class: String, pub class: String,
@ -105,7 +146,7 @@ pub struct PlayerData {
/// The party's shared stash: money and items usable by everyone. /// The party's shared stash: money and items usable by everyone.
#[derive(Clone, Debug, Store)] #[derive(Clone, Debug, Store, Serialize, Deserialize)]
pub struct CommonData { pub struct CommonData {
pub balance: Balance, pub balance: Balance,
pub inventory: Inventory, pub inventory: Inventory,
@ -113,7 +154,7 @@ pub struct CommonData {
/// Currency counts per denomination, platinum through copper. /// Currency counts per denomination, platinum through copper.
/// Edited with `Numput` on the inventories page. /// Edited with `Numput` on the inventories page.
#[derive(Clone, Debug, Store)] #[derive(Clone, Debug, Store, Serialize, Deserialize, Default)]
pub struct Balance { pub struct Balance {
pub platinum: u32, pub platinum: u32,
pub gold: u32, pub gold: u32,
@ -126,14 +167,14 @@ pub struct Balance {
/// ///
/// `#[store(key)]` gives every entry a stable reactive key (its `id`), /// `#[store(key)]` gives every entry a stable reactive key (its `id`),
/// which keeps `For`/`ForEnumerate` updates cheap. /// which keeps `For`/`ForEnumerate` updates cheap.
#[derive(Clone, Debug, Store)] #[derive(Clone, Debug, Store, Serialize, Deserialize, Default)]
pub struct Inventory { pub struct Inventory {
#[store(key: u64 = |row| row.id)] #[store(key: u64 = |row| row.id)]
pub rows: Vec<InventoryEntry> pub rows: Vec<InventoryEntry>
} }
/// One inventory row: a quantity of a single item. /// One inventory row: a quantity of a single item.
#[derive(Clone, Debug, Store, Default)] #[derive(Clone, Debug, Store, Serialize, Deserialize, Default)]
pub struct InventoryEntry { pub struct InventoryEntry {
pub id: u64, pub id: u64,
pub quantity: u32, pub quantity: u32,
@ -155,7 +196,7 @@ impl InventoryEntry {
/// An item inside an inventory: name, weight (basis points), optional /// An item inside an inventory: name, weight (basis points), optional
/// description, and free-form tags (e.g. `type`, `rarity`). /// description, and free-form tags (e.g. `type`, `rarity`).
#[derive(Clone, Debug, Store, Default)] #[derive(Clone, Debug, Store, Serialize, Deserialize, Default)]
pub struct InventoryItem { pub struct InventoryItem {
pub name: String, pub name: String,
pub weight: u32, pub weight: u32,
@ -164,12 +205,12 @@ pub struct InventoryItem {
} }
/// The party's quest list. No UI yet — see ROADMAP phase 4. /// The party's quest list. No UI yet — see ROADMAP phase 4.
#[derive(Clone, Debug, Store)] #[derive(Clone, Debug, Store, Serialize, Deserialize)]
pub struct QuestBook (pub Vec<Quest>); pub struct QuestBook (pub Vec<Quest>);
/// One quest: title, locations, dates, description, giver and reward. /// One quest: title, locations, dates, description, giver and reward.
/// Note: there is no completion state yet (ROADMAP phase 4 adds one). /// Note: there is no completion state yet (ROADMAP phase 4 adds one).
#[derive(Clone, Debug, Store)] #[derive(Clone, Debug, Store, Serialize, Deserialize)]
pub struct Quest { pub struct Quest {
pub title: String, pub title: String,
pub location_taken: Option<String>, pub location_taken: Option<String>,
@ -182,12 +223,12 @@ pub struct Quest {
} }
/// The party's session notes. No UI yet — see ROADMAP phase 5. /// The party's session notes. No UI yet — see ROADMAP phase 5.
#[derive(Clone, Debug, Store)] #[derive(Clone, Debug, Store, Serialize, Deserialize)]
pub struct NoteBook (pub Vec<Note>); pub struct NoteBook (pub Vec<Note>);
/// One note: content, in-game date, real-world timestamp, session number /// One note: content, in-game date, real-world timestamp, session number
/// and tags. /// and tags.
#[derive(Clone, Debug, Store)] #[derive(Clone, Debug, Store, Serialize, Deserialize)]
pub struct Note { pub struct Note {
pub content: String, pub content: String,
pub date_ingame: NaiveDate, pub date_ingame: NaiveDate,
@ -197,28 +238,28 @@ pub struct Note {
} }
/// Spell slots for the nine spell levels (index 0 = 1st level). /// Spell slots for the nine spell levels (index 0 = 1st level).
#[derive(Clone, Debug, Store)] #[derive(Clone, Debug, Store, Serialize, Deserialize)]
pub struct SpellSlots (pub [SpellSlotLevel; 9]); pub struct SpellSlots (pub [SpellSlotLevel; 9]);
/// Used/total slots for one spell level. /// Used/total slots for one spell level.
#[derive(Clone, Debug, Store)] #[derive(Clone, Debug, Store, Serialize, Deserialize)]
pub struct SpellSlotLevel { pub struct SpellSlotLevel {
pub used: u8, pub used: u8,
pub total: u8, pub total: u8,
} }
/// Three attunement slots (D&D rules allow three attuned items). /// Three attunement slots (D&D rules allow three attuned items).
#[derive(Clone, Debug, Store)] #[derive(Clone, Debug, Store, Serialize, Deserialize)]
pub struct Attunements (pub [Option<String>; 3]); pub struct Attunements (pub [Option<String>; 3]);
/// The spell list the character has prepared (or is learning). /// The spell list the character has prepared (or is learning).
#[derive(Clone, Debug, Store)] #[derive(Clone, Debug, Store, Serialize, Deserialize)]
pub struct PreparedSpells (pub Vec<PreparedSpell>); pub struct PreparedSpells (pub Vec<PreparedSpell>);
/// One prepared-spell slot: vacant or holding a spell. /// One prepared-spell slot: vacant or holding a spell.
#[derive(Clone, Debug, Store)] #[derive(Clone, Debug, Store, Serialize, Deserialize)]
pub enum PreparedSpell { pub enum PreparedSpell {
Vacant, Vacant,
Occupied (Spell) Occupied (Spell)
@ -227,13 +268,13 @@ pub enum PreparedSpell {
/// A spell. Only the name exists so far; the rest is a `@TODO` /// A spell. Only the name exists so far; the rest is a `@TODO`
/// (see ROADMAP phase 2). /// (see ROADMAP phase 2).
// @TODO // @TODO
#[derive(Clone, Debug, Store)] #[derive(Clone, Debug, Store, Serialize, Deserialize)]
pub struct Spell { pub struct Spell {
pub name: String pub name: String
} }
/// Hit dice: how many of the current die kind are already spent. /// Hit dice: how many of the current die kind are already spent.
#[derive(Clone, Debug, Store)] #[derive(Clone, Debug, Store, Serialize, Deserialize)]
pub struct HitDice { pub struct HitDice {
pub used: u8, pub used: u8,
pub kind: u8 pub kind: u8
@ -242,7 +283,7 @@ pub struct HitDice {
/// Death-saving-throw counters: three failures kill, three successes /// Death-saving-throw counters: three failures kill, three successes
/// stabilize. /// stabilize.
#[derive(Clone, Debug, Store)] #[derive(Clone, Debug, Store, Serialize, Deserialize)]
pub struct DeathSaveThrows { pub struct DeathSaveThrows {
pub failed: u8, pub failed: u8,
pub succeeded: u8, pub succeeded: u8,
@ -257,11 +298,11 @@ impl DeathSaveThrows {
} }
/// Known NPCs keyed by name. No UI yet — see ROADMAP phase 6. /// Known NPCs keyed by name. No UI yet — see ROADMAP phase 6.
#[derive(Clone, Debug, Store)] #[derive(Clone, Debug, Store, Serialize, Deserialize)]
pub struct ContactBook (pub HashMap<String, Contact>); pub struct ContactBook (pub HashMap<String, Contact>);
/// One NPC contact: name, optional image, status, location and notes. /// One NPC contact: name, optional image, status, location and notes.
#[derive(Clone, Debug, Store)] #[derive(Clone, Debug, Store, Serialize, Deserialize)]
pub struct Contact { pub struct Contact {
pub name: Name, pub name: Name,
pub image: Option<String>, pub image: Option<String>,
@ -271,7 +312,7 @@ pub struct Contact {
} }
/// Life status of a contact, used for filtering. /// Life status of a contact, used for filtering.
#[derive(Clone, Debug, Store)] #[derive(Clone, Debug, Store, Serialize, Deserialize)]
pub enum ContactStatus { pub enum ContactStatus {
Unknown, Unknown,
DeadBygone, DeadBygone,
@ -280,7 +321,7 @@ pub enum ContactStatus {
} }
/// A person's name: first, last, and an optional alias (nickname). /// A person's name: first, last, and an optional alias (nickname).
#[derive(Clone, Debug, Store, Default)] #[derive(Clone, Debug, Store, Serialize, Deserialize, Default)]
pub struct Name { pub struct Name {
pub first: String, pub first: String,
pub last: String, pub last: String,
@ -298,15 +339,28 @@ impl Name {
} }
} }
/// App settings. Empty today; will carry e.g. a persisted-format version. /// App settings: currently only the saved-format version.
#[derive(Clone, Debug, Store, Default)] #[derive(Clone, Debug, Store, Serialize, Deserialize)]
pub struct Settings { pub struct Settings {
/// Saved-format version; bumped on breaking JSON changes.
pub version: u32
}
impl Settings {
/// The current saved-format version.
pub const VERSION: u32 = 1;
}
impl Default for Settings {
/// Defaults carry the current format version.
fn default () -> Self {
Self { version: Self::VERSION }
}
} }
/// Which side of the party an inventory belongs to. /// Which side of the party an inventory belongs to.
#[derive(Clone, Copy, Debug, Store)] #[derive(Clone, Copy, Debug, Store, Serialize, Deserialize)]
pub enum InventoryKind { pub enum InventoryKind {
Personal, Personal,
Common Common