1
Some checks failed
CI / verify (push) Has been cancelled

This commit is contained in:
2026-04-20 11:30:19 +08:00
parent 50759f3c1e
commit 8a7bd90458
85 changed files with 7290 additions and 1903 deletions

View File

@@ -23,6 +23,7 @@ const EDITABLE_SECTION_IDS = {
thread: new Set(['title', 'summary', 'conflictType', 'stakes']),
chapter: new Set(['title', 'summary', 'openingEvent', 'playerGoal', 'understandingShift']),
camp: new Set(['name', 'description', 'dangerLevel']),
sceneChapter: new Set(['title', 'summary']),
} as const;
function normalizePatches(sections: DraftSectionPatch[]) {
@@ -52,6 +53,17 @@ function parseStringList(value: string) {
return [...new Set(value.split(/[\n;]+/u).map((item) => item.trim()).filter(Boolean))];
}
function parseReferenceList(value: string) {
return [
...new Set(
value
.split(/[\n,;]+/u)
.map((item) => item.trim())
.filter(Boolean),
),
];
}
function resolveThreadType(value: string) {
if (value.includes('暗') || value.toLowerCase() === 'hidden') {
return 'hidden' as const;
@@ -60,6 +72,61 @@ function resolveThreadType(value: string) {
return 'main' as const;
}
function parseSceneActSectionId(sectionId: string) {
const match = sectionId.match(
/^act:([^:]+):(title|summary|backgroundImageSrc|encounterNpcIds|actGoal|transitionHook)$/u,
);
if (!match) {
return null;
}
return {
actId: match[1],
field: match[2] as
| 'title'
| 'summary'
| 'backgroundImageSrc'
| 'encounterNpcIds'
| 'actGoal'
| 'transitionHook',
};
}
function resolveCharacterIdByReference(
value: string,
draftProfile: NonNullable<ReturnType<typeof normalizeFoundationDraftProfile>>,
) {
const characters = [...draftProfile.playableNpcs, ...draftProfile.storyNpcs];
return (
characters.find((entry) => entry.id === value)?.id ||
characters.find((entry) => entry.name === value)?.id ||
''
);
}
function parseEncounterNpcIds(
value: string,
draftProfile: NonNullable<ReturnType<typeof normalizeFoundationDraftProfile>>,
) {
const references = parseReferenceList(value);
if (references.length === 0) {
throw badRequest('scene act requires at least one encounter NPC');
}
const unresolvedReferences = references.filter(
(reference) => !resolveCharacterIdByReference(reference, draftProfile),
);
if (unresolvedReferences.length > 0) {
throw badRequest(
`unknown scene act NPC reference: ${unresolvedReferences.join('、')}`,
);
}
return references.map((reference) =>
resolveCharacterIdByReference(reference, draftProfile),
);
}
export function updateDraftCardSections(params: UpdateDraftCardSectionsParams) {
const draftProfile = normalizeFoundationDraftProfile(params.draftProfile);
if (!draftProfile) {
@@ -293,6 +360,70 @@ export function updateDraftCardSections(params: UpdateDraftCardSectionsParams) {
return draftProfile as unknown as Record<string, unknown>;
}
const sceneChapter = draftProfile.sceneChapters.find(
(entry) => entry.id === params.cardId,
);
if (sceneChapter) {
patches.forEach(({ sectionId, value }) => {
if (EDITABLE_SECTION_IDS.sceneChapter.has(sectionId as never)) {
if (sectionId === 'title') {
sceneChapter.title = value;
return;
}
if (sectionId === 'summary') {
sceneChapter.summary = value;
}
return;
}
const parsedSceneActSection = parseSceneActSectionId(sectionId);
if (!parsedSceneActSection) {
throw badRequest(`section ${sectionId} is not editable for scene_chapter`);
}
const targetAct = sceneChapter.acts.find(
(entry) => entry.id === parsedSceneActSection.actId,
);
if (!targetAct) {
throw notFound(`scene act ${parsedSceneActSection.actId} not found`);
}
if (parsedSceneActSection.field === 'title') {
targetAct.title = value;
return;
}
if (parsedSceneActSection.field === 'summary') {
targetAct.summary = value;
return;
}
if (parsedSceneActSection.field === 'backgroundImageSrc') {
targetAct.backgroundImageSrc = value || null;
return;
}
if (parsedSceneActSection.field === 'encounterNpcIds') {
const encounterNpcIds = parseEncounterNpcIds(value, draftProfile);
targetAct.encounterNpcIds = encounterNpcIds;
targetAct.primaryNpcId = encounterNpcIds[0] || '';
return;
}
if (parsedSceneActSection.field === 'actGoal') {
targetAct.actGoal = value;
return;
}
if (parsedSceneActSection.field === 'transitionHook') {
targetAct.transitionHook = value;
}
});
return draftProfile as unknown as Record<string, unknown>;
}
if (draftProfile.camp?.id === params.cardId) {
patches.forEach(({ sectionId, value }) => {
if (!EDITABLE_SECTION_IDS.camp.has(sectionId as never)) {