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

652 lines
20 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.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();
}
// 根据场景生成氛围色
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() {
// 如果是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.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;
}
startTypewriter(text) {
this.targetText = text || '';
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) {
// 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 > 10 ? this.story.title.substring(0, 10) + '...' : this.story.title;
ctx.fillText(title, this.screenWidth / 2, 35);
}
// 进度指示
ctx.textAlign = 'right';
ctx.font = '12px sans-serif';
ctx.fillStyle = 'rgba(255,255,255,0.5)';
const progress = this.main.storyManager.getProgress ? this.main.storyManager.getProgress() : '';
ctx.fillText(progress, this.screenWidth - 15, 35);
}
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 - 105; // 可见区域高度
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);
// 自动滚动到最新内容(打字时)
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.2)';
this.roundRect(ctx, this.screenWidth - 6, scrollBarY, 3, scrollBarHeight, 1.5);
ctx.fill();
}
// 打字机光标
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;
const 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);
// 提示文字
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;
// 选项背景
if (isSelected) {
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 ? 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 (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;
// 判断是否在对话框区域
const boxY = this.screenHeight * 0.42;
if (touch.clientY > boxY) {
this.isDragging = true;
}
}
onTouchMove(e) {
const touch = e.touches[0];
// 滑动对话框内容
if (this.isDragging && this.maxScrollY > 0) {
const deltaY = this.lastTouchY - touch.clientY;
if (Math.abs(deltaY) > 2) {
this.hasMoved = true;
}
this.textScrollY += deltaY;
this.textScrollY = Math.max(0, Math.min(this.textScrollY, this.maxScrollY));
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 (y < 60 && x < 80) {
this.main.sceneManager.switchScene('home');
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,
ending: {
name: this.aiContent.ending_name,
type: this.aiContent.ending_type,
content: this.aiContent.content,
score: 100
}
});
return;
}
// 检查是否是结局
if (this.main.storyManager.isEnding()) {
this.main.sceneManager.switchScene('ending', {
storyId: this.storyId,
ending: this.main.storyManager.getEndingInfo()
});
return;
}
// 显示选项
if (this.currentNode && this.currentNode.choices && this.currentNode.choices.length > 0) {
this.showChoices = true;
}
return;
}
// 选项点击
if (this.showChoices && this.currentNode && this.currentNode.choices) {
const choices = this.currentNode.choices;
const choiceHeight = 50;
const choiceMargin = 10;
const padding = 20;
const startY = this.screenHeight * 0.42 + 55;
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();
}
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 : ''
);
}
}
}