Split custom world generation into staged lightweight batches
Some checks failed
CI / verify (push) Has been cancelled
Some checks failed
CI / verify (push) Has been cancelled
This commit is contained in:
@@ -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),
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user