/** * 故事播放场景 - 视觉小说风格 */ 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 rawPathHistory = this.recapData?.pathHistory; // 兼容结局续写(pathHistory 是对象而非数组) const pathHistory = Array.isArray(rawPathHistory) ? rawPathHistory : []; // 保存卡片位置用于点击检测 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 boxY = this.screenHeight * 0.42; const btnW = 70; const btnH = 30; const btnX = this.screenWidth - btnW - 15; const btnY = boxY - btnH - 12; // 按钮背景 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, 15); ctx.fill(); // 按钮文字 ctx.fillStyle = '#ffffff'; ctx.font = '12px sans-serif'; ctx.textAlign = 'center'; ctx.fillText(this.isAIRewriting ? '✨ 生成中...' : '✨ AI改写', btnX + btnW / 2, btnY + 20); } 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 boxY = this.screenHeight * 0.42; const btnW = 70; const btnH = 30; const btnX = this.screenWidth - btnW - 15; const btnY = boxY - btnH - 12; 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 : '' ); } } }