feat: 自定义主角称呼(profile.name + {name} 占位符 + 自报句式预填 + 报告/分享卡展示)
This commit is contained in:
+32
-5
@@ -20,6 +20,8 @@ export interface EngineState {
|
||||
pendingFollowUp: PendingFollowUp | null;
|
||||
/** 分支变量(附录 A):从本轨迹的选择/讲述事件增量推导,驱动条件文本 */
|
||||
flags: Flags;
|
||||
/** 玩家自取称呼(C1-01 profile.name),驱动文案里的 {name} 占位符;未设置时回退"你" */
|
||||
playerName: string;
|
||||
/** 全部三章完成后为 true → 进入画像报告 */
|
||||
finished: boolean;
|
||||
}
|
||||
@@ -60,6 +62,15 @@ function newSessionId(): string {
|
||||
: `s-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 称呼预填的轻量本地启发式(故意不走 Jev:typed 模型抽不出任意字符串名字)。
|
||||
* 只认"我叫X / 叫我X / 称呼我X"三种自报句式,取不到就留空给用户手填。
|
||||
*/
|
||||
function guessName(transcript: string): string {
|
||||
const m = transcript.match(/(?:我叫|叫我|称呼我)([一-龥A-Za-z][一-龥A-Za-z·]{0,11})/);
|
||||
return m ? m[1] : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 剧情引擎:状态机。当前章节/节点、按选择推进、事件落 EventStore。
|
||||
* 与 UI 完全解耦——UI 只读 state、调 advance/choose/submitNarration。
|
||||
@@ -89,6 +100,7 @@ export class StoryEngine {
|
||||
activeVariant: null,
|
||||
pendingFollowUp: null,
|
||||
flags: {},
|
||||
playerName: '',
|
||||
finished: false,
|
||||
};
|
||||
this.skipHiddenNodes();
|
||||
@@ -139,8 +151,9 @@ export class StoryEngine {
|
||||
/**
|
||||
* 从持久化位置恢复。持久化的 nodeId 在新版章节数据里已不存在时,
|
||||
* 降级到该章开头而不是崩溃;章节本身不存在则不恢复(返回 false)。
|
||||
* 恢复同时从该轨迹的 profile 事件里取回玩家称呼(旧数据缺 name 字段时回退"你")。
|
||||
*/
|
||||
restore(snapshot: ProgressSnapshot | null): boolean {
|
||||
async restore(snapshot: ProgressSnapshot | null): Promise<boolean> {
|
||||
if (!snapshot) return false;
|
||||
const chapter = this.chapters.find((c) => c.id === snapshot.chapterId);
|
||||
if (!chapter) return false;
|
||||
@@ -152,6 +165,9 @@ export class StoryEngine {
|
||||
this.state.activeVariant = null;
|
||||
this.state.pendingFollowUp = null;
|
||||
this.state.finished = false;
|
||||
const events = await this.store.list(snapshot.sessionId);
|
||||
const profile = [...events].reverse().find((e) => e.kind === 'profile')?.profile;
|
||||
this.state.playerName = profile?.name ?? '';
|
||||
this.skipHiddenNodes();
|
||||
this.persist();
|
||||
return true;
|
||||
@@ -173,10 +189,12 @@ export class StoryEngine {
|
||||
|
||||
/**
|
||||
* 渲染文本:求值文案中的全部【若 …】条件标记(节点级 + 内联段),
|
||||
* 返回玩家可见文本。条件标记本身永不渲染给玩家。
|
||||
* 并把 {name} 占位符替换为玩家称呼(未设置时回退"你")。
|
||||
* 条件标记与占位符本身永不渲染给玩家。
|
||||
*/
|
||||
renderText(text: string): string {
|
||||
return resolveConditionalText(text, this.state.flags) ?? '';
|
||||
const resolved = resolveConditionalText(text, this.state.flags) ?? '';
|
||||
return resolved.replace(/\{name\}/g, this.state.playerName || '你');
|
||||
}
|
||||
|
||||
/** B 类选择:落事件并按选项跳转 */
|
||||
@@ -427,7 +445,7 @@ export class StoryEngine {
|
||||
answers = await this.jev.ask({ transcript }, questions);
|
||||
} catch (e) {
|
||||
console.warn('[engine] 身份字段抽取失败,降级为手填确认页', e);
|
||||
return { city: '', age: '', gender: '', occupation: '', education: '' };
|
||||
return { name: guessName(transcript), city: '', age: '', gender: '', occupation: '', education: '' };
|
||||
}
|
||||
const choiceVal = (id: string): string => {
|
||||
const a = answers[id];
|
||||
@@ -443,7 +461,14 @@ export class StoryEngine {
|
||||
let city = choiceVal('city');
|
||||
const mentioned = answers.city_mentioned;
|
||||
if (mentioned?.type === 'noul' && mentioned.probability < 0.5) city = '';
|
||||
return { city, age, gender: choiceVal('gender'), occupation: choiceVal('occupation'), education: choiceVal('education') };
|
||||
return {
|
||||
name: guessName(transcript),
|
||||
city,
|
||||
age,
|
||||
gender: choiceVal('gender'),
|
||||
occupation: choiceVal('occupation'),
|
||||
education: choiceVal('education'),
|
||||
};
|
||||
}
|
||||
|
||||
/** C1-01 确认页通过后调用:append profile 事件并推进 */
|
||||
@@ -461,6 +486,7 @@ export class StoryEngine {
|
||||
}),
|
||||
);
|
||||
this.state.nodeId = node.next;
|
||||
this.state.playerName = profile.name.trim();
|
||||
this.skipHiddenNodes();
|
||||
this.persist();
|
||||
}
|
||||
@@ -501,6 +527,7 @@ export class StoryEngine {
|
||||
this.state.activeVariant = null;
|
||||
this.state.pendingFollowUp = null;
|
||||
this.state.flags = {};
|
||||
this.state.playerName = '';
|
||||
this.state.finished = false;
|
||||
this.skipHiddenNodes();
|
||||
this.persist();
|
||||
|
||||
Reference in New Issue
Block a user