character: drop player card; RU headings; growable attunements; AC
- PlayerCard removed — it duplicated the sidebar editors; the character page now shows only prepared spells and attunements - headings are plain RU: Заготовленные заклинания / Атюнменты (no EN/RU double labels; h6 no longer lowercased) - Attunements is Vec<Option<String>> instead of a fixed 3-slot array; '+ слот' appends a slot, so custom campaigns can track more items (old saved JSON parses: arrays map to Vec) - PlayerData.ac (u8, serde default 0): manual armor-class input in the sidebar HP block (hp/оз, ac/КД), editable like the HP numbers
This commit is contained in:
parent
6d0846d160
commit
294bf430fd
@ -12,7 +12,7 @@ pub fn Attunements () -> impl IntoView {
|
||||
|
||||
view! {
|
||||
<div class="attunements">
|
||||
<h6>"attunements/слоты привязки:"</h6>
|
||||
<h6>"Атюнменты:"</h6>
|
||||
<For
|
||||
// Same state-in-key convention as prepared spells, so a
|
||||
// slot whose value changed re-renders.
|
||||
@ -22,6 +22,13 @@ pub fn Attunements () -> impl IntoView {
|
||||
>
|
||||
{slot_row(attunements, idx, slot)}
|
||||
</For>
|
||||
<button
|
||||
class="add-slot"
|
||||
title="Добавить слот привязки"
|
||||
on:click=move |_| attunements.update(|a| a.0.push(None))
|
||||
>
|
||||
"+ слот"
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
//! Character page blocks (route `/`): player card, prepared spells and
|
||||
//! attunements. The sidebar already carries the detailed editors (names,
|
||||
//! class/level/XP, HP, hit dice, death saves, spell slots); this module
|
||||
//! renders the page-level view of that data.
|
||||
//! Character page blocks (route `/`): prepared spells and attunements.
|
||||
//! The sidebar carries the character editors (names, class/level/XP,
|
||||
//! HP, hit dice, death saves, spell slots); this module renders the
|
||||
//! page-level view.
|
||||
|
||||
pub mod attunements;
|
||||
pub mod player_card;
|
||||
pub mod prepared_spells;
|
||||
|
||||
@ -1,45 +0,0 @@
|
||||
use crate::prelude::*;
|
||||
use leptos::prelude::*;
|
||||
|
||||
/// Read-only player summary: portrait, name, class, level and XP.
|
||||
///
|
||||
/// The sidebar holds the editors for this data (names, about); this card
|
||||
/// presents it at the top of the character page.
|
||||
#[component]
|
||||
pub fn PlayerCard () -> impl IntoView {
|
||||
let state = expect_context::<Context>();
|
||||
let player = state.player();
|
||||
|
||||
let image = player.image();
|
||||
let name = player.name();
|
||||
let class = player.class();
|
||||
let level = player.level();
|
||||
let xp = player.xp();
|
||||
|
||||
// "First «alias» Last" with the optional parts dropped when empty.
|
||||
let display_name = move || {
|
||||
let n = name.get();
|
||||
let mut s = n.first.clone();
|
||||
if n.display_alias() {
|
||||
s.push_str(&format!(" «{}»", n.alias));
|
||||
}
|
||||
if n.display_last() {
|
||||
s.push_str(&format!(" {}", n.last));
|
||||
}
|
||||
s
|
||||
};
|
||||
|
||||
view! {
|
||||
<div class="player-card">
|
||||
<Show when=move || !image.get().is_empty()>
|
||||
<img src=move || image.get() alt="portrait" />
|
||||
</Show>
|
||||
<div class="player-meta">
|
||||
<div class="player-name">{display_name}</div>
|
||||
<div class="player-class">{move || class.get()}</div>
|
||||
<div class="player-level">{move || format!("уровень {}", level.get())}</div>
|
||||
<div class="player-xp">{move || format!("{} XP", xp.get())}</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@ -16,7 +16,7 @@ pub fn PreparedSpells () -> impl IntoView {
|
||||
|
||||
view! {
|
||||
<div class="prepared-spells">
|
||||
<h6>"prepared spells/подготовленные заклинания:"</h6>
|
||||
<h6>"Заготовленные заклинания:"</h6>
|
||||
<For
|
||||
// The key carries the slot state, so a slot whose state
|
||||
// changed re-renders (same convention as sidebar pips).
|
||||
|
||||
@ -24,14 +24,12 @@ pub mod numput;
|
||||
pub mod item;
|
||||
pub mod modal;
|
||||
|
||||
/// Character page (route `/`): player card, prepared spells and
|
||||
/// attunements (ROADMAP phase 2). The sidebar carries the detailed
|
||||
/// editors; the HP block with its Numput-style inputs lives there too.
|
||||
/// Character page (route `/`): prepared spells and attunements
|
||||
/// (ROADMAP phase 2). The sidebar carries the character editors.
|
||||
#[component]
|
||||
pub fn Character () -> impl IntoView {
|
||||
view! {
|
||||
<div class="character-page">
|
||||
<character::player_card::PlayerCard />
|
||||
<character::prepared_spells::PreparedSpells />
|
||||
<character::attunements::Attunements />
|
||||
</div>
|
||||
|
||||
@ -2,7 +2,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.
|
||||
/// All three numbers are editable `type="number"` inputs; AC sits
|
||||
/// next to them and is set manually.
|
||||
#[component]
|
||||
pub fn HitPoints () -> impl IntoView {
|
||||
let state = expect_context::<Context>();
|
||||
@ -11,6 +12,7 @@ pub fn HitPoints () -> impl IntoView {
|
||||
let hp = player.hp();
|
||||
let max_hp = player.max_hp();
|
||||
let temp_hp = player.temp_hp();
|
||||
let ac = player.ac();
|
||||
|
||||
// All three inputs parse via `utils::adjust_checked`; invalid text
|
||||
// restores the previous value.
|
||||
@ -26,13 +28,16 @@ pub fn HitPoints () -> impl IntoView {
|
||||
utils::adjust_checked(ev, temp_hp);
|
||||
};
|
||||
|
||||
let adjust_ac = move |ev: Targeted<Event, HtmlInputElement>| {
|
||||
utils::adjust_checked(ev, ac);
|
||||
};
|
||||
|
||||
|
||||
let pc_hp = move |a: u16, b: u16| -> String {
|
||||
format!("width: {}%", utils::filled_pc(a, b).min(100.))
|
||||
};
|
||||
|
||||
view! {
|
||||
<h6>hp/оз:</h6>
|
||||
<h6>"hp/оз, ac/КД:"</h6>
|
||||
<div class="hp">
|
||||
<div class="perm">
|
||||
<div style=move || pc_hp(hp.get(), max_hp.get()) class="bar"></div>
|
||||
@ -48,6 +53,10 @@ pub fn HitPoints () -> impl IntoView {
|
||||
<input min=0 title="Временные хитпойнты" id="temp_hp" type="number" class="header-input" on:input:target=move |e| adjust_temp_hp(e) prop:value=move || temp_hp.get() />
|
||||
</div>
|
||||
</div>
|
||||
<div class="ac">
|
||||
<span>"КД:"</span>
|
||||
<input min=0 title="Класс доспеха (вручную)" id="ac" type="number" class="header-input" on:input:target=move |e| adjust_ac(e) prop:value=move || ac.get() />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@ -30,9 +30,10 @@ impl Dashboard {
|
||||
temp_hp: 12,
|
||||
hp: 1,
|
||||
max_hp: 61,
|
||||
ac: 16,
|
||||
balance: Balance { platinum: 12, gold: 200, electrum: 1, silver: 101, copper: 129021021 },
|
||||
spell_slots: SpellSlots(core::array::from_fn(|i| SpellSlotLevel { used: 1, total: 9 - i as u8 })),
|
||||
attunements: Attunements([const { None }; 3]),
|
||||
attunements: Attunements(vec![None; 3]),
|
||||
hit_dice: HitDice { used: 1, kind: 20 },
|
||||
death_save_throws: DeathSaveThrows { failed: 1, succeeded: 3 },
|
||||
prepared: generate_prepared_spells_list(),
|
||||
|
||||
@ -101,10 +101,11 @@ impl Dashboard {
|
||||
temp_hp: 0,
|
||||
hp: 0,
|
||||
max_hp: 0,
|
||||
ac: 0,
|
||||
balance: Balance::default(),
|
||||
spell_slots: SpellSlots (core::array::from_fn(|_| SpellSlotLevel { used: 0, total: 0 })),
|
||||
inventory: Inventory::default(),
|
||||
attunements: Attunements (core::array::from_fn(|_| None)),
|
||||
attunements: Attunements (vec![None; 3]),
|
||||
prepared: PreparedSpells (Vec::new()),
|
||||
hit_dice: HitDice { used: 0, kind: 8 },
|
||||
death_save_throws: DeathSaveThrows { failed: 0, succeeded: 0 },
|
||||
@ -134,6 +135,10 @@ pub struct PlayerData {
|
||||
pub temp_hp: u16,
|
||||
pub hp: u16,
|
||||
pub max_hp: u16,
|
||||
/// Armor class; set manually by the player (no derived calculation
|
||||
/// yet). Missing in old saved JSON, so default to 0.
|
||||
#[serde(default)]
|
||||
pub ac: u8,
|
||||
pub balance: Balance,
|
||||
pub spell_slots: SpellSlots,
|
||||
pub inventory: Inventory,
|
||||
@ -248,9 +253,10 @@ pub struct SpellSlotLevel {
|
||||
pub total: u8,
|
||||
}
|
||||
|
||||
/// Three attunement slots (D&D rules allow three attuned items).
|
||||
#[derive(Clone, Debug, Store, Serialize, Deserialize)]
|
||||
pub struct Attunements (pub [Option<String>; 3]);
|
||||
/// Attunement slots (D&D rules allow three attuned items; the UI can
|
||||
/// add more slots for custom campaigns).
|
||||
#[derive(Clone, Debug, Store, Serialize, Deserialize, Default)]
|
||||
pub struct Attunements (pub Vec<Option<String>>);
|
||||
|
||||
|
||||
/// The spell list the character has prepared (or is learning).
|
||||
|
||||
@ -8,39 +8,8 @@
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.player-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 18px;
|
||||
|
||||
img {
|
||||
width: 92px;
|
||||
height: 92px;
|
||||
object-fit: cover;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 0 0 2px crimson;
|
||||
}
|
||||
|
||||
.player-meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.player-name {
|
||||
font-size: 1.5em;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.player-class, .player-level, .player-xp {
|
||||
font-weight: 300;
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
.prepared-spells, .attunements {
|
||||
h6 {
|
||||
text-transform: lowercase;
|
||||
font-size: 1.1em;
|
||||
margin: 0 0 10px;
|
||||
font-weight: 900;
|
||||
@ -100,9 +69,7 @@
|
||||
font-weight: 900;
|
||||
min-width: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
.prepared-spells {
|
||||
.add-slot {
|
||||
@include mixins.button;
|
||||
margin-top: 10px;
|
||||
|
||||
@ -134,6 +134,14 @@ aside {
|
||||
.val {
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.ac {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding-left: 10px;
|
||||
border-left: 1px solid rgba(white, 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
.hit-dice {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user