feat: AI中间章节改写功能 + 滚动优化

This commit is contained in:
wangwuww111
2026-03-06 13:16:54 +08:00
parent 66d4bd60c1
commit bbdccfa843
9 changed files with 602 additions and 21 deletions

View File

@@ -9,6 +9,7 @@ export default class StoryManager {
this.currentStory = null;
this.currentNodeKey = 'start';
this.categories = [];
this.pathHistory = []; // 记录用户走过的路径
}
/**
@@ -56,6 +57,7 @@ export default class StoryManager {
try {
this.currentStory = await get(`/stories/${storyId}`);
this.currentNodeKey = 'start';
this.pathHistory = []; // 重置路径历史
// 记录游玩次数
await post(`/stories/${storyId}/play`);
@@ -85,6 +87,14 @@ export default class StoryManager {
}
const choice = currentNode.choices[choiceIndex];
// 记录路径历史
this.pathHistory.push({
nodeKey: this.currentNodeKey,
content: currentNode.content,
choice: choice.text
});
this.currentNodeKey = choice.nextNodeKey;
return this.getCurrentNode();
@@ -145,6 +155,39 @@ export default class StoryManager {
}
}
/**
* AI改写中间章节生成新的剧情分支
* @returns {Object|null} 成功返回新节点,失败返回 null不改变当前状态
*/
async rewriteBranch(storyId, prompt, userId) {
try {
const currentNode = this.getCurrentNode();
const result = await post(`/stories/${storyId}/rewrite-branch`, {
userId: userId,
currentNodeKey: this.currentNodeKey,
pathHistory: this.pathHistory,
currentContent: currentNode?.content || '',
prompt: prompt
}, { timeout: 300000 }); // 5分钟超时AI生成需要较长时间
// 检查是否有有效的 nodes
if (result && result.nodes) {
// AI 成功,将新分支合并到当前故事中
Object.assign(this.currentStory.nodes, result.nodes);
// 跳转到新分支的入口节点
this.currentNodeKey = result.entryNodeKey || 'branch_1';
return this.getCurrentNode();
}
// AI 失败,返回 null
console.log('AI服务不可用:', result?.error || '未知错误');
return null;
} catch (error) {
console.error('AI改写分支失败:', error?.errMsg || error?.message || JSON.stringify(error));
return null;
}
}
/**
* AI续写故事
*/

View File

@@ -71,8 +71,10 @@ export default class ChapterScene extends BaseScene {
const cardHeight = 85;
const gap = 12;
const headerHeight = 80;
const contentHeight = this.nodeList.length * (cardHeight + gap) + headerHeight;
this.maxScrollY = Math.max(0, contentHeight - this.screenHeight + 20);
const bottomPadding = 50; // 底部留出空间
const contentHeight = this.nodeList.length * (cardHeight + gap) + headerHeight + bottomPadding;
this.maxScrollY = Math.max(0, contentHeight - this.screenHeight);
console.log('[ChapterScene] nodeList长度:', this.nodeList.length, 'contentHeight:', contentHeight, 'screenHeight:', this.screenHeight, 'maxScrollY:', this.maxScrollY);
}
update() {}
@@ -173,9 +175,17 @@ export default class ChapterScene extends BaseScene {
if (this.maxScrollY > 0) {
const scrollBarHeight = 50;
const scrollBarY = startY + (this.scrollY / this.maxScrollY) * (this.screenHeight - startY - scrollBarHeight - 20);
ctx.fillStyle = 'rgba(255,255,255,0.2)';
this.roundRect(ctx, this.screenWidth - 5, scrollBarY, 3, scrollBarHeight, 1.5);
ctx.fillStyle = 'rgba(255,255,255,0.4)';
this.roundRect(ctx, this.screenWidth - 8, scrollBarY, 5, scrollBarHeight, 2.5);
ctx.fill();
// 如果还没滚动到底部,显示提示
if (this.scrollY < this.maxScrollY - 10) {
ctx.fillStyle = 'rgba(255,255,255,0.4)';
ctx.font = '12px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('↑ 上滑查看更多 ↑', this.screenWidth / 2, this.screenHeight - 15);
}
}
}

View File

