import { clamp } from './mood' import type { Dims, Relation, RelationTraits } from './types' const DAY = 86_400_000 export const DIMS = ['affection', 'trust', 'intimacy', 'dependence'] as const export type Dim = (typeof DIMS)[number] const zero = (): Dims => ({ affection: 0, trust: 0, intimacy: 0, dependence: 0 }) // 第 9.6 章:階段只升不降,條件全部滿足才升級(本里程碑只做到好友) export const STAGE_RULES: Partial>[] = [ { affection: 15 }, { affection: 30, trust: 25 }, { affection: 50, trust: 45, intimacy: 30 }, ] // 一般互動的原始增量(一般聊天 ±3)與失去信賴的基礎值 export const MEANINGFUL: Dims = { affection: 3, trust: 2, intimacy: 1.5, dependence: 1 } export const NEGATIVE_AFFECTION = -3 export const NEGATIVE_TRUST = 3 export const SEVERITY = { broken: 0.3, scold: 0.4, boundary: 0.6, lie: 1 } as const export const TRUST_LOSS_CAP = 30 const IDLE_GRACE_DAYS = 14 const IDLE_DAILY_LOSS = 0.5 const DEFAULT_TRAITS: Required = { guard: 0.5, shy: 0.5, coy: 0.5, clingy: 0.5 } export const dayKey = (ms: number) => new Date(ms).toISOString().slice(0, 10) export function initialRelation(now: number, base: Dims = zero()): Relation { return { base, save: zero(), stage: 0, dayKey: dayKey(now), dayGain: zero(), lastSeen: now } } export function effective(r: Relation): Dims { const out = zero() for (const d of DIMS) out[d] = clamp(r.base[d] + r.save[d], 0, 100) return out } // 社會滲透日限(Altman 與 Taylor) export function dailyLimits(t: RelationTraits = {}): Dims { const { guard, shy, coy, clingy } = { ...DEFAULT_TRAITS, ...t } return { affection: 4, trust: 5 * (1 - guard * 0.6), intimacy: 3.5 * (1 - shy * 0.4) * (1 + coy * 0.4), dependence: 2.5 * (0.7 + clingy * 0.6), } } // 條件全部滿足才升級;只升不降,所以數值回落時階段不變 export function stageFor(eff: Dims, current: number): number { let s = current while (s < STAGE_RULES.length && DIMS.every(d => eff[d] >= (STAGE_RULES[s]![d] ?? 0))) s++ return s } // 久未互動超過 14 天後,每天 −0.5;階段不會降低 export function idleDecay(r: Relation, now: number): Relation { const days = Math.floor((now - r.lastSeen) / DAY) - IDLE_GRACE_DAYS if (days <= 0) return r const save = zero() for (const d of DIMS) save[d] = Math.max(-r.base[d], r.save[d] - days * IDLE_DAILY_LOSS) return { ...r, save } } const withDay = (r: Relation, now: number): Relation => { const key = dayKey(now) return r.dayKey === key ? r : { ...r, dayKey: key, dayGain: zero() } } // 有效增量 = 原始增量 × max(0, 1 − 今日累計 / 日限) × (1 − 目前值 / 100) export function gain(r: Relation, d: Dim, raw: number, traits?: RelationTraits): Relation { const limit = dailyLimits(traits)[d] const applied = raw * Math.max(0, 1 - r.dayGain[d] / limit) * (1 - effective(r)[d] / 100) return { ...r, save: { ...r.save, [d]: r.save[d] + applied }, dayGain: { ...r.dayGain, [d]: r.dayGain[d] + applied } } } // 期望違反(Burgoon):越親密、心防越高,信賴損失越大;單次上限 −30 export function trustLoss(base: number, severity: number, intimacy: number, guard = DEFAULT_TRAITS.guard): number { return Math.min(TRUST_LOSS_CAP, base * (1.5 + 0.5 * severity) * (1 + intimacy / 100) * (0.7 + 0.6 * guard)) } export function interact(prev: Relation, kind: 'meaningful' | 'negative', now: number, traits?: RelationTraits, severity: number = SEVERITY.scold): Relation { let r = withDay(idleDecay(prev, now), now) if (kind === 'meaningful') { for (const d of DIMS) r = gain(r, d, MEANINGFUL[d], traits) } else { const loss = trustLoss(NEGATIVE_TRUST, severity, effective(r).intimacy, traits?.guard) r = { ...r, save: { ...r.save, affection: r.save.affection + NEGATIVE_AFFECTION, trust: r.save.trust - loss } } } const save = { ...r.save } for (const d of DIMS) save[d] = clamp(save[d], -r.base[d], 100 - r.base[d]) r = { ...r, save, lastSeen: now } return { ...r, stage: stageFor(effective(r), r.stage) } } // 舊版單一好感值(0–1)→ 四維度。其餘維度依比例推估,使新階段不低於舊階段 export type LegacyAffinity = { value: number; stage: number; dayKey: string; dayGain: number; lastSeen: number } export function migrateAffinity(a: LegacyAffinity): Relation { const v = a.value * 100 const save: Dims = { affection: v, trust: v * 0.8, intimacy: v * 0.8, dependence: v * 0.4 } const r: Relation = { base: zero(), save, stage: 0, dayKey: a.dayKey, dayGain: { ...zero(), affection: a.dayGain * 100 }, lastSeen: a.lastSeen } return { ...r, stage: Math.max(Math.min(a.stage, STAGE_RULES.length), stageFor(effective(r), 0)) } }