94 lines
3.5 KiB
TypeScript
94 lines
3.5 KiB
TypeScript
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
|
||
}
|