補測好感度與關係規則 #20
+3
-3
@@ -6,7 +6,7 @@ import type { Axes } from './characters'
|
|||||||
import { EXTRACT_COMMAND, runExtract } from './extractCommand'
|
import { EXTRACT_COMMAND, runExtract } from './extractCommand'
|
||||||
import { applyEvent, hearts, initialAffinity, stageName, stageVoice, dayKey } from './affinity'
|
import { applyEvent, hearts, initialAffinity, stageName, stageVoice, dayKey } from './affinity'
|
||||||
import type { Affinity } from './affinity'
|
import type { Affinity } from './affinity'
|
||||||
import { relationOf, relationText } from './relation'
|
import { latestEntry, relationOf, relationText } from './relation'
|
||||||
import { CLASSIFY_PROMPT, choose, parseRoute } from './router'
|
import { CLASSIFY_PROMPT, choose, parseRoute } from './router'
|
||||||
import type { Effort, ModelName, Route } from './router'
|
import type { Effort, ModelName, Route } from './router'
|
||||||
import { REWRITE_SYSTEM, needsSonnet, protect, restore, withPrefix } from './rewrite'
|
import { REWRITE_SYSTEM, needsSonnet, protect, restore, withPrefix } from './rewrite'
|
||||||
@@ -383,9 +383,9 @@ export const register: Register = (on, options) => {
|
|||||||
let text = personaSection(c, applyOverrides(c.axes, overrides()))
|
let text = personaSection(c, applyOverrides(c.axes, overrides()))
|
||||||
if (flag('affinity', true)) {
|
if (flag('affinity', true)) {
|
||||||
const a = await loadAffinity($, c.slug)
|
const a = await loadAffinity($, c.slug)
|
||||||
text += `\n${stageVoice(a.score)}\n${relationText(relationOf(c.timeline, a.score, flag('romance', false)))}`
|
text += `\n${stageVoice(a.score)}\n${relationText(relationOf(c.timeline, a.score, flag('romance', false)), latestEntry(c.timeline)?.romance)}`
|
||||||
} else {
|
} else {
|
||||||
text += `\n${relationText(relationOf(c.timeline, 0, false))}`
|
text += `\n${relationText(relationOf(c.timeline, 0, false), latestEntry(c.timeline)?.romance)}`
|
||||||
}
|
}
|
||||||
if (delegating) text += '\n本輪的實際工作已交給背景工作線處理,你不要自己動手做。請只用角色口吻簡短告知已開始處理;完成後會有回報,屆時再向使用者說明結果。'
|
if (delegating) text += '\n本輪的實際工作已交給背景工作線處理,你不要自己動手做。請只用角色口吻簡短告知已開始處理;完成後會有回報,屆時再向使用者說明結果。'
|
||||||
return {
|
return {
|
||||||
|
|||||||
+100
-2
@@ -1,11 +1,11 @@
|
|||||||
import { expect, mock, test } from 'claude-code/testing'
|
import { expect, mock, test } from 'claude-code/testing'
|
||||||
|
|
||||||
type Ctl = { complete: (e: any) => unknown; usage: unknown; seen?: any; submitted: string[] }
|
type Ctl = { complete: (e: any) => unknown; usage: unknown; seen?: any; submitted: string[]; clock?: any }
|
||||||
|
|
||||||
const setup = (on: any): Ctl => {
|
const setup = (on: any): Ctl => {
|
||||||
const ctl: Ctl = { complete: () => ({ isAnswered: false, reason: 'aborted' }), usage: { rateLimits: [], context: {} }, submitted: [] }
|
const ctl: Ctl = { complete: () => ({ isAnswered: false, reason: 'aborted' }), usage: { rateLimits: [], context: {} }, submitted: [] }
|
||||||
mock.store(on)
|
mock.store(on)
|
||||||
mock.clock(on, { now: Date.parse('2026-10-15T08:00:00Z') })
|
ctl.clock = mock.clock(on, { now: Date.parse('2026-10-15T08:00:00Z') })
|
||||||
on('ui.log', () => ({ value: undefined }) as never)
|
on('ui.log', () => ({ value: undefined }) as never)
|
||||||
on('command.register', () => ({ value: { name: 'x' } }) as never)
|
on('command.register', () => ({ value: { name: 'x' } }) as never)
|
||||||
on('session.start', () => ({ cwd: '/tmp' }) as never)
|
on('session.start', () => ({ cwd: '/tmp' }) as never)
|
||||||
@@ -143,6 +143,104 @@ test('未成年角色維持友情,即使好感度最高且開啟戀愛式依
|
|||||||
expect((await personaText($)).includes('關係定位:友情')).toBe(true)
|
expect((await personaText($)).includes('關係定位:友情')).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 以 fs 事件邊界提供角色包(characters/index.json),讓外掛自行載入成年角色。
|
||||||
|
const packs = (on: any, list: { slug: string; romance?: string }[]) => {
|
||||||
|
const files = new Map<string, string>()
|
||||||
|
files.set('index.json', JSON.stringify({ packs: list.map(p => ({ work: 'tw', workName: '測試作品', slug: p.slug })) }))
|
||||||
|
for (const p of list) {
|
||||||
|
files.set(
|
||||||
|
`tw/${p.slug}.json`,
|
||||||
|
JSON.stringify({
|
||||||
|
work: 'tw',
|
||||||
|
slug: p.slug,
|
||||||
|
name: '成年測試',
|
||||||
|
persona: { selfRef: '我', address: '你', tone: '平穩', catchphrase: '好', values: '可靠', taboos: '無' },
|
||||||
|
axes: { gentle: 5, tsundere: 5, lively: 5, formal: 5, talkative: 5, humor: 5, proactive: 5, expressive: 5 },
|
||||||
|
anime: { moe: '', speech: '', emotions: '', kaomoji: '', themeColor: 'cyan' },
|
||||||
|
timeline: [
|
||||||
|
{ chapter: '第1章', age: 17, relationships: [] },
|
||||||
|
{ chapter: '第2章', age: 20, relationships: p.romance ? [{ with: p.romance, kind: 'romantic' }] : [] },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const key = (path: string) => path.replace(/^.*\/characters\//, '')
|
||||||
|
on('fs.exists', (_$: any, e: any) => ({ value: files.has(key(e.path)) }) as never)
|
||||||
|
on('fs.read', (_$: any, e: any) => ({ value: files.get(key(e.path)) ?? '' }) as never)
|
||||||
|
}
|
||||||
|
|
||||||
|
test('成年且原作有戀愛關係:關係移轉給使用者,原戀人退為過去的重要人物', async ($, on) => {
|
||||||
|
setup(on)
|
||||||
|
packs(on, [{ slug: 'lover', romance: '桐人' }])
|
||||||
|
await start($, 'tw-lover')
|
||||||
|
const t = await personaText($)
|
||||||
|
expect(t.includes('已移轉給使用者')).toBe(true)
|
||||||
|
expect(t.includes('桐人')).toBe(true)
|
||||||
|
expect(t.includes('過去的重要人物')).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('成年但原作無戀愛關係:需最高階段且明確開啟才進入戀愛式依賴', async ($, on) => {
|
||||||
|
setup(on)
|
||||||
|
packs(on, [{ slug: 'plain' }])
|
||||||
|
await start($, 'tw-plain')
|
||||||
|
await $.command.run({ command: 'anima-affinity', args: '100' } as never)
|
||||||
|
expect((await personaText($)).includes('戀愛式依賴')).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('成年無戀愛、最高階段但未在 /config 開啟:維持夥伴關係', { options: { romance: false } }, async ($, on) => {
|
||||||
|
setup(on)
|
||||||
|
packs(on, [{ slug: 'plain' }])
|
||||||
|
await start($, 'tw-plain')
|
||||||
|
await $.command.run({ command: 'anima-affinity', args: '100' } as never)
|
||||||
|
expect((await personaText($)).includes('夥伴與朋友')).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('成年無戀愛、已開啟但好感度未達最高階段:不進入戀愛式依賴', { options: { romance: true } }, async ($, on) => {
|
||||||
|
setup(on)
|
||||||
|
packs(on, [{ slug: 'plain' }])
|
||||||
|
await start($, 'tw-plain')
|
||||||
|
await $.command.run({ command: 'anima-affinity', args: '79' } as never)
|
||||||
|
expect((await personaText($)).includes('戀愛式依賴')).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('成年無戀愛、最高階段且已開啟:進入戀愛式依賴', { options: { romance: true } }, async ($, on) => {
|
||||||
|
setup(on)
|
||||||
|
packs(on, [{ slug: 'plain' }])
|
||||||
|
await start($, 'tw-plain')
|
||||||
|
await $.command.run({ command: 'anima-affinity', args: '80' } as never)
|
||||||
|
expect((await personaText($)).includes('戀愛式依賴')).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('長期不互動好感度緩慢下降,但不跨階段掉落', async ($, on) => {
|
||||||
|
const ctl = setup(on)
|
||||||
|
ctl.complete = () => route('haiku')
|
||||||
|
await start($)
|
||||||
|
await submit($, '你好')
|
||||||
|
await $.command.run({ command: 'anima-affinity', args: '45' } as never)
|
||||||
|
await ctl.clock.advance(30 * 86_400_000)
|
||||||
|
await submit($, '我回來了')
|
||||||
|
const n = await affinity($)
|
||||||
|
expect(n < 45).toBe(true)
|
||||||
|
expect(n >= 40).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('個性軸影響扣分倍率:傲嬌角色掉得比溫柔角色多', async ($, on) => {
|
||||||
|
const ctl = setup(on)
|
||||||
|
ctl.complete = () => route('haiku')
|
||||||
|
packs(on, [{ slug: 'plain' }])
|
||||||
|
const run = async (slug: string) => {
|
||||||
|
await start($, slug)
|
||||||
|
await submit($, '你好')
|
||||||
|
await $.command.run({ command: 'anima-affinity', args: '70' } as never)
|
||||||
|
await ctl.clock.advance(8 * 86_400_000)
|
||||||
|
await submit($, '我回來了')
|
||||||
|
return affinity($)
|
||||||
|
}
|
||||||
|
const proud = await run('tw-plain')
|
||||||
|
const calm = await run('sao-suguha')
|
||||||
|
expect(proud < calm).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
const workRoute = (model: string, work: boolean) => ({ isAnswered: true, usage, text: `{"difficulty":8,"model":"${model}","effort":"high","tone":"neutral","ack":"好,我來處理。","work":${work}}` })
|
const workRoute = (model: string, work: boolean) => ({ isAnswered: true, usage, text: `{"difficulty":8,"model":"${model}","effort":"high","tone":"neutral","ack":"好,我來處理。","work":${work}}` })
|
||||||
|
|
||||||
test('需要動手的要求交給背景工作線,對話線改用 Haiku', async ($, on) => {
|
test('需要動手的要求交給背景工作線,對話線改用 Haiku', async ($, on) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user