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(),
notes: generate_mock_notebook(),
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
//! player character, the party's common stash, quests, notes, contacts and
//! settings. All of it currently comes from hardcoded mock data (the `mock`
//! module); nothing is persisted yet (see ROADMAP phase 1).
//! settings. `mock` provides demo data, `Dashboard::blank` an empty slate;
//! persistence (localStorage autosave, JSON export/import) lives in
//! `crate::state::persist`.
use std::fmt::Display;
use chrono::{ NaiveDate, NaiveDateTime };
use reactive_stores::Store;
use serde::{ Deserialize, Serialize };
use crate::prelude::*;
@ -66,7 +68,7 @@ pub struct ItemReferenceMap (pub HashMap<String, BookItem>);
/// party's common inventory, quests, notes, contacts and settings. The
/// whole tree derives `Store`, so every nested field is independently
/// reactive. `Dashboard::mock()` provides the initial state.
#[derive(Clone, Debug, Store)]
#[derive(Clone, Debug, Store, Serialize, Deserialize)]
pub struct Dashboard {
pub campaign: String,
pub campaign_image: String,
@ -80,10 +82,49 @@ pub struct Dashboard {
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,
/// spell slots, inventory, attunements, prepared spells, hit dice and
/// death saves. This is the heart of the character sheet.
#[derive(Clone, Debug, Store)]
#[derive(Clone, Debug, Store, Serialize, Deserialize)]
pub struct PlayerData {
pub name: Name,
pub class: String,
@ -105,7 +146,7 @@ pub struct PlayerData {
/// 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 balance: Balance,
pub inventory: Inventory,
@ -113,7 +154,7 @@ pub struct CommonData {
/// Currency counts per denomination, platinum through copper.
/// Edited with `Numput` on the inventories page.
#[derive(Clone, Debug, Store)]
#[derive(Clone, Debug, Store, Serialize, Deserialize, Default)]
pub struct Balance {
pub platinum: u32,
pub gold: u32,
@ -126,14 +167,14 @@ pub struct Balance {
///
/// `#[store(key)]` gives every entry a stable reactive key (its `id`),
/// which keeps `For`/`ForEnumerate` updates cheap.
#[derive(Clone, Debug, Store)]
#[derive(Clone, Debug, Store, Serialize, Deserialize, Default)]
pub struct Inventory {
#[store(key: u64 = |row| row.id)]
pub rows: Vec<InventoryEntry>
}
/// 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 id: u64,
pub quantity: u32,
@ -155,7 +196,7 @@ impl InventoryEntry {
/// An item inside an inventory: name, weight (basis points), optional
/// 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 name: String,
pub weight: u32,
@ -164,12 +205,12 @@ pub struct InventoryItem {
}
/// 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>);
/// One quest: title, locations, dates, description, giver and reward.
/// 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 title: 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.
#[derive(Clone, Debug, Store)]
#[derive(Clone, Debug, Store, Serialize, Deserialize)]
pub struct NoteBook (pub Vec<Note>);
/// One note: content, in-game date, real-world timestamp, session number
/// and tags.
#[derive(Clone, Debug, Store)]
#[derive(Clone, Debug, Store, Serialize, Deserialize)]
pub struct Note {
pub content: String,
pub date_ingame: NaiveDate,
@ -197,28 +238,28 @@ pub struct Note {
}
/// 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]);
/// Used/total slots for one spell level.
#[derive(Clone, Debug, Store)]
#[derive(Clone, Debug, Store, Serialize, Deserialize)]
pub struct SpellSlotLevel {
pub used: u8,
pub total: u8,
}
/// 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]);
/// 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>);
/// One prepared-spell slot: vacant or holding a spell.
#[derive(Clone, Debug, Store)]
#[derive(Clone, Debug, Store, Serialize, Deserialize)]
pub enum PreparedSpell {
Vacant,
Occupied (Spell)
@ -227,13 +268,13 @@ pub enum PreparedSpell {
/// A spell. Only the name exists so far; the rest is a `@TODO`
/// (see ROADMAP phase 2).
// @TODO
#[derive(Clone, Debug, Store)]
#[derive(Clone, Debug, Store, Serialize, Deserialize)]
pub struct Spell {
pub name: String
}
/// 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 used: u8,
pub kind: u8
@ -242,7 +283,7 @@ pub struct HitDice {
/// Death-saving-throw counters: three failures kill, three successes
/// stabilize.
#[derive(Clone, Debug, Store)]
#[derive(Clone, Debug, Store, Serialize, Deserialize)]
pub struct DeathSaveThrows {
pub failed: u8,
pub succeeded: u8,
@ -257,11 +298,11 @@ impl DeathSaveThrows {
}
/// 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>);
/// One NPC contact: name, optional image, status, location and notes.
#[derive(Clone, Debug, Store)]
#[derive(Clone, Debug, Store, Serialize, Deserialize)]
pub struct Contact {
pub name: Name,
pub image: Option<String>,
@ -271,7 +312,7 @@ pub struct Contact {
}
/// Life status of a contact, used for filtering.
#[derive(Clone, Debug, Store)]
#[derive(Clone, Debug, Store, Serialize, Deserialize)]
pub enum ContactStatus {
Unknown,
DeadBygone,
@ -280,7 +321,7 @@ pub enum ContactStatus {
}
/// 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 first: String,
pub last: String,
@ -298,15 +339,28 @@ impl Name {
}
}
/// App settings. Empty today; will carry e.g. a persisted-format version.
#[derive(Clone, Debug, Store, Default)]
/// App settings: currently only the saved-format version.
#[derive(Clone, Debug, Store, Serialize, Deserialize)]
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.
#[derive(Clone, Copy, Debug, Store)]
#[derive(Clone, Copy, Debug, Store, Serialize, Deserialize)]
pub enum InventoryKind {
Personal,
Common