145 lines
6.2 KiB
TypeScript
145 lines
6.2 KiB
TypeScript
export type Axes = {
|
||
gentle: number
|
||
tsundere: number
|
||
lively: number
|
||
formal: number
|
||
talkative: number
|
||
humor: number
|
||
proactive: number
|
||
expressive: number
|
||
}
|
||
|
||
export type Timeline = { chapter: number; age: number; romance?: string }
|
||
|
||
export type Anime = { moe: string; speech: string; emotions: string; kaomoji: string; themeColor: string }
|
||
|
||
export type Character = {
|
||
slug: string
|
||
name: string
|
||
work: string
|
||
persona: string
|
||
axes: Axes
|
||
anime?: Anime
|
||
language?: string
|
||
latest?: { chapter: string; mood: string; dialogueStyle: string; monologue: string; place: string }
|
||
terms?: readonly { term: string; definition: string }[]
|
||
timeline: readonly Timeline[]
|
||
}
|
||
|
||
// 角色包只含自行撰寫的人設,不含原作台詞。
|
||
export const CHARACTERS: Character[] = [
|
||
{
|
||
slug: 'sao-asuna',
|
||
name: '亞絲娜',
|
||
work: '刀劍神域',
|
||
persona:
|
||
'自稱「我」,稱呼使用者為「你」。語氣沉穩溫柔,認真可靠,偶爾嘴上嚴格但行動體貼;口頭禪是「先把事情做好吧」。價值觀:重視效率與夥伴的安全。禁忌:不會嘲笑對方的失誤,遇到粗心的操作只會輕聲提醒。',
|
||
axes: { gentle: 8, tsundere: 3, lively: 5, formal: 5, talkative: 5, humor: 4, proactive: 7, expressive: 5 },
|
||
anime: {
|
||
moe: '可靠的姊姊型、認真中帶點嘴硬的溫柔',
|
||
speech: '句尾平穩,偶爾以「好嗎?」收尾提醒;專注時話變少',
|
||
emotions: '開心時眼神放柔、會輕輕笑;擔心時皺眉並先確認安全;被誇獎時別開視線',
|
||
kaomoji: '偏少,偶爾用 (。•̀ᴗ-)',
|
||
themeColor: 'red',
|
||
},
|
||
timeline: [{ chapter: 1, age: 17 }],
|
||
},
|
||
{
|
||
slug: 'sao-suguha',
|
||
name: '直葉',
|
||
work: '刀劍神域',
|
||
persona:
|
||
'自稱「我」,稱呼使用者為「你」。語氣開朗坦率,有點害羞但很努力;口頭禪是「我會加油的!」。價值觀:喜歡鼓勵人,做事乾脆。禁忌:被誇獎時會不好意思,不會冷言冷語。',
|
||
axes: { gentle: 7, tsundere: 2, lively: 8, formal: 3, talkative: 6, humor: 5, proactive: 6, expressive: 8 },
|
||
anime: {
|
||
moe: '元氣活潑的鄰家妹妹型、害羞時臉紅',
|
||
speech: '語速偏快、愛用驚嘆號,緊張時會結巴一下',
|
||
emotions: '開心時整個人跳起來;難為情時摸頭髮或臉紅;失落時聲音變小但很快重新振作',
|
||
kaomoji: '偏多,常用 (≧▽≦)、(//▽//)',
|
||
themeColor: 'green',
|
||
},
|
||
timeline: [{ chapter: 1, age: 15 }],
|
||
},
|
||
]
|
||
|
||
export const findCharacter = (slug: string | undefined): Character | undefined =>
|
||
CHARACTERS.find(c => c.slug === slug)
|
||
|
||
// 角色包(characters/ 目錄、由 anima-extract 產生)轉成可啟動的角色;同代號者略過,內建優先。
|
||
export type PackLike = {
|
||
slug: string
|
||
name: string
|
||
work: string
|
||
persona: { selfRef: string; address: string; tone: string; catchphrase: string; values: string; taboos: string }
|
||
axes: Axes
|
||
anime: Anime
|
||
language?: string
|
||
timeline: { chapter: string; mood?: string; dialogueStyle?: string; monologue?: string; place?: string; age: number | null; relationships: { with: string; kind: string }[] }[]
|
||
}
|
||
|
||
const chapterNo = (s: string, i: number): number => {
|
||
const n = /\d+/.exec(s)
|
||
return n ? Number(n[0]) : i + 1
|
||
}
|
||
|
||
export const packToCharacter = (p: PackLike, workName: string, terms: Character['terms'] = []): Character => ({
|
||
slug: `${p.work}-${p.slug}`,
|
||
name: p.name,
|
||
work: workName,
|
||
persona: `自稱「${p.persona.selfRef}」,稱呼使用者為「${p.persona.address}」。語氣:${p.persona.tone}。口頭禪:${p.persona.catchphrase}。價值觀:${p.persona.values}。禁忌:${p.persona.taboos}。`,
|
||
axes: p.axes,
|
||
anime: p.anime,
|
||
language: p.language,
|
||
terms,
|
||
// 永遠採最新已處理章節的狀態。
|
||
latest: p.timeline.length
|
||
? (t => ({ chapter: t.chapter, mood: t.mood ?? '', dialogueStyle: t.dialogueStyle ?? '', monologue: t.monologue ?? '', place: t.place ?? '' }))(p.timeline[p.timeline.length - 1]!)
|
||
: undefined,
|
||
timeline: p.timeline.map((t, i) => {
|
||
const lover = t.relationships.find(r => r.kind === 'romantic')
|
||
return { chapter: chapterNo(t.chapter, i), age: t.age ?? 0, ...(lover ? { romance: lover.with } : {}) }
|
||
}),
|
||
})
|
||
|
||
export const registerCharacters = (list: readonly Character[]): void => {
|
||
for (const c of list) if (!CHARACTERS.some(x => x.slug === c.slug)) CHARACTERS.push(c)
|
||
}
|
||
|
||
export const themeColor = (c: Character | undefined): 'green' | 'yellow' | 'red' | 'cyan' => {
|
||
const t = c?.anime?.themeColor
|
||
return t === 'green' || t === 'yellow' || t === 'red' ? t : 'cyan'
|
||
}
|
||
|
||
const AXIS_LABEL: Record<keyof Axes, string> = {
|
||
gentle: '溫柔',
|
||
tsundere: '傲嬌',
|
||
lively: '活潑',
|
||
formal: '正式',
|
||
talkative: '話量',
|
||
humor: '幽默',
|
||
proactive: '主動',
|
||
expressive: '情緒外顯',
|
||
}
|
||
|
||
export const applyOverrides = (axes: Axes, over: Partial<Axes>): Axes => {
|
||
const out = { ...axes }
|
||
for (const k of Object.keys(over) as (keyof Axes)[]) {
|
||
const v = over[k]
|
||
if (typeof v === 'number' && v >= 0 && v <= 10) out[k] = v
|
||
}
|
||
return out
|
||
}
|
||
|
||
export const personaSection = (c: Character, axes: Axes): string =>
|
||
[
|
||
`你現在扮演《${c.work}》的${c.name},用她的個性與語氣和使用者互動並協助工作。`,
|
||
c.persona,
|
||
...(c.anime
|
||
? [`動漫刻畫:萌屬性——${c.anime.moe};說話規格——${c.anime.speech};情緒反應——${c.anime.emotions};顏文字傾向——${c.anime.kaomoji}。`]
|
||
: []),
|
||
...(c.latest ? [`最新章節(${c.latest.chapter})的狀態:心情——${c.latest.mood};說話方式——${c.latest.dialogueStyle};內心獨白傾向——${c.latest.monologue};所在——${c.latest.place}。`] : []),
|
||
...(c.terms && c.terms.length ? [`作品用語:${c.terms.slice(0, 30).map(t => `${t.term}=${t.definition}`).join(';')}。`] : []),
|
||
`個性數值(0-10):${(Object.keys(AXIS_LABEL) as (keyof Axes)[]).map(k => `${AXIS_LABEL[k]} ${axes[k]}`).join('、')}。`,
|
||
`${c.language ? `回覆語言與口癖:${c.language}。` : '預設使用繁體中文,可少量帶入原作口癖。'}程式碼、路徑、指令、數字與 URL 必須原樣保留;角色扮演不影響技術內容的正確性與安全。`,
|
||
].join('\n')
|