Split custom world generation into staged lightweight batches
Some checks failed
CI / verify (push) Has been cancelled

This commit is contained in:
2026-04-05 22:20:30 +08:00
parent 89cecda7da
commit fcd8d727b0
57 changed files with 7646 additions and 1425 deletions

View File

@@ -20,6 +20,11 @@ import {
WorldTemplateType,
WorldType,
} from '../types';
import {
AFFINITY_BACKSTORY_CHAPTER_THRESHOLDS,
DEFAULT_PRIVATE_CHAT_UNLOCK_AFFINITY,
} from './affinityLevels';
import { resolveRoleCombatStats } from './attributeCombat';
import {
buildCharacterAttributeProfile,
buildCustomWorldPlayableNpcAttributeProfile,
@@ -40,6 +45,13 @@ function defineSkill(skill: CharacterSkillDefinition): CharacterSkillDefinition
return skill;
}
const [
BACKSTORY_UNLOCK_AFFINITY_EASED = 15,
BACKSTORY_UNLOCK_AFFINITY_FRIENDLY = 30,
BACKSTORY_UNLOCK_AFFINITY_TRUSTED = 60,
BACKSTORY_UNLOCK_AFFINITY_CLOSE = 90,
] = AFFINITY_BACKSTORY_CHAPTER_THRESHOLDS;
function effect(definition: CharacterSkillEffectDefinition) {
return definition;
}
@@ -144,16 +156,35 @@ export type CharacterPresetOverride = Partial<Omit<Character, 'attributes' | 'sk
const CHARACTER_OVERRIDES = characterOverridesJson as Record<string, CharacterPresetOverride>;
export const UNIVERSAL_MAX_MANA = 999;
function getLegacyCharacterMaxHp(character: Character) {
function getLegacyCharacterBaseMaxHp(character: Character) {
return Math.max(120, 90 + character.attributes.strength * 10 + character.attributes.spirit * 4);
}
function getCharacterBaseResourceProfile(character: Character) {
return character.resourceProfile ?? buildCharacterResourceProfile(character);
}
export function getCharacterMaxMana(character: Character) {
return character.resourceProfile?.maxMana ?? UNIVERSAL_MAX_MANA;
}
export function getCharacterMaxHp(character: Character) {
return character.resourceProfile?.maxHp ?? getLegacyCharacterMaxHp(character);
export function getCharacterCombatStats(
character: Character,
worldType: WorldType | null = null,
customWorldProfile: CustomWorldProfile | null = getRuntimeCustomWorldProfile(),
) {
return resolveRoleCombatStats(
resolveCharacterAttributeProfile(character, worldType, customWorldProfile) ?? character.attributeProfile,
);
}
export function getCharacterMaxHp(
character: Character,
worldType: WorldType | null = null,
customWorldProfile: CustomWorldProfile | null = getRuntimeCustomWorldProfile(),
) {
return getCharacterBaseResourceProfile(character).maxHp
+ getCharacterCombatStats(character, worldType, customWorldProfile).maxHpBonus;
}
export function createCharacterSkillCooldowns(character: Character) {
@@ -175,7 +206,10 @@ function buildCharacterResourceProfile(character: Character) {
: 188;
return {
maxHp: baseHp + Math.min(18, character.skills.length * 4),
maxHp: Math.max(
getLegacyCharacterBaseMaxHp(character),
baseHp + Math.min(18, character.skills.length * 4),
),
maxMana: UNIVERSAL_MAX_MANA,
};
}
@@ -206,6 +240,50 @@ function hydrateCharacterRoleData(
initialAffinity: 18,
relationshipHooks: character.combatTags?.slice(0, 3) ?? [],
tags: character.combatTags ?? [],
backstoryReveal: {
publicSummary: character.description,
chapters: [
{
id: 'surface',
title: '表层来意',
affinityRequired: BACKSTORY_UNLOCK_AFFINITY_EASED,
teaser: character.description,
content: character.backstory,
contextSnippet: character.backstory,
},
{
id: 'scar',
title: '旧事裂痕',
affinityRequired: BACKSTORY_UNLOCK_AFFINITY_FRIENDLY,
teaser: character.backstory,
content: character.backstory,
contextSnippet: character.backstory,
},
{
id: 'hidden',
title: '隐藏执念',
affinityRequired: BACKSTORY_UNLOCK_AFFINITY_TRUSTED,
teaser: character.personality,
content: character.personality,
contextSnippet: character.personality,
},
{
id: 'final',
title: '最终底牌',
affinityRequired: BACKSTORY_UNLOCK_AFFINITY_CLOSE,
teaser: character.skills[0]?.name ?? character.title,
content: character.backstory,
contextSnippet: character.backstory,
},
],
},
skills: character.skills.slice(0, 3).map((skill, index) => ({
id: `preset-skill-${index + 1}`,
name: skill.name,
summary: skill.name,
style: skill.style,
})),
initialItems: [],
},
options.customWorldProfile.attributeSchema,
character.attributes,
@@ -232,8 +310,10 @@ export function buildCompanionState(
npcId: string,
character: Character,
joinedAtAffinity: number,
worldType: WorldType | null = null,
customWorldProfile: CustomWorldProfile | null = getRuntimeCustomWorldProfile(),
): CompanionState {
const maxHp = Math.max(180, getCharacterMaxHp(character));
const maxHp = Math.max(180, getCharacterMaxHp(character, worldType, customWorldProfile));
const maxMana = getCharacterMaxMana(character);
return {
@@ -1434,6 +1514,8 @@ function buildCustomWorldSkillVariant(
index: number,
) {
const themeMode = detectCustomWorldThemeMode(profile);
const generatedSkill =
role.skills[index % Math.max(1, role.skills.length)] ?? null;
const contextText = [
profile.name,
profile.settingText,
@@ -1445,7 +1527,13 @@ function buildCustomWorldSkillVariant(
role.backstory,
role.personality,
role.combatStyle,
role.backstoryReveal.publicSummary,
role.skills.map((item) => `${item.name} ${item.summary} ${item.style}`).join(' '),
role.initialItems.map((item) => `${item.name} ${item.category} ${item.description}`).join(' '),
role.tags.join(' '),
generatedSkill?.name ?? '',
generatedSkill?.summary ?? '',
generatedSkill?.style ?? '',
].join(' ');
const seed = hashText(`${contextText}:${baseCharacter.id}:${skill.id}:${index}`);
const isRangedSkill = skill.delivery === 'ranged' || skill.style === 'projectile';
@@ -1469,7 +1557,9 @@ function buildCustomWorldSkillVariant(
return {
...skill,
name: buildThemedSkillName(profile, baseCharacter, skill, index, role),
name:
generatedSkill?.name?.trim()
|| buildThemedSkillName(profile, baseCharacter, skill, index, role),
damage: clampInteger(skill.damage + damageBoost, Math.max(6, skill.damage - 4), skill.damage + 12),
manaCost: clampInteger(skill.manaCost + manaShift, 0, skill.manaCost + 5),
cooldownTurns: clampInteger(skill.cooldownTurns + cooldownShift, 1, skill.cooldownTurns + 2),
@@ -1523,12 +1613,16 @@ function buildCustomWorldCharacter(baseCharacter: Character, profile: CustomWorl
title: role.title,
description: role.description,
backstory: role.backstory,
backstoryReveal: role.backstoryReveal,
personality: role.personality,
conversationStyle: inferConversationStyleFromText([
role.personality,
role.description,
role.backstory,
role.combatStyle,
role.backstoryReveal.publicSummary,
role.skills.map((skill) => `${skill.name} ${skill.summary}`).join('、'),
role.initialItems.map((item) => `${item.name} ${item.description}`).join('、'),
role.tags.join('、'),
].join(' ')),
combatTags,
@@ -1604,8 +1698,6 @@ export function getCharacterAdventureOpening(character: Character, worldType: Wo
return character.adventureOpenings?.[worldType] ?? null;
}
const DEFAULT_PRIVATE_CHAT_UNLOCK_AFFINITY = 70;
function truncateText(text: string, maxLength = 26) {
const normalized = text.trim().replace(/\s+/g, ' ');
if (normalized.length <= maxLength) {
@@ -1640,7 +1732,7 @@ function buildFallbackBackstoryRevealConfig(
{
id: 'surface-hook',
title: '表层来意',
affinityRequired: 20,
affinityRequired: BACKSTORY_UNLOCK_AFFINITY_EASED,
teaser: truncateText(opening?.surfaceHook ?? opening?.guardedMotive ?? backstoryLead),
content: [
opening?.surfaceHook ? `最先能看出来的,只是:${opening.surfaceHook}` : null,
@@ -1655,7 +1747,7 @@ function buildFallbackBackstoryRevealConfig(
{
id: 'old-scars',
title: '旧事残痕',
affinityRequired: 40,
affinityRequired: BACKSTORY_UNLOCK_AFFINITY_FRIENDLY,
teaser: truncateText(backstoryLead),
content: backstoryDetail,
contextSnippet: `${character.name}的旧事里埋着一段尚未完全说开的经历:${truncateText(backstoryLead, 36)}`,
@@ -1663,7 +1755,7 @@ function buildFallbackBackstoryRevealConfig(
{
id: 'real-reason',
title: '真正来由',
affinityRequired: 65,
affinityRequired: BACKSTORY_UNLOCK_AFFINITY_TRUSTED,
teaser: truncateText(opening?.reason ?? backstoryDetail),
content: opening?.reason
? `${character.name}来到此地真正的原因是:${opening.reason}`
@@ -1675,7 +1767,7 @@ function buildFallbackBackstoryRevealConfig(
{
id: 'current-goal',
title: '当前执念',
affinityRequired: 85,
affinityRequired: BACKSTORY_UNLOCK_AFFINITY_CLOSE,
teaser: truncateText(opening?.goal ?? normalizedBackstory),
content: opening?.goal
? [
@@ -1705,17 +1797,23 @@ export function getCharacterBackstoryRevealConfig(
publicSummary: configured.publicSummary?.trim() || fallback.publicSummary,
privateChatUnlockAffinity:
configured.privateChatUnlockAffinity ?? fallback.privateChatUnlockAffinity,
chapters:
configured.chapters?.map((chapter, index) => ({
...chapter,
id: chapter.id?.trim() || `chapter-${index + 1}`,
title: chapter.title?.trim() || `背景片段 ${index + 1}`,
teaser: chapter.teaser?.trim() || truncateText(chapter.content),
content: chapter.content?.trim() || fallback.chapters[index]?.content || '',
chapters: fallback.chapters.map((fallbackChapter, index) => {
const chapter = configured.chapters?.[index];
const content = chapter?.content?.trim() || fallbackChapter.content || '';
return {
...fallbackChapter,
id: chapter?.id?.trim() || fallbackChapter.id || `chapter-${index + 1}`,
title:
chapter?.title?.trim() ||
fallbackChapter.title ||
`背景片段 ${index + 1}`,
affinityRequired: fallbackChapter.affinityRequired,
teaser: chapter?.teaser?.trim() || truncateText(content),
content,
contextSnippet:
chapter.contextSnippet?.trim()
|| truncateText(chapter.content || fallback.chapters[index]?.content || '', 48),
})) ?? fallback.chapters,
chapter?.contextSnippet?.trim() || truncateText(content, 48),
};
}),
};
}