61 lines
2.0 KiB
TypeScript
61 lines
2.0 KiB
TypeScript
import { clamp } from './types.ts'
|
|
|
|
export const GAIN = 0.01
|
|
export const DAILY_CAP = 0.05
|
|
export const LOSS = 0.03
|
|
export const IDLE_DAYS = 7
|
|
export const HALF_LIFE_DAYS = 30
|
|
const DAY = 86_400_000
|
|
|
|
export type Stage = 0 | 1 | 2 | 3
|
|
export const STAGE_NAMES = ['陌生', '熟悉', '親近', '特別'] as const
|
|
|
|
// 升階門檻;降階門檻 = 升階 − 0.05(遲滯)
|
|
const UP = [0.1, 0.3, 0.6]
|
|
const HYST = 0.05
|
|
|
|
export type Affinity = {
|
|
value: number
|
|
stage: Stage
|
|
/** 今日已增加量與其日期 */
|
|
gainDay: string
|
|
gainToday: number
|
|
lastSeen: number
|
|
}
|
|
|
|
export const initialAffinity = (now: number): Affinity => ({ value: 0, stage: 0, gainDay: dayKey(now), gainToday: 0, lastSeen: now })
|
|
|
|
export const dayKey = (t: number) => new Date(t).toISOString().slice(0, 10)
|
|
|
|
export function stageFor(value: number, prev: Stage): Stage {
|
|
let s = prev
|
|
while (s < 3 && value >= UP[s]!) s++
|
|
while (s > 0 && value < UP[s - 1]! - HYST) s--
|
|
return s as Stage
|
|
}
|
|
|
|
/** 讀取時計算:離開超過 7 天後以 30 天半衰期回到 0 */
|
|
export function settle(a: Affinity, now: number): Affinity {
|
|
const idleDays = (now - a.lastSeen) / DAY
|
|
if (idleDays <= IDLE_DAYS) return a
|
|
const f = Math.pow(0.5, (idleDays - IDLE_DAYS) / HALF_LIFE_DAYS)
|
|
const value = a.value * f
|
|
return { ...a, value, stage: stageFor(value, a.stage), lastSeen: now }
|
|
}
|
|
|
|
export function meaningful(a: Affinity, now: number): Affinity {
|
|
const s = settle(a, now)
|
|
const today = dayKey(now)
|
|
const gainToday = s.gainDay === today ? s.gainToday : 0
|
|
const gain = clamp(Math.min(GAIN, DAILY_CAP - gainToday), 0, GAIN)
|
|
const value = clamp(s.value + gain, -1, 1)
|
|
return { value, stage: stageFor(value, s.stage), gainDay: today, gainToday: gainToday + gain, lastSeen: now }
|
|
}
|
|
|
|
export function negative(a: Affinity, now: number): Affinity {
|
|
const s = settle(a, now)
|
|
const value = clamp(s.value - LOSS, -1, 1)
|
|
const today = dayKey(now)
|
|
return { ...s, value, stage: stageFor(value, s.stage), gainDay: today, gainToday: s.gainDay === today ? s.gainToday : 0, lastSeen: now }
|
|
}
|