aex/ARCHITECTURE.md
yaroslav k 5e7cd0e709 docs: second-pass character page notes
- ROADMAP: Phase 2 done-note — player card removed, plain RU headings,
  growable attunements, manual AC
- ARCHITECTURE: character tree line and route contents updated
- README: character page and sidebar lines updated (no player card, AC)
2026-08-16 13:08:03 +03:00

179 lines
8.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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, std::sync::LazyLock 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 tracked for reproducible builds.
## 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<Dashboard> 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)
character/*.rs Character page: prepared_spells, attunements
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::<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 `LazyLock` map of name → `BookItem`, built
once by `utils::read_items` over the bundled JSON files (`items.json`,
`base.json`, `foundry.json`, `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
│ Save (download)/Load (file import)/Clear (wipe),
│ ↩ Undo/redo (buttons + Ctrl+Z / Ctrl+Shift+Z / Ctrl+Y)
├─ Modal overlay dialog: confirm (action) or error (close)
├─ 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 prepared spells (vacant/occupied slots with
│ free-text entry); attunements (growable slots)
├─ 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<Inventory>` and
`Field<Balance>` 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.
Persistence lives in `state::persist`: on hydration a deferred timer loads
the saved dashboard from localStorage (SSR markup stays authoritative),
then a debounced (500 ms) effect writes on any store change via
`store.track()`. `parse_dashboard` version-checks the JSON against
`Settings::VERSION` (the migration hook). `ModalState` holds title/message/
action as signals; the confirm button reads the action at click time.
Undo/redo lives in `state::undo`: the same store trigger feeds a
snapshot history (`HistoryHandle` in context). The first change of a
burst is remembered; when the burst goes quiet for 1 s, the pre-burst
state becomes an undo step. Undo/redo restore via `replace_state`
the autosave effect persists the undone state too. Clear and import
push a snapshot first, so they are undoable themselves.
`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
- Quests, notes, contacts: models and mock data only
- `Wiki` page renders a placeholder
- `Settings` has only `version`
- The item "+"/"…" buttons are dead
- `foundry.json` was loaded twice (fixed); ~3.9 MB of JSON is bundled
into the WASM and parsed on startup
- Several unused dependencies (see `MODERNIZE.md`); kept for now by
decision
- `Cargo.lock` is tracked since 2026-08-04; builds are reproducible
See [ROADMAP.md](./ROADMAP.md) for the ordered plan to fix these.