Files

145 lines
6.2 KiB
TypeScript
Raw Permalink 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.
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')