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::*
|
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]
|
#[component]
|
||||||
pub fn App() -> impl IntoView {
|
pub fn App() -> impl IntoView {
|
||||||
// Provides context that manages stylesheets, titles, meta tags, etc.
|
// Provides context that manages stylesheets, titles, meta tags, etc.
|
||||||
|
|||||||
@ -2,6 +2,9 @@ use crate::prelude::*;
|
|||||||
use chrono::TimeDelta;
|
use chrono::TimeDelta;
|
||||||
use leptos::prelude::*;
|
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]
|
#[component]
|
||||||
pub fn Header () -> impl IntoView {
|
pub fn Header () -> impl IntoView {
|
||||||
let state = expect_context::<Context>();
|
let state = expect_context::<Context>();
|
||||||
@ -10,10 +13,12 @@ pub fn Header () -> impl IntoView {
|
|||||||
let session = state.session();
|
let session = state.session();
|
||||||
let image = state.campaign_image();
|
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| {
|
let adjust_day = move |adjustment: i64| date.update(|d| {
|
||||||
*d = d.checked_add_signed(TimeDelta::days(adjustment)).unwrap();
|
*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| {
|
let adjust_session = move |adjustment: isize| session.update(|s| {
|
||||||
if let Some(new) = s.checked_add_signed(adjustment) {
|
if let Some(new) = s.checked_add_signed(adjustment) {
|
||||||
*s = new;
|
*s = new;
|
||||||
|
|||||||
@ -2,6 +2,9 @@ use leptos::{logging, prelude::*};
|
|||||||
use reactive_stores::Field;
|
use reactive_stores::Field;
|
||||||
use crate::{ components::{ item::Item as ItemRow, numput::Numput }, prelude::* };
|
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]
|
#[component]
|
||||||
pub fn Inventory (
|
pub fn Inventory (
|
||||||
kind: InventoryKind,
|
kind: InventoryKind,
|
||||||
@ -11,10 +14,12 @@ pub fn Inventory (
|
|||||||
|
|
||||||
let state = expect_context::<Context>();
|
let state = expect_context::<Context>();
|
||||||
|
|
||||||
|
// Removes the row at `idx` from this inventory.
|
||||||
let rm = move |idx: usize| inventory.update(|v| {
|
let rm = move |idx: usize| inventory.update(|v| {
|
||||||
v.rows.remove(idx);
|
v.rows.remove(idx);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Moves the row at `idx` to the other inventory (personal ↔ common).
|
||||||
let mv = move |idx: usize| {
|
let mv = move |idx: usize| {
|
||||||
let (from, to): (Field<Inventory>, Field<Inventory>) = match kind.dst() {
|
let (from, to): (Field<Inventory>, Field<Inventory>) = match kind.dst() {
|
||||||
InventoryKind::Personal => (state.common().inventory().into(), state.player().inventory().into()),
|
InventoryKind::Personal => (state.common().inventory().into(), state.player().inventory().into()),
|
||||||
|
|||||||
@ -3,6 +3,9 @@ use leptos::prelude::*;
|
|||||||
|
|
||||||
use reactive_stores::{Field, OptionStoreExt};
|
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]
|
#[component]
|
||||||
pub fn Item (
|
pub fn Item (
|
||||||
idx: ReadSignal<usize>,
|
idx: ReadSignal<usize>,
|
||||||
@ -16,10 +19,14 @@ pub fn Item (
|
|||||||
|
|
||||||
let empty = String::new();
|
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));
|
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());
|
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 weight_change = move |ev: Targeted<Event, HtmlInputElement>| {
|
||||||
let old = item().weight().get_untracked();
|
let old = item().weight().get_untracked();
|
||||||
let new = ev.target().value();
|
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 leptos::prelude::*;
|
||||||
use nucleo_matcher::{ pattern::{ CaseMatching, Pattern, Normalization }, Config, Matcher };
|
use nucleo_matcher::{ pattern::{ CaseMatching, Pattern, Normalization }, Config, Matcher };
|
||||||
use reactive_stores::Field;
|
use reactive_stores::Field;
|
||||||
@ -11,6 +17,8 @@ pub mod inventory;
|
|||||||
pub mod numput;
|
pub mod numput;
|
||||||
pub mod item;
|
pub mod item;
|
||||||
|
|
||||||
|
/// Character page (route `/`). Stub: renders a placeholder until
|
||||||
|
/// ROADMAP phase 2.
|
||||||
#[component]
|
#[component]
|
||||||
pub fn Character () -> impl IntoView {
|
pub fn Character () -> impl IntoView {
|
||||||
view! {
|
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]
|
#[component]
|
||||||
pub fn Inventories () -> impl IntoView {
|
pub fn Inventories () -> impl IntoView {
|
||||||
let state = expect_context::<Context>();
|
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]
|
#[component]
|
||||||
pub fn Wiki () -> impl IntoView {
|
pub fn Wiki () -> impl IntoView {
|
||||||
view! {
|
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]
|
#[component]
|
||||||
pub fn ItemSearchField () -> impl IntoView {
|
pub fn ItemSearchField () -> impl IntoView {
|
||||||
let (query, set_query) = signal("".to_owned());
|
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]
|
#[component]
|
||||||
pub fn ItemSearchCard <'a> (item: &'a BookItem) -> impl IntoView {
|
pub fn ItemSearchCard <'a> (item: &'a BookItem) -> impl IntoView {
|
||||||
let card_class = format!("item-search-card {}", item.rarity.as_ref().unwrap_or(&String::new()));
|
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::prelude::*;
|
||||||
use leptos_router::components::A;
|
use leptos_router::components::A;
|
||||||
|
|
||||||
|
/// Top navigation: links to the three routes (character, inventory, wiki).
|
||||||
#[component]
|
#[component]
|
||||||
pub fn Nav () -> impl IntoView {
|
pub fn Nav () -> impl IntoView {
|
||||||
view! {
|
view! {
|
||||||
|
|||||||
@ -5,6 +5,11 @@ use num_format::{Locale, ToFormattedString};
|
|||||||
use num_traits::{ SaturatingSub, SaturatingAdd };
|
use num_traits::{ SaturatingSub, SaturatingAdd };
|
||||||
use crate::prelude::*;
|
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]
|
#[component]
|
||||||
pub fn Numput <T, U, 'a> (
|
pub fn Numput <T, U, 'a> (
|
||||||
field: U,
|
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]
|
#[component]
|
||||||
pub fn NumputChange <U, G, 'a> (
|
pub fn NumputChange <U, G, 'a> (
|
||||||
mut field_with: U,
|
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 crate::prelude::*;
|
||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
|
|
||||||
@ -9,6 +11,8 @@ mod hit_dice;
|
|||||||
mod death_saving_throws;
|
mod death_saving_throws;
|
||||||
mod spell_slots;
|
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]
|
#[component]
|
||||||
pub fn Sidebar () -> impl IntoView {
|
pub fn Sidebar () -> impl IntoView {
|
||||||
view! {
|
view! {
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
|
|
||||||
|
/// Class (text input), level (+/− steppers) and XP (number input).
|
||||||
#[component]
|
#[component]
|
||||||
pub fn About () -> impl IntoView {
|
pub fn About () -> impl IntoView {
|
||||||
let state = expect_context::<Context>();
|
let state = expect_context::<Context>();
|
||||||
@ -10,12 +11,14 @@ pub fn About () -> impl IntoView {
|
|||||||
let level = player.level();
|
let level = player.level();
|
||||||
let xp = player.xp();
|
let xp = player.xp();
|
||||||
|
|
||||||
|
// Steps the level by ±1, guarding against overflow.
|
||||||
let adjust_level = move |adjustment: i8| level.update(|l| {
|
let adjust_level = move |adjustment: i8| level.update(|l| {
|
||||||
if let Some(new) = l.checked_add_signed(adjustment) {
|
if let Some(new) = l.checked_add_signed(adjustment) {
|
||||||
*l = new;
|
*l = new;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Applies the XP input with checked parsing (invalid text is reverted).
|
||||||
let adjust_xp = move |ev: Targeted<Event, HtmlInputElement>| {
|
let adjust_xp = move |ev: Targeted<Event, HtmlInputElement>| {
|
||||||
utils::adjust_checked(ev, xp);
|
utils::adjust_checked(ev, xp);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use leptos::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]
|
#[component]
|
||||||
pub fn DeathSavingThrows () -> impl IntoView {
|
pub fn DeathSavingThrows () -> impl IntoView {
|
||||||
let state = expect_context::<Context>();
|
let state = expect_context::<Context>();
|
||||||
@ -8,6 +10,7 @@ pub fn DeathSavingThrows () -> impl IntoView {
|
|||||||
|
|
||||||
let dt = player.death_save_throws();
|
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| {
|
let process_dt_click = move |success: bool, slot_id: u8| {
|
||||||
dt.update(|d| {
|
dt.update(|d| {
|
||||||
let v = if success { &mut d.succeeded } else { &mut d.failed };
|
let v = if success { &mut d.succeeded } else { &mut d.failed };
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
|
|
||||||
|
/// Hit dice: a die-kind selector (d4 … d20) and one clickable die per
|
||||||
|
/// character level; clicking toggles spent/unspent.
|
||||||
#[component]
|
#[component]
|
||||||
pub fn HitDice () -> impl IntoView {
|
pub fn HitDice () -> impl IntoView {
|
||||||
let state = expect_context::<Context>();
|
let state = expect_context::<Context>();
|
||||||
@ -8,6 +10,8 @@ pub fn HitDice () -> impl IntoView {
|
|||||||
|
|
||||||
let hit_dice = player.hit_dice();
|
let hit_dice = player.hit_dice();
|
||||||
let level = player.level();
|
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| {
|
let process_die_click = move |slot_id: u8| {
|
||||||
hit_dice.update(|d| {
|
hit_dice.update(|d| {
|
||||||
if slot_id < d.used {
|
if slot_id < d.used {
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use leptos::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]
|
#[component]
|
||||||
pub fn HitPoints () -> impl IntoView {
|
pub fn HitPoints () -> impl IntoView {
|
||||||
let state = expect_context::<Context>();
|
let state = expect_context::<Context>();
|
||||||
@ -10,6 +12,8 @@ pub fn HitPoints () -> impl IntoView {
|
|||||||
let max_hp = player.max_hp();
|
let max_hp = player.max_hp();
|
||||||
let temp_hp = player.temp_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>| {
|
let adjust_hp = move |ev: Targeted<Event, HtmlInputElement>| {
|
||||||
utils::adjust_checked(ev, hp);
|
utils::adjust_checked(ev, hp);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
|
|
||||||
|
/// Character portrait; hidden while no image URL is set.
|
||||||
#[component]
|
#[component]
|
||||||
pub fn Image () -> impl IntoView {
|
pub fn Image () -> impl IntoView {
|
||||||
let state = expect_context::<Context>();
|
let state = expect_context::<Context>();
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
|
|
||||||
|
/// First name, alias (in «»), last name — three text inputs bound to the
|
||||||
|
/// player's name store.
|
||||||
#[component]
|
#[component]
|
||||||
pub fn Names () -> impl IntoView {
|
pub fn Names () -> impl IntoView {
|
||||||
let state = expect_context::<Context>();
|
let state = expect_context::<Context>();
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
|
|
||||||
|
/// Spell slots for all nine levels: a row of clickable pips per level.
|
||||||
|
/// Clicking toggles a slot used/unused.
|
||||||
#[component]
|
#[component]
|
||||||
pub fn SpellSlots () -> impl IntoView {
|
pub fn SpellSlots () -> impl IntoView {
|
||||||
let state = expect_context::<Context>();
|
let state = expect_context::<Context>();
|
||||||
@ -8,6 +10,7 @@ pub fn SpellSlots () -> impl IntoView {
|
|||||||
|
|
||||||
let slots = player.spell_slots();
|
let slots = player.spell_slots();
|
||||||
|
|
||||||
|
// Toggles pip `slot_id` of `level` used/unused.
|
||||||
let process_spell_slot_click = move |level: usize, slot_id: u8| {
|
let process_spell_slot_click = move |level: usize, slot_id: u8| {
|
||||||
slots.update(|s| {
|
slots.update(|s| {
|
||||||
let l = &mut s.0[level];
|
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 crate::prelude::*;
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
impl Dashboard {
|
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 {
|
pub fn mock () -> Self {
|
||||||
let (p_inv, c_inv) = generate_mock_inventory();
|
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) {
|
fn generate_mock_inventory() -> (Inventory, Inventory) {
|
||||||
let a = vec![
|
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()), ]), }, },
|
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 {
|
fn generate_mock_notebook () -> NoteBook {
|
||||||
NoteBook(vec![
|
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()]) },
|
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 {
|
fn generate_mock_questbook () -> QuestBook {
|
||||||
QuestBook(vec![
|
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 }) },
|
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 {
|
fn generate_mock_contact_book () -> ContactBook {
|
||||||
let mut contacts = HashMap::new();
|
let mut contacts = HashMap::new();
|
||||||
// 40 Characters Met by Party (80%)
|
// 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 {
|
fn generate_prepared_spells_list () -> PreparedSpells {
|
||||||
use PreparedSpell::*;
|
use PreparedSpell::*;
|
||||||
let list = vec![
|
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 std::fmt::Display;
|
||||||
|
|
||||||
use chrono::{ NaiveDate, NaiveDateTime };
|
use chrono::{ NaiveDate, NaiveDateTime };
|
||||||
@ -7,6 +20,10 @@ use crate::prelude::*;
|
|||||||
|
|
||||||
mod mock;
|
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)]
|
#[derive(Clone, Debug, Store)]
|
||||||
pub struct BookItem {
|
pub struct BookItem {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
@ -16,6 +33,8 @@ pub struct BookItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl 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> {
|
pub fn from_json (t: Value) -> Option<Self> {
|
||||||
let Value::String(name) = t["name"].clone() else { return None };
|
let Value::String(name) = t["name"].clone() else { return None };
|
||||||
let weight = match t["weight"].clone() {
|
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)]
|
#[derive(Clone, Debug, Store)]
|
||||||
pub struct ItemReferenceMap (pub HashMap<String, BookItem>);
|
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)]
|
#[derive(Clone, Debug, Store)]
|
||||||
pub struct Dashboard {
|
pub struct Dashboard {
|
||||||
pub campaign: String,
|
pub campaign: String,
|
||||||
@ -53,6 +80,9 @@ pub struct Dashboard {
|
|||||||
pub settings: Settings
|
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)]
|
#[derive(Clone, Debug, Store)]
|
||||||
pub struct PlayerData {
|
pub struct PlayerData {
|
||||||
pub name: Name,
|
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)]
|
#[derive(Clone, Debug, Store)]
|
||||||
pub struct CommonData {
|
pub struct CommonData {
|
||||||
pub balance: Balance,
|
pub balance: Balance,
|
||||||
pub inventory: Inventory,
|
pub inventory: Inventory,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Currency counts per denomination, platinum through copper.
|
||||||
|
/// Edited with `Numput` on the inventories page.
|
||||||
#[derive(Clone, Debug, Store)]
|
#[derive(Clone, Debug, Store)]
|
||||||
pub struct Balance {
|
pub struct Balance {
|
||||||
pub platinum: u32,
|
pub platinum: u32,
|
||||||
@ -89,12 +122,17 @@ pub struct Balance {
|
|||||||
pub copper: u32,
|
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)]
|
#[derive(Clone, Debug, Store)]
|
||||||
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.
|
||||||
#[derive(Clone, Debug, Store, Default)]
|
#[derive(Clone, Debug, Store, Default)]
|
||||||
pub struct InventoryEntry {
|
pub struct InventoryEntry {
|
||||||
pub id: u64,
|
pub id: u64,
|
||||||
@ -103,6 +141,7 @@ pub struct InventoryEntry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl InventoryEntry {
|
impl InventoryEntry {
|
||||||
|
/// Returns a stable key string for this entry (quantity + item name).
|
||||||
pub fn key (&self) -> String {
|
pub fn key (&self) -> String {
|
||||||
format!("{}-{}", self.quantity, self.item.name)
|
format!("{}-{}", self.quantity, self.item.name)
|
||||||
}
|
}
|
||||||
@ -114,6 +153,8 @@ impl InventoryEntry {
|
|||||||
// Book (Item),
|
// 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)]
|
#[derive(Clone, Debug, Store, Default)]
|
||||||
pub struct InventoryItem {
|
pub struct InventoryItem {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
@ -122,9 +163,12 @@ pub struct InventoryItem {
|
|||||||
pub tags: HashMap<String, String>,
|
pub tags: HashMap<String, String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The party's quest list. No UI yet — see ROADMAP phase 4.
|
||||||
#[derive(Clone, Debug, Store)]
|
#[derive(Clone, Debug, Store)]
|
||||||
pub struct QuestBook (pub Vec<Quest>);
|
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)]
|
||||||
pub struct Quest {
|
pub struct Quest {
|
||||||
pub title: String,
|
pub title: String,
|
||||||
@ -137,9 +181,12 @@ pub struct Quest {
|
|||||||
pub reward: Option<Balance>,
|
pub reward: Option<Balance>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The party's session notes. No UI yet — see ROADMAP phase 5.
|
||||||
#[derive(Clone, Debug, Store)]
|
#[derive(Clone, Debug, Store)]
|
||||||
pub struct NoteBook (pub Vec<Note>);
|
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)]
|
||||||
pub struct Note {
|
pub struct Note {
|
||||||
pub content: String,
|
pub content: String,
|
||||||
@ -149,35 +196,43 @@ pub struct Note {
|
|||||||
pub tags: HashSet<String>,
|
pub tags: HashSet<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Spell slots for the nine spell levels (index 0 = 1st level).
|
||||||
#[derive(Clone, Debug, Store)]
|
#[derive(Clone, Debug, Store)]
|
||||||
pub struct SpellSlots (pub [SpellSlotLevel; 9]);
|
pub struct SpellSlots (pub [SpellSlotLevel; 9]);
|
||||||
|
|
||||||
|
/// Used/total slots for one spell level.
|
||||||
#[derive(Clone, Debug, Store)]
|
#[derive(Clone, Debug, Store)]
|
||||||
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).
|
||||||
#[derive(Clone, Debug, Store)]
|
#[derive(Clone, Debug, Store)]
|
||||||
pub struct Attunements (pub [Option<String>; 3]);
|
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)]
|
||||||
pub struct PreparedSpells (pub Vec<PreparedSpell>);
|
pub struct PreparedSpells (pub Vec<PreparedSpell>);
|
||||||
|
|
||||||
|
|
||||||
|
/// One prepared-spell slot: vacant or holding a spell.
|
||||||
#[derive(Clone, Debug, Store)]
|
#[derive(Clone, Debug, Store)]
|
||||||
pub enum PreparedSpell {
|
pub enum PreparedSpell {
|
||||||
Vacant,
|
Vacant,
|
||||||
Occupied (Spell)
|
Occupied (Spell)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A spell. Only the name exists so far; the rest is a `@TODO`
|
||||||
|
/// (see ROADMAP phase 2).
|
||||||
// @TODO
|
// @TODO
|
||||||
#[derive(Clone, Debug, Store)]
|
#[derive(Clone, Debug, Store)]
|
||||||
pub struct Spell {
|
pub struct Spell {
|
||||||
pub name: String
|
pub name: String
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Hit dice: how many of the current die kind are already spent.
|
||||||
#[derive(Clone, Debug, Store)]
|
#[derive(Clone, Debug, Store)]
|
||||||
pub struct HitDice {
|
pub struct HitDice {
|
||||||
pub used: u8,
|
pub used: u8,
|
||||||
@ -185,6 +240,8 @@ pub struct HitDice {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Death-saving-throw counters: three failures kill, three successes
|
||||||
|
/// stabilize.
|
||||||
#[derive(Clone, Debug, Store)]
|
#[derive(Clone, Debug, Store)]
|
||||||
pub struct DeathSaveThrows {
|
pub struct DeathSaveThrows {
|
||||||
pub failed: u8,
|
pub failed: u8,
|
||||||
@ -192,15 +249,18 @@ pub struct DeathSaveThrows {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl DeathSaveThrows {
|
impl DeathSaveThrows {
|
||||||
|
/// Clears both counters. Bound to the ⟳ button in the sidebar.
|
||||||
pub fn reset (&mut self) {
|
pub fn reset (&mut self) {
|
||||||
self.failed = 0;
|
self.failed = 0;
|
||||||
self.succeeded = 0;
|
self.succeeded = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Known NPCs keyed by name. No UI yet — see ROADMAP phase 6.
|
||||||
#[derive(Clone, Debug, Store)]
|
#[derive(Clone, Debug, Store)]
|
||||||
pub struct ContactBook (pub HashMap<String, Contact>);
|
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)]
|
||||||
pub struct Contact {
|
pub struct Contact {
|
||||||
pub name: Name,
|
pub name: Name,
|
||||||
@ -210,6 +270,7 @@ pub struct Contact {
|
|||||||
pub notes: Option<String>,
|
pub notes: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Life status of a contact, used for filtering.
|
||||||
#[derive(Clone, Debug, Store)]
|
#[derive(Clone, Debug, Store)]
|
||||||
pub enum ContactStatus {
|
pub enum ContactStatus {
|
||||||
Unknown,
|
Unknown,
|
||||||
@ -218,6 +279,7 @@ pub enum ContactStatus {
|
|||||||
Alive,
|
Alive,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A person's name: first, last, and an optional alias (nickname).
|
||||||
#[derive(Clone, Debug, Store, Default)]
|
#[derive(Clone, Debug, Store, Default)]
|
||||||
pub struct Name {
|
pub struct Name {
|
||||||
pub first: String,
|
pub first: String,
|
||||||
@ -226,26 +288,31 @@ pub struct Name {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Name {
|
impl Name {
|
||||||
|
/// True when an alias is set.
|
||||||
pub fn display_alias (&self) -> bool {
|
pub fn display_alias (&self) -> bool {
|
||||||
!self.alias.is_empty()
|
!self.alias.is_empty()
|
||||||
}
|
}
|
||||||
|
/// True when a last name is set.
|
||||||
pub fn display_last (&self) -> bool {
|
pub fn display_last (&self) -> bool {
|
||||||
!self.last.is_empty()
|
!self.last.is_empty()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// App settings. Empty today; will carry e.g. a persisted-format version.
|
||||||
#[derive(Clone, Debug, Store, Default)]
|
#[derive(Clone, Debug, Store, Default)]
|
||||||
pub struct Settings {
|
pub struct Settings {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Which side of the party an inventory belongs to.
|
||||||
#[derive(Clone, Copy, Debug, Store)]
|
#[derive(Clone, Copy, Debug, Store)]
|
||||||
pub enum InventoryKind {
|
pub enum InventoryKind {
|
||||||
Personal,
|
Personal,
|
||||||
Common
|
Common
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Russian display name ("Личный" / "Общий").
|
||||||
impl Display for InventoryKind {
|
impl Display for InventoryKind {
|
||||||
fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
write!(f, "{}", match self {
|
write!(f, "{}", match self {
|
||||||
@ -256,6 +323,7 @@ impl Display for InventoryKind {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl InventoryKind {
|
impl InventoryKind {
|
||||||
|
/// The other inventory (for the move-between button).
|
||||||
pub fn dst (&self) -> Self {
|
pub fn dst (&self) -> Self {
|
||||||
match self {
|
match self {
|
||||||
Self::Common => Self::Personal,
|
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 {
|
pub fn class (&self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
Self::Common => "inventory inventory-common",
|
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)]
|
#![feature(type_alias_impl_trait)]
|
||||||
pub mod app;
|
pub mod app;
|
||||||
|
|
||||||
@ -21,6 +26,8 @@ mod components;
|
|||||||
#[allow(unused_imports)]
|
#[allow(unused_imports)]
|
||||||
use prelude::*;
|
use prelude::*;
|
||||||
|
|
||||||
|
/// WASM entry point: hydrates `App` into the server-rendered DOM.
|
||||||
|
/// Only compiled with the `hydrate` feature.
|
||||||
#[cfg(feature = "hydrate")]
|
#[cfg(feature = "hydrate")]
|
||||||
#[wasm_bindgen::prelude::wasm_bindgen]
|
#[wasm_bindgen::prelude::wasm_bindgen]
|
||||||
pub fn hydrate() {
|
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")]
|
#[cfg(feature = "ssr")]
|
||||||
#[actix_web::main]
|
#[actix_web::main]
|
||||||
async fn main() -> std::io::Result<()> {
|
async fn main() -> std::io::Result<()> {
|
||||||
@ -58,6 +63,7 @@ async fn main() -> std::io::Result<()> {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Serves `/favicon.ico` from the site root.
|
||||||
#[cfg(feature = "ssr")]
|
#[cfg(feature = "ssr")]
|
||||||
#[actix_web::get("favicon.ico")]
|
#[actix_web::get("favicon.ico")]
|
||||||
async fn favicon(
|
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")))]
|
#[cfg(not(any(feature = "ssr", feature = "csr")))]
|
||||||
pub fn main() {
|
pub fn main() {
|
||||||
// no client-side main function
|
// no client-side main function
|
||||||
@ -78,6 +86,8 @@ pub fn main() {
|
|||||||
// see optional feature `csr` instead
|
// 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"))]
|
#[cfg(all(not(feature = "ssr"), feature = "csr"))]
|
||||||
pub fn main() {
|
pub fn main() {
|
||||||
// a client-side main function is required for using `trunk serve`
|
// 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(unused_imports)]
|
||||||
#![allow(dead_code)]
|
#![allow(dead_code)]
|
||||||
pub use std::collections::{ HashMap, HashSet };
|
pub use std::collections::{ HashMap, HashSet };
|
||||||
@ -16,6 +20,12 @@ pub (crate) use crate::{
|
|||||||
components::{ self, numput::{ Numput, NumputChange } },
|
components::{ self, numput::{ Numput, NumputChange } },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// Crate-wide error type. Alias of `anyhow::Error`.
|
||||||
pub type Error = anyhow::Error;
|
pub type Error = anyhow::Error;
|
||||||
|
/// Crate-wide result type. Alias of `anyhow::Result<T>`.
|
||||||
pub type Result <T> = 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>;
|
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::*;
|
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 crate::prelude::*;
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::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 = {
|
pub static ref ITEMS_REFS: ItemReferenceMap = {
|
||||||
let mut map = HashMap::with_capacity(5_000);
|
let mut map = HashMap::with_capacity(5_000);
|
||||||
|
|
||||||
@ -17,6 +27,8 @@ lazy_static! {
|
|||||||
ItemReferenceMap(HashMap::from_iter(map.into_iter()))
|
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> = {
|
pub static ref ITEMS_NAMES: Vec<&'static String> = {
|
||||||
ITEMS_REFS.0.keys().collect()
|
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 crate::prelude::*;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use leptos::ev::{ Event, Targeted };
|
use leptos::ev::{ Event, Targeted };
|
||||||
@ -5,6 +7,11 @@ use leptos::web_sys::HtmlInputElement;
|
|||||||
use leptos::reactive::traits::{ Get, Set };
|
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 {
|
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 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 };
|
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
|
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) -> ()
|
pub fn adjust_checked <T, U> (ev: Targeted<Event, HtmlInputElement>, store: U) -> ()
|
||||||
where
|
where
|
||||||
U: Set<Value = T> + Get<Value = T>,
|
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> {
|
pub fn filled_pc <T> (cur: T, max: T) -> f32 where T: Into<f32> {
|
||||||
let cur: f32 = cur.into();
|
let cur: f32 = cur.into();
|
||||||
let max: f32 = max.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 {
|
pub trait BasisPoints {
|
||||||
fn to_decimal_string (&self) -> String;
|
fn to_decimal_string (&self) -> String;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Weights are stored as `u32` basis points: 100 = 1.0.
|
||||||
impl BasisPoints for u32 {
|
impl BasisPoints for u32 {
|
||||||
fn to_decimal_string (&self) -> String {
|
fn to_decimal_string (&self) -> String {
|
||||||
let int = self / 100;
|
let int = self / 100;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user