Files
ai_game/client/js/scenes/EndingScene.js

1264 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 EndingScene extends BaseScene {
constructor(main, params) {
super(main, params);
this.storyId = params.storyId;
this.ending = params.ending;
this.draftId = params.draftId || null; // 保存草稿ID
this.isReplay = params.isReplay || false; // 是否是回放模式
console.log('EndingScene 接收到的结局:', JSON.stringify(this.ending), ', isReplay:', this.isReplay);
this.showButtons = false;
this.fadeIn = 0;
this.particles = [];
this.isLiked = false;
this.isCollected = false;
// AI改写面板
this.showRewritePanel = false;
this.rewritePrompt = '';
this.rewriteTags = ['主角逆袭', '甜蜜HE', '虐心BE', '反转剧情', '意外重逢'];
this.selectedTag = -1;
// AI续写面板
this.showContinuePanel = false;
this.continuePrompt = '';
this.continueTags = ['故事未完', '新的冒险', '多年以后', '意外转折', '番外篇'];
this.selectedContinueTag = -1;
// 改写历史
this.rewriteHistory = [];
this.currentHistoryIndex = -1;
// 配额信息
this.aiQuota = { daily: 3, used: 0, purchased: 0 };
// 加载状态
this.isRewriting = false;
this.rewriteProgress = 0;
this.initParticles();
this.loadQuota();
}
init() {
setTimeout(() => {
this.showButtons = true;
}, 1500);
// 保存游玩记录回放模式和AI草稿不保存
if (!this.isReplay && !this.draftId) {
this.savePlayRecord();
}
}
async savePlayRecord() {
try {
// 获取当前游玩路径
const pathHistory = this.main.storyManager.pathHistory || [];
const endingName = this.ending?.name || '未知结局';
const endingType = this.ending?.type || '';
// 调用保存接口
await this.main.userManager.savePlayRecord(
this.storyId,
endingName,
endingType,
pathHistory
);
console.log('游玩记录保存成功');
} catch (e) {
console.error('保存游玩记录失败:', e);
}
}
async loadQuota() {
try {
const quota = await this.main.userManager.getAIQuota();
if (quota) {
this.aiQuota = quota;
}
} catch (e) {
console.error('加载配额失败:', e);
}
}
initParticles() {
for (let i = 0; i < 50; i++) {
this.particles.push({
x: Math.random() * this.screenWidth,
y: Math.random() * this.screenHeight,
size: Math.random() * 3 + 1,
speedY: Math.random() * 0.5 + 0.2,
alpha: Math.random() * 0.5 + 0.3
});
}
}
update() {
if (this.fadeIn < 1) {
this.fadeIn += 0.02;
}
this.particles.forEach(p => {
p.y -= p.speedY;
if (p.y < 0) {
p.y = this.screenHeight;
p.x = Math.random() * this.screenWidth;
}
});
}
render(ctx) {
this.renderBackground(ctx);
this.renderParticles(ctx);
this.renderEndingContent(ctx);
if (this.showButtons) {
this.renderButtons(ctx);
}
// AI改写面板
if (this.showRewritePanel) {
this.renderRewritePanel(ctx);
}
// AI续写面板
if (this.showContinuePanel) {
this.renderContinuePanel(ctx);
}
}
renderBackground(ctx) {
const gradient = ctx.createLinearGradient(0, 0, 0, this.screenHeight);
switch (this.ending?.type) {
case 'good':
gradient.addColorStop(0, '#0f2027');
gradient.addColorStop(0.5, '#203a43');
gradient.addColorStop(1, '#2c5364');
break;
case 'bad':
gradient.addColorStop(0, '#1a0a0a');
gradient.addColorStop(0.5, '#3a1515');
gradient.addColorStop(1, '#2d1f1f');
break;
case 'hidden':
gradient.addColorStop(0, '#1a1a0a');
gradient.addColorStop(0.5, '#3a3515');
gradient.addColorStop(1, '#2d2d1f');
break;
case 'rewrite':
gradient.addColorStop(0, '#1a0a2e');
gradient.addColorStop(0.5, '#2d1b4e');
gradient.addColorStop(1, '#4a1942');
break;
default:
gradient.addColorStop(0, '#0f0c29');
gradient.addColorStop(0.5, '#302b63');
gradient.addColorStop(1, '#24243e');
}
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, this.screenWidth, this.screenHeight);
}
renderParticles(ctx) {
this.particles.forEach(p => {
ctx.fillStyle = `rgba(255, 255, 255, ${p.alpha * this.fadeIn})`;
ctx.beginPath();
ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
ctx.fill();
});
}
renderEndingContent(ctx) {
const centerX = this.screenWidth / 2;
const alpha = this.fadeIn;
const padding = 20;
// 结局卡片背景
const cardY = 80;
const cardHeight = 320;
ctx.fillStyle = `rgba(255, 255, 255, ${0.05 * alpha})`;
this.roundRect(ctx, padding, cardY, this.screenWidth - padding * 2, cardHeight, 20);
ctx.fill();
// 装饰线
const lineGradient = ctx.createLinearGradient(padding + 30, cardY + 20, this.screenWidth - padding - 30, cardY + 20);
lineGradient.addColorStop(0, 'transparent');
lineGradient.addColorStop(0.5, this.getEndingColorRgba(alpha * 0.5));
lineGradient.addColorStop(1, 'transparent');
ctx.strokeStyle = lineGradient;
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(padding + 30, cardY + 20);
ctx.lineTo(this.screenWidth - padding - 30, cardY + 20);
ctx.stroke();
// 结局标签
ctx.fillStyle = `rgba(255, 255, 255, ${alpha * 0.6})`;
ctx.font = '13px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('— 达成结局 —', centerX, cardY + 50);
// 结局名称(自动调整字号)
const endingName = this.ending?.name || '未知结局';
let fontSize = 24;
ctx.font = `bold ${fontSize}px sans-serif`;
while (ctx.measureText(endingName).width > this.screenWidth - padding * 2 - 40 && fontSize > 14) {
fontSize -= 2;
ctx.font = `bold ${fontSize}px sans-serif`;
}
ctx.fillStyle = this.getEndingColorRgba(alpha);
ctx.fillText(endingName, centerX, cardY + 90);
// 结局类型标签
const typeLabel = this.getTypeLabel();
if (typeLabel) {
ctx.font = '11px sans-serif';
const labelWidth = ctx.measureText(typeLabel).width + 20;
ctx.fillStyle = this.getEndingColorRgba(alpha * 0.3);
this.roundRect(ctx, centerX - labelWidth / 2, cardY + 100, labelWidth, 22, 11);
ctx.fill();
ctx.fillStyle = `rgba(255, 255, 255, ${alpha * 0.9})`;
ctx.fillText(typeLabel, centerX, cardY + 115);
}
// 评分
if (this.ending?.score !== undefined) {
this.renderScore(ctx, centerX, cardY + 155, alpha);
}
// 结局描述(居中显示,限制在卡片内)
ctx.fillStyle = `rgba(255, 255, 255, ${alpha * 0.6})`;
ctx.font = '12px sans-serif';
ctx.textAlign = 'center';
const content = this.ending?.content || '';
const lastParagraph = content.split('\n').filter(p => p.trim()).pop() || '';
const maxWidth = this.screenWidth - padding * 2 - 30;
// 限制只显示2行居中
this.wrapTextCentered(ctx, lastParagraph, centerX, cardY + 250, maxWidth, 20, 2);
}
getTypeLabel() {
switch (this.ending?.type) {
case 'good': return '✨ 完美结局';
case 'bad': return '💔 悲伤结局';
case 'hidden': return '🔮 隐藏结局';
case 'rewrite': return '🤖 AI改写结局';
default: return '📖 普通结局';
}
}
truncateText(ctx, text, maxWidth) {
if (!text) return '';
if (ctx.measureText(text).width <= maxWidth) return text;
let truncated = text;
while (truncated.length > 0 && ctx.measureText(truncated + '...').width > maxWidth) {
truncated = truncated.slice(0, -1);
}
return truncated + '...';
}
renderScore(ctx, x, y, alpha) {
const score = this.ending?.score || 0;
const stars = Math.ceil(score / 20);
// 星星
const starSize = 22;
const gap = 6;
const totalWidth = 5 * starSize + 4 * gap;
const startX = x - totalWidth / 2;
for (let i = 0; i < 5; i++) {
const filled = i < stars;
ctx.fillStyle = filled ? `rgba(255, 215, 0, ${alpha})` : `rgba(100, 100, 100, ${alpha * 0.5})`;
ctx.font = `${starSize}px sans-serif`;
ctx.textAlign = 'left';
ctx.fillText(filled ? '★' : '☆', startX + i * (starSize + gap), y);
}
// 分数
ctx.fillStyle = `rgba(255, 215, 0, ${alpha})`;
ctx.font = 'bold 16px sans-serif';
ctx.textAlign = 'center';
ctx.fillText(`${score}`, x, y + 28);
}
getEndingColorRgba(alpha) {
switch (this.ending?.type) {
case 'good': return `rgba(100, 255, 150, ${alpha})`;
case 'bad': return `rgba(255, 100, 100, ${alpha})`;
case 'hidden': return `rgba(255, 215, 0, ${alpha})`;
case 'rewrite': return `rgba(168, 85, 247, ${alpha})`;
default: return `rgba(150, 150, 255, ${alpha})`;
}
}
renderButtons(ctx) {
const padding = 15;
const buttonHeight = 38;
const buttonMargin = 8;
const startY = this.screenHeight - 220;
const buttonWidth = (this.screenWidth - padding * 2 - buttonMargin) / 2;
// AI改写按钮和AI续写按钮第一行
const remaining = this.aiQuota.daily - this.aiQuota.used + this.aiQuota.purchased;
const rewriteBtnText = remaining > 0 ? '✨ AI改写' : '⚠️ 次数不足';
const continueBtnText = remaining > 0 ? '📖 AI续写' : '⚠️ 次数不足';
this.renderGradientButton(ctx, padding, startY, buttonWidth, buttonHeight, rewriteBtnText, ['#a855f7', '#ec4899']);
this.renderGradientButton(ctx, padding + buttonWidth + buttonMargin, startY, buttonWidth, buttonHeight, continueBtnText, ['#10b981', '#059669']);
// 分享按钮
const row2Y = startY + buttonHeight + buttonMargin;
this.renderGradientButton(ctx, padding, row2Y, buttonWidth, buttonHeight, '分享结局', ['#ff6b6b', '#ffd700']);
// 章节选择按钮
this.renderGradientButton(ctx, padding + buttonWidth + buttonMargin, row2Y, buttonWidth, buttonHeight, '章节选择', ['#667eea', '#764ba2']);
// 从头开始
const row3Y = row2Y + buttonHeight + buttonMargin;
ctx.fillStyle = 'rgba(255,255,255,0.1)';
this.roundRect(ctx, padding, row3Y, buttonWidth, buttonHeight, 19);
ctx.fill();
ctx.strokeStyle = 'rgba(255,255,255,0.2)';
ctx.lineWidth = 1;
this.roundRect(ctx, padding, row3Y, buttonWidth, buttonHeight, 19);
ctx.stroke();
ctx.fillStyle = '#ffffff';
ctx.font = '12px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('从头开始', padding + buttonWidth / 2, row3Y + 24);
// 返回首页
ctx.fillStyle = 'rgba(255,255,255,0.1)';
this.roundRect(ctx, padding + buttonWidth + buttonMargin, row3Y, buttonWidth, buttonHeight, 19);
ctx.fill();
ctx.strokeStyle = 'rgba(255,255,255,0.2)';
this.roundRect(ctx, padding + buttonWidth + buttonMargin, row3Y, buttonWidth, buttonHeight, 19);
ctx.stroke();
ctx.fillStyle = '#ffffff';
ctx.fillText('返回首页', padding + buttonWidth + buttonMargin + buttonWidth / 2, row3Y + 24);
// 点赞和收藏
const actionY = row3Y + buttonHeight + 18;
const centerX = this.screenWidth / 2;
ctx.font = '20px sans-serif';
ctx.textAlign = 'center';
ctx.fillText(this.isLiked ? '❤️' : '🤍', centerX - 40, actionY);
ctx.fillStyle = 'rgba(255,255,255,0.6)';
ctx.font = '10px sans-serif';
ctx.fillText('点赞', centerX - 40, actionY + 16);
ctx.font = '20px sans-serif';
ctx.fillText(this.isCollected ? '⭐' : '☆', centerX + 40, actionY);
ctx.fillStyle = 'rgba(255,255,255,0.6)';
ctx.font = '10px sans-serif';
ctx.fillText('收藏', centerX + 40, actionY + 16);
}
renderGradientButton(ctx, x, y, width, height, text, colors) {
const gradient = ctx.createLinearGradient(x, y, x + width, y);
gradient.addColorStop(0, colors[0]);
gradient.addColorStop(1, colors[1]);
ctx.fillStyle = gradient;
this.roundRect(ctx, x, y, width, height, 22);
ctx.fill();
ctx.fillStyle = '#ffffff';
ctx.font = 'bold 14px sans-serif';
ctx.textAlign = 'center';
ctx.fillText(text, x + width / 2, y + height / 2 + 5);
}
renderRewritePanel(ctx) {
const padding = 20;
const panelWidth = this.screenWidth - padding * 2;
const panelHeight = 450;
const panelX = padding;
const panelY = (this.screenHeight - panelHeight) / 2;
// 遮罩层
ctx.fillStyle = 'rgba(0, 0, 0, 0.85)';
ctx.fillRect(0, 0, this.screenWidth, this.screenHeight);
// 面板背景渐变
const panelGradient = ctx.createLinearGradient(panelX, panelY, panelX, panelY + panelHeight);
panelGradient.addColorStop(0, '#1a1a3e');
panelGradient.addColorStop(1, '#0d0d1a');
ctx.fillStyle = panelGradient;
this.roundRect(ctx, panelX, panelY, panelWidth, panelHeight, 20);
ctx.fill();
// 面板边框渐变
const borderGradient = ctx.createLinearGradient(panelX, panelY, panelX + panelWidth, panelY);
borderGradient.addColorStop(0, '#a855f7');
borderGradient.addColorStop(1, '#ec4899');
ctx.strokeStyle = borderGradient;
ctx.lineWidth = 2;
this.roundRect(ctx, panelX, panelY, panelWidth, panelHeight, 20);
ctx.stroke();
// 标题栏
ctx.fillStyle = '#ffffff';
ctx.font = 'bold 18px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('✨ AI改写结局', this.screenWidth / 2, panelY + 35);
// 配额提示
const remaining = this.aiQuota.daily - this.aiQuota.used + this.aiQuota.purchased;
ctx.fillStyle = remaining > 0 ? 'rgba(255,255,255,0.6)' : 'rgba(255,100,100,0.8)';
ctx.font = '11px sans-serif';
ctx.textAlign = 'right';
ctx.fillText(`剩余次数:${remaining}`, panelX + panelWidth - 15, panelY + 35);
// 副标题
ctx.fillStyle = 'rgba(255,255,255,0.6)';
ctx.font = '12px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('输入你想要的剧情走向AI将为你重新创作', this.screenWidth / 2, panelY + 58);
// 分隔线
const lineGradient = ctx.createLinearGradient(panelX + 20, panelY + 75, panelX + panelWidth - 20, panelY + 75);
lineGradient.addColorStop(0, 'transparent');
lineGradient.addColorStop(0.5, 'rgba(168,85,247,0.5)');
lineGradient.addColorStop(1, 'transparent');
ctx.strokeStyle = lineGradient;
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(panelX + 20, panelY + 75);
ctx.lineTo(panelX + panelWidth - 20, panelY + 75);
ctx.stroke();
// 快捷标签标题
ctx.fillStyle = 'rgba(255,255,255,0.8)';
ctx.font = '13px sans-serif';
ctx.textAlign = 'left';
ctx.fillText('快捷选择:', panelX + 15, panelY + 105);
// 快捷标签
const tagStartX = panelX + 15;
const tagY = panelY + 120;
const tagHeight = 32;
const tagGap = 8;
let currentX = tagStartX;
let currentY = tagY;
this.tagRects = [];
this.rewriteTags.forEach((tag, index) => {
ctx.font = '12px sans-serif';
const tagWidth = ctx.measureText(tag).width + 24;
// 换行
if (currentX + tagWidth > panelX + panelWidth - 15) {
currentX = tagStartX;
currentY += tagHeight + tagGap;
}
// 标签背景
const isSelected = index === this.selectedTag;
if (isSelected) {
const tagGradient = ctx.createLinearGradient(currentX, currentY, currentX + tagWidth, currentY);
tagGradient.addColorStop(0, '#a855f7');
tagGradient.addColorStop(1, '#ec4899');
ctx.fillStyle = tagGradient;
} else {
ctx.fillStyle = 'rgba(255,255,255,0.1)';
}
this.roundRect(ctx, currentX, currentY, tagWidth, tagHeight, 16);
ctx.fill();
// 标签边框
ctx.strokeStyle = isSelected ? 'transparent' : 'rgba(255,255,255,0.2)';
ctx.lineWidth = 1;
this.roundRect(ctx, currentX, currentY, tagWidth, tagHeight, 16);
ctx.stroke();
// 标签文字
ctx.fillStyle = '#ffffff';
ctx.textAlign = 'center';
ctx.fillText(tag, currentX + tagWidth / 2, currentY + 21);
// 存储标签位置
this.tagRects.push({ x: currentX, y: currentY, width: tagWidth, height: tagHeight, index });
currentX += tagWidth + tagGap;
});
// 自定义输入提示
ctx.fillStyle = 'rgba(255,255,255,0.8)';
ctx.font = '13px sans-serif';
ctx.textAlign = 'left';
ctx.fillText('或自定义输入:', panelX + 15, panelY + 215);
// 输入框背景
const inputY = panelY + 230;
const inputHeight = 45;
ctx.fillStyle = 'rgba(255,255,255,0.08)';
this.roundRect(ctx, panelX + 15, inputY, panelWidth - 30, inputHeight, 12);
ctx.fill();
ctx.strokeStyle = 'rgba(255,255,255,0.2)';
ctx.lineWidth = 1;
this.roundRect(ctx, panelX + 15, inputY, panelWidth - 30, inputHeight, 12);
ctx.stroke();
// 输入框文字或占位符
ctx.font = '14px sans-serif';
ctx.textAlign = 'left';
if (this.rewritePrompt) {
ctx.fillStyle = '#ffffff';
ctx.fillText(this.rewritePrompt, panelX + 28, inputY + 28);
} else {
ctx.fillStyle = 'rgba(255,255,255,0.4)';
ctx.fillText('点击输入你的改写想法...', panelX + 28, inputY + 28);
}
// 按钮
const btnY = panelY + panelHeight - 70;
const btnWidth = (panelWidth - 50) / 2;
const btnHeight = 44;
// 取消按钮
ctx.fillStyle = 'rgba(255,255,255,0.1)';
this.roundRect(ctx, panelX + 15, btnY, btnWidth, btnHeight, 22);
ctx.fill();
ctx.strokeStyle = 'rgba(255,255,255,0.3)';
ctx.lineWidth = 1;
this.roundRect(ctx, panelX + 15, btnY, btnWidth, btnHeight, 22);
ctx.stroke();
ctx.fillStyle = '#ffffff';
ctx.font = '14px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('取消', panelX + 15 + btnWidth / 2, btnY + 28);
// 确认按钮
const confirmGradient = ctx.createLinearGradient(panelX + 35 + btnWidth, btnY, panelX + 35 + btnWidth * 2, btnY);
confirmGradient.addColorStop(0, '#a855f7');
confirmGradient.addColorStop(1, '#ec4899');
ctx.fillStyle = confirmGradient;
this.roundRect(ctx, panelX + 35 + btnWidth, btnY, btnWidth, btnHeight, 22);
ctx.fill();
ctx.fillStyle = '#ffffff';
ctx.font = 'bold 14px sans-serif';
ctx.fillText('✨ 开始改写', panelX + 35 + btnWidth + btnWidth / 2, btnY + 28);
// 存储按钮区域
this.cancelBtnRect = { x: panelX + 15, y: btnY, width: btnWidth, height: btnHeight };
this.confirmBtnRect = { x: panelX + 35 + btnWidth, y: btnY, width: btnWidth, height: btnHeight };
this.inputRect = { x: panelX + 15, y: inputY, width: panelWidth - 30, height: inputHeight };
}
renderContinuePanel(ctx) {
const padding = 20;
const panelWidth = this.screenWidth - padding * 2;
const panelHeight = 450;
const panelX = padding;
const panelY = (this.screenHeight - panelHeight) / 2;
// 遮罩层
ctx.fillStyle = 'rgba(0, 0, 0, 0.85)';
ctx.fillRect(0, 0, this.screenWidth, this.screenHeight);
// 面板背景渐变
const panelGradient = ctx.createLinearGradient(panelX, panelY, panelX, panelY + panelHeight);
panelGradient.addColorStop(0, '#0d2818');
panelGradient.addColorStop(1, '#0a1a10');
ctx.fillStyle = panelGradient;
this.roundRect(ctx, panelX, panelY, panelWidth, panelHeight, 20);
ctx.fill();
// 面板边框渐变
const borderGradient = ctx.createLinearGradient(panelX, panelY, panelX + panelWidth, panelY);
borderGradient.addColorStop(0, '#10b981');
borderGradient.addColorStop(1, '#059669');
ctx.strokeStyle = borderGradient;
ctx.lineWidth = 2;
this.roundRect(ctx, panelX, panelY, panelWidth, panelHeight, 20);
ctx.stroke();
// 标题栏
ctx.fillStyle = '#ffffff';
ctx.font = 'bold 18px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('📖 AI续写结局', this.screenWidth / 2, panelY + 35);
// 配额提示
const remaining = this.aiQuota.daily - this.aiQuota.used + this.aiQuota.purchased;
ctx.fillStyle = remaining > 0 ? 'rgba(255,255,255,0.6)' : 'rgba(255,100,100,0.8)';
ctx.font = '11px sans-serif';
ctx.textAlign = 'right';
ctx.fillText(`剩余次数:${remaining}`, panelX + panelWidth - 15, panelY + 35);
// 副标题
ctx.fillStyle = 'rgba(255,255,255,0.6)';
ctx.font = '12px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('从当前结局出发AI将为你续写新的剧情分支', this.screenWidth / 2, panelY + 58);
// 分隔线
const lineGradient = ctx.createLinearGradient(panelX + 20, panelY + 75, panelX + panelWidth - 20, panelY + 75);
lineGradient.addColorStop(0, 'transparent');
lineGradient.addColorStop(0.5, 'rgba(16,185,129,0.5)');
lineGradient.addColorStop(1, 'transparent');
ctx.strokeStyle = lineGradient;
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(panelX + 20, panelY + 75);
ctx.lineTo(panelX + panelWidth - 20, panelY + 75);
ctx.stroke();
// 快捷标签标题
ctx.fillStyle = 'rgba(255,255,255,0.8)';
ctx.font = '13px sans-serif';
ctx.textAlign = 'left';
ctx.fillText('快捷选择:', panelX + 15, panelY + 105);
// 快捷标签
const tagStartX = panelX + 15;
const tagY = panelY + 120;
const tagHeight = 32;
const tagGap = 8;
let currentX = tagStartX;
let currentY = tagY;
this.continueTagRects = [];
this.continueTags.forEach((tag, index) => {
ctx.font = '12px sans-serif';
const tagWidth = ctx.measureText(tag).width + 24;
// 换行
if (currentX + tagWidth > panelX + panelWidth - 15) {
currentX = tagStartX;
currentY += tagHeight + tagGap;
}
// 标签背景
const isSelected = index === this.selectedContinueTag;
if (isSelected) {
const tagGradient = ctx.createLinearGradient(currentX, currentY, currentX + tagWidth, currentY);
tagGradient.addColorStop(0, '#10b981');
tagGradient.addColorStop(1, '#059669');
ctx.fillStyle = tagGradient;
} else {
ctx.fillStyle = 'rgba(255,255,255,0.1)';
}
this.roundRect(ctx, currentX, currentY, tagWidth, tagHeight, 16);
ctx.fill();
// 标签边框
ctx.strokeStyle = isSelected ? 'transparent' : 'rgba(255,255,255,0.2)';
ctx.lineWidth = 1;
this.roundRect(ctx, currentX, currentY, tagWidth, tagHeight, 16);
ctx.stroke();
// 标签文字
ctx.fillStyle = '#ffffff';
ctx.textAlign = 'center';
ctx.fillText(tag, currentX + tagWidth / 2, currentY + 21);
// 存储标签位置
this.continueTagRects.push({ x: currentX, y: currentY, width: tagWidth, height: tagHeight, index });
currentX += tagWidth + tagGap;
});
// 自定义输入提示
ctx.fillStyle = 'rgba(255,255,255,0.8)';
ctx.font = '13px sans-serif';
ctx.textAlign = 'left';
ctx.fillText('或自定义输入:', panelX + 15, panelY + 215);
// 输入框背景
const inputY = panelY + 230;
const inputHeight = 45;
ctx.fillStyle = 'rgba(255,255,255,0.08)';
this.roundRect(ctx, panelX + 15, inputY, panelWidth - 30, inputHeight, 12);
ctx.fill();
ctx.strokeStyle = 'rgba(255,255,255,0.2)';
ctx.lineWidth = 1;
this.roundRect(ctx, panelX + 15, inputY, panelWidth - 30, inputHeight, 12);
ctx.stroke();
// 输入框文字或占位符
ctx.font = '14px sans-serif';
ctx.textAlign = 'left';
if (this.continuePrompt) {
ctx.fillStyle = '#ffffff';
ctx.fillText(this.continuePrompt, panelX + 28, inputY + 28);
} else {
ctx.fillStyle = 'rgba(255,255,255,0.4)';
ctx.fillText('点击输入你的续写想法...', panelX + 28, inputY + 28);
}
// 按钮
const btnY = panelY + panelHeight - 70;
const btnWidth = (panelWidth - 50) / 2;
const btnHeight = 44;
// 取消按钮
ctx.fillStyle = 'rgba(255,255,255,0.1)';
this.roundRect(ctx, panelX + 15, btnY, btnWidth, btnHeight, 22);
ctx.fill();
ctx.strokeStyle = 'rgba(255,255,255,0.3)';
ctx.lineWidth = 1;
this.roundRect(ctx, panelX + 15, btnY, btnWidth, btnHeight, 22);
ctx.stroke();
ctx.fillStyle = '#ffffff';
ctx.font = '14px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('取消', panelX + 15 + btnWidth / 2, btnY + 28);
// 确认按钮
const confirmGradient = ctx.createLinearGradient(panelX + 35 + btnWidth, btnY, panelX + 35 + btnWidth * 2, btnY);
confirmGradient.addColorStop(0, '#10b981');
confirmGradient.addColorStop(1, '#059669');
ctx.fillStyle = confirmGradient;
this.roundRect(ctx, panelX + 35 + btnWidth, btnY, btnWidth, btnHeight, 22);
ctx.fill();
ctx.fillStyle = '#ffffff';
ctx.font = 'bold 14px sans-serif';
ctx.fillText('📖 开始续写', panelX + 35 + btnWidth + btnWidth / 2, btnY + 28);
// 存储按钮区域
this.continueCancelBtnRect = { x: panelX + 15, y: btnY, width: btnWidth, height: btnHeight };
this.continueConfirmBtnRect = { x: panelX + 35 + btnWidth, y: btnY, width: btnWidth, height: btnHeight };
this.continueInputRect = { x: panelX + 15, y: inputY, width: panelWidth - 30, height: inputHeight };
}
// 圆角矩形
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();
}
// 文字换行
wrapText(ctx, text, x, y, maxWidth, lineHeight) {
if (!text) return;
let line = '';
let lineY = y;
for (let char of text) {
const testLine = line + char;
if (ctx.measureText(testLine).width > maxWidth) {
ctx.fillText(line, x, lineY);
line = char;
lineY += lineHeight;
} else {
line = testLine;
}
}
ctx.fillText(line, x, lineY);
}
// 限制行数的文字换行
wrapTextLimited(ctx, text, x, y, maxWidth, lineHeight, maxLines) {
if (!text) return;
let line = '';
let lineY = y;
let lineCount = 0;
for (let i = 0; i < text.length; i++) {
const char = text[i];
const testLine = line + char;
if (ctx.measureText(testLine).width > maxWidth) {
lineCount++;
if (lineCount >= maxLines) {
// 最后一行加省略号
while (line.length > 0 && ctx.measureText(line + '...').width > maxWidth) {
line = line.slice(0, -1);
}
ctx.fillText(line + '...', x, lineY);
return;
}
ctx.fillText(line, x, lineY);
line = char;
lineY += lineHeight;
} else {
line = testLine;
}
}
ctx.fillText(line, x, lineY);
}
// 居中显示的限制行数文字换行
wrapTextCentered(ctx, text, centerX, y, maxWidth, lineHeight, maxLines) {
if (!text) return;
// 先分行
const lines = [];
let line = '';
for (let i = 0; i < text.length; i++) {
const char = text[i];
const testLine = line + char;
if (ctx.measureText(testLine).width > maxWidth) {
lines.push(line);
line = char;
if (lines.length >= maxLines) break;
} else {
line = testLine;
}
}
if (line && lines.length < maxLines) {
lines.push(line);
}
// 如果超出行数,最后一行加省略号
if (lines.length >= maxLines && line) {
let lastLine = lines[maxLines - 1];
while (lastLine.length > 0 && ctx.measureText(lastLine + '...').width > maxWidth) {
lastLine = lastLine.slice(0, -1);
}
lines[maxLines - 1] = lastLine + '...';
}
// 居中绘制
lines.slice(0, maxLines).forEach((l, i) => {
ctx.fillText(l, centerX, y + i * lineHeight);
});
}
onTouchEnd(e) {
const touch = e.changedTouches[0];
const x = touch.clientX;
const y = touch.clientY;
// 如果改写面板打开,优先处理
if (this.showRewritePanel) {
this.handleRewritePanelTouch(x, y);
return;
}
// 如果续写面板打开,优先处理
if (this.showContinuePanel) {
this.handleContinuePanelTouch(x, y);
return;
}
if (!this.showButtons) return;
const padding = 15;
const buttonHeight = 38;
const buttonMargin = 8;
const startY = this.screenHeight - 220;
const buttonWidth = (this.screenWidth - padding * 2 - buttonMargin) / 2;
// AI改写按钮
if (this.isInRect(x, y, padding, startY, buttonWidth, buttonHeight)) {
this.handleAIRewrite();
return;
}
// AI续写按钮
if (this.isInRect(x, y, padding + buttonWidth + buttonMargin, startY, buttonWidth, buttonHeight)) {
this.handleAIContinue();
return;
}
// 分享按钮
const row2Y = startY + buttonHeight + buttonMargin;
if (this.isInRect(x, y, padding, row2Y, buttonWidth, buttonHeight)) {
this.handleShare();
return;
}
// 章节选择按钮
if (this.isInRect(x, y, padding + buttonWidth + buttonMargin, row2Y, buttonWidth, buttonHeight)) {
this.handleChapterSelect();
return;
}
// 从头开始
const row3Y = row2Y + buttonHeight + buttonMargin;
if (this.isInRect(x, y, padding, row3Y, buttonWidth, buttonHeight)) {
this.handleReplay();
return;
}
// 返回首页
if (this.isInRect(x, y, padding + buttonWidth + buttonMargin, row3Y, buttonWidth, buttonHeight)) {
// 清除当前故事状态
this.main.storyManager.currentStory = null;
this.main.storyManager.currentNodeKey = 'start';
this.main.sceneManager.switchScene('home');
return;
}
// 点赞收藏
const actionY = row3Y + buttonHeight + 18;
const centerX = this.screenWidth / 2;
if (this.isInRect(x, y, centerX - 70, actionY - 20, 60, 45)) {
this.handleLike();
return;
}
if (this.isInRect(x, y, centerX + 10, actionY - 20, 60, 45)) {
this.handleCollect();
return;
}
}
isInRect(x, y, rx, ry, rw, rh) {
return x >= rx && x <= rx + rw && y >= ry && y <= ry + rh;
}
handleShare() {
wx.shareAppMessage({
title: `我在《星域故事汇》达成了「${this.ending?.name}」结局!`,
imageUrl: '',
query: `storyId=${this.storyId}`
});
}
handleChapterSelect() {
this.main.sceneManager.switchScene('chapter', { storyId: this.storyId });
}
handleAIRewrite() {
// 检查配额
const remaining = this.aiQuota.daily - this.aiQuota.used + this.aiQuota.purchased;
if (remaining <= 0) {
this.showQuotaModal();
return;
}
// 显示AI改写面板
this.showRewritePanel = true;
this.rewritePrompt = '';
this.selectedTag = -1;
}
handleAIContinue() {
// 检查配额
const remaining = this.aiQuota.daily - this.aiQuota.used + this.aiQuota.purchased;
if (remaining <= 0) {
this.showQuotaModal();
return;
}
// 显示AI续写面板
this.showContinuePanel = true;
this.continuePrompt = '';
this.selectedContinueTag = -1;
}
handleRewritePanelTouch(x, y) {
// 点击标签
if (this.tagRects) {
for (const tag of this.tagRects) {
if (this.isInRect(x, y, tag.x, tag.y, tag.width, tag.height)) {
this.selectedTag = tag.index;
this.rewritePrompt = this.rewriteTags[tag.index];
return true;
}
}
}
// 点击输入框
if (this.inputRect && this.isInRect(x, y, this.inputRect.x, this.inputRect.y, this.inputRect.width, this.inputRect.height)) {
this.showCustomInput();
return true;
}
// 点击取消
if (this.cancelBtnRect && this.isInRect(x, y, this.cancelBtnRect.x, this.cancelBtnRect.y, this.cancelBtnRect.width, this.cancelBtnRect.height)) {
this.showRewritePanel = false;
return true;
}
// 点击确认
if (this.confirmBtnRect && this.isInRect(x, y, this.confirmBtnRect.x, this.confirmBtnRect.y, this.confirmBtnRect.width, this.confirmBtnRect.height)) {
if (this.rewritePrompt) {
this.showRewritePanel = false;
this.callAIRewrite(this.rewritePrompt);
} else {
wx.showToast({ title: '请选择或输入改写内容', icon: 'none' });
}
return true;
}
return false;
}
handleContinuePanelTouch(x, y) {
// 点击标签
if (this.continueTagRects) {
for (const tag of this.continueTagRects) {
if (this.isInRect(x, y, tag.x, tag.y, tag.width, tag.height)) {
this.selectedContinueTag = tag.index;
this.continuePrompt = this.continueTags[tag.index];
return true;
}
}
}
// 点击输入框
if (this.continueInputRect && this.isInRect(x, y, this.continueInputRect.x, this.continueInputRect.y, this.continueInputRect.width, this.continueInputRect.height)) {
this.showContinueInput();
return true;
}
// 点击取消
if (this.continueCancelBtnRect && this.isInRect(x, y, this.continueCancelBtnRect.x, this.continueCancelBtnRect.y, this.continueCancelBtnRect.width, this.continueCancelBtnRect.height)) {
this.showContinuePanel = false;
return true;
}
// 点击确认
if (this.continueConfirmBtnRect && this.isInRect(x, y, this.continueConfirmBtnRect.x, this.continueConfirmBtnRect.y, this.continueConfirmBtnRect.width, this.continueConfirmBtnRect.height)) {
if (this.continuePrompt) {
this.showContinuePanel = false;
this.callAIContinue(this.continuePrompt);
} else {
wx.showToast({ title: '请选择或输入续写方向', icon: 'none' });
}
return true;
}
return false;
}
showCustomInput() {
wx.showModal({
title: '输入改写想法',
editable: true,
placeholderText: '例如:让主角获得逆袭',
content: this.rewritePrompt,
success: (res) => {
if (res.confirm && res.content) {
this.rewritePrompt = res.content;
this.selectedTag = -1;
}
}
});
}
showContinueInput() {
wx.showModal({
title: '输入续写想法',
editable: true,
placeholderText: '例如:主角开启新的冒险',
content: this.continuePrompt,
success: (res) => {
if (res.confirm && res.content) {
this.continuePrompt = res.content;
this.selectedContinueTag = -1;
}
}
});
}
async callAIRewrite(prompt) {
// 检查配额
const remaining = this.aiQuota.daily - this.aiQuota.used + this.aiQuota.purchased;
if (remaining <= 0) {
this.showQuotaModal();
return;
}
this.isRewriting = true;
// 显示加载动画
wx.showLoading({
title: '提交中...',
mask: true
});
try {
const userId = this.main.userManager.userId || 1;
const result = await this.main.storyManager.rewriteEndingAsync(
this.storyId,
this.ending,
prompt,
userId
);
wx.hideLoading();
if (result && result.draftId) {
// 扣除配额
this.aiQuota.used += 1;
// 提交成功提示
wx.showModal({
title: '提交成功',
content: 'AI正在后台生成新结局完成后会通知您。\n您可以在草稿箱中查看。',
showCancel: false,
confirmText: '知道了'
});
// 启动专门的草稿检查每5秒检查一次持续2分钟
this.startDraftPolling(result.draftId);
} else {
wx.showToast({ title: '提交失败,请重试', icon: 'none' });
}
} catch (error) {
wx.hideLoading();
console.error('改写失败:', error);
wx.showToast({ title: error.message || '网络错误', icon: 'none' });
} finally {
this.isRewriting = false;
}
}
async callAIContinue(prompt) {
// 检查配额
const remaining = this.aiQuota.daily - this.aiQuota.used + this.aiQuota.purchased;
if (remaining <= 0) {
this.showQuotaModal();
return;
}
this.isRewriting = true;
// 显示加载动画
wx.showLoading({
title: '提交中...',
mask: true
});
try {
const userId = this.main.userManager.userId || 1;
const result = await this.main.storyManager.continueEndingAsync(
this.storyId,
this.ending,
prompt,
userId
);
wx.hideLoading();
if (result && result.draftId) {
// 扣除配额
this.aiQuota.used += 1;
// 提交成功提示
wx.showModal({
title: '提交成功',
content: 'AI正在后台生成续写剧情完成后会通知您。\n您可以在草稿箱中查看。',
showCancel: false,
confirmText: '知道了'
});
// 启动专门的草稿检查每5秒检查一次持续2分钟
this.startDraftPolling(result.draftId);
} else {
wx.showToast({ title: '提交失败,请重试', icon: 'none' });
}
} catch (error) {
wx.hideLoading();
console.error('续写失败:', error);
wx.showToast({ title: error.message || '网络错误', icon: 'none' });
} finally {
this.isRewriting = false;
}
}
showQuotaModal() {
wx.showModal({
title: 'AI次数不足',
content: `今日剩余免费次数已用完。\n\n观看广告可获得1次 AI改写机会`,
confirmText: '看广告',
cancelText: '取消',
success: (res) => {
if (res.confirm) {
this.watchAdForQuota();
}
}
});
}
watchAdForQuota() {
// 模拟看广告获得配额
wx.showToast({
title: '获得1次 AI 次数',
icon: 'success'
});
this.aiQuota.purchased += 1;
}
handleReplay() {
this.main.storyManager.resetStory();
// 如果是从草稿进入的,重头游玩时保留草稿上下文
if (this.draftId) {
this.main.sceneManager.switchScene('story', {
storyId: this.storyId,
draftId: this.draftId
});
} else {
this.main.sceneManager.switchScene('story', { storyId: this.storyId });
}
}
handleLike() {
this.isLiked = !this.isLiked;
this.main.userManager.likeStory(this.storyId, this.isLiked);
this.main.storyManager.likeStory(this.isLiked);
}
handleCollect() {
this.isCollected = !this.isCollected;
this.main.userManager.collectStory(this.storyId, this.isCollected);
}
// 启动草稿完成轮询每5秒检查一次持续2分钟
startDraftPolling(draftId) {
// 清除之前的轮询
if (this.draftPollTimer) {
clearInterval(this.draftPollTimer);
}
let pollCount = 0;
const maxPolls = 24; // 2分钟 / 5秒 = 24次
console.log('[EndingScene] 启动草稿轮询, draftId:', draftId);
this.draftPollTimer = setInterval(async () => {
pollCount++;
if (pollCount > maxPolls) {
console.log('[EndingScene] 轮询超时,停止检查');
clearInterval(this.draftPollTimer);
this.draftPollTimer = null;
return;
}
try {
const userId = this.main.userManager.userId;
if (!userId) return;
const result = await this.main.storyManager.checkNewDrafts(userId);
if (result && result.hasNew && result.count > 0) {
console.log('[EndingScene] 检测到新草稿:', result.count);
// 停止轮询
clearInterval(this.draftPollTimer);
this.draftPollTimer = null;
// 标记为已读
await this.main.storyManager.markAllDraftsRead(userId);
// 弹窗通知
wx.showModal({
title: 'AI改写完成',
content: `您有 ${result.count} 个新的AI改写已完成是否前往查看`,
confirmText: '查看',
cancelText: '稍后',
success: (res) => {
if (res.confirm) {
this.main.sceneManager.switchScene('profile', { tab: 1 });
}
}
});
}
} catch (e) {
console.warn('[EndingScene] 草稿检查失败:', e);
}
}, 5000); // 每5秒检查一次
}
// 场景销毁时清理轮询
destroy() {
if (this.draftPollTimer) {
clearInterval(this.draftPollTimer);
this.draftPollTimer = null;
}
}
}