import { useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react'
import Tip from './Tip.jsx'
import { compute, effectiveRules, kitItems, normalize } from './calc.js'
import { explainAttr, explainCapacity, explainCarry, explainDerived, explainSkill } from './explain.js'
const PAPER_PX = 794 // 210 mm at 96 dpi
// Light/dark choice for the sheet, shared by all sheets and switches on the page and remembered in the browser
const THEME_KEY = 'aw_sheettheme'
const listeners = new Set()
const readTheme = () => { try { return localStorage.getItem(THEME_KEY) === 'dark' } catch { return false } }
let darkNow = readTheme()
export function useSheetTheme() {
const dark = useSyncExternalStore((cb) => { listeners.add(cb); return () => listeners.delete(cb) }, () => darkNow)
const set = (d) => { darkNow = d; try { localStorage.setItem(THEME_KEY, d ? 'dark' : 'light') } catch { /* storage may be blocked */ } listeners.forEach((f) => f()) }
return [dark, set]
}
export function ThemeSwitch() {
const [dark, setDark] = useSheetTheme()
return (
)
}
// Opens the sheet pages in a clean window (only the paper, A4, no margins) and starts the print dialog:
// "Save as PDF" there gives an A4 PDF that looks exactly like the screen.
export function printSheet(root, title) {
const win = window.open('', '_blank')
if (!win) { window.alert('Please allow pop-ups for this site to print the sheet.'); return }
const clone = root.cloneNode(true)
const src = root.querySelectorAll('textarea'), dst = clone.querySelectorAll('textarea')
dst.forEach((t, i) => { t.textContent = src[i].value }) // typed text does not survive cloning on its own
clone.style.zoom = ''
clone.classList.remove('dark') // printed pages are always light
const head = [...document.querySelectorAll('link[rel=stylesheet], style')].map((n) => n.outerHTML).join('')
win.document.open()
win.document.write(`
${(title || 'Character').replace(/[<>&]/g, '')}${head}
${clone.outerHTML}`)
win.document.close()
const go = () => { win.focus(); win.print() }
const links = [...win.document.querySelectorAll('link[rel=stylesheet]')]
let pending = links.length
if (!pending) return go()
links.forEach((l) => { l.addEventListener('load', () => { if (--pending === 0) setTimeout(go, 150) }); l.addEventListener('error', () => { if (--pending === 0) go() }) })
}
// Ruled note area. With onChange (own sheet in the summary or the lobby) it can be typed into, otherwise it just shows the text.
function Notes({ title, value, onChange }) {
return (
{title}
{onChange
?
)
}
// Scales the A4 pages down when the space is narrower than the paper
function Fit({ children, rootRef, dark }) {
const wrap = useRef(null)
const [z, setZ] = useState(1)
useLayoutEffect(() => {
const f = () => wrap.current && setZ(Math.min(1, wrap.current.clientWidth / PAPER_PX))
f()
const ro = new ResizeObserver(f)
ro.observe(wrap.current)
return () => ro.disconnect()
}, [])
return
{children}
}
const Pips = ({ n, locked }) => (
= 1 ? 'on' : ''} />= 2 ? 'on' : ''} />
)
// The character sheet as A4 pages (page 1: attributes and skills, page 2: talents, mutations, equipment, notes).
export default function PaperSheet({ data, baseRules, tools, bare, onRoll, onNotes }) {
const ch = useMemo(() => normalize(data, baseRules), [data, baseRules])
const rules = useMemo(() => effectiveRules(baseRules, ch), [baseRules, ch])
const c = useMemo(() => compute(ch, rules), [ch, rules])
const x = { ch, rules, c }
const root = useRef(null)
const [dark] = useSheetTheme()
const prof = rules.professions.find((p) => p.id === ch.profession)
const cls = rules.classes.find((k) => k.id === ch.class_id)
const sub = cls?.subclasses.find((s) => s.id === ch.subclass_id)
const preset = ch.preset === 'custom' ? 'Custom' : baseRules.presets?.find((p) => p.id === ch.preset)?.name || 'Custom'
const short = (s) => rules.attributes.filter((a) => s.attrs.includes(a.id)).map((a) => a.short).join('/')
const talents = rules.talents.filter((t) => ch.talents.includes(t.id))
const augments = rules.augments.filter((a) => ch.augments.includes(a.id))
const gear = kitItems(ch, rules)
const name = ch.name || 'Nameless'
const sign = (n) => (n > 0 ? `+${n}` : `${n}`)
// With onRoll (own character in a lobby) attributes and usable skills can be clicked to roll a check
const roll = (kind, id, label, ok = true) => (onRoll && ok ? {
className: 'rollable', role: 'button', tabIndex: 0,
onClick: () => onRoll(kind, id, label), onKeyDown: (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onRoll(kind, id, label) } },
} : {})
const Field = ({ label, children }) =>