- 兩本帳(原典基準、存檔累積),日限、有效增量、期望違反公式 - 階段 陌生/熟人/朋友/好友,只升不降;14 天無互動每日 −0.5 - 舊 affinity 存檔讀檔時自動升級 - 版本 0.0.2 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
109 lines
4.7 KiB
TypeScript
109 lines
4.7 KiB
TypeScript
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<Record<Dim, number>>[] = [
|
||
{ 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<RelationTraits> = { 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)) }
|
||
}
|