{label &&
);
}
function Section({ title, children, style }) {
return (
{label}
}
{value}
{title}
{children}
{/* Hidden file input */}
{/* Toast notification */}
{toastMsg && (
{/* Toolbar */}
{(slots.length === 0 ? [{ key: “default”, name: state.charName || “Default” }] : slots).map(slot => (
)}
{/* Tabs */}
{toastMsg}
)}
{/* Header */}
WARHAMMER FANTASY
ROLEPLAY — 4TH EDITION
{state.charName || “Unnamed Character”}
{/* Character switcher */}
{/* Export/Import */}
{/* Slot manager dropdown */}
{showSlotMgr && (
Auto-saves • Share .json files between players
Saved Characters
slot.key !== activeSlot && switchSlot(slot.key)}>
{slot.key === activeSlot ? “▸ ” : “”}{slot.name || “Unnamed”}
{slots.length > 1 && (
)}
))}
{tabs.map((t, i) => (
))}
{/* ==================== TAB 0: CHARACTER ==================== */}
{state.activeTab === 0 && (
set(“charName”, v)} />
set(“className”, v)} />
set(“career”, v)} />
set(“level”, v)} />
set(“careerPath”, v)} />
set(“age”, v)} />
set(“height”, v)} />
set(“hair”, v)} />
set(“eyes”, v)} />
set(“trait”, v)} />
set(“motivation”, v)} />
set(“socialStanding”, v)} max={7} />
set(“shortAmbition”, v)} />
set(“longAmbition”, v)} />
set(“fate”, v)} style={{width:”100%”}} />
set(“fortune”, v)} style={{width:”100%”}} />
set(“resilience”, v)} style={{width:”100%”}} />
set(“resolve”, v)} style={{width:”100%”}} />
set(“hardy”, v)} style={{width:”100%”}} />
set(“corruption”, v)} style={{width:”100%”}} />
set(“maxCorruption”, v)} style={{width:”100%”}} />
set(“xpCurrent”, v)} style={{width:”100%”}} />
set(“xpTotal”, v)} style={{width:”100%”}} />
)}
{/* ==================== TAB 1: CHARACTERISTICS & SKILLS ==================== */}
{state.activeTab === 1 && (
)}
{/* ==================== TAB 2: COMBAT & EQUIPMENT ==================== */}
{state.activeTab === 2 && (
{/* Quick Reference */}
{[“1–5: 25xp each”, “6–10: 30xp each”, “11–15: 40xp each”, “16–20: 50xp each”, “21–25: 70xp each”,
“26–30: 90xp each”, “31–35: 120xp each”, “36–40: 150xp each”, “41–45: 190xp each”, “46–50: 230xp each”
].map((s,i) => (
{[“1–5: 10xp each”, “6–10: 15xp each”, “11–15: 20xp each”, “16–20: 30xp each”, “21–25: 40xp each”,
“26–30: 60xp each”, “31–35: 80xp each”, “36–40: 110xp each”, “41–45: 140xp each”, “46–50: 180xp each”
].map((s,i) => (
{/* Data Management */}
)}
);
}
Character Name
Species
Class
Career
Level
Career Path
Age
Height
Hair
Eyes
Distinguishing Trait
Motivation
Tier
Standing
Short Ambition
Long Ambition
Fate
Fortune (current)
Spend Fortune to: Re-roll a failed test • Add +1 SL • Change Initiative
Spend Fate to: Survive death or critical injury
Resilience
Resolve (current)
Spend Resolve to: Become immune to Psychology (1 round) • Eliminate 1 condition • Ignore modifiers from critical injury
Spend Resilience to: Avoid 1 Corruption • Choose result of a roll
Hardy Ranks
Current Wounds
set(“currentWounds”, parseInt(e.target.value))}
style={{ flex:1, accentColor: colors.accent }} />
set(“currentWounds”, v)}
max={maxWounds} style={{width:”50px”}} />
/ {maxWounds}
0 ? (Math.min(state.currentWounds, maxWounds)/maxWounds)*100 : 0}%`,
background: state.currentWounds / maxWounds > 0.5 ? colors.success :
state.currentWounds / maxWounds > 0.25 ? colors.accentDark : colors.danger,
}} />
Formula: SB({sb}) + 2×TB({2*tb}) + WPB({wpb}){state.hardy > 0 ? ` + Hardy(${state.hardy}×TB=${state.hardy*tb})` : “”} = {maxWounds}
Corruption
Max Corruption
XP Current
XP Total
Mutations & Psychology
XP Costs per 5 advances: 25 / 30 / 40 / 50 / 70 / 90 / 120 / 150 / 190 / 230
| {CHAR_KEYS.map(k => ( | {CHAR_MAP[k]} | ))}
|---|---|
| Initial | {CHAR_KEYS.map(k => (
|
))}
| Advances | {CHAR_KEYS.map(k => (
|
))}
| Total | {CHAR_KEYS.map(k => (
{charTotals[k]}
|
))}
| Bonus | {CHAR_KEYS.map(k => (
{charBonuses[k]}
|
))}
XP Costs per 5 advances: 10 / 15 / 20 / 30 / 40 / 60 / 80 / 110 / 140 / 180
| Skill | Char | Base | Adv (+) | Total |
|---|---|---|---|---|
| {skill.name} | {CHAR_MAP[skill.char]} | {charTotals[skill.char]} |
|
{basicSkillTotals[skill.name]}
|
Characteristic is only added to total when you have at least 1 advance (trained).
| Skill | Char | Adv (+) | Total |
|---|---|---|---|
|
|
|
{advSkillTotals[i] > 0 ?
{advSkillTotals[i]} :
—
}
|
{/* Derived Combat Stats */}
set(“movement”, v)} style={{width:”100%”}} max={10} />
{state.weapons.map((w, i) => (
{
const a = […state.weapons]; a[i] = {…a[i], name:v}; set(“weapons”, a);
}} />
{
const a = […state.weapons]; a[i] = {…a[i], group:v}; set(“weapons”, a);
}} placeholder=”e.g. Melee (Basic)” />
{
const a = […state.weapons]; a[i] = {…a[i], damage:v}; set(“weapons”, a);
}} />
{
const a = […state.weapons]; a[i] = {…a[i], range:v}; set(“weapons”, a);
}} style={{ width:”70px” }} />
{
const a = […state.weapons]; a[i] = {…a[i], enc:v}; set(“weapons”, a);
}} />
{
const a = […state.weapons]; a[i] = {…a[i], qualities:v}; set(“weapons”, a);
}} />
))}
set(“shield”, {…state.shield, name:v})} />
set(“shield”, {…state.shield, ap:v})} />
set(“brass”, v)} style={{width:”100%”}} />
set(“silver”, v)} style={{width:”100%”}} />
set(“gold”, v)} style={{width:”100%”}} />
)}
{/* ==================== TAB 3: TALENTS & TRAPPINGS ==================== */}
{state.activeTab === 3 && (
Movement
1 Action / 1 Movement / 1 Free Action per turn
Attack — Basic attack with WS/BS | Charge! — Move and attack (+10 WS) | Run — Athletics roll (+20) to Move ×4 | Defend — +20 to dodge/parry until next round | Special — Use a skill or talent | Evaluate — +2 SL on any skill (requires narration)
Attack — Basic attack with WS/BS | Charge! — Move and attack (+10 WS) | Run — Athletics roll (+20) to Move ×4 | Defend — +20 to dodge/parry until next round | Special — Use a skill or talent | Evaluate — +2 SL on any skill (requires narration)
Weapon Name
Group
Dmg (+SB)
Range
Enc
Qualities / Flaws
Shield
AP
| Hit Loc | Armour Name | AP | Qualities | Enc | Worn |
|---|---|---|---|---|---|
|
{loc.range}
{loc.name}
|
|
|
|
|
{ set(“armour”, { …state.armour, [loc.name]: { …state.armour[loc.name], equipped:e.target.checked } }); }} style={{ accentColor: colors.accent, width:”16px”, height:”16px”, cursor:”pointer” }} /> |
Worn armour has 0 Encumbrance. Uncheck “Worn” to add its Enc to carried total.
Brass (D)
Silver (SS)
Gold (GC)
12D = 1SS | 20SS = 1GC
Weapons:
{encumbrance.weapon}
Armour (carried):
{encumbrance.armour}
Trappings:
{encumbrance.trappings}
Max (SB+TB):
{encumbrance.max}
encumbrance.max ? colors.danger : colors.highlight,
color: encumbrance.total > encumbrance.max ? “#ff6b6b” : colors.accent,
border: `1px solid ${encumbrance.total > encumbrance.max ? “#8b2020” : colors.borderGold}`,
}}>
Total: {encumbrance.total} / {encumbrance.max}
{encumbrance.total > encumbrance.max &&
OVERBURDENED! −1 Movement per excess Enc
}
100 XP per talent rank. Some talents (e.g. Hardy, Strong Back, Sturdy) auto-modify calculations.
| Talent | Description / Effect | Rank |
|---|---|---|
|
|
|
|
| Item | Enc | Carried |
|---|---|---|
|
|
|
{ const a = […state.trappings]; a[i] = {…a[i], carried:e.target.checked}; set(“trappings”, a); }} style={{ accentColor: colors.accent, width:”16px”, height:”16px”, cursor:”pointer” }} /> |
Characteristic Advances
| {s} |
Skill Advances
| {s} |
Talents: 100 XP per rank
Sharing with your group: Use “Export” to download a .json file, then send it to your GM or fellow players via Discord, email, etc. They can “Import” it to load your character on their device. Clipboard Copy/Paste works too for quick sharing.