2 Commits

Author SHA1 Message Date
ce1f0fee41 feat: Add class levels; only allow for 3 primary classes
All checks were successful
Deploy / deploy (push) Successful in 49s
2026-06-28 12:32:45 -04:00
4a61eeaf46 fix: Add empty content from 161 2026-06-28 12:27:52 -04:00
3 changed files with 108 additions and 11 deletions

View File

@@ -1,4 +1,54 @@
<em>Classic Characters</em> starting on Every Class provides the following information:
<a href="/books/core/#page-172">page 172</a>: these level 5 archetypes
<em>Classic Characters</em> starting on <ul>
<a href="/books/core/#page-172">page 172</a>: these level 5 archetypes <li>
A short description of the Class, covering its general role and premise.
</li>
<li>
A list of questions that will help you define the goals, past experiences,
and behavior of your character. You dont have to answer all of them
straight away — in fact, it's much better to let these elements emerge
during play, through your characters actions and choices. You also can
decide to ignore some of these questions if your character concept isn't
strongly tied to that particular Class.
</li>
<li>
A list of free benefits that your character will gain simply by acquiring
the Class, such as the ability to equip martial equipment or a slight
increase to Hit Points, Mind Points or Inventory Points. If two or more of
your Classes give you the same free benefits, they will stack!
</li>
<li>
A set of five Class Skills that are unique to the Class in question.
Whenever you invest a level in a Class, you acquire one of its Skills; any
Skills marked with a (ç) can be acquired multiple times, usually making them
stronger or more flexible. The number following the (ç) symbol tells you the
maximum number of times that Skill can be acquired — for instance, a (ç5)
Skill can be taken up to five times. When the text of a Skill mentions【
SL】, that is the Skill Level, which is to say how many times you have
acquired that Skill.
</li>
</ul>
<p class="example">
When choosing Classes, keep in mind these simply represent your current
abilities — they will not impact your character's role in the narrative unless
you want them to, and you may steer your character in entirely different
directions later on.
</p>
<p>
For instance, a knight might have invested levels in Elementalist to learn a
few combat spells, without necessarily being a student of elemental magic.
</p>
<div class="example">
If you are short on time or ideas, feel free to take a look at the Classic
Characters starting on page 172: these level 5 archetypes will prove solid in
play. Robert is intrigued by the Loremaster and Orator Classes but also
realizes Camilla trained with some of the best fighters in the land, which
means Weaponmaster would be another fitting choice. In the end, he decides to
invest two levels in Orator (acquiring Encourage and Unexpected Ally), then
three levels in Weaponmaster (acquiring Bone Crusher twice and then the
powerful Counterattack).
</div>

View File

