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

1306 lines
40 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 故事播放场景 - 视觉小说风格
*/
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 : ''
);
}
}
}