diff --git a/TARGET.md b/TARGET.md index c947868..06986a5 100644 --- a/TARGET.md +++ b/TARGET.md @@ -125,10 +125,17 @@ Q5–Q10 六題已於 2026/08/03 由使用者拍板,設計不再需要確認 - 為什麼:真人在聊天軟體上的情緒,一半是靠「又補了一句」傳出去的。這是文字獨有、目前完全沒用到的手段 - 驗收:慌的時候會多一則短的;平靜時永遠只有一則 -- [ ] **5.3 稱呼隨情緒偏移** +- [x] **5.3 稱呼隨情緒偏移**(2026/08/03 16:32:49 完成) - `relation style` 的 `--except <情緒>>=<值>` 已能做條件替換(`anger>=40` → 退回全名)。缺的是**害羞方向**。 - - [ ] 害羞方向:改口、講到一半換稱呼、把名字吞掉不叫 - - [ ] 每個關係節點可設「害羞時的稱呼」,沒設就退回吞掉名字(改用「你」) + - [x] 害羞方向:改口、講到一半換稱呼、把名字吞掉不叫 + —— `shyAddress()`。**刻意不走 `except` 那條路**:`except` 是「規則被暫停、 + 退回更正式的講法」(生氣方向),害羞是**叫不出來**,方向相反 + - [x] 每個關係節點可設「害羞時的稱呼」,沒設就退回吞掉名字(改用「你」) + —— 新的 `羞怯稱呼` facet(`relation style --facet 羞怯稱呼`), + 門檻 `SHY_ADDRESS_AT = 60`,跟破口/標點/嘴硬同一條線 + (所以女性預設 70 本來就落在這條線上,這是 G3 要的)。 + 它不列進「他要求過的語氣規則」那行——只在過門檻時生效,列兩次會變成兩件事; + `relation style` 的清單裡照樣看得到 - 驗收:同一個對象,生氣時被叫全名、害羞時直接不叫名字 - [x] **5.4 鬧彆扭的階梯與三段式句法(G3 核心,依賴 5.7 的動作額度)**(2026/08/03 16:06:10 完成) diff --git a/scripts/persona-lib.mjs b/scripts/persona-lib.mjs index 02c7e9f..2ea292e 100644 --- a/scripts/persona-lib.mjs +++ b/scripts/persona-lib.mjs @@ -4593,7 +4593,28 @@ export function toneFor(node) { // 個人化語氣規則直接掛在關係節點的 `style` 上(不另外開檔案): // node.style = { 稱呼: { value: "親愛的", except: "anger>=40", since: "2026-07-30" }, ... } // 放這裡的理由:關係圖每輪都會被讀進 ``,稱呼規則就不用跟關鍵詞搶召回。 -export const STYLE_FACETS = ["稱呼", "敬語", "口頭禪", "禁忌", "習慣"]; +export const STYLE_FACETS = ["稱呼", "敬語", "口頭禪", "禁忌", "習慣", "羞怯稱呼"]; + +/** 羞恥度到這裡就換稱呼(跟破口、標點同一條門檻線)。 */ +export const SHY_ADDRESS_AT = 60; +/** 「害羞時要怎麼叫他」掛在關係節點的這個 facet 上。 */ +export const SHY_FACET = "羞怯稱呼"; + +/** + * 害羞時的稱呼。 + * + * `except`(`anger>=40` → 退回全名)走的是**生氣**方向:規則被暫停、退回查表的講法。 + * 害羞是反方向——不是退回更正式,是**叫不出來**:改口、講到一半換稱呼、把名字吞掉。 + * + * 節點自己設了 `羞怯稱呼` 就用它;沒設就吞掉名字改用「你」。 + * 不叫名字本身就是訊號,這是預設值而不是缺省行為。 + */ +export function shyAddress(slug, node, modesty) { + const value = Number(modesty); + if (!node || !Number.isFinite(value) || value < SHY_ADDRESS_AT) return null; + const own = node.style?.[SHY_FACET]?.value; + return { modesty: Math.round(value), value: own ? injectSafeLine(own) : null, own: Boolean(own) }; +} /** `except` 形如 `anger>=40`:拿當下情緒判斷這條規則現在生不生效。 */ function styleSuspended(except, emotion) { @@ -4713,7 +4734,7 @@ export function toneLayerOf(slug) { return node ? toneFor(node).layer : null; } -export function toneDirective(slug) { +export function toneDirective(slug, opts = {}) { const node = speakerNode(slug); if (!node) return ""; const t = toneFor(node); @@ -4722,7 +4743,9 @@ export function toneDirective(slug) { `${t.inferred ? ",bond 由 tags 推測" : ""})→ **語氣層:${t.layer}** — ${t.guide}。`, ]; // 他本人要求過的規則優先於查表:查表算距離,這裡記「他要你怎麼叫他」。 - const rules = styleRules(slug, node); + // `羞怯稱呼` 不列在這裡——它只在羞恥度過門檻時生效,由下面那行單獨講。 + // 兩邊都列會讓人格同時看到「叫他⋯那個」與「平常叫他⋯那個」,那是兩件事。 + const rules = styleRules(slug, node).filter((r) => r.facet !== SHY_FACET); const live = rules.filter((r) => !r.suspended); const held = rules.filter((r) => r.suspended); if (live.length) { @@ -4738,6 +4761,18 @@ export function toneDirective(slug) { " — 這時候退回語氣層的預設講法。", ); } + // 害羞是「叫不出來」,不是「退回更正式」——所以它不走 except 那條路(見 shyAddress)。 + const shy = shyAddress(slug, node, modestyState(slug, opts).value); + if (shy) { + lines.push( + shy.own + ? ` **現在害羞(羞恥度 ${shy.modesty}):叫他「${shy.value}」**,不是平常那個叫法。` + + "改口、講到一半換稱呼都算——卡住的那個地方就是害羞。" + : ` **現在害羞(羞恥度 ${shy.modesty}):把名字吞掉,改用「你」**。` + + "不叫名字本身就是訊號;改口、講到一半換稱呼都算。" + + `要專屬的叫法就在那個節點設 \`${SHY_FACET}\`。`, + ); + } lines.push( " 這一層決定距離感與稱呼,情緒只負責溫度與句長,不會蓋過它;" + "檢查標準:這句話換成對一個「禮貌層」的人說也毫無違和,就代表你沒進到這一層。", @@ -5859,7 +5894,7 @@ export function turnContext(slug, sessionId, prompt = "") { split: Boolean(split), }); // 語氣層:由「使用者在關係圖裡是誰」+ bond × 親近度算出來,不靠人格自己記得。 - const tone = toneDirective(slug); + const tone = toneDirective(slug, speakOpts); if (tone) lines.push(tone); // 語氣層:他講過的原句與「遇到事會做什麼」。每輪最多 2 到 3 條,而且挑跟這一輪 // 對象/話題對得上的——全注入會變成表演(他開始照抄自己的舊台詞)。 diff --git a/scripts/selftest.mjs b/scripts/selftest.mjs index 3de8368..7e95943 100644 --- a/scripts/selftest.mjs +++ b/scripts/selftest.mjs @@ -2026,6 +2026,34 @@ check("relation speaker 設定後,context 會注入語氣層與稱呼規則", return res.status === 0 && pl.loadConfig("alpha").speaker_node === pl.slugify("伴侶甲") && ctx.includes("語氣層:老夫老妻") && ctx.includes("親愛的"); })()); +// 稱呼隨情緒偏移:生氣走 except(退回全名),害羞是反方向(叫不出來) +check("羞怯稱呼是 STYLE_FACETS 的一格", pl.STYLE_FACETS.includes(pl.SHY_FACET)); +check("羞恥度不到門檻就不換稱呼", + pl.shyAddress("alpha", { style: {} }, pl.SHY_ADDRESS_AT - 1) === null); +check("到門檻、沒設專屬叫法 → 吞掉名字(不叫名字本身就是訊號)", (() => { + const s = pl.shyAddress("alpha", { style: {} }, 70); + return s !== null && s.own === false && s.value === null; +})(), JSON.stringify(pl.shyAddress("alpha", { style: {} }, 70))); +check("節點設了羞怯稱呼就用它", (() => { + const s = pl.shyAddress("alpha", { style: { 羞怯稱呼: { value: "⋯那個" } } }, 70); + return s !== null && s.own === true && s.value === "⋯那個"; +})(), JSON.stringify(pl.shyAddress("alpha", { style: { 羞怯稱呼: { value: "⋯那個" } } }, 70))); +check("害羞時的語氣指示叫他把名字吞掉(女性預設 70 就落在這條線上)", (() => { + cli(["relation", "speaker", "--session", S_HOST, "--name", "伴侶甲"]); + return pl.modestyState("alpha", {}).value >= pl.SHY_ADDRESS_AT && + pl.toneDirective("alpha", {}).includes("把名字吞掉"); +})(), `${pl.modestyState("alpha", {}).value}|${pl.toneDirective("alpha", {})}`); +check("設了羞怯稱呼之後改成叫那個,而且不重複列在「他要求過的規則」裡", (() => { + cli(["relation", "style", "--session", S_HOST, "--name", "伴侶甲", + "--facet", "羞怯稱呼", "--value", "⋯那個"]); + const d = pl.toneDirective("alpha", {}); + return d.includes("叫他「⋯那個」") && !d.includes("把名字吞掉") && + !d.includes("羞怯稱呼→"); +})(), pl.toneDirective("alpha", {})); +check("羞怯稱呼在 relation style 的清單裡還是查得到(那裡該看得見全部)", (() => { + const out = cli(["relation", "style", "--session", S_HOST, "--name", "伴侶甲"]).stdout; + return out.includes("羞怯稱呼"); +})()); check("relation speaker --clear 之後就不再注入語氣指示", (() => { cli(["relation", "speaker", "--session", S_HOST, "--clear"]); return !pl.loadConfig("alpha").speaker_node &&