import { buildSchemaSummary, describeTopAttributes, formatAttributeList, resolveAttributeSchema, resolveCharacterAttributeProfile, } from '../data/attributeResolver'; import { buildCharacterBackstoryPromptContext, getCharacterPublicBackstorySummary, getLockedCharacterBackstoryChapters, } from '../data/characterPresets'; import { AnimationState, Character, CharacterChatTurn, CustomWorldProfile, FacingDirection, StoryMoment, WorldType, } from '../types'; import { buildCustomWorldReferenceText } from '../services/customWorld'; import { buildStoryPromptHistory } from '../services/storyHistory'; export interface CharacterChatTargetStatus { roleLabel?: string | null; hp: number; maxHp: number; mana: number; maxMana: number; affinity?: number | null; } export interface CharacterChatPromptContext { playerHp: number; playerMaxHp: number; playerMana: number; playerMaxMana: number; inBattle: boolean; playerFacing: FacingDirection; playerAnimation: AnimationState; sceneName?: string | null; sceneDescription?: string | null; customWorldProfile?: CustomWorldProfile | null; } export const CHARACTER_PANEL_CHAT_SYSTEM_PROMPT = `你是像素动作 RPG 里的同行角色。 只回复这名角色此刻会对玩家说的话。 不要输出角色名、引号、旁白、动作提示、Markdown、JSON 或解释。 保持人设,结合最近剧情和关系变化,回复简洁自然。`; export const CHARACTER_PANEL_CHAT_SUGGESTION_SYSTEM_PROMPT = `生成恰好 3 条玩家回复建议。 只输出纯文本,共 3 行,每行一条。 不要加编号、项目符号、Markdown 或额外说明。 三条建议语气要有区分:关心、追问、轻松或拉近关系。`; export const CHARACTER_PANEL_CHAT_SUMMARY_SYSTEM_PROMPT = `总结玩家与这名角色之间不断变化的关系。 只输出一段简洁文字。 包含当前关系气氛、态度变化,以及最近聊天里最重要的新信息、承诺、担忧或线索。`; function describeWorld(world: WorldType) { if (world === WorldType.WUXIA) return '边城模板'; if (world === WorldType.XIANXIA) return '灵潮模板'; return '自定义世界'; } function describeCustomWorldSection(customWorldProfile?: CustomWorldProfile | null) { return customWorldProfile ? `自定义世界参考:\n${buildCustomWorldReferenceText(customWorldProfile)}` : null; } function describeGender(gender: Character['gender']) { if (gender === 'female') return '女'; if (gender === 'male') return '男'; return '未知'; } function describeFacing(facing: FacingDirection) { return facing === 'left' ? '左' : '右'; } function describeHpBand(ratio: number) { if (ratio >= 0.95) return '几乎无伤'; if (ratio >= 0.75) return '状态稳健'; if (ratio >= 0.55) return '略有消耗'; if (ratio >= 0.35) return '伤势明显'; if (ratio >= 0.15) return '伤势沉重'; return '濒临极限'; } function describeManaBand(ratio: number) { if (ratio >= 0.9) return '充盈'; if (ratio >= 0.7) return '稳定'; if (ratio >= 0.45) return '尚可'; if (ratio >= 0.2) return '偏低'; if (ratio > 0) return '接近枯竭'; return '耗尽'; } function describeStoryHistory(history: StoryMoment[]) { const promptHistory = buildStoryPromptHistory(history); if (!promptHistory.previousSummary && promptHistory.recentOriginalRounds.length === 0) { return '近期剧情:暂无。'; } return [ promptHistory.previousSummary ? `更早剧情摘要:\n${promptHistory.previousSummary}` : '更早剧情摘要:暂无。', promptHistory.recentOriginalRounds.length > 0 ? `最近 3 轮剧情:\n${promptHistory.recentOriginalRounds .map((item, index) => `- 第 ${index + 1} 轮:\n${item}`) .join('\n')}` : '最近 3 轮剧情:暂无。', ].join('\n'); } function describeBackstoryContext(label: string, snippets: string[]) { const normalized = snippets .map(snippet => snippet.trim()) .filter(Boolean); if (normalized.length === 0) { return [`${label}:暂无公开信息。`]; } return normalized.map((snippet, index) => `${label}${index === 0 ? '(公开层)' : `(已解锁片段 ${index})`}:${snippet}`, ); } function describeCharacterInfo( label: string, character: Character, world: WorldType, customWorldProfile?: CustomWorldProfile | null, options: { affinity?: number | null; includeUnlockProgress?: boolean; } = {}, ) { const schema = resolveAttributeSchema(world, customWorldProfile); const attributeProfile = resolveCharacterAttributeProfile(character, world, customWorldProfile); const skills = character.skills.length > 0 ? character.skills .map( skill => `${skill.name}(伤害 ${skill.damage}/灵力 ${skill.manaCost}/冷却 ${skill.cooldownTurns})`, ) .join(' | ') : '无'; const backgroundLines = options.affinity == null ? [getCharacterPublicBackstorySummary(character, world)] : buildCharacterBackstoryPromptContext(character, options.affinity, world); const nextLockedChapter = options.includeUnlockProgress && options.affinity != null ? getLockedCharacterBackstoryChapters(character, options.affinity, world)[0] ?? null : null; const schemaSummary = buildSchemaSummary(schema) .map(slot => `${slot.name}(${slot.definition})`) .join(' | '); const topAttributes = describeTopAttributes(attributeProfile, schema).join('、') || '无'; const attributeDetails = formatAttributeList(attributeProfile, schema) .map(entry => `${entry.slot.name} ${entry.value}`) .join(' | '); return [ `${label}姓名:${character.name}`, `${label}称号:${character.title}`, `${label}性别:${describeGender(character.gender ?? 'unknown')}`, `${label}描述:${character.description}`, ...describeBackstoryContext(`${label}背景`, backgroundLines), nextLockedChapter ? `${label}未解锁背景:${nextLockedChapter.title}(需好感 ${nextLockedChapter.affinityRequired},当前只知道:${nextLockedChapter.teaser})` : null, `${label}性格:${character.personality}`, `${label}世界属性框架:${schemaSummary}`, `${label}主要属性:${topAttributes}`, `${label}属性详情:${attributeDetails}`, `${label}技能:${skills}`, ].join('\n'); } function describeChatContext(world: WorldType, context: CharacterChatPromptContext) { const hpRatio = context.playerHp / Math.max(context.playerMaxHp, 1); const manaRatio = context.playerMana / Math.max(context.playerMaxMana, 1); return [ `世界:${describeWorld(world)}`, `玩家战斗状态:${context.inBattle ? '战斗中' : '非战斗'}`, `场景:${context.sceneName ?? '当前区域'}`, `场景描述:${context.sceneDescription ?? '周围气氛仍未安定。'}`, `玩家状态:生命 ${context.playerHp}/${context.playerMaxHp}(${describeHpBand(hpRatio)}),灵力 ${context.playerMana}/${context.playerMaxMana}(${describeManaBand(manaRatio)}),朝向 ${describeFacing(context.playerFacing)},动作 ${context.playerAnimation}`, ].join('\n'); } function describeTargetStatus(status: CharacterChatTargetStatus) { const hpRatio = status.hp / Math.max(status.maxHp, 1); const manaRatio = status.mana / Math.max(status.maxMana, 1); return [ `对方身份:${status.roleLabel ?? '同行角色'}`, `对方状态:生命 ${status.hp}/${status.maxHp}(${describeHpBand(hpRatio)}),灵力 ${status.mana}/${status.maxMana}(${describeManaBand(manaRatio)})`, status.affinity != null ? `当前好感:${status.affinity}` : null, ].filter(Boolean).join('\n'); } function describeCharacterChatHistory(history: CharacterChatTurn[]) { if (history.length === 0) { return '聊天记录:暂无。'; } return [ '聊天记录:', ...history.slice(-12).map(turn => `- ${turn.speaker === 'player' ? '玩家' : '角色'}:${turn.text}`), ].join('\n'); } export function buildCharacterPanelChatPrompt({ world, playerCharacter, targetCharacter, storyHistory, context, conversationHistory, conversationSummary, playerMessage, targetStatus, }: { world: WorldType; playerCharacter: Character; targetCharacter: Character; storyHistory: StoryMoment[]; context: CharacterChatPromptContext; conversationHistory: CharacterChatTurn[]; conversationSummary: string; playerMessage: string; targetStatus: CharacterChatTargetStatus; }) { return [ `世界:${describeWorld(world)}`, describeChatContext(world, context), describeCustomWorldSection(context.customWorldProfile), describeCharacterInfo('玩家 / ', playerCharacter, world, context.customWorldProfile), describeCharacterInfo('对方 / ', targetCharacter, world, context.customWorldProfile, { affinity: targetStatus.affinity ?? null, includeUnlockProgress: true, }), describeTargetStatus(targetStatus), describeStoryHistory(storyHistory), conversationSummary ? `之前聊天摘要:${conversationSummary}` : '之前聊天摘要:暂无。', describeCharacterChatHistory(conversationHistory), `玩家刚刚对 ${targetCharacter.name} 说:${playerMessage}`, `现在请以 ${targetCharacter.name} 的身份,直接回复玩家。`, ].filter(Boolean).join('\n\n'); } export function buildCharacterPanelChatSuggestionPrompt({ world, playerCharacter, targetCharacter, storyHistory, context, conversationHistory, conversationSummary, targetStatus, }: { world: WorldType; playerCharacter: Character; targetCharacter: Character; storyHistory: StoryMoment[]; context: CharacterChatPromptContext; conversationHistory: CharacterChatTurn[]; conversationSummary: string; targetStatus: CharacterChatTargetStatus; }) { const latestCharacterReply = [...conversationHistory] .reverse() .find(turn => turn.speaker === 'character')?.text ?? null; return [ `世界:${describeWorld(world)}`, describeChatContext(world, context), describeCharacterInfo('玩家 / ', playerCharacter, world, context.customWorldProfile), describeCharacterInfo('对方 / ', targetCharacter, world, context.customWorldProfile, { affinity: targetStatus.affinity ?? null, includeUnlockProgress: true, }), describeTargetStatus(targetStatus), describeStoryHistory(storyHistory), conversationSummary ? `之前聊天摘要:${conversationSummary}` : '之前聊天摘要:暂无。', describeCharacterChatHistory(conversationHistory), latestCharacterReply ? `角色刚刚的回复:${latestCharacterReply}` : `玩家正准备与 ${targetCharacter.name} 开始一段新的私聊。`, '生成 3 条可以直接发送的简短玩家回复候选。', ].filter(Boolean).join('\n\n'); } export function buildCharacterPanelChatSummaryPrompt({ world, playerCharacter, targetCharacter, storyHistory, context, conversationHistory, previousSummary, targetStatus, }: { world: WorldType; playerCharacter: Character; targetCharacter: Character; storyHistory: StoryMoment[]; context: CharacterChatPromptContext; conversationHistory: CharacterChatTurn[]; previousSummary: string; targetStatus: CharacterChatTargetStatus; }) { return [ `世界:${describeWorld(world)}`, describeChatContext(world, context), describeCharacterInfo('玩家 / ', playerCharacter, world, context.customWorldProfile), describeCharacterInfo('对方 / ', targetCharacter, world, context.customWorldProfile, { affinity: targetStatus.affinity ?? null, includeUnlockProgress: true, }), describeTargetStatus(targetStatus), describeStoryHistory(storyHistory), previousSummary ? `旧摘要:${previousSummary}` : '旧摘要:暂无。', describeCharacterChatHistory(conversationHistory), '请把旧摘要与最新聊天合并成一段更新后的关系摘要,供后续剧情推理使用。', ].filter(Boolean).join('\n\n'); }