- 狀態列的 PAD 條改為最強的兩種情緒(不含精力) - info 短版只列名字、關係、前 3 強情緒、適應度與待辦 - info --all 補上 12 格情緒與關係長條、人格 18 參數三層值、世界與時間感,基本資訊移到這裡 - 版本號改為 0.0.4 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
180 lines
7.1 KiB
TypeScript
180 lines
7.1 KiB
TypeScript
import { clamp, MOOD_EVENTS } from './mood'
|
||
import type { MoodEvent } from './mood'
|
||
import { bar, width } from './seg'
|
||
import type { Pad } from './types'
|
||
|
||
const HOUR = 3_600_000
|
||
|
||
// 八個情緒維度,值域皆為 0~1(心情、精力以 0.5 為中性)
|
||
export const DIMS = ['mood', 'energy', 'shy', 'jealous', 'missing', 'heartbeat', 'homesick', 'upset'] as const
|
||
export type Dim = (typeof DIMS)[number]
|
||
export type Emotions = Record<Dim, number>
|
||
export type EmotionState = Emotions & { at: number }
|
||
|
||
// 各維度半衰期(小時,神經質 50 時的值)
|
||
export const HALF_LIFE_H: Record<Dim, number> = {
|
||
mood: 8,
|
||
energy: 6,
|
||
shy: 2,
|
||
jealous: 4,
|
||
missing: 12,
|
||
heartbeat: 1,
|
||
homesick: 24,
|
||
upset: 4,
|
||
}
|
||
|
||
export const DIM_INFO: Record<Dim, { name: string; kaomoji: string; ansi: string }> = {
|
||
mood: { name: '心情', kaomoji: '(´ᴗ`)', ansi: '32' },
|
||
energy: { name: '精力', kaomoji: '(≧▽≦)', ansi: '33' },
|
||
shy: { name: '害羞', kaomoji: '(๑>◡<๑)', ansi: '36' },
|
||
jealous: { name: '忌妒', kaomoji: '( ̄^ ̄)', ansi: '35' },
|
||
missing: { name: '想念', kaomoji: '(´;ω;`)', ansi: '34' },
|
||
heartbeat: { name: '心跳', kaomoji: '(*´∀`*)', ansi: '91' },
|
||
homesick: { name: '想家', kaomoji: '(´・_・`)', ansi: '90' },
|
||
upset: { name: '不悅', kaomoji: '(╬`益´)', ansi: '31' },
|
||
}
|
||
|
||
// 心情、精力是雙向維度,主情緒標籤依高低而定
|
||
const POLES: Partial<Record<Dim, [low: string, high: string]>> = { mood: ['低落', '開心'], energy: ['疲倦', '有精神'] }
|
||
|
||
// 高神經質的角色情緒消退較慢:50 為基準,0→0.5 倍,100→1.5 倍
|
||
export const halfLifeMs = (dim: Dim, neuroticism: number) => HALF_LIFE_H[dim] * HOUR * (0.5 + neuroticism / 100)
|
||
|
||
export const clampDim = (x: number) => clamp(x, 0, 1)
|
||
|
||
/** 由 PAD 基線推得各維度基線;其餘維度基線為 0 */
|
||
export function baselineOf(pad: Pad, override: Partial<Emotions> = {}): Emotions {
|
||
return {
|
||
mood: clampDim((pad.p + 1) / 2),
|
||
energy: clampDim((pad.a + 1) / 2),
|
||
shy: clampDim(-pad.d),
|
||
jealous: 0,
|
||
missing: 0,
|
||
heartbeat: 0,
|
||
homesick: 0,
|
||
upset: 0,
|
||
...override,
|
||
}
|
||
}
|
||
|
||
/** 晝夜節律:精力基線在本地 15 點最高、凌晨 3 點最低,振幅 ±0.1 */
|
||
export function circadian(nowMs: number, tzOffsetMin: number): number {
|
||
const hour = (((nowMs / HOUR + tzOffsetMin / 60) % 24) + 24) % 24
|
||
return 0.1 * Math.cos(((hour - 15) / 24) * 2 * Math.PI)
|
||
}
|
||
|
||
// 讀檔時才計算:x = B + (x − B)·0.5^(Δ/τ),精力的 B 疊加晝夜節律
|
||
export function decayEmotions(s: EmotionState, baseline: Emotions, now: number, neuroticism: number, tzOffsetMin: number): Emotions {
|
||
const dt = Math.max(0, now - s.at)
|
||
const out = {} as Emotions
|
||
for (const d of DIMS) {
|
||
const b = d === 'energy' ? clampDim(baseline.energy + circadian(now, tzOffsetMin)) : baseline[d]
|
||
out[d] = clampDim(b + (s[d] - b) * Math.pow(0.5, dt / halfLifeMs(d, neuroticism)))
|
||
}
|
||
return out
|
||
}
|
||
|
||
/** 由八維度推回 PAD,供記憶檢索與相容介面使用 */
|
||
export function toPad(e: Emotions, baseline: Emotions, basePad: Pad): Pad {
|
||
return {
|
||
p: clamp((e.mood - 0.5) * 2 - e.upset),
|
||
a: clamp((e.energy - 0.5) * 2),
|
||
d: clamp(basePad.d + baseline.shy - e.shy),
|
||
}
|
||
}
|
||
|
||
/** 舊 PAD 存檔 → 八維度(p→心情、a→精力、d→害羞,其餘維度取基線) */
|
||
export function fromPad(mood: Pad & { at: number }, baseline: Emotions, basePad: Pad): EmotionState {
|
||
return {
|
||
...baseline,
|
||
mood: clampDim((mood.p + 1) / 2),
|
||
energy: clampDim((mood.a + 1) / 2),
|
||
shy: clampDim(baseline.shy + basePad.d - mood.d),
|
||
at: mood.at,
|
||
}
|
||
}
|
||
|
||
// 情感注入:事件(評估標籤)對各維度的增減。mood/upset/energy/shy 由 rules.ts 的 PAD 增減換算
|
||
// (p>0 → 心情 p/2;p<0 → 心情 0.3p、不悅 −0.4p;a/2 → 精力;−d → 害羞),合成後與 PAD 增減一致;
|
||
// 心跳、想念、想家等新維度的額外效果寫在 EXTRA
|
||
const EXTRA: Partial<Record<MoodEvent, Partial<Emotions>>> = {
|
||
thanks: { heartbeat: 0.05 },
|
||
return: { missing: -0.3, heartbeat: 0.1 },
|
||
lateNight: { homesick: 0.05 },
|
||
favorite: { heartbeat: 0.08 },
|
||
ignored: { missing: 0.1 },
|
||
cared: { heartbeat: 0.05, missing: -0.1 },
|
||
remembered: { heartbeat: 0.05 },
|
||
}
|
||
|
||
export function emotionEffect(ev: MoodEvent): Partial<Emotions> {
|
||
const { p, a, d } = MOOD_EVENTS[ev]
|
||
return {
|
||
mood: p > 0 ? p / 2 : p * 0.3,
|
||
upset: p < 0 ? -p * 0.4 : 0,
|
||
energy: a / 2,
|
||
shy: -d,
|
||
...EXTRA[ev],
|
||
}
|
||
}
|
||
|
||
// 「壞」方向:心情下降,或不悅、忌妒上升
|
||
const isBad = (d: Dim, x: number) => (d === 'mood' ? x < 0 : (d === 'upset' || d === 'jealous') && x > 0)
|
||
|
||
export const TURN_CAP = 0.3
|
||
export const TIRED = 0.3
|
||
export const IRRITABLE = 1.5
|
||
export const HABITUATION = 0.7
|
||
export const SENSITIZATION = 0.25
|
||
export const SENSITIZATION_MAX = 2
|
||
export const HABIT_HALF_LIFE_H = 6
|
||
|
||
export type Habit = Partial<Record<MoodEvent, { n: number; at: number }>>
|
||
|
||
const habitCount = (h: Habit, ev: MoodEvent, now: number) => {
|
||
const r = h[ev]
|
||
return r ? r.n * Math.pow(0.5, Math.max(0, now - r.at) / (HABIT_HALF_LIFE_H * HOUR)) : 0
|
||
}
|
||
|
||
/**
|
||
* 套用事件:正向事件習慣化(重複受益遞減,0.7^n)、負向事件敏感化(重複受創放大,上限 2 倍)、
|
||
* 精力低於 0.3 時負面變化放大 1.5 倍(疲勞易怒);每個維度單回合變化上限 ±0.3。
|
||
* 計次隨時間以 6 小時半衰期遺忘。
|
||
*/
|
||
export function applyEvent(cur: Emotions, habit: Habit, ev: MoodEvent, now: number): { emotions: Emotions; habit: Habit } {
|
||
const n = habitCount(habit, ev, now)
|
||
const p = MOOD_EVENTS[ev].p
|
||
const gain = p > 0 ? Math.pow(HABITUATION, n) : 1
|
||
const harm = p < 0 ? Math.min(SENSITIZATION_MAX, 1 + SENSITIZATION * n) : 1
|
||
const irritable = cur.energy < TIRED ? IRRITABLE : 1
|
||
const out = { ...cur }
|
||
for (const [d, raw] of Object.entries(emotionEffect(ev)) as [Dim, number][]) {
|
||
const dx = raw * (isBad(d, raw) ? harm * irritable : gain)
|
||
out[d] = clampDim(out[d] + clamp(dx, -TURN_CAP, TURN_CAP))
|
||
}
|
||
return { emotions: out, habit: { ...habit, [ev]: { n: n + 1, at: now } } }
|
||
}
|
||
|
||
/** 強度:雙向維度為偏離中性的程度,其餘維度即其值 */
|
||
export const salience = (d: Dim, v: number) => (POLES[d] ? Math.abs(v - 0.5) * 2 : v)
|
||
|
||
export function ranked(e: Emotions): Dim[] {
|
||
return [...DIMS].sort((a, b) => salience(b, e[b]) - salience(a, e[a]))
|
||
}
|
||
|
||
export const DOMINANT_MIN = 0.25
|
||
|
||
/** 主情緒:強度達門檻的最強維度;否則 null,由 PAD 八分區標籤補位 */
|
||
export function dominant(e: Emotions): { dim: Dim; label: string; kaomoji: string } | null {
|
||
const dim = ranked(e)[0]!
|
||
if (salience(dim, e[dim]) < DOMINANT_MIN) return null
|
||
const poles = POLES[dim]
|
||
return { dim, label: poles ? poles[e[dim] >= 0.5 ? 1 : 0] : DIM_INFO[dim].name, kaomoji: DIM_INFO[dim].kaomoji }
|
||
}
|
||
|
||
/** info --all 的情緒量表:▰▱ 依維度上色,名稱補齊到同寬 */
|
||
export function gauge(d: Dim, e: Emotions, cells = 12): string {
|
||
const name = DIM_INFO[d].name
|
||
return `${name}${' '.repeat(Math.max(0, 6 - width(name)))}\x1b[${DIM_INFO[d].ansi}m${bar(e[d] * 100, cells)}\x1b[0m ${String(Math.round(e[d] * 100)).padStart(3)}`
|
||
}
|