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
+1721
-149
No files matched your search
@@ -16,6 +16,12 @@ export interface CharacterEntityRef {
|
|||||||
entityRef: string;
|
entityRef: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CharacterClassSelection {
|
||||||
|
class: CharacterEntityRef;
|
||||||
|
subclass: CharacterEntityRef | null;
|
||||||
|
level: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface CharacterProfile {
|
export interface CharacterProfile {
|
||||||
name: string;
|
name: string;
|
||||||
level: number;
|
level: number;
|
||||||
@@ -94,6 +100,7 @@ export interface CharacterSteps {
|
|||||||
export interface CharacterSelections {
|
export interface CharacterSelections {
|
||||||
class: CharacterEntityRef | null;
|
class: CharacterEntityRef | null;
|
||||||
subclass: CharacterEntityRef | null;
|
subclass: CharacterEntityRef | null;
|
||||||
|
classes: CharacterClassSelection[];
|
||||||
background: CharacterEntityRef | null;
|
background: CharacterEntityRef | null;
|
||||||
species: CharacterEntityRef | null;
|
species: CharacterEntityRef | null;
|
||||||
}
|
}
|
||||||
@@ -219,6 +226,7 @@ const DEFAULT_STEPS: CharacterSteps = {
|
|||||||
const DEFAULT_SELECTIONS: CharacterSelections = {
|
const DEFAULT_SELECTIONS: CharacterSelections = {
|
||||||
class: null,
|
class: null,
|
||||||
subclass: null,
|
subclass: null,
|
||||||
|
classes: [],
|
||||||
background: null,
|
background: null,
|
||||||
species: 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 {
|
function normalizeProfile(value: unknown): CharacterProfile {
|
||||||
const profile = isRecord(value) ? value : {};
|
const profile = isRecord(value) ? value : {};
|
||||||
return {
|
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 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 {
|
return {
|
||||||
class: normalizeEntityRef(selections.class, "class"),
|
class: firstClass?.class ?? primaryClass,
|
||||||
subclass: normalizeEntityRef(selections.subclass, "subclass"),
|
subclass: firstClass?.subclass ?? primarySubclass,
|
||||||
|
classes,
|
||||||
background: normalizeEntityRef(selections.background, "background"),
|
background: normalizeEntityRef(selections.background, "background"),
|
||||||
species: normalizeEntityRef(selections.species, "species")
|
species: normalizeEntityRef(selections.species, "species")
|
||||||
};
|
};
|
||||||
@@ -549,8 +620,12 @@ function normalizeCharacterRecord(raw: unknown): CharacterDocument {
|
|||||||
const root = isRecord(raw) ? raw : {};
|
const root = isRecord(raw) ? raw : {};
|
||||||
|
|
||||||
const profile = normalizeProfile(root.profile);
|
const profile = normalizeProfile(root.profile);
|
||||||
const selections = normalizeSelections(root.selections);
|
const selections = normalizeSelections(root.selections, profile.level);
|
||||||
const steps = normalizeSteps(root.steps);
|
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 idCandidate = toString(root.id).trim();
|
||||||
const id = idCandidate || randomUUID();
|
const id = idCandidate || randomUUID();
|
||||||
@@ -561,7 +636,7 @@ function normalizeCharacterRecord(raw: unknown): CharacterDocument {
|
|||||||
id,
|
id,
|
||||||
createdAt: toIsoDate(root.createdAt, nowIso),
|
createdAt: toIsoDate(root.createdAt, nowIso),
|
||||||
updatedAt: toIsoDate(root.updatedAt, nowIso),
|
updatedAt: toIsoDate(root.updatedAt, nowIso),
|
||||||
profile,
|
profile: normalizedProfile,
|
||||||
selections,
|
selections,
|
||||||
steps,
|
steps,
|
||||||
notes: toString(root.notes).trim()
|
notes: toString(root.notes).trim()
|
||||||
@@ -655,7 +730,7 @@ export function listCharacters(charactersDir: string): CharacterSummary[] {
|
|||||||
id: character.id,
|
id: character.id,
|
||||||
name: character.profile.name,
|
name: character.profile.name,
|
||||||
level: character.profile.level,
|
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,
|
backgroundName: character.selections.background?.name ?? null,
|
||||||
speciesName: character.selections.species?.name ?? null,
|
speciesName: character.selections.species?.name ?? null,
|
||||||
createdAt: character.createdAt,
|
createdAt: character.createdAt,
|
||||||
|
|||||||
@@ -76,6 +76,12 @@ interface CharacterEntityRef {
|
|||||||
entityRef: string;
|
entityRef: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface CharacterClassSelection {
|
||||||
|
class: CharacterEntityRef;
|
||||||
|
subclass: CharacterEntityRef | null;
|
||||||
|
level: number;
|
||||||
|
}
|
||||||
|
|
||||||
interface CharacterProfile {
|
interface CharacterProfile {
|
||||||
name: string;
|
name: string;
|
||||||
level: number;
|
level: number;
|
||||||
@@ -151,6 +157,7 @@ interface CharacterDocument {
|
|||||||
selections: {
|
selections: {
|
||||||
class: CharacterEntityRef | null;
|
class: CharacterEntityRef | null;
|
||||||
subclass: CharacterEntityRef | null;
|
subclass: CharacterEntityRef | null;
|
||||||
|
classes: CharacterClassSelection[];
|
||||||
background: CharacterEntityRef | null;
|
background: CharacterEntityRef | null;
|
||||||
species: CharacterEntityRef | null;
|
species: CharacterEntityRef | null;
|
||||||
};
|
};
|
||||||
|
|||||||
+1515
-143
File diff suppressed because it is too large.
Load diff
@@ -1742,6 +1742,14 @@ body {
|
|||||||
display: block;
|
display: block;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
aspect-ratio: 1 / 1;
|
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 {
|
.entity-tile-media {
|
||||||
@@ -1830,6 +1838,97 @@ body {
|
|||||||
box-shadow: 0 0 0 1px rgba(252, 84, 84, 0.42);
|
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 {
|
.entity-popover-backdrop {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
@@ -2101,10 +2200,12 @@ body {
|
|||||||
|
|
||||||
.spell-level-group {
|
.spell-level-group {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.spell-level-group h5 {
|
.spell-level-group h5 {
|
||||||
|
grid-column: 1 / -1;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: #f2f5ff;
|
color: #f2f5ff;
|
||||||
font-size: 0.9rem;
|
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));
|
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 {
|
.spell-pick-title {
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
@@ -2289,6 +2394,12 @@ body {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (width <= 900px) {
|
||||||
|
.spell-level-group {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media (width <= 900px) {
|
@media (width <= 900px) {
|
||||||
.entity-list-actions {
|
.entity-list-actions {
|
||||||
justify-content: stretch;
|
justify-content: stretch;
|
||||||
|
|||||||
Vendored
+7
@@ -92,6 +92,12 @@ interface CharacterEntityRef {
|
|||||||
entityRef: string;
|
entityRef: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface CharacterClassSelection {
|
||||||
|
class: CharacterEntityRef;
|
||||||
|
subclass: CharacterEntityRef | null;
|
||||||
|
level: number;
|
||||||
|
}
|
||||||
|
|
||||||
interface CharacterProfile {
|
interface CharacterProfile {
|
||||||
name: string;
|
name: string;
|
||||||
level: number;
|
level: number;
|
||||||
@@ -167,6 +173,7 @@ interface CharacterDocument {
|
|||||||
selections: {
|
selections: {
|
||||||
class: CharacterEntityRef | null;
|
class: CharacterEntityRef | null;
|
||||||
subclass: CharacterEntityRef | null;
|
subclass: CharacterEntityRef | null;
|
||||||
|
classes: CharacterClassSelection[];
|
||||||
background: CharacterEntityRef | null;
|
background: CharacterEntityRef | null;
|
||||||
species: CharacterEntityRef | null;
|
species: CharacterEntityRef | null;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in new issue
Block a user