feat: AI中间章节改写功能 + 滚动优化
This commit is contained in:
@@ -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续写故事
|
||||
*/
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -42,5 +42,7 @@
|
||||
"ignore": [],
|
||||
"include": []
|
||||
},
|
||||
"editorSetting": {}
|
||||
"editorSetting": {},
|
||||
"libVersion": "3.14.3",
|
||||
"isGameTourist": false
|
||||
}
|
||||
Reference in New Issue
Block a user