@@ -29,6 +29,8 @@ export default class StoryScene extends BaseScene {
// 场景图相关
this.sceneImage = null;
this.sceneColors = this.generateSceneColors();
// AI改写相关
this.isAIRewriting = false;
}
// 根据场景生成氛围色
@@ -246,16 +248,33 @@ export default class StoryScene extends BaseScene {
ctx.textAlign = 'center';
ctx.font = 'bold 15px sans-serif';
ctx.fillStyle = this.sceneColors.accent;
const title = this.story.title.length > 10 ? this.story.title.substring(0, 10) + '...' : this.story.title;
const title = this.story.title.length > 8 ? this.story.title.substring(0, 8) + '...' : this.story.title;
ctx.fillText(title, this.screenWidth / 2, 35);
}
// 进度指示
ctx.textAlign = 'right';
ctx.font = '12px sans-serif';
ctx.fillStyle = 'rgba(255,255,255,0.5)';
const progress = this.main.storyManager.getProgress ? this.main.storyManager.getProgress() : '';
ctx.fillText(progress, this.screenWidth - 15, 35);
// AI改写按钮右上角
const btnX = this.screenWidth - 70;
const btnY = 18;
const btnW = 55;
const btnH = 26;
// 按钮背景
if (this.isAIRewriting) {
ctx.fillStyle = 'rgba(255,255,255,0.2)';
} else {
const gradient = ctx.createLinearGradient(btnX, btnY, btnX + btnW, btnY);
gradient.addColorStop(0, '#a855f7');
gradient.addColorStop(1, '#ec4899');
ctx.fillStyle = gradient;
}
this.roundRect(ctx, btnX, btnY, btnW, btnH, 13);
ctx.fill();
// 按钮文字
ctx.fillStyle = '#ffffff';
ctx.font = '11px sans-serif';
ctx.textAlign = 'center';
ctx.fillText(this.isAIRewriting ? '生成中' : 'AI改写', btnX + btnW / 2, btnY + 17);
}
renderDialogBox(ctx) {
@@ -300,14 +319,14 @@ export default class StoryScene extends BaseScene {
const textY = boxY + 65;
const lineHeight = 26;
const maxWidth = this.screenWidth - padding * 2;
const visibleHeight = boxHeight - 105; // 可见区域高度
const visibleHeight = boxHeight - 90; // 增加可见区域高度
ctx.font = '15px sans-serif';
const allLines = this.getWrappedLines(ctx, this.displayText, maxWidth);
const totalTextHeight = allLines.length * lineHeight;
// 计算最大滚动距离
this.maxScrollY = Math.max(0, totalTextHeight - visibleHeight);
this.maxScrollY = Math.max(0, totalTextHeight - visibleHeight + 30);
// 自动滚动到最新内容(打字时)
if (this.isTyping) {
@@ -334,9 +353,17 @@ export default class StoryScene extends BaseScene {
if (this.maxScrollY > 0) {
const scrollBarHeight = 40;
const scrollBarY = boxY + 55 + (this.textScrollY / this.maxScrollY) * (visibleHeight - scrollBarHeight);
ctx.fillStyle = 'rgba(255,255,255,0.2)';
this.roundRect(ctx, this.screenWidth - 6, scrollBarY, 3, scrollBarHeight, 1.5);
ctx.fillStyle = 'rgba(255,255,255,0.3)';
this.roundRect(ctx, this.screenWidth - 8, scrollBarY, 4, scrollBarHeight, 2);
ctx.fill();
// 如果还没滚动到底部,显示滚动提示
if (this.textScrollY < this.maxScrollY - 10 && !this.isTyping) {
ctx.fillStyle = 'rgba(255,255,255,0.5)';
ctx.font = '12px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('↑ 上滑查看更多 ↑', this.screenWidth / 2, this.screenHeight - 25);
}
}
// 打字机光标
@@ -496,13 +523,15 @@ export default class StoryScene extends BaseScene {
const touch = e.touches[0];
// 滑动对话框内容
if (this.isDragging && this.maxScrollY > 0) {
if (this.isDragging) {
const deltaY = this.lastTouchY - touch.clientY;
if (Math.abs(deltaY) > 2) {
this.hasMoved = true;
}
this.textScrollY += deltaY;
this.textScrollY = Math.max(0, Math.min(this.textScrollY, this.maxScrollY));
if (this.maxScrollY > 0) {
this.textScrollY += deltaY;
this.textScrollY = Math.max(0, Math.min(this.textScrollY, this.maxScrollY));
}
this.lastTouchY = touch.clientY;
}
}
@@ -525,6 +554,18 @@ export default class StoryScene extends BaseScene {
return;
}
// AI改写按钮点击
const btnX = this.screenWidth - 70;
const btnY = 18;
const btnW = 55;
const btnH = 26;
if (y >= btnY && y <= btnY + btnH && x >= btnX && x <= btnX + btnW) {
if (!this.isAIRewriting) {
this.showAIRewriteInput();
}
return;
}
// 加速打字
if (this.isTyping) {
this.displayText = this.targetText;
@@ -638,6 +679,71 @@ export default class StoryScene extends BaseScene {
ctx.closePath();
}
/**
* 显示AI改写输入框
*/
showAIRewriteInput() {
wx.showModal({
title: 'AI改写剧情',
editable: true,
placeholderText: '输入你的改写指令,如"让主角暴富"',
success: (res) => {
if (res.confirm && res.content) {
this.doAIRewrite(res.content);
}
}
});
}
/**
* 执行AI改写
*/
async doAIRewrite(prompt) {
if (this.isAIRewriting) return;
this.isAIRewriting = true;
this.main.showLoading('AI正在改写剧情...');
try {
const userId = this.main.userManager.userId || 0;
const newNode = await this.main.storyManager.rewriteBranch(
this.storyId,
prompt,
userId
);
this.main.hideLoading();
if (newNode) {
// 成功获取新分支,开始播放
this.currentNode = newNode;
this.startTypewriter(newNode.content);
wx.showToast({
title: '改写成功!',
icon: 'success',
duration: 1500
});
} else {
// AI 失败,继续原故事
wx.showToast({
title: 'AI暂时不可用继续原故事',
icon: 'none',
duration: 2000
});
}
} catch (error) {
this.main.hideLoading();
console.error('AI改写出错:', error);
wx.showToast({
title: '网络错误,请重试',
icon: 'none',
duration: 2000
});
} finally {
this.isAIRewriting = false;
}
}
destroy() {
if (this.main.userManager.isLoggedIn && this.story) {
this.main.userManager.saveProgress(