Files
jsc-marketplace/plugins/cockpit/companion/mood.ts
T

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