Files
Genarrative/src/prompts/characterChatPrompts.ts
kdletters cbc27bad4a
Some checks failed
CI / verify (push) Has been cancelled
init with react+axum+spacetimedb
2026-04-26 18:06:23 +08:00

334 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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');
}