Files
ai_game/client/js/scenes/StoryScene.js

1306 lines
40 KiB
JavaScript
Raw Normal View History

/**
* 故事播放场景 - 视觉小说风格
*/
import BaseScene from './BaseScene';
export default class StoryScene extends BaseScene {
constructor(main, params) {
super(main, params);
this.storyId = params.storyId;
this.draftId = params.draftId || null; // 草稿ID
this.aiContent = params.aiContent || null; // AI改写内容
this.story = null;
this.currentNode = null;
this.displayText = '';
this.targetText = '';
this.charIndex = 0;
this.typewriterSpeed = 40;
this.lastTypeTime = 0;
this.isTyping = false;
this.showChoices = false;
this.waitingForClick = false;
this.selectedChoice = -1;
this.fadeAlpha = 0;
this.isFading = false;
// 滚动相关
this.textScrollY = 0;
this.maxScrollY = 0;
this.isDragging = false;
this.lastTouchY = 0;
// 场景图相关
this.sceneImage = null;
this.sceneColors = this.generateSceneColors();
// AI改写相关
this.isAIRewriting = false;
// 剧情回顾模式
this.isRecapMode = false;
this.recapData = null;
this.recapScrollY = 0;
this.recapMaxScrollY = 0;
this.recapBtnRect = null;
this.recapReplayBtnRect = null;
this.recapCardRects = [];
// 重头游玩模式
this.isReplayMode = false;
this.replayPath = [];
this.replayPathIndex = 0;
}
// 根据场景生成氛围色
generateSceneColors() {
const themes = [
{ bg1: '#1a0a2e', bg2: '#16213e', accent: '#ff6b9d' }, // 浪漫
{ bg1: '#0d1b2a', bg2: '#1b263b', accent: '#778da9' }, // 悬疑
{ bg1: '#2d132c', bg2: '#801336', accent: '#ffd700' }, // 古风
{ bg1: '#1a1a2e', bg2: '#0f3460', accent: '#00fff5' }, // 科幻
{ bg1: '#0b1215', bg2: '#1e3a3a', accent: '#4ecca3' }, // 校园
];
return themes[Math.floor(Math.random() * themes.length)];
}
async init() {
// 如果是从Draft加载先获取草稿详情进入回顾模式
if (this.draftId) {
this.main.showLoading('加载AI改写内容...');
const draft = await this.main.storyManager.getDraftDetail(this.draftId);
if (draft && draft.aiNodes && draft.storyId) {
// 先加载原故事
this.story = await this.main.storyManager.loadStoryDetail(draft.storyId);
if (this.story) {
this.setThemeByCategory(this.story.category);
// 将AI生成的节点合并到故事中
Object.assign(this.story.nodes, draft.aiNodes);
// 获取 AI 入口节点的内容
const entryKey = draft.entryNodeKey || 'branch_1';
const aiEntryNode = draft.aiNodes[entryKey];
// 保存回顾数据,包含 AI 内容
this.recapData = {
pathHistory: draft.pathHistory || [],
userPrompt: draft.userPrompt || '',
entryNodeKey: entryKey,
aiContent: aiEntryNode // 保存 AI 入口节点内容
};
// 同时保存到 aiContent方便后续访问
this.aiContent = aiEntryNode;
// 进入回顾模式
this.isRecapMode = true;
this.calculateRecapScroll();
this.main.hideLoading();
return;
}
}
this.main.hideLoading();
this.main.showError('草稿加载失败');
this.main.sceneManager.switchScene('home');
return;
}
// 如果是AI改写内容直接播放
if (this.aiContent) {
this.story = this.main.storyManager.currentStory;
if (this.story) {
this.setThemeByCategory(this.story.category);
}
this.currentNode = this.aiContent;
this.startTypewriter(this.aiContent.content);
return;
}
// 检查是否是重新开始(已有故事数据)
const existingStory = this.main.storyManager.currentStory;
if (existingStory && existingStory.id === this.storyId) {
// 重新开始,使用已有数据
this.story = existingStory;
this.setThemeByCategory(this.story.category);
// 重置到起点并清空历史
this.main.storyManager.resetStory();
this.currentNode = this.main.storyManager.getCurrentNode();
if (this.currentNode) {
this.startTypewriter(this.currentNode.content);
}
return;
}
// 首次加载故事
this.main.showLoading('加载故事中...');
this.story = await this.main.storyManager.loadStoryDetail(this.storyId);
if (!this.story) {
this.main.showError('故事加载失败');
this.main.sceneManager.switchScene('home');
return;
}
// 根据故事分类设置氛围
this.setThemeByCategory(this.story.category);
this.main.hideLoading();
this.currentNode = this.main.storyManager.getCurrentNode();
if (this.currentNode) {
this.startTypewriter(this.currentNode.content);
}
}
setThemeByCategory(category) {
const themes = {
'都市言情': { bg1: '#1a0a2e', bg2: '#2d1b4e', accent: '#ff6b9d' },
'悬疑推理': { bg1: '#0d1b2a', bg2: '#1b263b', accent: '#778da9' },
'古风宫廷': { bg1: '#2d132c', bg2: '#4a1942', accent: '#ffd700' },
'校园青春': { bg1: '#0a2540', bg2: '#1e5162', accent: '#4ecca3' },
'修仙玄幻': { bg1: '#0f0f2d', bg2: '#1a1a4e', accent: '#a855f7' },
'穿越重生': { bg1: '#1a0a2e', bg2: '#3d1a5c', accent: '#f472b6' },
'职场商战': { bg1: '#0c1929', bg2: '#1e3a5f', accent: '#60a5fa' },
'科幻未来': { bg1: '#0a1628', bg2: '#162033', accent: '#00fff5' },
'恐怖惊悚': { bg1: '#0a0a0a', bg2: '#1a1a1a', accent: '#ef4444' },
'搞笑轻喜': { bg1: '#1a1a2e', bg2: '#2d2d52', accent: '#fbbf24' }
};
this.sceneColors = themes[category] || this.sceneColors;
}
// 计算回顾页面滚动范围
calculateRecapScroll() {
if (!this.recapData) return;
const itemHeight = 90;
const headerHeight = 120;
const promptHeight = 80;
const buttonHeight = 80;
const contentHeight = headerHeight + this.recapData.pathHistory.length * itemHeight + promptHeight + buttonHeight;
this.recapMaxScrollY = Math.max(0, contentHeight - this.screenHeight + 40);
}
// 渲染剧情回顾页面
renderRecapPage(ctx) {
// 背景
const gradient = ctx.createLinearGradient(0, 0, 0, this.screenHeight);
gradient.addColorStop(0, this.sceneColors.bg1);
gradient.addColorStop(1, this.sceneColors.bg2);
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, this.screenWidth, this.screenHeight);
// 返回按钮
ctx.fillStyle = '#ffffff';
ctx.font = '16px sans-serif';
ctx.textAlign = 'left';
ctx.fillText(' 返回', 15, 35);
// 标题
ctx.textAlign = 'center';
ctx.font = 'bold 18px sans-serif';
ctx.fillStyle = this.sceneColors.accent;
ctx.fillText('📖 剧情回顾', this.screenWidth / 2, 35);
// 故事标题
if (this.story) {
ctx.fillStyle = 'rgba(255,255,255,0.6)';
ctx.font = '13px sans-serif';
ctx.fillText(this.story.title, this.screenWidth / 2, 60);
}
// 内容区域裁剪(调整起点避免被标题挡住)
ctx.save();
ctx.beginPath();
ctx.rect(0, 70, this.screenWidth, this.screenHeight - 150);
ctx.clip();
const padding = 16;
let y = 100 - this.recapScrollY;
const pathHistory = this.recapData?.pathHistory || [];
// 保存卡片位置用于点击检测
this.recapCardRects = [];
// 计算可用文字宽度
const maxTextWidth = this.screenWidth - padding * 2 - 50;
// 绘制每个路径项
pathHistory.forEach((item, index) => {
if (y > 50 && y < this.screenHeight - 80) {
// 卡片背景
ctx.fillStyle = 'rgba(255,255,255,0.06)';
this.roundRect(ctx, padding, y, this.screenWidth - padding * 2, 80, 12);
ctx.fill();
// 序号圆圈
ctx.fillStyle = this.sceneColors.accent;
ctx.beginPath();
ctx.arc(padding + 20, y + 28, 12, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#ffffff';
ctx.font = 'bold 11px sans-serif';
ctx.textAlign = 'center';
ctx.fillText(`${index + 1}`, padding + 20, y + 32);
// 内容摘要(限制宽度)
ctx.fillStyle = 'rgba(255,255,255,0.85)';
ctx.font = '12px sans-serif';
ctx.textAlign = 'left';
const contentText = this.truncateTextByWidth(ctx, item.content || '', maxTextWidth - 40);
ctx.fillText(contentText, padding + 40, y + 28);
// 选择(限制宽度)
ctx.fillStyle = this.sceneColors.accent;
ctx.font = '11px sans-serif';
const choiceText = `${this.truncateTextByWidth(ctx, item.choice || '', maxTextWidth - 60)}`;
ctx.fillText(choiceText, padding + 40, y + 52);
// 点击提示图标
ctx.fillStyle = 'rgba(255,255,255,0.4)';
ctx.font = '14px sans-serif';
ctx.textAlign = 'right';
ctx.fillText('', this.screenWidth - padding - 12, y + 40);
// 保存卡片区域
this.recapCardRects.push({
x: padding,
y: y + this.recapScrollY,
width: this.screenWidth - padding * 2,
height: 80,
index: index,
item: item
});
}
y += 90;
});
// 空状态
if (pathHistory.length === 0) {
ctx.fillStyle = 'rgba(255,255,255,0.4)';
ctx.font = '14px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('没有历史记录', this.screenWidth / 2, y + 30);
y += 60;
}
// AI改写指令可点击查看详情
this.recapPromptRect = null;
if (y > 40 && y < this.screenHeight - 30) {
ctx.fillStyle = 'rgba(168, 85, 247, 0.15)';
this.roundRect(ctx, padding, y + 10, this.screenWidth - padding * 2, 60, 12);
ctx.fill();
ctx.fillStyle = '#a855f7';
ctx.font = 'bold 12px sans-serif';
ctx.textAlign = 'left';
ctx.fillText('✨ AI改写指令', padding + 12, y + 32);
ctx.fillStyle = 'rgba(255,255,255,0.8)';
ctx.font = '11px sans-serif';
const promptText = this.truncateTextByWidth(ctx, this.recapData?.userPrompt || '无', maxTextWidth - 30);
ctx.fillText(`${promptText}`, padding + 12, y + 52);
// 点击提示
ctx.fillStyle = 'rgba(255,255,255,0.4)';
ctx.font = '14px sans-serif';
ctx.textAlign = 'right';
ctx.fillText('', this.screenWidth - padding - 12, y + 42);
// 保存点击区域
this.recapPromptRect = {
x: padding,
y: y + 10 + this.recapScrollY,
width: this.screenWidth - padding * 2,
height: 60
};
}
y += 80;
ctx.restore();
// 底部按钮区域(固定位置,两个按钮)
const btnY = this.screenHeight - 70;
const btnH = 42;
const btnGap = 12;
const btnW = (this.screenWidth - padding * 2 - btnGap) / 2;
// 左边按钮:重头游玩
ctx.fillStyle = 'rgba(255,255,255,0.1)';
this.roundRect(ctx, padding, btnY, btnW, btnH, 21);
ctx.fill();
ctx.strokeStyle = 'rgba(255,255,255,0.3)';
ctx.lineWidth = 1;
this.roundRect(ctx, padding, btnY, btnW, btnH, 21);
ctx.stroke();
ctx.fillStyle = '#ffffff';
ctx.font = '14px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('🔄 重头游玩', padding + btnW / 2, btnY + 26);
// 右边按钮:开始新剧情
const btn2X = padding + btnW + btnGap;
const btnGradient = ctx.createLinearGradient(btn2X, btnY, btn2X + btnW, btnY);
btnGradient.addColorStop(0, '#a855f7');
btnGradient.addColorStop(1, '#ec4899');
ctx.fillStyle = btnGradient;
this.roundRect(ctx, btn2X, btnY, btnW, btnH, 21);
ctx.fill();
ctx.fillStyle = '#ffffff';
ctx.font = 'bold 14px sans-serif';
ctx.fillText('新剧情 →', btn2X + btnW / 2, btnY + 26);
// 保存按钮区域
this.recapReplayBtnRect = { x: padding, y: btnY, width: btnW, height: btnH };
this.recapBtnRect = { x: btn2X, y: btnY, width: btnW, height: btnH };
// 滚动提示
if (this.recapMaxScrollY > 0) {
ctx.fillStyle = 'rgba(255,255,255,0.3)';
ctx.font = '11px sans-serif';
ctx.textAlign = 'center';
if (this.recapScrollY < this.recapMaxScrollY - 10) {
ctx.fillText('↓ 上滑查看更多', this.screenWidth / 2, btnY - 15);
}
}
}
// 开始播放AI改写内容从回顾模式退出
startAIContent() {
if (!this.recapData) return;
this.isRecapMode = false;
this.main.storyManager.currentNodeKey = this.recapData.entryNodeKey || 'branch_1';
this.currentNode = this.main.storyManager.getCurrentNode();
if (this.currentNode) {
this.startTypewriter(this.currentNode.content);
}
}
// 显示历史项详情
showRecapDetail(item, index) {
const content = item.content || '无内容';
const choice = item.choice || '无选择';
wx.showModal({
title: `${index + 1}`,
content: `【剧情】\n${content}\n\n【你的选择】\n${choice}`,
showCancel: false,
confirmText: '关闭'
});
}
// 显示AI改写指令详情
showPromptDetail() {
const prompt = this.recapData?.userPrompt || '无';
wx.showModal({
title: '✨ AI改写指令',
content: prompt,
showCancel: false,
confirmText: '关闭'
});
}
// 根据宽度截断文字
truncateTextByWidth(ctx, text, maxWidth) {
if (!text) return '';
if (ctx.measureText(text).width <= maxWidth) return text;
let t = text;
while (t.length > 0 && ctx.measureText(t + '...').width > maxWidth) {
t = t.slice(0, -1);
}
return t + '...';
}
// 重头游玩自动快进到AI改写点
startReplayMode() {
if (!this.recapData) return;
this.isRecapMode = false;
this.isReplayMode = true;
this.replayPathIndex = 0;
this.replayPath = this.recapData.pathHistory || [];
// 从 start 节点开始
this.main.storyManager.currentNodeKey = 'start';
this.main.storyManager.pathHistory = [];
this.currentNode = this.main.storyManager.getCurrentNode();
if (this.currentNode) {
this.startTypewriter(this.currentNode.content);
}
}
// 自动选择回放路径中的选项
autoSelectReplayChoice() {
if (!this.isReplayMode || this.replayPathIndex >= this.replayPath.length) {
// 回放结束进入AI改写内容
this.isReplayMode = false;
this.enterAIContent();
return;
}
// 找到对应的选项并自动选择
const currentPath = this.replayPath[this.replayPathIndex];
const currentNode = this.main.storyManager.getCurrentNode();
if (currentNode && currentNode.choices) {
const choiceIndex = currentNode.choices.findIndex(c => c.text === currentPath.choice);
if (choiceIndex >= 0) {
this.replayPathIndex++;
this.main.storyManager.selectChoice(choiceIndex);
this.currentNode = this.main.storyManager.getCurrentNode();
if (this.currentNode) {
this.startTypewriter(this.currentNode.content);
}
return;
}
}
// 找不到匹配的选项直接进入AI内容
this.isReplayMode = false;
this.enterAIContent();
}
// 进入AI改写内容
enterAIContent() {
console.log('进入AI改写内容');
// AI 节点已经合并到 story.nodes 中,使用 storyManager 来管理
const entryKey = this.recapData?.entryNodeKey || 'branch_1';
// 检查节点是否存在
if (this.story && this.story.nodes && this.story.nodes[entryKey]) {
this.main.storyManager.currentNodeKey = entryKey;
this.currentNode = this.main.storyManager.getCurrentNode();
if (this.currentNode) {
this.startTypewriter(this.currentNode.content);
return;
}
}
// 节点不存在,显示错误
console.error('AI入口节点不存在:', entryKey);
wx.showModal({
title: '内容加载失败',
content: 'AI改写内容未找到',
showCancel: false,
confirmText: '返回',
success: () => {
this.main.sceneManager.switchScene('home');
}
});
}
startTypewriter(text) {
let content = text || '';
// 回放模式下过滤掉结局提示因为后面还有AI改写内容
if (this.isReplayMode) {
content = content.replace(/【达成结局[:][^】]*】/g, '').trim();
}
this.targetText = content;
this.displayText = '';
this.charIndex = 0;
this.isTyping = true;
this.showChoices = false;
this.waitingForClick = false;
this.lastTypeTime = Date.now();
// 重置滚动
this.textScrollY = 0;
this.maxScrollY = 0;
}
update() {
if (this.isTyping && this.charIndex < this.targetText.length) {
const now = Date.now();
if (now - this.lastTypeTime >= this.typewriterSpeed) {
this.displayText += this.targetText[this.charIndex];
this.charIndex++;
this.lastTypeTime = now;
}
} else if (this.isTyping && this.charIndex >= this.targetText.length) {
this.isTyping = false;
// 打字完成,等待用户点击再显示选项
this.waitingForClick = true;
}
if (this.isFading) {
this.fadeAlpha = Math.min(1, this.fadeAlpha + 0.05);
if (this.fadeAlpha >= 1) {
this.isFading = false;
this.fadeAlpha = 0;
}
}
}
render(ctx) {
// 如果是回顾模式,渲染回顾页面
if (this.isRecapMode) {
this.renderRecapPage(ctx);
return;
}
// 1. 绘制场景背景
this.renderSceneBackground(ctx);
// 2. 绘制场景装饰
this.renderSceneDecoration(ctx);
// 3. 绘制顶部UI
this.renderHeader(ctx);
// 4. 绘制对话框
this.renderDialogBox(ctx);
// 5. 绘制选项
if (this.showChoices) {
this.renderChoices(ctx);
}
// 6. 淡入淡出
if (this.fadeAlpha > 0) {
ctx.fillStyle = `rgba(0, 0, 0, ${this.fadeAlpha})`;
ctx.fillRect(0, 0, this.screenWidth, this.screenHeight);
}
}
renderSceneBackground(ctx) {
// 场景区域上方45%
const sceneHeight = this.screenHeight * 0.42;
// 渐变背景
const gradient = ctx.createLinearGradient(0, 0, 0, sceneHeight);
gradient.addColorStop(0, this.sceneColors.bg1);
gradient.addColorStop(1, this.sceneColors.bg2);
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, this.screenWidth, sceneHeight);
// 底部渐变过渡到对话框
const fadeGradient = ctx.createLinearGradient(0, sceneHeight - 60, 0, sceneHeight);
fadeGradient.addColorStop(0, 'rgba(0,0,0,0)');
fadeGradient.addColorStop(1, 'rgba(15,15,30,1)');
ctx.fillStyle = fadeGradient;
ctx.fillRect(0, sceneHeight - 60, this.screenWidth, 60);
// 对话框区域背景
ctx.fillStyle = '#0f0f1e';
ctx.fillRect(0, sceneHeight, this.screenWidth, this.screenHeight - sceneHeight);
}
renderSceneDecoration(ctx) {
const sceneHeight = this.screenHeight * 0.42;
const centerX = this.screenWidth / 2;
// 场景氛围光效
const glowGradient = ctx.createRadialGradient(centerX, sceneHeight * 0.5, 0, centerX, sceneHeight * 0.5, 200);
glowGradient.addColorStop(0, this.sceneColors.accent + '30');
glowGradient.addColorStop(1, 'transparent');
ctx.fillStyle = glowGradient;
ctx.fillRect(0, 0, this.screenWidth, sceneHeight);
// 装饰粒子
ctx.fillStyle = this.sceneColors.accent + '40';
const particles = [[50, 100], [120, 180], [200, 80], [280, 150], [320, 60], [80, 250], [250, 220]];
particles.forEach(([x, y]) => {
ctx.beginPath();
ctx.arc(x, y, 2, 0, Math.PI * 2);
ctx.fill();
});
// 场景提示文字(中央)
ctx.fillStyle = 'rgba(255,255,255,0.15)';
ctx.font = '14px sans-serif';
ctx.textAlign = 'center';
const sceneHint = this.getSceneHint();
ctx.fillText(sceneHint, centerX, sceneHeight * 0.45);
}
getSceneHint() {
if (!this.currentNode) return '故事开始...';
const speaker = this.currentNode.speaker;
if (speaker && speaker !== '旁白') {
return `${speaker}`;
}
return '— 旁白 —';
}
renderHeader(ctx) {
// 顶部渐变遮罩
const headerGradient = ctx.createLinearGradient(0, 0, 0, 80);
headerGradient.addColorStop(0, 'rgba(0,0,0,0.7)');
headerGradient.addColorStop(1, 'rgba(0,0,0,0)');
ctx.fillStyle = headerGradient;
ctx.fillRect(0, 0, this.screenWidth, 80);
// 返回按钮
ctx.fillStyle = '#ffffff';
ctx.font = '16px sans-serif';
ctx.textAlign = 'left';
ctx.fillText(' 返回', 15, 35);
// 故事标题
if (this.story) {
ctx.textAlign = 'center';
ctx.font = 'bold 15px sans-serif';
ctx.fillStyle = this.sceneColors.accent;
const title = this.story.title.length > 8 ? this.story.title.substring(0, 8) + '...' : this.story.title;
ctx.fillText(title, this.screenWidth / 2, 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) {
const boxY = this.screenHeight * 0.42;
const boxHeight = this.screenHeight * 0.58;
const padding = 20;
// 对话框背景
ctx.fillStyle = 'rgba(20, 20, 40, 0.95)';
ctx.fillRect(0, boxY, this.screenWidth, boxHeight);
// 顶部装饰线
const lineGradient = ctx.createLinearGradient(0, boxY, this.screenWidth, boxY);
lineGradient.addColorStop(0, 'transparent');
lineGradient.addColorStop(0.5, this.sceneColors.accent);
lineGradient.addColorStop(1, 'transparent');
ctx.strokeStyle = lineGradient;
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(0, boxY);
ctx.lineTo(this.screenWidth, boxY);
ctx.stroke();
// 如果显示选项,不显示对话内容
if (this.showChoices) return;
// 角色名
if (this.currentNode && this.currentNode.speaker && this.currentNode.speaker !== '旁白') {
// 角色名背景
ctx.font = 'bold 14px sans-serif';
const nameWidth = ctx.measureText(this.currentNode.speaker).width + 30;
ctx.fillStyle = this.sceneColors.accent;
this.roundRect(ctx, padding, boxY + 15, nameWidth, 28, 14);
ctx.fill();
ctx.fillStyle = '#ffffff';
ctx.textAlign = 'left';
ctx.fillText(this.currentNode.speaker, padding + 15, boxY + 34);
}
// 对话内容
const textY = boxY + 65;
const lineHeight = 26;
const maxWidth = this.screenWidth - padding * 2;
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 + 30);
// 自动滚动到最新内容(打字时)
if (this.isTyping) {
this.textScrollY = this.maxScrollY;
}
// 设置裁剪区域(从文字顶部开始)
ctx.save();
ctx.beginPath();
ctx.rect(0, textY - 18, this.screenWidth, visibleHeight + 18);
ctx.clip();
// 绘制文字(带滚动偏移)
ctx.fillStyle = '#ffffff';
ctx.textAlign = 'left';
allLines.forEach((line, i) => {
const y = textY + i * lineHeight - this.textScrollY;
ctx.fillText(line, padding, y);
});
ctx.restore();
// 滚动指示器(如果可以滚动)
if (this.maxScrollY > 0) {
const scrollBarHeight = 40;
const scrollBarY = boxY + 55 + (this.textScrollY / this.maxScrollY) * (visibleHeight - scrollBarHeight);
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);
}
}
// 打字机光标
if (this.isTyping) {
const cursorBlink = Math.floor(Date.now() / 500) % 2 === 0;
if (cursorBlink) {
ctx.fillStyle = this.sceneColors.accent;
ctx.font = '15px sans-serif';
ctx.fillText('▌', padding + this.getTextEndX(ctx, this.displayText, maxWidth), textY + this.getTextEndY(this.displayText, maxWidth, lineHeight));
}
}
// 继续提示
if (!this.isTyping && !this.main.storyManager.isEnding()) {
ctx.fillStyle = 'rgba(255,255,255,0.4)';
ctx.font = '13px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('点击继续 ▼', this.screenWidth / 2, this.screenHeight - 25);
}
}
getTextEndX(ctx, text, maxWidth) {
const lines = this.getWrappedLines(ctx, text, maxWidth);
if (lines.length === 0) return 0;
return ctx.measureText(lines[lines.length - 1]).width;
}
getTextEndY(text, maxWidth, lineHeight) {
const lines = text.split('\n');
return (lines.length - 1) * lineHeight;
}
getWrappedLines(ctx, text, maxWidth) {
const lines = [];
const paragraphs = text.split('\n');
paragraphs.forEach(para => {
let line = '';
for (let char of para) {
const testLine = line + char;
if (ctx.measureText(testLine).width > maxWidth) {
lines.push(line);
line = char;
} else {
line = testLine;
}
}
lines.push(line);
});
return lines;
}
renderChoices(ctx) {
if (!this.currentNode || !this.currentNode.choices) return;
let choices = this.currentNode.choices;
const choiceHeight = 50;
const choiceMargin = 10;
const padding = 20;
const startY = this.screenHeight * 0.42 + 30;
// 半透明遮罩
ctx.fillStyle = 'rgba(0,0,0,0.5)';
ctx.fillRect(0, this.screenHeight * 0.42, this.screenWidth, this.screenHeight * 0.58);
// 回放模式下的处理
let replayChoice = null;
if (this.isReplayMode && this.replayPathIndex < this.replayPath.length) {
const previousChoice = this.replayPath[this.replayPathIndex]?.choice;
replayChoice = choices.find(c => c.text === previousChoice);
// 提示文字
ctx.fillStyle = this.sceneColors.accent;
ctx.font = '13px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('📍 你之前选择的是:', this.screenWidth / 2, startY);
// 只显示之前选过的选项
if (replayChoice) {
choices = [replayChoice];
}
} else {
// 正常模式提示文字
ctx.fillStyle = '#ffffff';
ctx.font = '14px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('请做出选择', this.screenWidth / 2, startY);
}
choices.forEach((choice, index) => {
const y = startY + 25 + index * (choiceHeight + choiceMargin);
const isSelected = index === this.selectedChoice;
const isReplayItem = this.isReplayMode && replayChoice && choice.text === replayChoice.text;
// 选项背景
if (isSelected || isReplayItem) {
const gradient = ctx.createLinearGradient(padding, y, this.screenWidth - padding, y);
gradient.addColorStop(0, this.sceneColors.accent);
gradient.addColorStop(1, this.sceneColors.accent + 'aa');
ctx.fillStyle = gradient;
} else {
ctx.fillStyle = 'rgba(255,255,255,0.1)';
}
this.roundRect(ctx, padding, y, this.screenWidth - padding * 2, choiceHeight, 25);
ctx.fill();
// 选项边框
ctx.strokeStyle = (isSelected || isReplayItem) ? this.sceneColors.accent : 'rgba(255,255,255,0.2)';
ctx.lineWidth = 1.5;
this.roundRect(ctx, padding, y, this.screenWidth - padding * 2, choiceHeight, 25);
ctx.stroke();
// 选项文本
ctx.fillStyle = '#ffffff';
ctx.font = '14px sans-serif';
ctx.textAlign = 'center';
ctx.fillText(choice.text, this.screenWidth / 2, y + 30);
// 回放模式下显示点击继续提示
if (isReplayItem) {
ctx.fillStyle = 'rgba(255,255,255,0.5)';
ctx.font = '11px sans-serif';
ctx.fillText('点击继续 ', this.screenWidth / 2, y + 45);
}
// 锁定图标
if (choice.isLocked) {
ctx.fillStyle = '#ffd700';
ctx.font = '12px sans-serif';
ctx.textAlign = 'right';
ctx.fillText('🔒 看广告解锁', this.screenWidth - padding - 15, y + 30);
}
});
}
// 文字换行
wrapText(ctx, text, x, y, maxWidth, lineHeight) {
if (!text) return;
const lines = this.getWrappedLines(ctx, text, maxWidth);
lines.forEach((line, i) => {
ctx.fillText(line, x, y + i * lineHeight);
});
}
// 文字换行(限制行数)
wrapTextWithLimit(ctx, text, x, y, maxWidth, lineHeight, maxLines) {
if (!text) return;
let lines = this.getWrappedLines(ctx, text, maxWidth);
// 如果超出最大行数,只显示最后几行(滚动效果)
if (lines.length > maxLines) {
lines = lines.slice(lines.length - maxLines);
}
lines.forEach((line, i) => {
ctx.fillText(line, x, y + i * lineHeight);
});
}
// 把行数组分成多页
splitIntoPages(lines, linesPerPage) {
const pages = [];
for (let i = 0; i < lines.length; i += linesPerPage) {
pages.push(lines.slice(i, i + linesPerPage));
}
return pages.length > 0 ? pages : [[]];
}
onTouchStart(e) {
const touch = e.touches[0];
this.touchStartY = touch.clientY;
this.touchStartX = touch.clientX;
this.lastTouchY = touch.clientY;
this.hasMoved = false;
// 回顾模式下的滚动
if (this.isRecapMode) {
if (touch.clientY > 75) {
this.isDragging = true;
}
return;
}
// 判断是否在对话框区域
const boxY = this.screenHeight * 0.42;
if (touch.clientY > boxY) {
this.isDragging = true;
}
}
onTouchMove(e) {
const touch = e.touches[0];
// 回顾模式下的滚动
if (this.isRecapMode && this.isDragging) {
const deltaY = this.lastTouchY - touch.clientY;
if (Math.abs(deltaY) > 2) {
this.hasMoved = true;
}
if (this.recapMaxScrollY > 0) {
this.recapScrollY += deltaY;
this.recapScrollY = Math.max(0, Math.min(this.recapScrollY, this.recapMaxScrollY));
}
this.lastTouchY = touch.clientY;
return;
}
// 滑动对话框内容
if (this.isDragging) {
const deltaY = this.lastTouchY - touch.clientY;
if (Math.abs(deltaY) > 2) {
this.hasMoved = true;
}
if (this.maxScrollY > 0) {
this.textScrollY += deltaY;
this.textScrollY = Math.max(0, Math.min(this.textScrollY, this.maxScrollY));
}
this.lastTouchY = touch.clientY;
}
}
onTouchEnd(e) {
this.isDragging = false;
const touch = e.changedTouches[0];
const x = touch.clientX;
const y = touch.clientY;
// 如果滑动过,不处理点击
if (this.hasMoved) {
return;
}
// 回顾模式下的点击处理
if (this.isRecapMode) {
// 返回按钮
if (y < 60 && x < 80) {
this.main.sceneManager.switchScene('profile', { tab: 1 });
return;
}
// 重头游玩按钮
if (this.recapReplayBtnRect) {
const btn = this.recapReplayBtnRect;
if (x >= btn.x && x <= btn.x + btn.width && y >= btn.y && y <= btn.y + btn.height) {
this.startReplayMode();
return;
}
}
// 开始新剧情按钮
if (this.recapBtnRect) {
const btn = this.recapBtnRect;
if (x >= btn.x && x <= btn.x + btn.width && y >= btn.y && y <= btn.y + btn.height) {
this.startAIContent();
return;
}
}
// 历史项卡片点击(显示详情)
if (this.recapCardRects) {
const adjustedY = y + this.recapScrollY;
for (const rect of this.recapCardRects) {
if (x >= rect.x && x <= rect.x + rect.width &&
adjustedY >= rect.y && adjustedY <= rect.y + rect.height) {
this.showRecapDetail(rect.item, rect.index);
return;
}
}
}
// AI改写指令点击显示完整指令
if (this.recapPromptRect) {
const adjustedY = y + this.recapScrollY;
const rect = this.recapPromptRect;
if (x >= rect.x && x <= rect.x + rect.width &&
adjustedY >= rect.y && adjustedY <= rect.y + rect.height) {
this.showPromptDetail();
return;
}
}
return;
}
// 返回按钮
if (y < 60 && x < 80) {
this.main.sceneManager.switchScene('home');
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;
this.charIndex = this.targetText.length;
this.isTyping = false;
this.waitingForClick = true;
return;
}
// 等待点击后显示选项或结局
if (this.waitingForClick) {
this.waitingForClick = false;
// AI改写内容 - 直接跳转到新结局
if (this.aiContent && this.aiContent.is_ending) {
console.log('AI改写内容:', JSON.stringify(this.aiContent));
this.main.sceneManager.switchScene('ending', {
storyId: this.storyId,
draftId: this.draftId,
ending: {
name: this.aiContent.ending_name,
type: this.aiContent.ending_type,
content: this.aiContent.content,
score: this.aiContent.ending_score || 80
}
});
return;
}
// 检查是否是结局回放模式下跳过因为要进入AI改写内容
if (!this.isReplayMode && this.main.storyManager.isEnding()) {
this.main.sceneManager.switchScene('ending', {
storyId: this.storyId,
draftId: this.draftId,
ending: this.main.storyManager.getEndingInfo()
});
return;
}
// 回放模式下如果到达原结局或没有选项进入AI改写内容
if (this.isReplayMode) {
const currentNode = this.main.storyManager.getCurrentNode();
if (!currentNode || !currentNode.choices || currentNode.choices.length === 0 || currentNode.is_ending) {
// 回放结束进入AI改写内容
this.isReplayMode = false;
this.enterAIContent();
return;
}
}
// 显示选项
if (this.currentNode && this.currentNode.choices && this.currentNode.choices.length > 0) {
// 回放模式下也显示选项,但只显示之前选过的
this.showChoices = true;
} else if (this.currentNode && (!this.currentNode.choices || this.currentNode.choices.length === 0)) {
// 没有选项的节点,检查是否是死胡同(故事数据问题)
console.log('当前节点没有选项:', this.main.storyManager.currentNodeKey, this.currentNode);
// 如果有 AI 改写内容,跳转到 AI 内容
if (this.recapData && this.recapData.entryNodeKey) {
this.main.storyManager.currentNodeKey = this.recapData.entryNodeKey;
this.currentNode = this.main.storyManager.getCurrentNode();
if (this.currentNode) {
this.startTypewriter(this.currentNode.content);
}
return;
}
// 否则当作结局处理
wx.showModal({
title: '故事结束',
content: '当前剧情已结束',
showCancel: false,
confirmText: '返回',
success: () => {
this.main.sceneManager.switchScene('home');
}
});
}
return;
}
// 选项点击
if (this.showChoices && this.currentNode && this.currentNode.choices) {
const choiceHeight = 50;
const choiceMargin = 10;
const padding = 20;
const startY = this.screenHeight * 0.42 + 55;
// 回放模式下只有一个选项
if (this.isReplayMode && this.replayPathIndex < this.replayPath.length) {
const choiceY = startY;
if (y >= choiceY && y <= choiceY + choiceHeight && x >= padding && x <= this.screenWidth - padding) {
this.autoSelectReplayChoice();
return;
}
} else {
// 正常模式
const choices = this.currentNode.choices;
for (let i = 0; i < choices.length; i++) {
const choiceY = startY + i * (choiceHeight + choiceMargin);
if (y >= choiceY && y <= choiceY + choiceHeight && x >= padding && x <= this.screenWidth - padding) {
this.handleChoiceSelect(i);
return;
}
}
}
}
}
handleChoiceSelect(index) {
const choice = this.currentNode.choices[index];
if (choice.isLocked) {
wx.showModal({
title: '解锁剧情',
content: '观看广告解锁隐藏剧情?',
success: (res) => {
if (res.confirm) {
this.unlockAndSelect(index);
}
}
});
return;
}
this.selectChoice(index);
}
unlockAndSelect(index) {
this.selectChoice(index);
}
selectChoice(index) {
this.isFading = true;
this.fadeAlpha = 0;
this.showChoices = false;
setTimeout(() => {
this.currentNode = this.main.storyManager.selectChoice(index);
if (this.currentNode) {
this.startTypewriter(this.currentNode.content);
}
}, 300);
}
// 圆角矩形
roundRect(ctx, x, y, width, height, radius) {
ctx.beginPath();
ctx.moveTo(x + radius, y);
ctx.lineTo(x + width - radius, y);
ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
ctx.lineTo(x + width, y + height - radius);
ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
ctx.lineTo(x + radius, y + height);
ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
ctx.lineTo(x, y + radius);
ctx.quadraticCurveTo(x, y, x + radius, y);
ctx.closePath();
}
/**
* 显示AI改写输入框
*/
showAIRewriteInput() {
wx.showModal({
title: 'AI改写剧情',
editable: true,
placeholderText: '输入你的改写指令,如"让主角暴富"',
success: (res) => {
if (res.confirm && res.content) {
this.doAIRewriteAsync(res.content);
}
}
});
}
/**
* 异步提交AI改写到草稿箱
*/
async doAIRewriteAsync(prompt) {
if (this.isAIRewriting) return;
this.isAIRewriting = true;
this.main.showLoading('正在提交...');
try {
const userId = this.main.userManager.userId || 0;
const result = await this.main.storyManager.rewriteBranchAsync(
this.storyId,
prompt,
userId
);
this.main.hideLoading();
if (result && result.draftId) {
// 提交成功
wx.showModal({
title: '提交成功',
content: 'AI正在后台生成中完成后会通知您。\n您可以继续播放当前故事。',
showCancel: false,
confirmText: '知道了'
});
} else {
// 提交失败
wx.showToast({
title: '提交失败,请重试',
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(
this.storyId,
this.main.storyManager.currentNodeKey,
this.main.storyManager.isEnding(),
this.main.storyManager.isEnding() ? this.main.storyManager.getEndingInfo().name : ''
);
}
}
}