From ec4e771792d749813b59976fdbcc6ae52dc7b671 Mon Sep 17 00:00:00 2001 From: yaroslav k Date: Tue, 4 Aug 2026 09:21:25 +0300 Subject: [PATCH] add ARCHITECTURE.md: concise overview of the codebase State model (Store via context), bundled item data, SSR/hydrate pipeline, component tree, conventions, and current gaps. --- ARCHITECTURE.md | 165 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 165 insertions(+) create mode 100644 ARCHITECTURE.md diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 0000000..ef96b07 --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,165 @@ +# ARCHITECTURE + +This document describes how `aex` works today. It is a snapshot, not a design +spec: several pieces are stubs or unwired (see "Current gaps" at the end and +the phased plan in [ROADMAP.md](./ROADMAP.md)). + +## What it is + +A D&D 5e character sheet and campaign dashboard for one party's play-by-text +game. The UI is in Russian; code and comments are in English. The app tracks +the campaign (name, in-game date, session number), one player character +(identity, class, HP, money, spell slots, inventory, hit dice, death saves), +a party-shared stash, and — in the data model only, no UI yet — quests, +notes, and contacts. + +## Stack + +- Rust, edition 2024, nightly toolchain (`rust-toolchain.toml`, channel + pinned to `nightly` without a date) +- Leptos 0.8.20 (SSR + hydration), reactive_stores 0.4.3 for state +- leptos_actix 0.8.7 + actix-web 4 for the server +- nucleo-matcher for fuzzy item search, num-format for number display, + chrono for dates, lazy_static for one-time data loading +- SCSS sources in `style/` (compiled by cargo-leptos) +- Bundled game data in `data/` (5e.tools-format JSON, ~3.9 MB total) + +Features: `ssr` (server binary), `hydrate` (WASM), `csr` (optional pure-client +mode for `trunk`). `Cargo.lock` is gitignored. + +## Directory layout + +``` +src/ + main.rs SSR entry point: actix-web server, favicon + lib.rs crate root, module declarations, WASM hydrate() + app.rs App component: context, shell, routes + prelude.rs crate-wide re-exports and the Context alias + state/ App state (one Store in context) + entities/ Domain model (Store-derived structs) + mock data + statics/ Bundled item data: ITEMS_REFS, ITEMS_NAMES + utils/ read_items, adjust_checked, BasisPoints trait + components/ + header.rs Campaign header (name, date, session, Save/Load/Clear) + nav.rs Route links + sidebar.rs Character sidebar, composes the seven panels below + sidebar/*.rs image, names, about, hit_points, hit_dice, + death_saving_throws, spell_slots + inventory.rs One inventory section (rows + money) + item.rs One inventory row + numput.rs Numeric input components (Numput, NumputChange) + mod.rs Pages (Character, Inventories, Wiki) + item search +``` + +## State model + +The whole dashboard is one reactive tree: + +- `entities::Dashboard` is the root struct: campaign fields, `player: + PlayerData`, `common: CommonData`, plus quest/note/contact books and + settings. +- Every entity struct derives `reactive_stores::Store`, so every nested + field is independently reactive (copy-on-write paths). +- `App` creates `Store::new(Dashboard::mock())` and provides it as Leptos + context. Components read it with `expect_context::()` and grab + field handles, e.g. `let hp = state.player().hp();`. +- Writes go through `field.update(|v| ...)`, `field.set(...)`, or + `field.write()`. All arithmetic is saturating (`saturating_add/sub`, + `checked_add_signed`) so values never underflow. + +The state is mock data only — `Dashboard::mock()` runs on every load and +nothing is persisted. The header's Save/Load/Clear buttons do nothing yet +(ROADMAP phase 1). + +## Data layer (items) + +- `statics::ITEMS_REFS` is a `lazy_static` map of name → `BookItem`, built + once by `utils::read_items` over the bundled JSON files (`items.json`, + `base.json`, `foundry.json` loaded twice — a known duplication — and + `magic.json`), roughly 5k items. +- `BookItem` keeps `name`, `weight`, `rarity`, and `rest`, which holds all + other JSON fields verbatim for future detail views. +- Duplicate names merge: the later entry fills a missing weight and appends + its `rest` fields. +- `ITEMS_NAMES` is the flat name list fed to the fuzzy matcher. + +Weights are stored as `u32` basis points (100 = 1.0) in inventory items and +formatted with the `utils::BasisPoints` trait; book items use `f64` from the +JSON as-is. + +## Rendering pipeline + +1. `main.rs` (feature `ssr`) starts an actix-web server, generates the route + list from `App`, and renders each route server-side through + `leptos_routes`. It serves the WASM/CSS from `/pkg` and static assets + from `/assets`. +2. The WASM bundle (`hydrate` feature) calls `hydrate()` in `lib.rs`, which + mounts `App` with `hydrate_body` over the server-rendered DOM. +3. Routing: `/` → Character page, `/inv` → Inventories, `/wiki` → Wiki, + anything else → `NotFound` (sets HTTP 404 during SSR). + +## Component tree + +``` +App +├─ Header name, image, date steppers, session steppers, Save/Load/Clear (dead) +├─ Sidebar +│ ├─ Image portrait +│ ├─ Names first / alias «» / last +│ ├─ About class, level ±, XP +│ ├─ HitPoints current/max with bar, temp HP +│ ├─ HitDice die-kind select, per-level clickable dice +│ ├─ DeathSavingThrows fail/success pips + reset +│ └─ SpellSlots 9 levels of clickable pips +├─ Nav links to the three routes +└─ Routes + ├─ Character stub text + ├─ Inventories two Inventory sections (Personal/Common) + │ └─ Inventory balance (5 × Numput), rows (ForEnumerate) + │ └─ Item name, quantity ±, weight ±, more/swap/remove + └─ Wiki stub text +``` + +The inventories page reads `Context`, splits it into `Field` and +`Field` handles, and passes them down. Item rows get move/remove +closures by index; "move" transfers a row between the personal and common +inventories, "remove" deletes it. The "…" (details) button and the search +"+" button are unwired. + +`Numput` is the shared numeric input: `+n`/`-n` adjust relative to the +current value, a plain number sets absolutely, invalid input is reverted, +and the value renders with thousands separators. `NumputChange` is the same +pattern with caller-supplied display and parse functions (used for weights +in basis points). + +Item search (`ItemSearchField`) re-runs a nucleo fuzzy match on every +keystroke over `ITEMS_NAMES` and renders `ItemSearchCard`s. It is not +currently mounted on any page. + +## Conventions + +- UI strings are Russian and hardcoded in the views; ids like `id="hp"` + exist for e2e tests. +- Collections come from `std::collections` via the prelude (`HashMap`, + `HashSet`). +- `For` lists are keyed: inventories use `#[store(key)]` on `row.id`, + sidebar pips key by index. +- Logging goes through the `log` macros re-exported in the prelude + (`info!`, `warn!`, ...). + +## Current gaps + +- No persistence; state is recreated from `mock()` each load +- `Character` and `Wiki` pages render placeholders +- Quests, notes, contacts: models and mock data only +- Prepared spells list is empty; `Spell` is a name-only stub; attunements + have no UI; `Settings` is empty +- Save/Load/Clear buttons and the item "+"/"…" buttons are dead +- `foundry.json` is loaded twice; ~3.9 MB of JSON is bundled into the WASM + and parsed on startup +- Several unused dependencies (`leptos-use`, `dotenvy`, + `pretty_env_logger`, `seq_macro`, `itertools`, `thiserror`) and 12 + compiler warnings +- `Cargo.lock` is gitignored, so builds are not reproducible + +See [ROADMAP.md](./ROADMAP.md) for the ordered plan to fix these.