Add explanation tooltips for derived values
Hover/focus/tap tooltips show how attributes, derived stats, skill target numbers and limits, capacity, carry weight and point pools are calculated. Used in the wizard pages, the side sheet and the summary. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HJhXuLD2Z6hv3Wqeq9UL9K
This commit is contained in:
+18
-15
@@ -1,6 +1,8 @@
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { applyPreset, applyProfession, compute, effectiveRules, emptyCharacter, kitItems, normalize, resetStep, setProfessionPicks } from './calc.js'
|
||||
import SheetPanel from './SheetPanel.jsx'
|
||||
import Tip from './Tip.jsx'
|
||||
import { explainAttr, explainAttrPool, explainCapacity, explainCarry, explainDerived, explainProfPool, explainSkill, explainSkillPool } from './explain.js'
|
||||
|
||||
const STEPS = ['Concept', 'Attributes', 'Profession', 'Skills', 'Talents', 'Mutations', 'Equipment', 'Summary']
|
||||
const SOURCE = { noxium: 'Noxium', diesium: 'Diesium', hybrid: 'Hybrid', precyber: 'Pre-meteor cybernetics' }
|
||||
@@ -40,6 +42,7 @@ export default function Creator({ rules: baseRules, user, loadRequest, clearLoad
|
||||
const sub = cls?.subclasses.find((x) => x.id === ch.subclass_id)
|
||||
const set = (patch) => setCh((x) => ({ ...x, ...patch }))
|
||||
const cr = rules.creation
|
||||
const ctx = { ch, rules, c }
|
||||
|
||||
useEffect(() => { onName?.(ch.name) }, [ch.name]) // eslint-disable-line
|
||||
|
||||
@@ -190,7 +193,7 @@ export default function Creator({ rules: baseRules, user, loadRequest, clearLoad
|
||||
<>
|
||||
<h2>Attributes</h2>
|
||||
<p className="muted">All attributes start at {cr.attr_start}. Distribute {cr.attr_points} points (at most +{cr.attr_max_raise} per attribute, cap {cr.attr_max}).</p>
|
||||
<div className={`pool ${c.attrLeft < 0 ? 'over' : ''}`}>Points left: <strong>{c.attrLeft}</strong> / {cr.attr_points}</div>
|
||||
<div className={`pool ${c.attrLeft < 0 ? 'over' : ''}`}><Tip text={explainAttrPool(ctx)}>Points left</Tip>: <strong>{c.attrLeft}</strong> / {cr.attr_points}</div>
|
||||
<div className="attrs">
|
||||
{rules.attributes.map((a) => {
|
||||
const v = ch.attributes[a.id]
|
||||
@@ -198,14 +201,14 @@ export default function Creator({ rules: baseRules, user, loadRequest, clearLoad
|
||||
const setV = (n) => set({ attributes: { ...ch.attributes, [a.id]: n } })
|
||||
return (
|
||||
<div key={a.id} className="attr card">
|
||||
<div><strong>{a.name}</strong> <span className="muted small">{a.short}</span><div className="small muted">{a.desc}</div></div>
|
||||
<div><strong>{a.name}</strong> <span className="muted small">{a.short}</span><Tip icon text={explainAttr(a, ctx)} /><div className="small muted">{a.desc}</div></div>
|
||||
<div className="stepper">
|
||||
<button onClick={() => setV(Math.max(v - 5, cr.attr_start))} aria-label={`${a.name} minus 5`}>−5</button>
|
||||
<button onClick={() => setV(Math.max(v - 1, cr.attr_start))} aria-label={`${a.name} minus 1`}>−</button>
|
||||
<span className="val">{v}</span>
|
||||
<button onClick={() => setV(Math.min(v + 1, cap))} disabled={c.attrLeft <= 0} aria-label={`${a.name} plus 1`}>+</button>
|
||||
<button onClick={() => setV(Math.min(v + Math.min(5, c.attrLeft), cap))} disabled={c.attrLeft <= 0} aria-label={`${a.name} plus 5`}>+5</button>
|
||||
<span className="mod" title="Modifier">Mod {c.mods[a.id]}</span>
|
||||
<Tip text={explainAttr(a, ctx)}><span className="mod">Mod {c.mods[a.id]}</span></Tip>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -263,11 +266,11 @@ export default function Creator({ rules: baseRules, user, loadRequest, clearLoad
|
||||
(free if your profession grants them). The pool size comes from the rules preset.
|
||||
</p>
|
||||
<div className="pools">
|
||||
<div className={`pool ${c.skillLeft < 0 ? 'over' : ''}`} title="Change the pool size in the rules preset on the Concept page">
|
||||
Points left: <strong>{c.skillLeft}</strong> / {cr.skill_points_default}
|
||||
<div className={`pool ${c.skillLeft < 0 ? 'over' : ''}`}>
|
||||
<Tip text={[...explainSkillPool(ctx), 'The pool size comes from the rules preset (Concept page).']}>Points left</Tip>: <strong>{c.skillLeft}</strong> / {cr.skill_points_default}
|
||||
</div>
|
||||
<div className={`pool ${c.profLeft < 0 ? 'over' : ''}`} title="Two picks on the same skill make it Expertise">
|
||||
Proficiency picks left: <strong>{c.profLeft}</strong> / {c.profAllowed}
|
||||
<div className={`pool ${c.profLeft < 0 ? 'over' : ''}`}>
|
||||
<Tip text={explainProfPool(ctx)}>Proficiency picks left</Tip>: <strong>{c.profLeft}</strong> / {c.profAllowed}
|
||||
</div>
|
||||
</div>
|
||||
<p className="small muted">
|
||||
@@ -283,7 +286,7 @@ export default function Creator({ rules: baseRules, user, loadRequest, clearLoad
|
||||
return (
|
||||
<div key={s.id} className={`skill ${s.usable ? '' : 'locked'}`} title={s.desc}>
|
||||
<div>
|
||||
<strong>{s.name}</strong> {s.placeholder && <Badge />}
|
||||
<strong>{s.name}</strong> {s.placeholder && <Badge />}<Tip icon text={explainSkill(s, ctx)} />
|
||||
<div className="small muted">
|
||||
{s.attrs.map((id) => rules.attributes.find((a) => a.id === id).short).join('/')} · {s.type === 'basic' ? 'Basic' : s.usable ? 'Advanced (unlocked)' : 'Advanced (locked)'}
|
||||
{s.pips === 1 && ' · Proficient'}{s.pips === 2 && ' · Expertise'}
|
||||
@@ -312,7 +315,7 @@ export default function Creator({ rules: baseRules, user, loadRequest, clearLoad
|
||||
<button onClick={() => setPts(s.points - 1)} aria-label={`${s.name} minus`}>−</button>
|
||||
<span className="val">{s.points}</span>
|
||||
<button onClick={() => setPts(s.points + 1)} disabled={s.points >= s.max || c.skillLeft <= 0} aria-label={`${s.name} plus`}>+</button>
|
||||
<span className="mod" title="Target number">{s.total}</span>
|
||||
<Tip text={explainSkill(s, ctx)}><span className="mod">{s.total}</span></Tip><span className="small muted" title="Most points you can add"> max {s.max}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -379,7 +382,7 @@ export default function Creator({ rules: baseRules, user, loadRequest, clearLoad
|
||||
<>
|
||||
<h2>Equipment</h2>
|
||||
<p className="muted">Pick everything your character carries. Ask your GM what is allowed. Use + and − on a tile to change the amount.</p>
|
||||
<div className={`pool ${overWeight ? 'over' : ''}`}>Carried: <strong>{c.weight}</strong> / {c.carryMax} kg (Strength Mod x {cr.carry_per_strength_mod}){overWeight && ' – overloaded'}</div>
|
||||
<div className={`pool ${overWeight ? 'over' : ''}`}><Tip text={explainCarry(ctx)}>Carried</Tip>: <strong>{c.weight}</strong> / {c.carryMax} kg (Strength Mod x {cr.carry_per_strength_mod}){overWeight && ' – overloaded'}</div>
|
||||
{rules.equipment_categories.map((cat) => {
|
||||
const list = items.filter((e) => e.cat === cat.id)
|
||||
const n = list.reduce((sum, e) => sum + (ch.kit.extras[e.id] || 0), 0)
|
||||
@@ -435,18 +438,18 @@ export default function Creator({ rules: baseRules, user, loadRequest, clearLoad
|
||||
<div className="sheet-cols">
|
||||
<div>
|
||||
<h4>Attributes</h4>
|
||||
<table><tbody>{rules.attributes.map((a) => <tr key={a.id}><td>{a.name}</td><td>{c.attrs[a.id]}</td><td className="muted">Mod {c.mods[a.id]}</td></tr>)}</tbody></table>
|
||||
<table><tbody>{rules.attributes.map((a) => <tr key={a.id}><td><Tip text={explainAttr(a, ctx)}>{a.name}</Tip></td><td>{c.attrs[a.id]}</td><td className="muted">Mod {c.mods[a.id]}</td></tr>)}</tbody></table>
|
||||
<h4>Derived stats</h4>
|
||||
<table><tbody>{rules.derived.map((d) => <tr key={d.id}><td>{d.name}</td><td>{c.derived[d.id]}</td></tr>)}</tbody></table>
|
||||
<table><tbody>{rules.derived.map((d) => <tr key={d.id}><td><Tip text={explainDerived(d, ctx)}>{d.name}</Tip></td><td>{c.derived[d.id]}</td></tr>)}</tbody></table>
|
||||
</div>
|
||||
<div>
|
||||
<h4>Talents</h4>
|
||||
<ul className="desclist">{rules.talents.filter((t) => ch.talents.includes(t.id)).map((t) => <li key={t.id}><strong>{t.name}</strong><div className="rowdesc">{t.desc}</div></li>)}</ul>
|
||||
{!ch.talents.length && <p>–</p>}
|
||||
<h4>Mutations / implants ({c.capacityUsed}/{c.capacityMax})</h4>
|
||||
<h4><Tip text={explainCapacity(ctx)}>Mutations / implants ({c.capacityUsed}/{c.capacityMax})</Tip></h4>
|
||||
<ul className="desclist">{rules.augments.filter((a) => ch.augments.includes(a.id)).map((a) => <li key={a.id}><strong>{a.name}</strong> <span className="muted small">cost {a.cost}</span><div className="rowdesc">{a.desc}</div></li>)}</ul>
|
||||
{!ch.augments.length && <p>–</p>}
|
||||
<h4>Equipment ({c.weight}/{c.carryMax} kg)</h4>
|
||||
<h4><Tip text={explainCarry(ctx)}>Equipment ({c.weight}/{c.carryMax} kg)</Tip></h4>
|
||||
<ul className="desclist">{gearLines.map((g) => <li key={g.item.id}><strong>{g.item.name}{g.qty > 1 ? ` x${g.qty}` : ''}</strong><div className="rowdesc">{[g.item.stat, `${Math.round(g.item.kg * g.qty * 10) / 10} kg`, g.item.note].filter(Boolean).join(' · ')}</div></li>)}</ul>
|
||||
{!gearLines.length && <p>–</p>}
|
||||
{ch.gear.other && <p className="small"><strong>Other:</strong> {ch.gear.other}</p>}
|
||||
@@ -460,7 +463,7 @@ export default function Creator({ rules: baseRules, user, loadRequest, clearLoad
|
||||
<table><tbody>
|
||||
{c.skills.filter((s) => s.group === g.id).map((s) => (
|
||||
<tr key={s.id} className={s.usable ? '' : 'dim'}>
|
||||
<td>{s.name}{s.type === 'advanced' && <span className="muted small"> (adv.)</span>}</td>
|
||||
<td><Tip text={explainSkill(s, ctx)}>{s.name}</Tip>{s.type === 'advanced' && <span className="muted small"> (adv.)</span>}</td>
|
||||
<td>{s.total}</td>
|
||||
<td className="muted">{s.pips === 2 ? 'Expertise' : s.pips === 1 ? 'Proficient' : !s.usable ? 'Locked' : s.points ? `+${s.points}` : ''}</td>
|
||||
</tr>
|
||||
|
||||
Reference in New Issue
Block a user