fix(state): changing species clears language, citizenship, and subspecies selections
This commit is contained in:
parent
48f8b46827
commit
c220815b89
20 changed files with 172 additions and 104 deletions
|
|
@ -43,6 +43,7 @@
|
|||
|
||||
<xs:simpleType name="fieldType">
|
||||
<xs:restriction base="xs:string">
|
||||
<xs:enumeration value="name" />
|
||||
<xs:enumeration value="text" />
|
||||
<xs:enumeration value="textarea" />
|
||||
<xs:enumeration value="list" />
|
||||
|
|
@ -57,6 +58,7 @@
|
|||
<xs:enumeration value="subspecies" />
|
||||
<xs:enumeration value="citizenship" />
|
||||
<xs:enumeration value="languages" />
|
||||
<xs:enumeration value="separator" />
|
||||
</xs:restriction>
|
||||
</xs:simpleType>
|
||||
|
||||
|
|
|
|||
|
|
@ -4,18 +4,8 @@
|
|||
|
||||
<record type="public">
|
||||
<note>Basic identification information visible on all records.</note>
|
||||
<field label="Designation" type="text" />
|
||||
<field label="Positronic Manufacture Date" type="date" placeholder="24-03-2460" />
|
||||
<field label="Chassis Manufacture Date" type="date" placeholder="24-03-2460" />
|
||||
<field label="Designation" type="name" />
|
||||
<field label="IPC Model" type="subspecies" />
|
||||
<field label="Tag" type="text" />
|
||||
<field label="Ownership Status" type="select">
|
||||
<option value="company-owned" label="Company Owned" />
|
||||
<option value="owned" label="Private Owner" />
|
||||
<option value="free" label="Free" />
|
||||
</field>
|
||||
<field label="Owner Name" type="text" />
|
||||
<field label="Owner Contact Information" type="text" />
|
||||
<field label="Pronouns" type="multi-select">
|
||||
<option value="he/him" label="he/him" />
|
||||
<option value="she/her" label="she/her" />
|
||||
|
|
@ -25,9 +15,21 @@
|
|||
</field>
|
||||
<field label="Citizenship" type="citizenship" />
|
||||
<field label="Spoken Languages" type="languages" />
|
||||
<field label="Separator" type="separator" label="Chassis" />
|
||||
<field label="Positronic Manufacture Date" type="date" placeholder="24-03-2460" />
|
||||
<field label="Chassis Manufacture Date" type="date" placeholder="24-03-2460" />
|
||||
<field label="Tag" type="text" />
|
||||
<field label="Height" type="height" />
|
||||
<field label="Weight" type="weight" />
|
||||
<field label="Distinguishing Features" type="textarea" />
|
||||
<field label="Separator" type="separator" label="Ownership" />
|
||||
<field label="Ownership Status" type="select">
|
||||
<option value="company-owned" label="Company Owned" />
|
||||
<option value="owned" label="Private Owner" />
|
||||
<option value="free" label="Free" />
|
||||
</field>
|
||||
<field label="Owner Name" type="text" />
|
||||
<field label="Owner Contact Information" type="text" />
|
||||
</record>
|
||||
|
||||
<record type="employment">
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
<record type="public">
|
||||
<note>Basic identification information visible on all records.</note>
|
||||
<field label="Name" type="text" />
|
||||
<field label="Name" type="name" />
|
||||
<field label="Species" type="species" />
|
||||
<field label="Subspecies" type="subspecies" />
|
||||
<field label="Pronouns" type="multi-select">
|
||||
|
|
@ -19,6 +19,7 @@
|
|||
<field label="Spoken Languages" type="languages" />
|
||||
<field label="Employed As" type="text" />
|
||||
<field label="Next of Kin" type="text" />
|
||||
<field label="Separator" type="separator" label="Appearance" />
|
||||
<field label="Height" type="height" />
|
||||
<field label="Weight" type="weight" />
|
||||
<field label="Skin Color" type="text" />
|
||||
|
|
|
|||
63
package-lock.json
generated
63
package-lock.json
generated
|
|
@ -828,9 +828,6 @@
|
|||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -845,9 +842,6 @@
|
|||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -862,9 +856,6 @@
|
|||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -879,9 +870,6 @@
|
|||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -896,9 +884,6 @@
|
|||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -913,9 +898,6 @@
|
|||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -930,9 +912,6 @@
|
|||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -947,9 +926,6 @@
|
|||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -964,9 +940,6 @@
|
|||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -981,9 +954,6 @@
|
|||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -998,9 +968,6 @@
|
|||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -1015,9 +982,6 @@
|
|||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -1032,9 +996,6 @@
|
|||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -1375,9 +1336,6 @@
|
|||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -1395,9 +1353,6 @@
|
|||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -1415,9 +1370,6 @@
|
|||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -1435,9 +1387,6 @@
|
|||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -2285,9 +2234,6 @@
|
|||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -2309,9 +2255,6 @@
|
|||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -2333,9 +2276,6 @@
|
|||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -2357,9 +2297,6 @@
|
|||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
|
|||
|
|
@ -2,8 +2,10 @@
|
|||
import { roster } from '$lib/state.svelte';
|
||||
import { slugify } from '$lib/utils/slugify';
|
||||
|
||||
function displayName(char: { data: Record<string, unknown> }): string {
|
||||
const name = char.data[slugify('Name')];
|
||||
function displayName(char: { template: { records: { fields: { type: string; label: string }[] }[] }; data: Record<string, unknown> }): string {
|
||||
const nameField = char.template.records.flatMap((r) => r.fields).find((f) => f.type === 'name');
|
||||
const key = nameField ? slugify(nameField.label) : slugify('Name');
|
||||
const name = char.data[key];
|
||||
return (name as string) || 'Unnamed Character';
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -46,7 +46,9 @@
|
|||
function displayName(): string {
|
||||
const char = roster.active;
|
||||
if (!char) return '';
|
||||
const name = char.data[slugify('Name')];
|
||||
const nameField = char.template.records.flatMap((r) => r.fields).find((f) => f.type === 'name');
|
||||
const key = nameField ? slugify(nameField.label) : slugify('Name');
|
||||
const name = char.data[key];
|
||||
return (name as string) || 'Unnamed Character';
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -63,7 +63,9 @@
|
|||
|
||||
function charName(): string {
|
||||
if (!charData) return 'Unknown';
|
||||
return (charData.data[slugify('Name')] as string) || 'Unnamed Character';
|
||||
const nameField = charData.template.records.flatMap((r: any) => r.fields).find((f: any) => f.type === 'name');
|
||||
const key = nameField ? slugify(nameField.label) : slugify('Name');
|
||||
return (charData.data[key] as string) || 'Unnamed Character';
|
||||
}
|
||||
|
||||
async function importCharacter() {
|
||||
|
|
|
|||
|
|
@ -13,8 +13,9 @@
|
|||
|
||||
let expanded = $state(false);
|
||||
|
||||
let dataFields = $derived(record.fields.filter((f) => f.type !== 'separator'));
|
||||
let filled = $derived(
|
||||
record.fields.filter((f) => {
|
||||
dataFields.filter((f) => {
|
||||
const v = data[slugify(f.label)];
|
||||
if (v === undefined || v === null || v === '' || v === 0) return false;
|
||||
if (Array.isArray(v) && v.length === 0) return false;
|
||||
|
|
@ -47,7 +48,7 @@
|
|||
{/if}
|
||||
</div>
|
||||
<span class="text-sm tabular-nums" style="color: var(--text-muted);">
|
||||
{filled}/{record.fields.length}
|
||||
{filled}/{dataFields.length}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,17 @@
|
|||
let showTemplateSwitcher = $state(false);
|
||||
let showMigrationModal = $state(false);
|
||||
|
||||
let speciesKeys = $derived(new Set(
|
||||
character.template.records.flatMap((r) => r.fields)
|
||||
.filter((f) => f.type === 'species')
|
||||
.map((f) => slugify(f.label))
|
||||
));
|
||||
const SPECIES_DEPENDENT_TYPES = new Set(['subspecies', 'citizenship', 'languages']);
|
||||
let speciesDependentKeys = $derived(new Set(
|
||||
character.template.records.flatMap((r) => r.fields)
|
||||
.filter((f) => SPECIES_DEPENDENT_TYPES.has(f.type))
|
||||
.map((f) => slugify(f.label))
|
||||
));
|
||||
let speciesKey = slugify('Species');
|
||||
|
||||
let pendingMigration = $derived.by(() => {
|
||||
|
|
@ -59,9 +70,8 @@
|
|||
return null;
|
||||
});
|
||||
|
||||
function switchTemplate(template: Template) {
|
||||
character.template = $state.snapshot(template);
|
||||
roster.scheduleSave(character);
|
||||
async function switchTemplate(template: Template) {
|
||||
await roster.migrateToPreset(character, template);
|
||||
dismissed = null;
|
||||
showTemplateSwitcher = false;
|
||||
}
|
||||
|
|
@ -143,6 +153,11 @@
|
|||
data={character.data}
|
||||
onFieldChange={(key, value) => {
|
||||
character.data[key] = value;
|
||||
if (speciesKeys.has(key)) {
|
||||
for (const depKey of speciesDependentKeys) {
|
||||
character.data[depKey] = '';
|
||||
}
|
||||
}
|
||||
roster.scheduleSave(character);
|
||||
}}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,21 @@
|
|||
<script lang="ts">
|
||||
import type { CitizenshipField } from '$lib/types';
|
||||
import { citizenships } from '$lib/data';
|
||||
import { citizenships, species } from '$lib/data';
|
||||
import { slugify } from '$lib/utils/slugify';
|
||||
|
||||
let { field, value = '', onChange }: { field: CitizenshipField; value: string; onChange: (v: string) => void } = $props();
|
||||
let { field, value = '', onChange, data }: {
|
||||
field: CitizenshipField;
|
||||
value: string;
|
||||
onChange: (v: string) => void;
|
||||
data: Record<string, unknown>;
|
||||
} = $props();
|
||||
|
||||
let currentSpecies = $derived(species.find((s) => s.id === data[slugify('Species')]));
|
||||
let filtered = $derived(
|
||||
currentSpecies
|
||||
? citizenships.filter((c) => currentSpecies!.citizenships.includes(c.id))
|
||||
: citizenships
|
||||
);
|
||||
|
||||
let custom = $state(false);
|
||||
|
||||
|
|
@ -16,7 +29,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
let isCustom = $derived(custom || (value !== '' && !citizenships.some((c) => c.name === value)));
|
||||
let isCustom = $derived(custom || (value !== '' && !filtered.some((c) => c.name === value)));
|
||||
</script>
|
||||
|
||||
<label class="block">
|
||||
|
|
@ -47,7 +60,7 @@
|
|||
style="background: var(--bg-input); border: 1px solid var(--border); color: var(--text);"
|
||||
>
|
||||
<option value="">—</option>
|
||||
{#each citizenships as c}
|
||||
{#each filtered as c}
|
||||
<option value={c.name}>{c.name}</option>
|
||||
{/each}
|
||||
<option value="__custom">Other...</option>
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@
|
|||
import SubspeciesField from './SubspeciesField.svelte';
|
||||
import CitizenshipField from './CitizenshipField.svelte';
|
||||
import LanguagesField from './LanguagesField.svelte';
|
||||
import SeparatorField from './SeparatorField.svelte';
|
||||
|
||||
let { field, value, onChange, data }: {
|
||||
field: FieldDef;
|
||||
|
|
@ -23,7 +24,9 @@
|
|||
} = $props();
|
||||
</script>
|
||||
|
||||
{#if field.type === 'text'}
|
||||
{#if field.type === 'name'}
|
||||
<TextField field={{ ...field, type: 'text' }} {value} {onChange} />
|
||||
{:else if field.type === 'text'}
|
||||
<TextField {field} {value} {onChange} />
|
||||
{:else if field.type === 'textarea'}
|
||||
<TextareaField {field} {value} {onChange} />
|
||||
|
|
@ -48,7 +51,9 @@
|
|||
{:else if field.type === 'subspecies'}
|
||||
<SubspeciesField {field} {value} {onChange} {data} />
|
||||
{:else if field.type === 'citizenship'}
|
||||
<CitizenshipField {field} {value} {onChange} />
|
||||
<CitizenshipField {field} {value} {onChange} {data} />
|
||||
{:else if field.type === 'languages'}
|
||||
<LanguagesField {field} {value} {onChange} />
|
||||
<LanguagesField {field} {value} {onChange} {data} />
|
||||
{:else if field.type === 'separator'}
|
||||
<SeparatorField {field} />
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,24 @@
|
|||
<script lang="ts">
|
||||
import type { LanguagesField } from '$lib/types';
|
||||
import { languages } from '$lib/data';
|
||||
import { languages, species } from '$lib/data';
|
||||
import { slugify } from '$lib/utils/slugify';
|
||||
import MultiSelectField from './MultiSelectField.svelte';
|
||||
|
||||
let { field, value = ['Tau Ceti Basic'], onChange }: {
|
||||
let { field, value = [], onChange, data }: {
|
||||
field: LanguagesField;
|
||||
value: string[];
|
||||
onChange: (v: string[]) => void;
|
||||
data: Record<string, unknown>;
|
||||
} = $props();
|
||||
|
||||
const options = languages.map((l) => ({ value: l.name, label: l.name }));
|
||||
const multiField = $derived({ ...field, type: 'multi-select' as const, options });
|
||||
let currentSpecies = $derived(species.find((s) => s.id === data[slugify('Species')]));
|
||||
let filtered = $derived(
|
||||
currentSpecies
|
||||
? languages.filter((l) => currentSpecies!.languages.includes(l.id))
|
||||
: languages
|
||||
);
|
||||
let options = $derived(filtered.map((l) => ({ value: l.name, label: l.name })));
|
||||
let multiField = $derived({ ...field, type: 'multi-select' as const, options });
|
||||
</script>
|
||||
|
||||
<MultiSelectField field={multiField} {value} {onChange} />
|
||||
|
|
|
|||
15
src/lib/components/fields/SeparatorField.svelte
Normal file
15
src/lib/components/fields/SeparatorField.svelte
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<script lang="ts">
|
||||
import type { SeparatorField } from '$lib/types';
|
||||
|
||||
let { field }: { field: SeparatorField } = $props();
|
||||
</script>
|
||||
|
||||
{#if field.label}
|
||||
<div class="flex items-center gap-3 pt-2">
|
||||
<hr class="flex-1" style="border-color: var(--border);" />
|
||||
<span class="text-xs font-medium uppercase tracking-wide" style="color: var(--text-muted);">{field.label}</span>
|
||||
<hr class="flex-1" style="border-color: var(--border);" />
|
||||
</div>
|
||||
{:else}
|
||||
<hr class="mt-2" style="border-color: var(--border);" />
|
||||
{/if}
|
||||
|
|
@ -68,6 +68,7 @@ function parseField(raw: any): FieldDef {
|
|||
const type = raw['@_type'];
|
||||
|
||||
switch (type) {
|
||||
case 'name':
|
||||
case 'text':
|
||||
case 'textarea':
|
||||
case 'date':
|
||||
|
|
@ -80,6 +81,8 @@ function parseField(raw: any): FieldDef {
|
|||
case 'citizenship':
|
||||
case 'languages':
|
||||
return { ...base, type };
|
||||
case 'separator':
|
||||
return { type: 'separator', label: raw['@_label'] ?? '' };
|
||||
case 'number':
|
||||
return {
|
||||
...base,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import type { Character, Template } from './types';
|
||||
import { pruneEmpty } from './sharing';
|
||||
import { presets } from './presets';
|
||||
import { slugify } from './utils/slugify';
|
||||
|
||||
interface CharacterFilePayload {
|
||||
version: number;
|
||||
|
|
@ -44,7 +45,9 @@ export function parseCharacterFile(json: string): { template: Template | Omit<Te
|
|||
}
|
||||
|
||||
export function characterFileName(char: Character): string {
|
||||
const name = char.data.name as string | undefined;
|
||||
const nameField = char.template.records.flatMap((r) => r.fields).find((f) => f.type === 'name');
|
||||
const key = nameField ? slugify(nameField.label) : 'name';
|
||||
const name = char.data[key] as string | undefined;
|
||||
if (!name || !name.trim()) return 'character.json';
|
||||
return name.trim().replace(/[^a-zA-Z0-9'-]/g, '-').replace(/-+/g, '-') + '.json';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ export function formatFieldOutput(
|
|||
currentSpecies?: string
|
||||
): string | null {
|
||||
switch (field.type) {
|
||||
case 'name':
|
||||
case 'text':
|
||||
case 'date':
|
||||
case 'citizenship':
|
||||
|
|
@ -125,12 +126,32 @@ function renderFields(
|
|||
speciesData?: SpeciesData[],
|
||||
currentSpecies?: string
|
||||
): string[] {
|
||||
const lines: string[] = [];
|
||||
// Split fields into groups by separator boundaries
|
||||
const groups: FieldDef[][] = [[]];
|
||||
for (const field of fields) {
|
||||
const out = formatFieldOutput(field, data[slugify(field.label)], speciesData, currentSpecies);
|
||||
if (out) lines.push(out);
|
||||
if (field.type === 'separator') {
|
||||
groups.push([]);
|
||||
} else {
|
||||
groups[groups.length - 1].push(field);
|
||||
}
|
||||
}
|
||||
return lines;
|
||||
|
||||
const rendered = groups.map((group) => {
|
||||
const lines: string[] = [];
|
||||
for (const field of group) {
|
||||
const out = formatFieldOutput(field, data[slugify(field.label)], speciesData, currentSpecies);
|
||||
if (out) lines.push(out);
|
||||
}
|
||||
return lines;
|
||||
});
|
||||
|
||||
const result: string[] = [];
|
||||
for (const lines of rendered) {
|
||||
if (!lines.length) continue;
|
||||
if (result.length) result.push('');
|
||||
result.push(...lines);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function splitLines(text: string | undefined): string[] {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { slugify } from './utils/slugify';
|
|||
|
||||
function zodForField(field: FieldDef): z.ZodTypeAny {
|
||||
switch (field.type) {
|
||||
case 'name':
|
||||
case 'text':
|
||||
case 'textarea':
|
||||
case 'list':
|
||||
|
|
@ -30,6 +31,7 @@ export function buildCharacterSchema(template: Template): z.ZodObject<Record<str
|
|||
const shape: Record<string, z.ZodTypeAny> = {};
|
||||
for (const record of template.records) {
|
||||
for (const field of record.fields) {
|
||||
if (field.type === 'separator') continue;
|
||||
shape[slugify(field.label)] = zodForField(field);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,14 @@ let saveStatus = $state<'idle' | 'saving' | 'saved'>('idle');
|
|||
let saveTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
let statusTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
const SINGLETON_TYPES = new Set([
|
||||
'name', 'species', 'subspecies', 'citizenship', 'languages', 'height', 'weight'
|
||||
]);
|
||||
|
||||
function allFields(template: Template) {
|
||||
return template.records.flatMap((r) => r.fields).filter((f) => f.type !== 'separator');
|
||||
}
|
||||
|
||||
function migrateData(char: Character, preset: Template) {
|
||||
for (const record of preset.records) {
|
||||
for (const field of record.fields) {
|
||||
|
|
@ -26,6 +34,22 @@ function migrateData(char: Character, preset: Template) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
const oldByType = new Map<string, string>();
|
||||
for (const f of allFields(char.template)) {
|
||||
if (SINGLETON_TYPES.has(f.type)) {
|
||||
oldByType.set(f.type, slugify(f.label));
|
||||
}
|
||||
}
|
||||
for (const f of allFields(preset)) {
|
||||
if (!SINGLETON_TYPES.has(f.type)) continue;
|
||||
const newKey = slugify(f.label);
|
||||
if (char.data[newKey] !== undefined) continue;
|
||||
const oldKey = oldByType.get(f.type);
|
||||
if (oldKey && oldKey !== newKey && char.data[oldKey] !== undefined) {
|
||||
char.data[newKey] = char.data[oldKey];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const roster = {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,11 @@ export interface BaseFieldDef {
|
|||
from?: string;
|
||||
}
|
||||
|
||||
export interface NameField extends BaseFieldDef {
|
||||
type: 'name';
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
export interface TextField extends BaseFieldDef {
|
||||
type: 'text';
|
||||
placeholder?: string;
|
||||
|
|
@ -74,7 +79,13 @@ export interface LanguagesField extends BaseFieldDef {
|
|||
type: 'languages';
|
||||
}
|
||||
|
||||
export interface SeparatorField {
|
||||
type: 'separator';
|
||||
label: string;
|
||||
}
|
||||
|
||||
export type FieldDef =
|
||||
| NameField
|
||||
| TextField
|
||||
| TextareaField
|
||||
| ListField
|
||||
|
|
@ -88,7 +99,8 @@ export type FieldDef =
|
|||
| SpeciesField
|
||||
| SubspeciesField
|
||||
| CitizenshipField
|
||||
| LanguagesField;
|
||||
| LanguagesField
|
||||
| SeparatorField;
|
||||
|
||||
export interface RecordDef {
|
||||
type: string;
|
||||
|
|
|
|||
|
|
@ -1,13 +1,11 @@
|
|||
import type { Character } from '../types';
|
||||
|
||||
const DEFAULT_LANGUAGES = ['Tau Ceti Basic'];
|
||||
|
||||
export function isBlankCharacter(char: Character): boolean {
|
||||
if (!char.data) return true;
|
||||
for (const value of Object.values(char.data)) {
|
||||
if (value === '' || value === undefined || value === null || value === 0) continue;
|
||||
if (Array.isArray(value)) {
|
||||
if (value.length === 0) continue;
|
||||
if (value.length === 1 && DEFAULT_LANGUAGES.includes(value[0])) continue;
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue