document every function, trait and struct in src/
Doc comments on all modules, components, entities, helpers and type aliases; larger comments on the important entities (Dashboard, PlayerData, Context, App, Numput, item data). No behavior changes.
This commit is contained in:
parent
da405758ce
commit
257f254b13
@ -17,6 +17,12 @@ use crate::{
|
||||
prelude::*
|
||||
};
|
||||
|
||||
/// Root component of the app.
|
||||
///
|
||||
/// Sets up meta context, creates the `Store<Dashboard>` (mock data for now)
|
||||
/// and injects it as context, then renders the shell: header, sidebar, nav
|
||||
/// and the routed page. Routes: `/` character, `/inv` inventories,
|
||||
/// `/wiki` wiki; anything else falls to `NotFound`.
|
||||
#[component]
|
||||
pub fn App() -> impl IntoView {
|
||||
// Provides context that manages stylesheets, titles, meta tags, etc.
|
||||
|
||||
@ -2,6 +2,9 @@ use crate::prelude::*;
|
||||
use chrono::TimeDelta;
|
||||
use leptos::prelude::*;
|
||||
|
||||
/// Campaign header: editable name, campaign image, in-game date and
|
||||
/// session steppers, and Save/Load/Clear buttons (not wired yet — see
|
||||
/// ROADMAP phase 1).
|
||||
#[component]
|
||||
pub fn Header () -> impl IntoView {
|
||||
let state = expect_context::<Context>();
|
||||
@ -10,10 +13,12 @@ pub fn Header () -> impl IntoView {
|
||||
let session = state.session();
|
||||
let image = state.campaign_image();
|
||||
|
||||
// Shifts the in-game date by ±1 day (saturating, via checked_add_signed).
|
||||
let adjust_day = move |adjustment: i64| date.update(|d| {
|
||||
*d = d.checked_add_signed(TimeDelta::days(adjustment)).unwrap();
|
||||
});
|
||||
|
||||
// Shifts the session counter by ±1, guarding against underflow.
|
||||
let adjust_session = move |adjustment: isize| session.update(|s| {
|
||||
if let Some(new) = s.checked_add_signed(adjustment) {
|
||||
*s = new;
|
||||
|
||||
@ -2,6 +2,9 @@ use leptos::{logging, prelude::*};
|
||||
use reactive_stores::Field;
|
||||
use crate::{ components::{ item::Item as ItemRow, numput::Numput }, prelude::* };
|
||||
|
||||
/// One inventory section: a titled balance block (five `Numput`s) and the
|
||||
/// item rows. `kind` decides personal/common styling and the move
|
||||
/// direction.
|
||||
#[component]
|
||||
pub fn Inventory (
|
||||
kind: InventoryKind,
|
||||
@ -11,10 +14,12 @@ pub fn Inventory (
|
||||
|
||||
let state = expect_context::<Context>();
|
||||
|
||||
// Removes the row at `idx` from this inventory.
|
||||
let rm = move |idx: usize| inventory.update(|v| {
|
||||
v.rows.remove(idx);
|
||||
});
|
||||
|
||||
// Moves the row at `idx` to the other inventory (personal ↔ common).
|
||||
let mv = move |idx: usize| {
|
||||
let (from, to): (Field<Inventory>, Field<Inventory>) = match kind.dst() {
|
||||
InventoryKind::Personal => (state.common().inventory().into(), state.player().inventory().into()),
|
||||
|
||||
@ -3,6 +3,9 @@ use leptos::prelude::*;
|
||||
|
||||
use reactive_stores::{Field, OptionStoreExt};
|
||||
|
||||
/// One inventory row: name, quantity stepper + `Numput`, weight stepper +
|
||||
/// `Numput`, and more/swap/remove buttons. A weight-based tag drives the
|
||||
/// CSS class. The "…" button is not wired yet (ROADMAP phase 3).
|
||||
#[component]
|
||||
pub fn Item (
|
||||
idx: ReadSignal<usize>,
|
||||
@ -16,10 +19,14 @@ pub fn Item (
|
||||
|
||||
let empty = String::new();
|
||||
|
||||
// CSS class from the item's weight tag ("weight" key).
|
||||
let class = move || format!("item {}", item().tags().get().get("weight").unwrap_or(&empty));
|
||||
|
||||
// Weight shown as a decimal: basis points → "x.yy".
|
||||
let weight_display = move || item().weight().with(|f| f.to_decimal_string());
|
||||
|
||||
// Parses weight input: "+n"/"-n" adjust relative, plain numbers set
|
||||
// absolutely, anything else restores the current value.
|
||||
let weight_change = move |ev: Targeted<Event, HtmlInputElement>| {
|
||||
let old = item().weight().get_untracked();
|
||||
let new = ev.target().value();
|
||||
|
||||
@ -1,3 +1,9 @@
|
||||
//! UI components.
|
||||
//!
|
||||
//! `header`, `sidebar` and `nav` form the app shell; `inventory`, `item`
|
||||
//! and `numput` implement the inventories page; the page components
|
||||
//! (`Character`, `Inventories`, `Wiki`) and the item search live in this
|
||||
//! module root.
|
||||
use leptos::prelude::*;
|
||||
use nucleo_matcher::{ pattern::{ CaseMatching, Pattern, Normalization }, Config, Matcher };
|
||||
use reactive_stores::Field;
|
||||
@ -11,6 +17,8 @@ pub mod inventory;
|
||||
pub mod numput;
|
||||
pub mod item;
|
||||
|
||||
/// Character page (route `/`). Stub: renders a placeholder until
|
||||
/// ROADMAP phase 2.
|
||||
#[component]
|
||||
pub fn Character () -> impl IntoView {
|
||||
view! {
|
||||
@ -18,6 +26,8 @@ pub fn Character () -> impl IntoView {
|
||||
}
|
||||
}
|
||||
|
||||
/// Inventories page (route `/inv`): the player's and the party's
|
||||
/// inventories side by side, each with money and item rows.
|
||||
#[component]
|
||||
pub fn Inventories () -> impl IntoView {
|
||||
let state = expect_context::<Context>();
|
||||
@ -40,6 +50,8 @@ pub fn Inventories () -> impl IntoView {
|
||||
}
|
||||
}
|
||||
|
||||
/// Wiki page (route `/wiki`). Stub: renders a placeholder until
|
||||
/// ROADMAP phase 7.
|
||||
#[component]
|
||||
pub fn Wiki () -> impl IntoView {
|
||||
view! {
|
||||
@ -47,6 +59,11 @@ pub fn Wiki () -> impl IntoView {
|
||||
}
|
||||
}
|
||||
|
||||
/// Fuzzy item search over the bundled item names.
|
||||
///
|
||||
/// Re-matches on every keystroke (nucleo) and renders result cards. The
|
||||
/// "+" buttons on the cards are not wired to the inventory yet (ROADMAP
|
||||
/// phase 3).
|
||||
#[component]
|
||||
pub fn ItemSearchField () -> impl IntoView {
|
||||
let (query, set_query) = signal("".to_owned());
|
||||
@ -86,6 +103,7 @@ pub fn ItemSearchField () -> impl IntoView {
|
||||
}
|
||||
|
||||
|
||||
/// One search result: name, weight, rarity and an (unwired) add button.
|
||||
#[component]
|
||||
pub fn ItemSearchCard <'a> (item: &'a BookItem) -> impl IntoView {
|
||||
let card_class = format!("item-search-card {}", item.rarity.as_ref().unwrap_or(&String::new()));
|
||||
|
||||
@ -2,6 +2,7 @@ use crate::prelude::*;
|
||||
use leptos::prelude::*;
|
||||
use leptos_router::components::A;
|
||||
|
||||
/// Top navigation: links to the three routes (character, inventory, wiki).
|
||||
#[component]
|
||||
pub fn Nav () -> impl IntoView {
|
||||
view! {
|
||||
|
||||
@ -5,6 +5,11 @@ use num_format::{Locale, ToFormattedString};
|
||||
use num_traits::{ SaturatingSub, SaturatingAdd };
|
||||
use crate::prelude::*;
|
||||
|
||||
/// Number input bound to a reactive field.
|
||||
///
|
||||
/// `+n` / `-n` adjust the current value relatively, a plain number sets it
|
||||
/// absolutely, and anything else is rejected (the input is restored). The
|
||||
/// value renders with thousands separators (`num_format`).
|
||||
#[component]
|
||||
pub fn Numput <T, U, 'a> (
|
||||
field: U,
|
||||
@ -37,6 +42,11 @@ pub fn Numput <T, U, 'a> (
|
||||
}
|
||||
}
|
||||
|
||||
/// `Numput` with caller-supplied display and change handlers.
|
||||
///
|
||||
/// For fields whose display differs from the stored value (e.g. weights
|
||||
/// in basis points): `field_with` renders the input, `change` parses the
|
||||
/// edit.
|
||||
#[component]
|
||||
pub fn NumputChange <U, G, 'a> (
|
||||
mut field_with: U,
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
//! Character sidebar: portrait, names, class/level/XP, HP, hit dice,
|
||||
//! death saves and spell slots.
|
||||
use crate::prelude::*;
|
||||
use leptos::prelude::*;
|
||||
|
||||
@ -9,6 +11,8 @@ mod hit_dice;
|
||||
mod death_saving_throws;
|
||||
mod spell_slots;
|
||||
|
||||
/// Left-hand character panel. Reads `Context` and stacks the sub-panels
|
||||
/// (image, names, about, HP, hit dice, death saves, spell slots).
|
||||
#[component]
|
||||
pub fn Sidebar () -> impl IntoView {
|
||||
view! {
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
use crate::prelude::*;
|
||||
use leptos::prelude::*;
|
||||
|
||||
/// Class (text input), level (+/− steppers) and XP (number input).
|
||||
#[component]
|
||||
pub fn About () -> impl IntoView {
|
||||
let state = expect_context::<Context>();
|
||||
@ -10,12 +11,14 @@ pub fn About () -> impl IntoView {
|
||||
let level = player.level();
|
||||
let xp = player.xp();
|
||||
|
||||
// Steps the level by ±1, guarding against overflow.
|
||||
let adjust_level = move |adjustment: i8| level.update(|l| {
|
||||
if let Some(new) = l.checked_add_signed(adjustment) {
|
||||
*l = new;
|
||||
}
|
||||
});
|
||||
|
||||
// Applies the XP input with checked parsing (invalid text is reverted).
|
||||
let adjust_xp = move |ev: Targeted<Event, HtmlInputElement>| {
|
||||
utils::adjust_checked(ev, xp);
|
||||
};
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
use crate::prelude::*;
|
||||
use leptos::prelude::*;
|
||||
|
||||
/// Three failure pips and three success pips, plus a reset button.
|
||||
/// Clicking a pip fills it; clicking the last filled pip empties it.
|
||||
#[component]
|
||||
pub fn DeathSavingThrows () -> impl IntoView {
|
||||
let state = expect_context::<Context>();
|
||||
@ -8,6 +10,7 @@ pub fn DeathSavingThrows () -> impl IntoView {
|
||||
|
||||
let dt = player.death_save_throws();
|
||||
|
||||
// Toggles pip `slot_id` of the fail/success track.
|
||||
let process_dt_click = move |success: bool, slot_id: u8| {
|
||||
dt.update(|d| {
|
||||
let v = if success { &mut d.succeeded } else { &mut d.failed };
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
use crate::prelude::*;
|
||||
use leptos::prelude::*;
|
||||
|
||||
/// Hit dice: a die-kind selector (d4 … d20) and one clickable die per
|
||||
/// character level; clicking toggles spent/unspent.
|
||||
#[component]
|
||||
pub fn HitDice () -> impl IntoView {
|
||||
let state = expect_context::<Context>();
|
||||
@ -8,6 +10,8 @@ pub fn HitDice () -> impl IntoView {
|
||||
|
||||
let hit_dice = player.hit_dice();
|
||||
let level = player.level();
|
||||
// Toggles die `slot_id` spent/unspent; clicking the last spent die
|
||||
// frees it again.
|
||||
let process_die_click = move |slot_id: u8| {
|
||||
hit_dice.update(|d| {
|
||||
if slot_id < d.used {
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
use crate::prelude::*;
|
||||
use leptos::prelude::*;
|
||||
|
||||
/// Current/max HP with a fill bar, plus temp HP with its own bar.
|
||||
/// All three numbers are editable `type="number"` inputs.
|
||||
#[component]
|
||||
pub fn HitPoints () -> impl IntoView {
|
||||
let state = expect_context::<Context>();
|
||||
@ -10,6 +12,8 @@ pub fn HitPoints () -> impl IntoView {
|
||||
let max_hp = player.max_hp();
|
||||
let temp_hp = player.temp_hp();
|
||||
|
||||
// All three inputs parse via `utils::adjust_checked`; invalid text
|
||||
// restores the previous value.
|
||||
let adjust_hp = move |ev: Targeted<Event, HtmlInputElement>| {
|
||||
utils::adjust_checked(ev, hp);
|
||||
};
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
use crate::prelude::*;
|
||||
use leptos::prelude::*;
|
||||
|
||||
/// Character portrait; hidden while no image URL is set.
|
||||
#[component]
|
||||
pub fn Image () -> impl IntoView {
|
||||
let state = expect_context::<Context>();
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
use crate::prelude::*;
|
||||
use leptos::prelude::*;
|
||||
|
||||
/// First name, alias (in «»), last name — three text inputs bound to the
|
||||
/// player's name store.
|
||||
#[component]
|
||||
pub fn Names () -> impl IntoView {
|
||||
let state = expect_context::<Context>();
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
use crate::prelude::*;
|
||||
use leptos::prelude::*;
|
||||
|
||||
/// Spell slots for all nine levels: a row of clickable pips per level.
|
||||
/// Clicking toggles a slot used/unused.
|
||||
#[component]
|
||||
pub fn SpellSlots () -> impl IntoView {
|
||||
let state = expect_context::<Context>();
|
||||
@ -8,6 +10,7 @@ pub fn SpellSlots () -> impl IntoView {
|
||||
|
||||
let slots = player.spell_slots();
|
||||
|
||||
// Toggles pip `slot_id` of `level` used/unused.
|
||||
let process_spell_slot_click = move |level: usize, slot_id: u8| {
|
||||
slots.update(|s| {
|
||||
let l = &mut s.0[level];
|
||||
|
||||
@ -1,7 +1,14 @@
|
||||
//! Hardcoded demo data.
|
||||
//!
|
||||
//! `Dashboard::mock()` is the app's only state source until persistence
|
||||
//! lands (ROADMAP phase 1).
|
||||
use crate::prelude::*;
|
||||
use super::*;
|
||||
|
||||
impl Dashboard {
|
||||
/// Builds a fully populated demo dashboard: a level-8 character, two
|
||||
/// inventories, quests, notes and contacts. Used at startup and as the
|
||||
/// fallback state.
|
||||
pub fn mock () -> Self {
|
||||
let (p_inv, c_inv) = generate_mock_inventory();
|
||||
|
||||
@ -47,6 +54,8 @@ impl Dashboard {
|
||||
|
||||
|
||||
|
||||
/// Demo inventories: 10 personal items and 10 common items, each with a
|
||||
/// weight, description and tags.
|
||||
fn generate_mock_inventory() -> (Inventory, Inventory) {
|
||||
let a = vec![
|
||||
InventoryEntry { id: 1, quantity: 1, item: InventoryItem { name: "Vorpal Sword".to_string(), weight: 400, description: Some("A blade that severs heads with a single swing.".to_string()), tags: HashMap::from([ ("type".to_string(), "weapon".to_string()), ("rarity".to_string(), "legendary".to_string()), ]), }, },
|
||||
@ -79,6 +88,8 @@ fn generate_mock_inventory() -> (Inventory, Inventory) {
|
||||
|
||||
|
||||
|
||||
/// Demo notes: one per session, with in-game dates, real timestamps and
|
||||
/// tags.
|
||||
fn generate_mock_notebook () -> NoteBook {
|
||||
NoteBook(vec![
|
||||
Note { session: 1, content: "Met Elminster in Waterdeep; hinted at Netherese ruin.".to_string(), date_ingame: NaiveDate::from_ymd_opt(1481, 4, 15).unwrap(), date_real: NaiveDateTime::parse_from_str("2025-09-01 14:30:00", "%Y-%m-%d %H:%M:%S").unwrap(), tags: HashSet::from_iter(vec!["Elminster".to_string(), "Waterdeep".to_string()]) },
|
||||
@ -96,6 +107,7 @@ fn generate_mock_notebook () -> NoteBook {
|
||||
])
|
||||
}
|
||||
|
||||
/// Demo quests with givers, locations, dates and rewards.
|
||||
fn generate_mock_questbook () -> QuestBook {
|
||||
QuestBook(vec![
|
||||
Quest { title: "Slay the Bandit Chief".to_string(), location_taken: Some("Waterdeep".to_string()), location_task: Some("Phandalin".to_string()), date_taken: Some(NaiveDate::from_ymd_opt(1481, 4, 15).unwrap()), date_by: Some(NaiveDate::from_ymd_opt(1481, 5, 15).unwrap()), description: Some("Kill the bandit leader.".to_string()), from: Some(Name { first: "Dagult".to_string(), last: "Neverember".to_string(), alias: "".to_string(), ..Default::default() }), reward: Some(Balance { platinum: 0, gold: 100, electrum: 0, silver: 0, copper: 0 }) },
|
||||
@ -111,6 +123,8 @@ fn generate_mock_questbook () -> QuestBook {
|
||||
])
|
||||
}
|
||||
|
||||
/// Demo contacts: mostly NPCs met by the party, plus a few merchants,
|
||||
/// living legends and the dead.
|
||||
fn generate_mock_contact_book () -> ContactBook {
|
||||
let mut contacts = HashMap::new();
|
||||
// 40 Characters Met by Party (80%)
|
||||
@ -172,6 +186,7 @@ fn generate_mock_contact_book () -> ContactBook {
|
||||
}
|
||||
|
||||
|
||||
/// Demo prepared spells. Currently an empty list — `Spell` is a stub.
|
||||
fn generate_prepared_spells_list () -> PreparedSpells {
|
||||
use PreparedSpell::*;
|
||||
let list = vec![
|
||||
|
||||
@ -1,3 +1,16 @@
|
||||
//! The domain model of the campaign dashboard.
|
||||
//!
|
||||
//! Every type here is a plain data struct that derives
|
||||
//! [`reactive_stores::Store`], which makes each field independently
|
||||
//! reactive: components subscribe to single fields and re-render only when
|
||||
//! those change. The root type is [`Dashboard`], wrapped in a
|
||||
//! `Store<Dashboard>` and provided to the whole app through Leptos context
|
||||
//! (see `crate::prelude::Context`).
|
||||
//!
|
||||
//! 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).
|
||||
use std::fmt::Display;
|
||||
|
||||
use chrono::{ NaiveDate, NaiveDateTime };
|
||||
@ -7,6 +20,10 @@ use crate::prelude::*;
|
||||
|
||||
mod mock;
|
||||
|
||||
/// A single item as it appears in the bundled 5e.tools JSON files.
|
||||
///
|
||||
/// `rest` keeps every other JSON field verbatim, so the raw source data
|
||||
/// survives lookup (used by future item detail views).
|
||||
#[derive(Clone, Debug, Store)]
|
||||
pub struct BookItem {
|
||||
pub name: String,
|
||||
@ -16,6 +33,8 @@ pub struct BookItem {
|
||||
}
|
||||
|
||||
impl BookItem {
|
||||
/// Parses one 5e.tools JSON object into a `BookItem`.
|
||||
/// Returns `None` when the object has no string `name` or is not an object.
|
||||
pub fn from_json (t: Value) -> Option<Self> {
|
||||
let Value::String(name) = t["name"].clone() else { return None };
|
||||
let weight = match t["weight"].clone() {
|
||||
@ -36,9 +55,17 @@ impl BookItem {
|
||||
}
|
||||
}
|
||||
|
||||
/// Name → `BookItem` lookup, built once from the bundled JSON (see `statics`).
|
||||
#[derive(Clone, Debug, Store)]
|
||||
pub struct ItemReferenceMap (pub HashMap<String, BookItem>);
|
||||
|
||||
/// The root application state.
|
||||
///
|
||||
/// One `Dashboard` holds everything the UI edits: campaign header data
|
||||
/// (name, image, session number, in-game date), the player character, the
|
||||
/// 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)]
|
||||
pub struct Dashboard {
|
||||
pub campaign: String,
|
||||
@ -53,6 +80,9 @@ pub struct Dashboard {
|
||||
pub settings: Settings
|
||||
}
|
||||
|
||||
/// 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)]
|
||||
pub struct PlayerData {
|
||||
pub name: Name,
|
||||
@ -74,12 +104,15 @@ pub struct PlayerData {
|
||||
}
|
||||
|
||||
|
||||
/// The party's shared stash: money and items usable by everyone.
|
||||
#[derive(Clone, Debug, Store)]
|
||||
pub struct CommonData {
|
||||
pub balance: Balance,
|
||||
pub inventory: Inventory,
|
||||
}
|
||||
|
||||
/// Currency counts per denomination, platinum through copper.
|
||||
/// Edited with `Numput` on the inventories page.
|
||||
#[derive(Clone, Debug, Store)]
|
||||
pub struct Balance {
|
||||
pub platinum: u32,
|
||||
@ -89,12 +122,17 @@ pub struct Balance {
|
||||
pub copper: u32,
|
||||
}
|
||||
|
||||
/// A named inventory (personal or common): a list of entries.
|
||||
///
|
||||
/// `#[store(key)]` gives every entry a stable reactive key (its `id`),
|
||||
/// which keeps `For`/`ForEnumerate` updates cheap.
|
||||
#[derive(Clone, Debug, Store)]
|
||||
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)]
|
||||
pub struct InventoryEntry {
|
||||
pub id: u64,
|
||||
@ -103,6 +141,7 @@ pub struct InventoryEntry {
|
||||
}
|
||||
|
||||
impl InventoryEntry {
|
||||
/// Returns a stable key string for this entry (quantity + item name).
|
||||
pub fn key (&self) -> String {
|
||||
format!("{}-{}", self.quantity, self.item.name)
|
||||
}
|
||||
@ -114,6 +153,8 @@ impl InventoryEntry {
|
||||
// Book (Item),
|
||||
// }
|
||||
|
||||
/// An item inside an inventory: name, weight (basis points), optional
|
||||
/// description, and free-form tags (e.g. `type`, `rarity`).
|
||||
#[derive(Clone, Debug, Store, Default)]
|
||||
pub struct InventoryItem {
|
||||
pub name: String,
|
||||
@ -122,9 +163,12 @@ pub struct InventoryItem {
|
||||
pub tags: HashMap<String, String>,
|
||||
}
|
||||
|
||||
/// The party's quest list. No UI yet — see ROADMAP phase 4.
|
||||
#[derive(Clone, Debug, Store)]
|
||||
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)]
|
||||
pub struct Quest {
|
||||
pub title: String,
|
||||
@ -137,9 +181,12 @@ pub struct Quest {
|
||||
pub reward: Option<Balance>,
|
||||
}
|
||||
|
||||
/// The party's session notes. No UI yet — see ROADMAP phase 5.
|
||||
#[derive(Clone, Debug, Store)]
|
||||
pub struct NoteBook (pub Vec<Note>);
|
||||
|
||||
/// One note: content, in-game date, real-world timestamp, session number
|
||||
/// and tags.
|
||||
#[derive(Clone, Debug, Store)]
|
||||
pub struct Note {
|
||||
pub content: String,
|
||||
@ -149,35 +196,43 @@ pub struct Note {
|
||||
pub tags: HashSet<String>,
|
||||
}
|
||||
|
||||
/// Spell slots for the nine spell levels (index 0 = 1st level).
|
||||
#[derive(Clone, Debug, Store)]
|
||||
pub struct SpellSlots (pub [SpellSlotLevel; 9]);
|
||||
|
||||
/// Used/total slots for one spell level.
|
||||
#[derive(Clone, Debug, Store)]
|
||||
pub struct SpellSlotLevel {
|
||||
pub used: u8,
|
||||
pub total: u8,
|
||||
}
|
||||
|
||||
/// Three attunement slots (D&D rules allow three attuned items).
|
||||
#[derive(Clone, Debug, Store)]
|
||||
pub struct Attunements (pub [Option<String>; 3]);
|
||||
|
||||
|
||||
/// The spell list the character has prepared (or is learning).
|
||||
#[derive(Clone, Debug, Store)]
|
||||
pub struct PreparedSpells (pub Vec<PreparedSpell>);
|
||||
|
||||
|
||||
/// One prepared-spell slot: vacant or holding a spell.
|
||||
#[derive(Clone, Debug, Store)]
|
||||
pub enum PreparedSpell {
|
||||
Vacant,
|
||||
Occupied (Spell)
|
||||
}
|
||||
|
||||
/// A spell. Only the name exists so far; the rest is a `@TODO`
|
||||
/// (see ROADMAP phase 2).
|
||||
// @TODO
|
||||
#[derive(Clone, Debug, Store)]
|
||||
pub struct Spell {
|
||||
pub name: String
|
||||
}
|
||||
|
||||
/// Hit dice: how many of the current die kind are already spent.
|
||||
#[derive(Clone, Debug, Store)]
|
||||
pub struct HitDice {
|
||||
pub used: u8,
|
||||
@ -185,6 +240,8 @@ pub struct HitDice {
|
||||
}
|
||||
|
||||
|
||||
/// Death-saving-throw counters: three failures kill, three successes
|
||||
/// stabilize.
|
||||
#[derive(Clone, Debug, Store)]
|
||||
pub struct DeathSaveThrows {
|
||||
pub failed: u8,
|
||||
@ -192,15 +249,18 @@ pub struct DeathSaveThrows {
|
||||
}
|
||||
|
||||
impl DeathSaveThrows {
|
||||
/// Clears both counters. Bound to the ⟳ button in the sidebar.
|
||||
pub fn reset (&mut self) {
|
||||
self.failed = 0;
|
||||
self.succeeded = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// Known NPCs keyed by name. No UI yet — see ROADMAP phase 6.
|
||||
#[derive(Clone, Debug, Store)]
|
||||
pub struct ContactBook (pub HashMap<String, Contact>);
|
||||
|
||||
/// One NPC contact: name, optional image, status, location and notes.
|
||||
#[derive(Clone, Debug, Store)]
|
||||
pub struct Contact {
|
||||
pub name: Name,
|
||||
@ -210,6 +270,7 @@ pub struct Contact {
|
||||
pub notes: Option<String>,
|
||||
}
|
||||
|
||||
/// Life status of a contact, used for filtering.
|
||||
#[derive(Clone, Debug, Store)]
|
||||
pub enum ContactStatus {
|
||||
Unknown,
|
||||
@ -218,6 +279,7 @@ pub enum ContactStatus {
|
||||
Alive,
|
||||
}
|
||||
|
||||
/// A person's name: first, last, and an optional alias (nickname).
|
||||
#[derive(Clone, Debug, Store, Default)]
|
||||
pub struct Name {
|
||||
pub first: String,
|
||||
@ -226,26 +288,31 @@ pub struct Name {
|
||||
}
|
||||
|
||||
impl Name {
|
||||
/// True when an alias is set.
|
||||
pub fn display_alias (&self) -> bool {
|
||||
!self.alias.is_empty()
|
||||
}
|
||||
/// True when a last name is set.
|
||||
pub fn display_last (&self) -> bool {
|
||||
!self.last.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
/// App settings. Empty today; will carry e.g. a persisted-format version.
|
||||
#[derive(Clone, Debug, Store, Default)]
|
||||
pub struct Settings {
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// Which side of the party an inventory belongs to.
|
||||
#[derive(Clone, Copy, Debug, Store)]
|
||||
pub enum InventoryKind {
|
||||
Personal,
|
||||
Common
|
||||
}
|
||||
|
||||
/// Russian display name ("Личный" / "Общий").
|
||||
impl Display for InventoryKind {
|
||||
fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", match self {
|
||||
@ -256,6 +323,7 @@ impl Display for InventoryKind {
|
||||
}
|
||||
|
||||
impl InventoryKind {
|
||||
/// The other inventory (for the move-between button).
|
||||
pub fn dst (&self) -> Self {
|
||||
match self {
|
||||
Self::Common => Self::Personal,
|
||||
@ -263,6 +331,7 @@ impl InventoryKind {
|
||||
}
|
||||
}
|
||||
|
||||
/// CSS classes for the inventory section (personal vs common styling).
|
||||
pub fn class (&self) -> &'static str {
|
||||
match self {
|
||||
Self::Common => "inventory inventory-common",
|
||||
|
||||
@ -1,3 +1,8 @@
|
||||
//! aex — a D&D 5e character sheet and campaign dashboard.
|
||||
//!
|
||||
//! Leptos 0.8 app with actix-web SSR and client-side hydration. The crate
|
||||
//! root declares the modules and the WASM entry point; `main.rs` holds the
|
||||
//! SSR server, `app.rs` the component tree.
|
||||
#![feature(type_alias_impl_trait)]
|
||||
pub mod app;
|
||||
|
||||
@ -21,6 +26,8 @@ mod components;
|
||||
#[allow(unused_imports)]
|
||||
use prelude::*;
|
||||
|
||||
/// WASM entry point: hydrates `App` into the server-rendered DOM.
|
||||
/// Only compiled with the `hydrate` feature.
|
||||
#[cfg(feature = "hydrate")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen]
|
||||
pub fn hydrate() {
|
||||
|
||||
10
src/main.rs
10
src/main.rs
@ -1,4 +1,9 @@
|
||||
|
||||
/// SSR entry point: starts the actix-web server that renders the app.
|
||||
///
|
||||
/// Serves the WASM/CSS bundle from `/pkg`, static assets from `/assets`,
|
||||
/// and renders every route through `leptos_routes`. Only compiled with
|
||||
/// the `ssr` feature.
|
||||
#[cfg(feature = "ssr")]
|
||||
#[actix_web::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
@ -58,6 +63,7 @@ async fn main() -> std::io::Result<()> {
|
||||
.await
|
||||
}
|
||||
|
||||
/// Serves `/favicon.ico` from the site root.
|
||||
#[cfg(feature = "ssr")]
|
||||
#[actix_web::get("favicon.ico")]
|
||||
async fn favicon(
|
||||
@ -70,6 +76,8 @@ async fn favicon(
|
||||
))?)
|
||||
}
|
||||
|
||||
/// No-op `main` for builds without `ssr` or `csr`: the WASM binary is
|
||||
/// hydrated from `lib.rs` instead.
|
||||
#[cfg(not(any(feature = "ssr", feature = "csr")))]
|
||||
pub fn main() {
|
||||
// no client-side main function
|
||||
@ -78,6 +86,8 @@ pub fn main() {
|
||||
// see optional feature `csr` instead
|
||||
}
|
||||
|
||||
/// Client-only `main` for `trunk serve --features csr`.
|
||||
/// Not the normal path — prefer `cargo leptos serve`.
|
||||
#[cfg(all(not(feature = "ssr"), feature = "csr"))]
|
||||
pub fn main() {
|
||||
// a client-side main function is required for using `trunk serve`
|
||||
|
||||
@ -1,3 +1,7 @@
|
||||
//! Crate-wide re-exports.
|
||||
//!
|
||||
//! Components and entities use `use crate::prelude::*` to get the common
|
||||
//! imports: collections, logging, error aliases, `Store`, entities, utils.
|
||||
#![allow(unused_imports)]
|
||||
#![allow(dead_code)]
|
||||
pub use std::collections::{ HashMap, HashSet };
|
||||
@ -16,6 +20,12 @@ pub (crate) use crate::{
|
||||
components::{ self, numput::{ Numput, NumputChange } },
|
||||
};
|
||||
|
||||
/// Crate-wide error type. Alias of `anyhow::Error`.
|
||||
pub type Error = anyhow::Error;
|
||||
/// Crate-wide result type. Alias of `anyhow::Result<T>`.
|
||||
pub type Result <T> = anyhow::Result<T>;
|
||||
/// The reactive application state: a `Store` wrapping [`crate::entities::Dashboard`].
|
||||
///
|
||||
/// Created once in `App` and injected through Leptos context. Components
|
||||
/// read it with `expect_context::<Context>()` and grab reactive field handles.
|
||||
pub type Context = Store<Dashboard>;
|
||||
|
||||
@ -1 +1,6 @@
|
||||
//! App state.
|
||||
//!
|
||||
//! The whole dashboard lives in one `reactive_stores::Store<Dashboard>`
|
||||
//! (alias `Context`), created in `App` and shared through Leptos context.
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
@ -1,8 +1,18 @@
|
||||
//! Bundled static game data.
|
||||
//!
|
||||
//! `ITEMS_REFS` loads the 5e.tools-format JSON files (`data/*.json`,
|
||||
//! roughly 5k items) once at first use via `lazy_static` and indexes them
|
||||
//! by name. `ITEMS_NAMES` is the flat name list used for fuzzy search.
|
||||
use crate::prelude::*;
|
||||
use lazy_static::lazy_static;
|
||||
|
||||
|
||||
lazy_static! {
|
||||
/// All known items, indexed by name, from the bundled 5e.tools JSON files.
|
||||
///
|
||||
/// Built once at startup: entries with the same name are merged (a missing
|
||||
/// weight fills in, `rest` fields concatenate). Note `foundry.json` is
|
||||
/// currently loaded twice — see `utils::read_items`.
|
||||
pub static ref ITEMS_REFS: ItemReferenceMap = {
|
||||
let mut map = HashMap::with_capacity(5_000);
|
||||
|
||||
@ -17,6 +27,8 @@ lazy_static! {
|
||||
ItemReferenceMap(HashMap::from_iter(map.into_iter()))
|
||||
};
|
||||
|
||||
/// Flat list of item names; the feed for the fuzzy matcher
|
||||
/// (`ItemSearchField`).
|
||||
pub static ref ITEMS_NAMES: Vec<&'static String> = {
|
||||
ITEMS_REFS.0.keys().collect()
|
||||
};
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
//! Small shared helpers: item parsing, checked input parsing, percentage
|
||||
//! math, and the `BasisPoints` display trait.
|
||||
use crate::prelude::*;
|
||||
use std::str::FromStr;
|
||||
use leptos::ev::{ Event, Targeted };
|
||||
@ -5,6 +7,11 @@ use leptos::web_sys::HtmlInputElement;
|
||||
use leptos::reactive::traits::{ Get, Set };
|
||||
|
||||
|
||||
/// Parses a 5e.tools-format JSON array of items into `map`, keyed by name.
|
||||
///
|
||||
/// Skips entries without a string `name` or without an object body.
|
||||
/// Duplicate names merge: the later entry's weight fills a missing one and
|
||||
/// its `rest` fields are appended. Returns the number of new names added.
|
||||
pub fn read_items (contents: &str, map: &mut HashMap<String, BookItem>) -> usize {
|
||||
let Ok(json): std::result::Result<Value, _> = serde_json::from_str(&contents) else { error!("Can't read Value from contents"); return 0 };
|
||||
let Value::Array(list) = json else { error!("Value isn't Array, skipping"); return 0 };
|
||||
@ -24,6 +31,10 @@ pub fn read_items (contents: &str, map: &mut HashMap<String, BookItem>) -> usize
|
||||
map.len() - start
|
||||
}
|
||||
|
||||
/// Applies a text input's value to a reactive store with checked parsing.
|
||||
///
|
||||
/// On a successful parse, sets the store to the value; otherwise restores
|
||||
/// the input to the store's current value. Used for `type="number"` inputs.
|
||||
pub fn adjust_checked <T, U> (ev: Targeted<Event, HtmlInputElement>, store: U) -> ()
|
||||
where
|
||||
U: Set<Value = T> + Get<Value = T>,
|
||||
@ -39,6 +50,8 @@ pub fn adjust_checked <T, U> (ev: Targeted<Event, HtmlInputElement>, store: U) -
|
||||
}
|
||||
}
|
||||
|
||||
/// Percent of `cur` relative to `max`; callers clamp to 100. Used for
|
||||
/// HP-bar widths.
|
||||
pub fn filled_pc <T> (cur: T, max: T) -> f32 where T: Into<f32> {
|
||||
let cur: f32 = cur.into();
|
||||
let max: f32 = max.into();
|
||||
@ -46,10 +59,13 @@ pub fn filled_pc <T> (cur: T, max: T) -> f32 where T: Into<f32> {
|
||||
}
|
||||
|
||||
|
||||
/// Formats an integer stored in basis points (hundredths) as a decimal
|
||||
/// string, e.g. `400` → `"4.00"`.
|
||||
pub trait BasisPoints {
|
||||
fn to_decimal_string (&self) -> String;
|
||||
}
|
||||
|
||||
/// Weights are stored as `u32` basis points: 100 = 1.0.
|
||||
impl BasisPoints for u32 {
|
||||
fn to_decimal_string (&self) -> String {
|
||||
let int = self / 100;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user