Files
jsc-marketplace/plugins/jsc-mod-companion/hooks/companion/relation.ts
T
adminandClaude Sonnet 5.5 4f86b6b7dc feat: companion 關係改為好感、信賴、親密、依賴四維度與四階段
- 兩本帳(原典基準、存檔累積),日限、有效增量、期望違反公式
- 階段 陌生/熟人/朋友/好友,只升不降;14 天無互動每日 −0.5
- 舊 affinity 存檔讀檔時自動升級
- 版本 0.0.2

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-14 07:54:08 +00:00

109 lines
4.7 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 './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)) }
}