Files
adminandClaude Sonnet 5.5 5bec18b44b feat: 階段 2 路由、改寫與好感度
- Haiku 一次分類(難度、模型、思考程度、語氣),角色立刻回應
- turn.step 覆寫模型與思考程度,額度低於門檻自動降一級,支援手動鎖定
- 主模型回覆改寫為角色口吻,程式碼/路徑/指令/數字/URL 原樣保留,失敗回原文並另存原文 log
- 好感度規則、五階段、不跨階段掉落、個性倍率、狀態列愛心、/jsc-anima:anima-affinity
- 年齡與關係規則(未成年友情、成年移轉、戀愛式依賴需明確開啟)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-15 06:16:22 +00:00

62 lines
2.4 KiB
TypeScript

export type Color = 'green' | 'yellow' | 'red' | 'cyan' | undefined
export type Part = { text: string; color?: Color; dim?: boolean }
export type Segment = { id: string; parts: Part[] }
export const BAR_LEN = 8
export const textWidth = (s: string): number => {
let w = 0
for (const ch of s) {
const c = ch.codePointAt(0)!
w += c >= 0x1100 && (c <= 0x115f || (c >= 0x2e80 && c <= 0xa4cf) || (c >= 0xac00 && c <= 0xd7a3) || (c >= 0xf900 && c <= 0xfaff) || (c >= 0xfe30 && c <= 0xfe6f) || (c >= 0xff00 && c <= 0xff60) || (c >= 0xffe0 && c <= 0xffe6)) ? 2 : 1
}
return w
}
export const bar = (pct: number, style: 'block' | 'dot' = 'block'): string => {
const n = Math.round((Math.max(0, Math.min(100, pct)) / 100) * BAR_LEN)
const [on, off] = style === 'dot' ? ['●', '○'] : ['█', '░']
return on.repeat(n) + off.repeat(BAR_LEN - n)
}
// 使用率:越高越紅
export const usedColor = (pct: number, warn: number, danger: number): Color =>
pct >= danger ? 'red' : pct >= warn ? 'yellow' : 'green'
// 剩餘:量表越少越紅
export const remainColor = (remain: number, warn: number, danger: number): Color =>
usedColor(100 - remain, warn, danger)
export const FIELD_IDS = ['character', 'love', 'model', 'dir', 'branch', '5h', '7d', 'ctx', 'sys'] as const
// 窄終端機時整欄隱藏的順序
export const HIDE_ORDER = ['sys', '7d', 'dir', 'branch'] as const
export const parseFields = (s: string): string[] => {
const ids = s.split(',').map(x => x.trim()).filter(x => (FIELD_IDS as readonly string[]).includes(x))
return ids.length ? [...new Set(ids)] : [...FIELD_IDS]
}
const SEP = ' │ '
export const segmentWidth = (s: Segment): number => s.parts.reduce((n, p) => n + textWidth(p.text), 0)
// 整欄隱藏,絕不截斷。
export const layout = (segments: Segment[], columns: number): Segment[] => {
const list = [...segments]
const total = () => list.reduce((n, s) => n + segmentWidth(s), 0) + Math.max(0, list.length - 1) * textWidth(SEP)
for (const id of HIDE_ORDER) {
if (total() <= columns) break
const i = list.findIndex(s => s.id === id)
if (i >= 0) list.splice(i, 1)
}
return list
}
export const SEPARATOR = SEP
export const meter = (label: string, pct: number, color: Color, style: 'block' | 'dot', suffix: string): Segment['parts'] => [
{ text: `${label} ` },
{ text: bar(pct, style), color },
{ text: ` ${Math.round(pct)}%${suffix}` },
]