@@ -82,6 +82,7 @@ interface Bond {
interface ClassEntry { interface ClassEntry {
name: string; name: string;
level: number;
benefits: string; benefits: string;
skills: string; skills: string;
} }
@@ -324,10 +325,11 @@ export default function CharacterSheet() {
bo: bonds.map((b) => ({ n: b.name, f: b.feelings })), bo: bonds.map((b) => ({ n: b.name, f: b.feelings })),
pc: primaryClasses.map((c) => ({ pc: primaryClasses.map((c) => ({
n: c.name, n: c.name,
lv: c.level,
b: c.benefits, b: c.benefits,
s: c.skills, s: c.skills,
})), })),
oc: otherClasses.map((c) => ({ n: c.name, b: c.benefits, s: c.skills })), oc: otherClasses.map((c) => ({ n: c.name, lv: c.level, b: c.benefits, s: c.skills })),
sp: spells.map((s) => ({ sp: spells.map((s) => ({
n: s.name, n: s.name,
cl: s.spellClass, cl: s.spellClass,
@@ -418,6 +420,7 @@ export default function CharacterSheet() {
setPrimaryClasses( setPrimaryClasses(
rawPrimary.map((c: SavedData) => ({ rawPrimary.map((c: SavedData) => ({
name: c.n ?? c.name ?? "", name: c.n ?? c.name ?? "",
level: c.lv ?? c.level ?? 1,
benefits: c.b ?? c.benefits ?? "", benefits: c.b ?? c.benefits ?? "",
skills: c.s ?? c.skills ?? "", skills: c.s ?? c.skills ?? "",
})), })),
@@ -428,6 +431,7 @@ export default function CharacterSheet() {
setOtherClasses( setOtherClasses(
rawOther.map((c: SavedData) => ({ rawOther.map((c: SavedData) => ({
name: c.n ?? c.name ?? "", name: c.n ?? c.name ?? "",
level: c.lv ?? c.level ?? 1,
benefits: c.b ?? c.benefits ?? "", benefits: c.b ?? c.benefits ?? "",
skills: c.s ?? c.skills ?? "", skills: c.s ?? c.skills ?? "",
})), })),
@@ -1237,8 +1241,7 @@ export default function CharacterSheet() {
<div className="grid-2" style={{ marginBottom: 20 }}> <div className="grid-2" style={{ marginBottom: 20 }}>
<div className="section"> <div className="section">
<div className="section-title"> <div className="section-title">
<span className="icon"></span> Primary Classes (up to 3 levels <span className="icon"></span> Primary Classes (up to 3)
each)
</div> </div>
{primaryClasses.map((cls, idx) => ( {primaryClasses.map((cls, idx) => (
<div key={idx} className="class-block"> <div key={idx} className="class-block">
@@ -1288,9 +1291,27 @@ export default function CharacterSheet() {
padding: "6px 10px", padding: "6px 10px",
borderTop: "1px solid var(--border)", borderTop: "1px solid var(--border)",
display: "flex", display: "flex",
justifyContent: "flex-end", alignItems: "center",
justifyContent: "space-between",
}} }}
> >
<label style={{ display: "flex", alignItems: "center", gap: "6px", fontSize: "0.85em" }}>
Level
<input
type="number"
min={1}
max={10}
value={cls.level ?? 1}
onChange={(e) =>
setPrimaryClasses((prev) =>
prev.map((c, i) =>
i === idx ? { ...c, level: Number(e.target.value) } : c,
),
)
}
style={{ width: "50px" }}
/>
</label>
<button <button
className="spell-del-btn" className="spell-del-btn"
onClick={() => onClick={() =>
@@ -1306,10 +1327,11 @@ export default function CharacterSheet() {
))} ))}
<button <button
className="add-btn" className="add-btn"
disabled={primaryClasses.length >= 3}
onClick={() => onClick={() =>
setPrimaryClasses((prev) => [ setPrimaryClasses((prev) => [
...prev, ...prev,
{ name: "", benefits: "", skills: "" }, { name: "", level: 1, benefits: "", skills: "" },
]) ])
} }
> >
@@ -1367,9 +1389,27 @@ export default function CharacterSheet() {
padding: "6px 10px", padding: "6px 10px",
borderTop: "1px solid var(--border)", borderTop: "1px solid var(--border)",
display: "flex", display: "flex",
justifyContent: "flex-end", alignItems: "center",
justifyContent: "space-between",
}} }}
> >
<label style={{ display: "flex", alignItems: "center", gap: "6px", fontSize: "0.85em" }}>
Level
<input
type="number"
min={1}
max={10}
value={cls.level ?? 1}
onChange={(e) =>
setOtherClasses((prev) =>
prev.map((c, i) =>
i === idx ? { ...c, level: Number(e.target.value) } : c,
),
)
}
style={{ width: "50px" }}
/>
</label>
<button <button
className="spell-del-btn" className="spell-del-btn"
onClick={() => onClick={() =>
@@ -1388,7 +1428,7 @@ export default function CharacterSheet() {
onClick={() => onClick={() =>
setOtherClasses((prev) => [ setOtherClasses((prev) => [
...prev, ...prev,
{ name: "", benefits: "", skills: "" }, { name: "", level: 1, benefits: "", skills: "" },
]) ])
} }
> >

View File

@@ -852,6 +852,13 @@ input[type="number"] {
color: var(--text-bright); color: var(--text-bright);
} }
.add-btn:disabled {
color: var(--text-dim, #555);
border-color: var(--text-dim, #555);
cursor: not-allowed;
opacity: 0.45;
}
.spell-picker-overlay { .spell-picker-overlay {
position: fixed; position: fixed;
inset: 0; inset: 0;