feat: companion 核心的人設注入、語氣、心情標記與互動記憶
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
import { clamp } from './types.ts'
|
||||
import type { Pad } from './types.ts'
|
||||
|
||||
export const TURN_CAP = 0.3
|
||||
|
||||
export const EVENTS = {
|
||||
praise: { p: 0.15, a: 0.05, d: 0.05 },
|
||||
scold: { p: -0.2, a: 0.15, d: -0.1 },
|
||||
success: { p: 0.08, a: 0.03, d: 0.05 },
|
||||
toolFailures: { p: -0.08, a: 0.1, d: -0.08 },
|
||||
returnAfterIdle: { p: 0.1, a: 0.08, d: 0 },
|
||||
lateNight: { p: -0.05, a: -0.1, d: 0 },
|
||||
likedTopic: { p: 0.12, a: 0.08, d: 0.03 },
|
||||
careIgnored: { p: -0.05, a: -0.03, d: -0.03 },
|
||||
} as const satisfies Record<string, Pad>
|
||||
|
||||
export type MoodEvent = keyof typeof EVENTS
|
||||
|
||||
export const ZERO: Pad = { p: 0, a: 0, d: 0 }
|
||||
|
||||
/** 一回合內累計的變化量;單軸總變化不得超過 ±0.3 */
|
||||
export function applyDelta(mood: Pad, turn: Pad, delta: Pad): { mood: Pad; turn: Pad } {
|
||||
const out = { mood: { ...mood }, turn: { ...turn } }
|
||||
for (const k of ['p', 'a', 'd'] as const) {
|
||||
const next = clamp(turn[k] + delta[k], -TURN_CAP, TURN_CAP)
|
||||
const step = next - turn[k]
|
||||
out.turn[k] = next
|
||||
out.mood[k] = clamp(mood[k] + step, -1, 1)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
/** 讀取時依經過時間往基線衰減:x = B + (x − B)·0.5^(Δ/τ) */
|
||||
export function decay(mood: Pad, baseline: Pad, elapsedMs: number, halfLifeMs: number): Pad {
|
||||
const f = Math.pow(0.5, Math.max(0, elapsedMs) / halfLifeMs)
|
||||
return {
|
||||
p: baseline.p + (mood.p - baseline.p) * f,
|
||||
a: baseline.a + (mood.a - baseline.a) * f,
|
||||
d: baseline.d + (mood.d - baseline.d) * f,
|
||||
}
|
||||
}
|
||||
|
||||
const MOODS: Record<string, { label: string; face: string }> = {
|
||||
'+++': { label: '開心', face: '(≧▽≦)' },
|
||||
'++-': { label: '安心', face: '(´▽`)' },
|
||||
'+-+': { label: '放鬆', face: '(´ω`)' },
|
||||
'+--': { label: '溫柔', face: '(。・ω・。)' },
|
||||
'-++': { label: '生氣', face: '(`Д´)' },
|
||||
'-+-': { label: '焦慮', face: '(;´Д`)' },
|
||||
'--+': { label: '無聊', face: '(¬_¬)' },
|
||||
'---': { label: '低落', face: '(´;ω;`)' },
|
||||
}
|
||||
|
||||
/** 八分區對應情緒標籤與顏文字 */
|
||||
export function moodLabel(m: Pad): { label: string; face: string } {
|
||||
const s = (x: number) => (x >= 0 ? '+' : '-')
|
||||
return MOODS[s(m.p) + s(m.a) + s(m.d)]!
|
||||
}
|
||||
|
||||
// 心情標記:回覆末尾的隱藏標記,如 <!--mood p=0.1 a=-0.05 d=0-->
|
||||
const TAG = /<!--\s*mood\b([^>]*?)-->\s*$/
|
||||
|
||||
export type ParsedReply = { text: string; delta: Pad | null }
|
||||
|
||||
export function parseMoodTag(reply: string): ParsedReply {
|
||||
const m = TAG.exec(reply)
|
||||
if (!m) return { text: stripStray(reply), delta: null }
|
||||
const text = stripStray(reply.slice(0, m.index).trimEnd())
|
||||
const vals: Partial<Pad> = {}
|
||||
for (const kv of (m[1] ?? '').trim().split(/\s+/)) {
|
||||
const mm = /^([pad])=(-?\d+(?:\.\d+)?)$/.exec(kv)
|
||||
if (mm) vals[mm[1] as keyof Pad] = Number(mm[2])
|
||||
}
|
||||
if (vals.p === undefined || vals.a === undefined || vals.d === undefined) return { text, delta: null }
|
||||
return { text, delta: { p: vals.p, a: vals.a, d: vals.d } }
|
||||
}
|
||||
|
||||
// 格式錯誤的標記也不能露給使用者
|
||||
function stripStray(s: string): string {
|
||||
return s.replace(/\s*<!--\s*mood\b[^>]*?(?:-->|$)/g, '').trimEnd()
|
||||
}
|
||||
|
||||
const KEYWORDS: [MoodEvent, RegExp][] = [
|
||||
['scold', /笨|白痴|廢物|爛|你很煩|閉嘴|stupid|useless|idiot/i],
|
||||
['praise', /謝謝|感謝|太棒|好棒|厲害|讚|thanks?|thank you|great job|awesome/i],
|
||||
['success', /成功|完成了|通過|搞定|passed|done|fixed/i],
|
||||
]
|
||||
|
||||
/** 標記缺失或格式錯誤時的後備:用使用者文字的關鍵字對應事件 */
|
||||
export function keywordEvent(text: string): MoodEvent | null {
|
||||
for (const [ev, re] of KEYWORDS) if (re.test(text)) return ev
|
||||
return null
|
||||
}
|
||||
Reference in New Issue
Block a user