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 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 = { '+++': { 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)]! } // 心情標記:回覆末尾的隱藏標記,如 const TAG = /\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 = {} 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*|$)/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 }