feat: 星域故事汇小游戏初始版本
This commit is contained in:
745
client/js/scenes/EndingScene.js
Normal file
745
client/js/scenes/EndingScene.js
Normal file
@@ -0,0 +1,745 @@
|
||||
/**
|
||||
* 结局场景
|
||||
*/
|
||||
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.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.initParticles();
|
||||
}
|
||||
|
||||
init() {
|
||||
setTimeout(() => {
|
||||
this.showButtons = true;
|
||||
}, 1500);
|
||||
}
|
||||
|
||||
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;
|
||||
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 '🔮 隐藏结局';
|
||||
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})`;
|
||||
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改写按钮(突出显示)
|
||||
this.renderGradientButton(ctx, padding, startY, this.screenWidth - padding * 2, buttonHeight, '✨ AI改写结局', ['#a855f7', '#ec4899']);
|
||||
|
||||
// 分享按钮
|
||||
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 = 380;
|
||||
const panelX = padding;
|
||||
const panelY = (this.screenHeight - panelHeight) / 2;
|
||||
|
||||
// 遮罩层
|
||||
ctx.fillStyle = 'rgba(0, 0, 0, 0.8)';
|
||||
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);
|
||||
|
||||
// 副标题
|
||||
ctx.fillStyle = 'rgba(255,255,255,0.6)';
|
||||
ctx.font = '12px sans-serif';
|
||||
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() {
|
||||
// 显示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) {
|
||||
wx.showLoading({ title: 'AI创作中...' });
|
||||
|
||||
try {
|
||||
const result = await this.main.storyManager.rewriteEnding(
|
||||
this.storyId,
|
||||
this.ending,
|
||||
prompt
|
||||
);
|
||||
|
||||
wx.hideLoading();
|
||||
|
||||
if (result && result.content) {
|
||||
// 跳转到故事场景播放新内容
|
||||
this.main.sceneManager.switchScene('story', {
|
||||
storyId: this.storyId,
|
||||
aiContent: result
|
||||
});
|
||||
} else {
|
||||
wx.showToast({ title: '改写失败,请重试', icon: 'none' });
|
||||
}
|
||||
} catch (error) {
|
||||
wx.hideLoading();
|
||||
wx.showToast({ title: '网络错误', icon: 'none' });
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user