861 lines
27 KiB
JavaScript
861 lines
27 KiB
JavaScript
/**
|
||
* 结局场景
|
||
*/
|
||
import BaseScene from './BaseScene';
|
||
|
||
export default class EndingScene extends BaseScene {
|
||
constructor(main, params) {
|
||
super(main, params);
|
||
this.storyId = params.storyId;
|
||
this.ending = params.ending;
|
||
console.log('EndingScene 接收到的结局:', JSON.stringify(this.ending));
|
||
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;
|
||
// 改写历史
|
||
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);
|
||
}
|
||
|
||
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);
|
||
}
|
||
}
|
||
|
||
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;
|
||
|
||
// AI改写按钮(带配额提示)
|
||
const remaining = this.aiQuota.daily - this.aiQuota.used + this.aiQuota.purchased;
|
||
const aiBtnText = remaining > 0 ? '✨ AI改写结局' : '⚠️ 次数不足';
|
||
this.renderGradientButton(ctx, padding, startY, this.screenWidth - padding * 2, buttonHeight, aiBtnText, ['#a855f7', '#ec4899']);
|
||
|
||
// 分享按钮
|
||
const row2Y = startY + buttonHeight + buttonMargin;
|
||
const buttonWidth = (this.screenWidth - padding * 2 - buttonMargin) / 2;
|
||
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 };
|
||
}
|
||
|
||
// 圆角矩形
|
||
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.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, this.screenWidth - padding * 2, buttonHeight)) {
|
||
this.handleAIRewrite();
|
||
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.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;
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
showCustomInput() {
|
||
wx.showModal({
|
||
title: '输入改写想法',
|
||
editable: true,
|
||
placeholderText: '例如:让主角获得逆袭',
|
||
content: this.rewritePrompt,
|
||
success: (res) => {
|
||
if (res.confirm && res.content) {
|
||
this.rewritePrompt = res.content;
|
||
this.selectedTag = -1;
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
async callAIRewrite(prompt) {
|
||
// 检查配额
|
||
const remaining = this.aiQuota.daily - this.aiQuota.used + this.aiQuota.purchased;
|
||
if (remaining <= 0) {
|
||
this.showQuotaModal();
|
||
return;
|
||
}
|
||
|
||
this.isRewriting = true;
|
||
this.rewriteProgress = 0;
|
||
|
||
// 显示加载动画
|
||
wx.showLoading({
|
||
title: 'AI创作中...',
|
||
mask: true
|
||
});
|
||
|
||
// 模拟进度条效果
|
||
const progressInterval = setInterval(() => {
|
||
this.rewriteProgress += Math.random() * 20;
|
||
if (this.rewriteProgress > 90) this.rewriteProgress = 90;
|
||
}, 500);
|
||
|
||
try {
|
||
const result = await this.main.storyManager.rewriteEnding(
|
||
this.storyId,
|
||
this.ending,
|
||
prompt
|
||
);
|
||
|
||
clearInterval(progressInterval);
|
||
this.rewriteProgress = 100;
|
||
wx.hideLoading();
|
||
|
||
if (result && result.content) {
|
||
// 记录改写历史
|
||
this.rewriteHistory.push({
|
||
prompt: prompt,
|
||
content: result.content,
|
||
timestamp: Date.now()
|
||
});
|
||
this.currentHistoryIndex = this.rewriteHistory.length - 1;
|
||
|
||
// 扣除配额
|
||
this.aiQuota.used += 1;
|
||
|
||
// 成功提示
|
||
wx.showToast({
|
||
title: '改写成功!',
|
||
icon: 'success',
|
||
duration: 1500
|
||
});
|
||
|
||
// 延迟跳转到故事场景播放新内容
|
||
setTimeout(() => {
|
||
this.main.sceneManager.switchScene('story', {
|
||
storyId: this.storyId,
|
||
aiContent: result
|
||
});
|
||
}, 1500);
|
||
} else {
|
||
wx.showToast({ title: '改写失败,请重试', icon: 'none' });
|
||
}
|
||
} catch (error) {
|
||
clearInterval(progressInterval);
|
||
wx.hideLoading();
|
||
console.error('改写失败:', error);
|
||
wx.showToast({ title: error.message || '网络错误', icon: 'none' });
|
||
} finally {
|
||
this.isRewriting = false;
|
||
this.rewriteProgress = 0;
|
||
}
|
||
}
|
||
|
||
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();
|
||
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);
|
||
}
|
||
}
|