177 lines
5.1 KiB
JavaScript
Raw Permalink Normal View History

/**
* M-PALACE · 人格分析引擎
* 实时分析玩家每一次输入识别并量化人格侧面
*
* 分析流程
* 关键词匹配扫描 persona-dict keywords
* 行为信号匹配判断选项类型对应的 behavior_signals
* 上下文推断结合对话历史推断语气/态度/策略倾向
* 输出 8 维人格侧面分数0~100
*/
const fs = require('fs');
const path = require('path');
const DICT_PATH = path.join(__dirname, '..', '..', 'data', 'persona-dict.json');
let _dictCache = null;
function loadDict() {
if (_dictCache) return _dictCache;
_dictCache = JSON.parse(fs.readFileSync(DICT_PATH, 'utf-8'));
return _dictCache;
}
/**
* 生成初始人格分数基于身份锚点
*/
function getInitialScores(role) {
const base = {
ambition: 50, warmth: 50, aggression: 50, suspicion: 50,
vanity: 50, cunning: 50, loyalty: 50, fear: 50
};
const adjustments = {
'皇帝': { ambition: 20, vanity: 15 },
'妃子': { warmth: 15, fear: 10 },
'重臣': { loyalty: 20, cunning: 10 },
'奸臣': { cunning: 25, aggression: 15 }
};
const adj = adjustments[role] || {};
for (const [k, v] of Object.entries(adj)) {
base[k] = Math.min(100, base[k] + v);
}
return base;
}
/**
* 关键词匹配在输入文本中扫描 persona-dict keywords
* @returns {{ [dimensionId]: number }} 命中次数 map
*/
function matchKeywords(text) {
const dict = loadDict();
const hits = {};
for (const dim of dict.dimensions) {
hits[dim.id] = 0;
for (const kw of dim.keywords) {
if (text.includes(kw)) hits[dim.id]++;
}
}
return hits;
}
/**
* 行为信号匹配将选项 index 映射到行为信号
* 选项设计规则A=最强侧面舒适区B=最弱侧面成长区C=中性
* @param {number|null} choiceIndex 0/1/2 or null(自由输入)
* @param {object} optionMeta 后端为本次选项附加的元数据 { a_dims, b_dims, c_dims }
*/
function matchBehavior(choiceIndex, optionMeta) {
if (choiceIndex === null || !optionMeta) return {};
const key = ['a_dims', 'b_dims', 'c_dims'][choiceIndex];
const dims = (optionMeta && optionMeta[key]) || [];
const hits = {};
for (const d of dims) {
hits[d] = (hits[d] || 0) + 1;
}
return hits;
}
/**
* 合并关键词 + 行为信号 更新人格分数
*/
function updateScores(currentScores, keywordHits, behaviorHits) {
const updated = { ...currentScores };
const KEYWORD_WEIGHT = 3;
const BEHAVIOR_WEIGHT = 5;
for (const dim of Object.keys(updated)) {
let delta = 0;
if (keywordHits[dim]) delta += keywordHits[dim] * KEYWORD_WEIGHT;
if (behaviorHits[dim]) delta += behaviorHits[dim] * BEHAVIOR_WEIGHT;
updated[dim] = Math.max(0, Math.min(100, updated[dim] + delta));
}
return updated;
}
/**
* 找到当前最突出特征
*/
function getDominantTrait(scores) {
let maxDim = null;
let maxVal = -1;
for (const [k, v] of Object.entries(scores)) {
if (v > maxVal) { maxVal = v; maxDim = k; }
}
return maxDim;
}
/**
* 检测分数突变单次 +15 以上
*/
function detectSurge(oldScores, newScores) {
const surges = [];
for (const dim of Object.keys(newScores)) {
const delta = newScores[dim] - (oldScores[dim] || 50);
if (delta >= 15) surges.push({ dim, delta });
}
return surges;
}
/**
* 检测交叉升高两个维度同时上升 10+
*/
function detectCrossRise(oldScores, newScores) {
const rising = [];
for (const dim of Object.keys(newScores)) {
const delta = newScores[dim] - (oldScores[dim] || 50);
if (delta >= 10) rising.push(dim);
}
return rising.length >= 2 ? rising : [];
}
/**
* 主分析入口
* @param {string} text 玩家输入文本
* @param {number|null} choiceIndex 选项序号null = 自由输入
* @param {object} optionMeta 选项元数据
* @param {object} currentScores 当前人格分数
* @returns {{ scores, dominant, surges, crossRise }}
*/
function analyze(text, choiceIndex, optionMeta, currentScores) {
const kwHits = text ? matchKeywords(text) : {};
const bhHits = matchBehavior(choiceIndex, optionMeta);
const oldScores = { ...currentScores };
const newScores = updateScores(currentScores, kwHits, bhHits);
const dominant = getDominantTrait(newScores);
const surges = detectSurge(oldScores, newScores);
const crossRise = detectCrossRise(oldScores, newScores);
return { scores: newScores, dominant, surges, crossRise };
}
/**
* 8 维人格分数转换为前端四维状态栏数值
* 权力值 = avg(ambition, cunning)
* 地位值 = avg(vanity, loyalty)
* 情感值 = avg(warmth, fear反转)
* 冲突值 = avg(aggression, suspicion)
*/
function toFourDimensions(scores) {
return {
power: Math.round((scores.ambition + scores.cunning) / 2),
status: Math.round((scores.vanity + scores.loyalty) / 2),
emotion: Math.round((scores.warmth + (100 - scores.fear)) / 2),
conflict: Math.round((scores.aggression + scores.suspicion) / 2)
};
}
module.exports = {
getInitialScores,
analyze,
getDominantTrait,
toFourDimensions,
matchKeywords,
detectSurge,
detectCrossRise
};