Fixed edition fixing / multiclass requirements. Refactored spell screen to be more readable
This commit is contained in:
1 parent
4491e22573
commit
7712e971a8
5 files changed
+1700
-128
No files matched your search
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
+1494
-122
File diff suppressed because it is too large.
Load diff
@@ -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;
|
||||
|
||||
Vendored
+7
@@ -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;
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user