Fixed edition fixing / multiclass requirements. Refactored spell screen to be more readable

This commit is contained in:
boris committed 2026-02-27 20:20:11 +00:00
1 parent 4491e22573
commit 7712e971a8
5 files changed
+1721 -149

No files matched your search

+81 -6
View File
@@ -16,6 +16,12 @@ export interface CharacterEntityRef {
entityRef: string;
}
export interface CharacterClassSelection {
class: CharacterEntityRef;
subclass: CharacterEntityRef | null;
level: number;
}
export interface CharacterProfile {
name: string;
level: number;
@@ -94,6 +100,7 @@ export interface CharacterSteps {
export interface CharacterSelections {
class: CharacterEntityRef | null;
subclass: CharacterEntityRef | null;
classes: CharacterClassSelection[];
background: CharacterEntityRef | null;
species: CharacterEntityRef | null;
}
@@ -219,6 +226,7 @@ const DEFAULT_STEPS: CharacterSteps = {
const DEFAULT_SELECTIONS: CharacterSelections = {
class: null,
subclass: null,
classes: [],
background: null,
species: null
};
@@ -375,6 +383,63 @@ function normalizeEntityRef(value: unknown, expectedType: CharacterEntityType):
};
}
function normalizeClassSelection(value: unknown): CharacterClassSelection | null {
if (!isRecord(value)) {
return null;
}
const classRef = normalizeEntityRef(value.class, "class");
if (!classRef) {
return null;
}
const subclassRef = normalizeEntityRef(value.subclass, "subclass");
return {
class: classRef,
subclass: subclassRef,
level: toInteger(value.level, 1, 1, 20)
};
}
function getTotalClassLevel(classes: CharacterClassSelection[]): number {
const total = classes.reduce((sum, cls) => sum + cls.level, 0);
return Math.max(1, Math.min(20, total));
}
function normalizeClassSelections(
value: unknown,
fallbackClass: CharacterEntityRef | null,
fallbackSubclass: CharacterEntityRef | null,
fallbackLevel: number
): CharacterClassSelection[] {
const out: CharacterClassSelection[] = [];
const seen = new Set<string>();
if (Array.isArray(value)) {
for (const raw of value) {
const normalized = normalizeClassSelection(raw);
if (!normalized) {
continue;
}
if (seen.has(normalized.class.entityRef)) {
continue;
}
seen.add(normalized.class.entityRef);
out.push(normalized);
}
}
if (!out.length && fallbackClass) {
out.push({
class: fallbackClass,
subclass: fallbackSubclass,
level: Math.max(1, Math.min(20, fallbackLevel))
});
}
return out;
}
function normalizeProfile(value: unknown): CharacterProfile {
const profile = isRecord(value) ? value : {};
return {
@@ -521,11 +586,17 @@ function normalizeAbout(value: unknown): CharacterAboutStep {
};
}
function normalizeSelections(value: unknown): CharacterSelections {
function normalizeSelections(value: unknown, fallbackLevel: number): CharacterSelections {
const selections = isRecord(value) ? value : {};
const primaryClass = normalizeEntityRef(selections.class, "class");
const primarySubclass = normalizeEntityRef(selections.subclass, "subclass");
const classes = normalizeClassSelections(selections.classes, primaryClass, primarySubclass, fallbackLevel);
const firstClass = classes[0];
return {
class: normalizeEntityRef(selections.class, "class"),
subclass: normalizeEntityRef(selections.subclass, "subclass"),
class: firstClass?.class ?? primaryClass,
subclass: firstClass?.subclass ?? primarySubclass,
classes,
background: normalizeEntityRef(selections.background, "background"),
species: normalizeEntityRef(selections.species, "species")
};
@@ -549,8 +620,12 @@ function normalizeCharacterRecord(raw: unknown): CharacterDocument {
const root = isRecord(raw) ? raw : {};
const profile = normalizeProfile(root.profile);
const selections = normalizeSelections(root.selections);
const selections = normalizeSelections(root.selections, profile.level);
const steps = normalizeSteps(root.steps);
const normalizedProfile: CharacterProfile = {
...profile,
level: selections.classes.length ? getTotalClassLevel(selections.classes) : profile.level
};
const idCandidate = toString(root.id).trim();
const id = idCandidate || randomUUID();
@@ -561,7 +636,7 @@ function normalizeCharacterRecord(raw: unknown): CharacterDocument {
id,
createdAt: toIsoDate(root.createdAt, nowIso),
updatedAt: toIsoDate(root.updatedAt, nowIso),
profile,
profile: normalizedProfile,
selections,
steps,
notes: toString(root.notes).trim()
@@ -655,7 +730,7 @@ export function listCharacters(charactersDir: string): CharacterSummary[] {
id: character.id,
name: character.profile.name,
level: character.profile.level,
className: character.selections.class?.name ?? null,
className: character.selections.classes[0]?.class.name ?? character.selections.class?.name ?? null,
backgroundName: character.selections.background?.name ?? null,
speciesName: character.selections.species?.name ?? null,
createdAt: character.createdAt,
+7
View File
@@ -76,6 +76,12 @@ interface CharacterEntityRef {
entityRef: string;
}
interface CharacterClassSelection {
class: CharacterEntityRef;
subclass: CharacterEntityRef | null;
level: number;
}
interface CharacterProfile {
name: string;
level: number;
@@ -151,6 +157,7 @@ interface CharacterDocument {
selections: {
class: CharacterEntityRef | null;
subclass: CharacterEntityRef | null;
classes: CharacterClassSelection[];
background: CharacterEntityRef | null;
species: CharacterEntityRef | null;
};
+1515 -143
View File
File diff suppressed because it is too large. Load diff
+111
View File
@@ -1742,6 +1742,14 @@ body {
display: block;
min-height: 0;
aspect-ratio: 1 / 1;
cursor: pointer;
transition: border-color 120ms ease, box-shadow 120ms ease, transform 120ms ease;
}
.entity-tile:hover {
border-color: rgba(134, 183, 247, 0.88);
box-shadow: 0 0 0 1px rgba(121, 178, 249, 0.28);
transform: translateY(-1px);
}
.entity-tile-media {
@@ -1830,6 +1838,97 @@ body {
box-shadow: 0 0 0 1px rgba(252, 84, 84, 0.42);
}
.entity-tile.is-disabled {
opacity: 0.45;
filter: grayscale(0.5);
cursor: not-allowed;
}
.character-class-list {
border: 1px solid var(--line);
border-radius: 12px;
background: rgba(9, 14, 24, 0.72);
padding: 10px;
display: grid;
gap: 8px;
}
.character-class-list h4 {
margin: 0;
color: #eef4ff;
font-size: 0.96rem;
}
.character-class-list-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
gap: 8px;
}
.character-class-chip {
border: 1px solid var(--line);
border-radius: 10px;
background: rgba(7, 11, 20, 0.84);
padding: 8px;
display: grid;
gap: 8px;
}
.character-class-chip header {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 8px;
}
.character-class-chip header strong {
color: #eef4ff;
}
.character-class-chip header span {
font-size: 0.76rem;
color: var(--text-2);
}
.character-class-chip-badges {
display: inline-flex;
align-items: center;
gap: 6px;
}
.character-class-chip-role {
border: 1px solid rgba(73, 92, 120, 0.8);
border-radius: 999px;
padding: 1px 7px;
font-size: 0.68rem !important;
color: #c5d4ea !important;
background: rgba(19, 28, 42, 0.74);
}
.character-class-chip-role.is-primary {
border-color: rgba(51, 158, 102, 0.8);
color: #dcfff1 !important;
background: rgba(21, 108, 67, 0.64);
}
.character-class-chip-note {
margin: -2px 0 0;
font-size: 0.76rem;
color: var(--text-2);
}
.character-class-chip-actions {
display: flex;
align-items: center;
gap: 6px;
flex-wrap: wrap;
}
.character-class-chip-actions span {
font-size: 0.86rem;
color: var(--text-1);
}
.entity-popover-backdrop {
position: fixed;
inset: 0;
@@ -2101,10 +2200,12 @@ body {
.spell-level-group {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 8px;
}
.spell-level-group h5 {
grid-column: 1 / -1;
margin: 0;
color: #f2f5ff;
font-size: 0.9rem;
@@ -2127,6 +2228,10 @@ body {
background: linear-gradient(145deg, rgba(121, 39, 39, 0.64), rgba(25, 22, 33, 0.95));
}
.spell-pick-card.is-disabled {
opacity: 0.7;
}
.spell-pick-title {
font-size: 0.9rem;
font-weight: 700;
@@ -2289,6 +2394,12 @@ body {
}
}
@media (width <= 900px) {
.spell-level-group {
grid-template-columns: 1fr;
}
}
@media (width <= 900px) {
.entity-list-actions {
justify-content: stretch;
+7
View File
@@ -92,6 +92,12 @@ interface CharacterEntityRef {
entityRef: string;
}
interface CharacterClassSelection {
class: CharacterEntityRef;
subclass: CharacterEntityRef | null;
level: number;
}
interface CharacterProfile {
name: string;
level: number;
@@ -167,6 +173,7 @@ interface CharacterDocument {
selections: {
class: CharacterEntityRef | null;
subclass: CharacterEntityRef | null;
classes: CharacterClassSelection[];
background: CharacterEntityRef | null;
species: CharacterEntityRef | null;
};