/** * AI创作中心场景 */ import BaseScene from './BaseScene'; export default class AICreateScene extends BaseScene { constructor(main, params) { super(main, params); this.currentTab = 0; // 0:改写 1:续写 2:创作 this.tabs = ['AI改写', 'AI续写', 'AI创作']; // 滚动 this.scrollY = 0; this.maxScrollY = 0; this.isDragging = false; this.lastTouchY = 0; this.hasMoved = false; // 用户数据 this.recentStories = []; this.aiHistory = []; this.quota = { daily: 3, used: 0, purchased: 0 }; // 创作表单 this.createForm = { genre: '', keywords: '', protagonist: '', conflict: '' }; // 选中的故事(用于改写/续写) this.selectedStory = null; // 快捷标签 this.rewriteTags = ['主角逆袭', '甜蜜HE', '虐心BE', '反转剧情', '意外重逢', '身份揭秘']; this.continueTags = ['增加悬念', '感情升温', '冲突加剧', '真相大白', '误会解除']; this.genreTags = ['都市言情', '古风宫廷', '悬疑推理', '校园青春', '修仙玄幻', '职场商战']; } async init() { await this.loadData(); } async loadData() { try { // 加载最近游玩的故事 this.recentStories = await this.main.userManager.getRecentPlayed() || []; // 加载AI创作历史 this.aiHistory = await this.main.userManager.getAIHistory() || []; // 加载配额 const quotaData = await this.main.userManager.getAIQuota(); if (quotaData) this.quota = quotaData; } catch (e) { console.error('加载数据失败:', e); } this.calculateMaxScroll(); } calculateMaxScroll() { let contentHeight = 400; if (this.currentTab === 0 || this.currentTab === 1) { contentHeight = 300 + this.recentStories.length * 80; } else { contentHeight = 600; } this.maxScrollY = Math.max(0, contentHeight - this.screenHeight + 200); } update() {} render(ctx) { this.renderBackground(ctx); this.renderHeader(ctx); this.renderQuotaBar(ctx); this.renderTabs(ctx); this.renderContent(ctx); } renderBackground(ctx) { const gradient = ctx.createLinearGradient(0, 0, 0, this.screenHeight); gradient.addColorStop(0, '#0f0c29'); gradient.addColorStop(0.5, '#302b63'); gradient.addColorStop(1, '#24243e'); ctx.fillStyle = gradient; ctx.fillRect(0, 0, this.screenWidth, this.screenHeight); // 装饰光效 const glow = ctx.createRadialGradient(this.screenWidth / 2, 150, 0, this.screenWidth / 2, 150, 200); glow.addColorStop(0, 'rgba(168, 85, 247, 0.15)'); glow.addColorStop(1, 'transparent'); ctx.fillStyle = glow; ctx.fillRect(0, 0, this.screenWidth, 300); } renderHeader(ctx) { // 顶部遮罩 const headerGradient = ctx.createLinearGradient(0, 0, 0, 80); headerGradient.addColorStop(0, 'rgba(15,12,41,1)'); headerGradient.addColorStop(1, 'rgba(15,12,41,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, 40); // 标题 ctx.textAlign = 'center'; ctx.font = 'bold 18px sans-serif'; const titleGradient = ctx.createLinearGradient(this.screenWidth / 2 - 50, 0, this.screenWidth / 2 + 50, 0); titleGradient.addColorStop(0, '#a855f7'); titleGradient.addColorStop(1, '#ec4899'); ctx.fillStyle = titleGradient; ctx.fillText('✨ AI创作中心', this.screenWidth / 2, 40); } renderQuotaBar(ctx) { const barY = 60; const remaining = this.quota.daily - this.quota.used + this.quota.purchased; // 配额背景 ctx.fillStyle = 'rgba(255,255,255,0.08)'; this.roundRect(ctx, 15, barY, this.screenWidth - 30, 36, 18); ctx.fill(); // 配额文字 ctx.fillStyle = 'rgba(255,255,255,0.7)'; ctx.font = '12px sans-serif'; ctx.textAlign = 'left'; ctx.fillText(`今日剩余: ${remaining}次`, 30, barY + 23); // 充值按钮 const btnWidth = 70; const btnX = this.screenWidth - 30 - btnWidth; const btnGradient = ctx.createLinearGradient(btnX, barY + 5, btnX + btnWidth, barY + 5); btnGradient.addColorStop(0, '#a855f7'); btnGradient.addColorStop(1, '#ec4899'); ctx.fillStyle = btnGradient; this.roundRect(ctx, btnX, barY + 5, btnWidth, 26, 13); ctx.fill(); ctx.fillStyle = '#ffffff'; ctx.font = 'bold 11px sans-serif'; ctx.textAlign = 'center'; ctx.fillText('获取更多', btnX + btnWidth / 2, barY + 22); this.quotaBtnRect = { x: btnX, y: barY + 5, width: btnWidth, height: 26 }; } renderTabs(ctx) { const tabY = 110; const tabWidth = (this.screenWidth - 40) / 3; const padding = 15; this.tabRects = []; this.tabs.forEach((tab, index) => { const x = padding + index * (tabWidth + 5); const isActive = index === this.currentTab; if (isActive) { const gradient = ctx.createLinearGradient(x, tabY, x + tabWidth, tabY); gradient.addColorStop(0, '#a855f7'); gradient.addColorStop(1, '#ec4899'); ctx.fillStyle = gradient; } else { ctx.fillStyle = 'rgba(255,255,255,0.1)'; } this.roundRect(ctx, x, tabY, tabWidth, 36, 18); ctx.fill(); ctx.fillStyle = isActive ? '#ffffff' : 'rgba(255,255,255,0.6)'; ctx.font = isActive ? 'bold 13px sans-serif' : '13px sans-serif'; ctx.textAlign = 'center'; ctx.fillText(tab, x + tabWidth / 2, tabY + 23); this.tabRects.push({ x, y: tabY, width: tabWidth, height: 36, index }); }); } renderContent(ctx) { const contentY = 160; ctx.save(); ctx.beginPath(); ctx.rect(0, contentY, this.screenWidth, this.screenHeight - contentY); ctx.clip(); switch (this.currentTab) { case 0: this.renderRewriteTab(ctx, contentY); break; case 1: this.renderContinueTab(ctx, contentY); break; case 2: this.renderCreateTab(ctx, contentY); break; } ctx.restore(); } renderRewriteTab(ctx, startY) { const y = startY - this.scrollY; const padding = 15; // 说明文字 ctx.fillStyle = 'rgba(255,255,255,0.6)'; ctx.font = '13px sans-serif'; ctx.textAlign = 'center'; ctx.fillText('选择一个已玩过的故事,AI帮你改写结局', this.screenWidth / 2, y + 25); // 快捷标签 ctx.fillStyle = 'rgba(255,255,255,0.8)'; ctx.font = '12px sans-serif'; ctx.textAlign = 'left'; ctx.fillText('热门改写方向:', padding, y + 60); this.renderTags(ctx, this.rewriteTags, padding, y + 75, 'rewrite'); // 选择故事 ctx.fillStyle = 'rgba(255,255,255,0.8)'; ctx.font = '13px sans-serif'; ctx.fillText('选择要改写的故事:', padding, y + 145); this.renderStoryList(ctx, y + 160, 'rewrite'); } renderContinueTab(ctx, startY) { const y = startY - this.scrollY; const padding = 15; ctx.fillStyle = 'rgba(255,255,255,0.6)'; ctx.font = '13px sans-serif'; ctx.textAlign = 'center'; ctx.fillText('选择一个进行中的故事,AI帮你续写剧情', this.screenWidth / 2, y + 25); ctx.fillStyle = 'rgba(255,255,255,0.8)'; ctx.font = '12px sans-serif'; ctx.textAlign = 'left'; ctx.fillText('续写方向:', padding, y + 60); this.renderTags(ctx, this.continueTags, padding, y + 75, 'continue'); ctx.fillStyle = 'rgba(255,255,255,0.8)'; ctx.font = '13px sans-serif'; ctx.fillText('选择要续写的故事:', padding, y + 145); this.renderStoryList(ctx, y + 160, 'continue'); } renderCreateTab(ctx, startY) { const y = startY - this.scrollY; const padding = 15; const inputWidth = this.screenWidth - padding * 2; ctx.fillStyle = 'rgba(255,255,255,0.6)'; ctx.font = '13px sans-serif'; ctx.textAlign = 'center'; ctx.fillText('输入关键词,AI为你创作全新故事', this.screenWidth / 2, y + 25); // 题材选择 ctx.fillStyle = 'rgba(255,255,255,0.8)'; ctx.font = '13px sans-serif'; ctx.textAlign = 'left'; ctx.fillText('选择题材:', padding, y + 60); this.renderTags(ctx, this.genreTags, padding, y + 75, 'genre'); // 关键词输入 ctx.fillText('故事关键词:', padding, y + 145); this.renderInputBox(ctx, padding, y + 160, inputWidth, 45, this.createForm.keywords || '例如:霸总、契约婚姻、追妻火葬场', 'keywords'); // 主角设定 ctx.fillText('主角设定:', padding, y + 225); this.renderInputBox(ctx, padding, y + 240, inputWidth, 45, this.createForm.protagonist || '例如:独立女性设计师', 'protagonist'); // 核心冲突 ctx.fillText('核心冲突:', padding, y + 305); this.renderInputBox(ctx, padding, y + 320, inputWidth, 45, this.createForm.conflict || '例如:假结婚变真爱', 'conflict'); // 开始创作按钮 const btnY = y + 400; const btnGradient = ctx.createLinearGradient(padding, btnY, this.screenWidth - padding, btnY); btnGradient.addColorStop(0, '#a855f7'); btnGradient.addColorStop(1, '#ec4899'); ctx.fillStyle = btnGradient; this.roundRect(ctx, padding, btnY, inputWidth, 50, 25); ctx.fill(); ctx.fillStyle = '#ffffff'; ctx.font = 'bold 16px sans-serif'; ctx.textAlign = 'center'; ctx.fillText('✨ 开始AI创作', this.screenWidth / 2, btnY + 32); this.createBtnRect = { x: padding, y: btnY + this.scrollY, width: inputWidth, height: 50 }; } renderTags(ctx, tags, startX, startY, type) { const tagHeight = 30; const tagGap = 8; let currentX = startX; let currentY = startY; if (!this.tagRects) this.tagRects = {}; this.tagRects[type] = []; tags.forEach((tag, index) => { ctx.font = '12px sans-serif'; const tagWidth = ctx.measureText(tag).width + 20; if (currentX + tagWidth > this.screenWidth - 15) { currentX = startX; currentY += tagHeight + tagGap; } const isSelected = (type === 'genre' && this.createForm.genre === tag) || (type === 'rewrite' && this.selectedRewriteTag === index) || (type === 'continue' && this.selectedContinueTag === index); if (isSelected) { const gradient = ctx.createLinearGradient(currentX, currentY, currentX + tagWidth, currentY); gradient.addColorStop(0, '#a855f7'); gradient.addColorStop(1, '#ec4899'); ctx.fillStyle = gradient; } else { ctx.fillStyle = 'rgba(255,255,255,0.1)'; } this.roundRect(ctx, currentX, currentY, tagWidth, tagHeight, 15); ctx.fill(); if (!isSelected) { ctx.strokeStyle = 'rgba(255,255,255,0.2)'; ctx.lineWidth = 1; this.roundRect(ctx, currentX, currentY, tagWidth, tagHeight, 15); ctx.stroke(); } ctx.fillStyle = '#ffffff'; ctx.textAlign = 'center'; ctx.fillText(tag, currentX + tagWidth / 2, currentY + 20); this.tagRects[type].push({ x: currentX, y: currentY + this.scrollY, width: tagWidth, height: tagHeight, index, value: tag }); currentX += tagWidth + tagGap; }); } renderInputBox(ctx, x, y, width, height, placeholder, field) { ctx.fillStyle = 'rgba(255,255,255,0.08)'; this.roundRect(ctx, x, y, width, height, 12); ctx.fill(); ctx.strokeStyle = 'rgba(255,255,255,0.2)'; ctx.lineWidth = 1; this.roundRect(ctx, x, y, width, height, 12); ctx.stroke(); ctx.font = '13px sans-serif'; ctx.textAlign = 'left'; if (this.createForm[field]) { ctx.fillStyle = '#ffffff'; ctx.fillText(this.createForm[field], x + 15, y + height / 2 + 5); } else { ctx.fillStyle = 'rgba(255,255,255,0.4)'; ctx.fillText(placeholder, x + 15, y + height / 2 + 5); } if (!this.inputRects) this.inputRects = {}; this.inputRects[field] = { x, y: y + this.scrollY, width, height, field }; } renderStoryList(ctx, startY, type) { const padding = 15; const cardHeight = 70; const cardGap = 10; if (!this.storyRects) this.storyRects = {}; this.storyRects[type] = []; if (this.recentStories.length === 0) { ctx.fillStyle = 'rgba(255,255,255,0.4)'; ctx.font = '13px sans-serif'; ctx.textAlign = 'center'; ctx.fillText('暂无游玩记录,去首页体验故事吧', this.screenWidth / 2, startY + 40); return; } this.recentStories.forEach((story, index) => { const y = startY + index * (cardHeight + cardGap); const isSelected = this.selectedStory?.id === story.id; // 卡片背景 if (isSelected) { ctx.fillStyle = 'rgba(168, 85, 247, 0.2)'; } else { ctx.fillStyle = 'rgba(255,255,255,0.06)'; } this.roundRect(ctx, padding, y, this.screenWidth - padding * 2, cardHeight, 12); ctx.fill(); if (isSelected) { ctx.strokeStyle = '#a855f7'; ctx.lineWidth = 2; this.roundRect(ctx, padding, y, this.screenWidth - padding * 2, cardHeight, 12); ctx.stroke(); } // 封面占位 const coverSize = 50; ctx.fillStyle = 'rgba(255,255,255,0.1)'; this.roundRect(ctx, padding + 10, y + 10, coverSize, coverSize, 8); ctx.fill(); // 故事标题 ctx.fillStyle = '#ffffff'; ctx.font = 'bold 14px sans-serif'; ctx.textAlign = 'left'; const title = story.title?.length > 12 ? story.title.substring(0, 12) + '...' : story.title; ctx.fillText(title || '未知故事', padding + 70, y + 28); // 分类和进度 ctx.fillStyle = 'rgba(255,255,255,0.5)'; ctx.font = '11px sans-serif'; ctx.fillText(`${story.category || '未分类'} · ${story.progress || '进行中'}`, padding + 70, y + 50); // 选择按钮 const btnX = this.screenWidth - padding - 60; ctx.fillStyle = isSelected ? '#a855f7' : 'rgba(255,255,255,0.2)'; this.roundRect(ctx, btnX, y + 20, 50, 30, 15); ctx.fill(); ctx.fillStyle = '#ffffff'; ctx.font = '11px sans-serif'; ctx.textAlign = 'center'; ctx.fillText(isSelected ? '已选' : '选择', btnX + 25, y + 40); this.storyRects[type].push({ x: padding, y: y + this.scrollY, width: this.screenWidth - padding * 2, height: cardHeight, story }); }); // 开始按钮 if (this.selectedStory) { const btnY = startY + this.recentStories.length * (cardHeight + cardGap) + 20; const btnGradient = ctx.createLinearGradient(padding, btnY, this.screenWidth - padding, btnY); btnGradient.addColorStop(0, '#a855f7'); btnGradient.addColorStop(1, '#ec4899'); ctx.fillStyle = btnGradient; this.roundRect(ctx, padding, btnY, this.screenWidth - padding * 2, 48, 24); ctx.fill(); ctx.fillStyle = '#ffffff'; ctx.font = 'bold 15px sans-serif'; ctx.textAlign = 'center'; const btnText = type === 'rewrite' ? '✨ 开始AI改写' : '✨ 开始AI续写'; ctx.fillText(btnText, this.screenWidth / 2, btnY + 30); this.actionBtnRect = { x: padding, y: btnY + this.scrollY, width: this.screenWidth - padding * 2, height: 48, type }; } } 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(); } onTouchStart(e) { const touch = e.touches[0]; this.lastTouchY = touch.clientY; this.touchStartY = touch.clientY; this.hasMoved = false; if (touch.clientY > 160) { this.isDragging = true; } } onTouchMove(e) { if (!this.isDragging) return; const touch = e.touches[0]; const deltaY = this.lastTouchY - touch.clientY; if (Math.abs(deltaY) > 3) { this.hasMoved = true; } this.scrollY += deltaY; this.scrollY = Math.max(0, Math.min(this.scrollY, this.maxScrollY)); this.lastTouchY = touch.clientY; } onTouchEnd(e) { this.isDragging = false; if (this.hasMoved) return; const touch = e.changedTouches[0]; const x = touch.clientX; const y = touch.clientY; // 返回按钮 if (y < 60 && x < 80) { this.main.sceneManager.switchScene('home'); return; } // 配额按钮 if (this.quotaBtnRect && this.isInRect(x, y, this.quotaBtnRect)) { this.showQuotaModal(); return; } // Tab切换 if (this.tabRects) { for (const tab of this.tabRects) { if (this.isInRect(x, y, tab)) { if (this.currentTab !== tab.index) { this.currentTab = tab.index; this.scrollY = 0; this.selectedStory = null; this.calculateMaxScroll(); } return; } } } // 调整y坐标(考虑滚动) const scrolledY = y + this.scrollY; // 标签点击 if (this.tagRects) { const tagType = this.currentTab === 0 ? 'rewrite' : this.currentTab === 1 ? 'continue' : 'genre'; const tags = this.tagRects[tagType]; if (tags) { for (const tag of tags) { if (this.isInRect(x, scrolledY, tag)) { this.handleTagSelect(tagType, tag); return; } } } } // 输入框点击(创作Tab) if (this.currentTab === 2 && this.inputRects) { for (const key in this.inputRects) { const rect = this.inputRects[key]; if (this.isInRect(x, scrolledY, rect)) { this.showInputModal(rect.field); return; } } } // 故事列表点击 if (this.currentTab < 2 && this.storyRects) { const type = this.currentTab === 0 ? 'rewrite' : 'continue'; const stories = this.storyRects[type]; if (stories) { for (const rect of stories) { if (this.isInRect(x, scrolledY, rect)) { this.selectedStory = rect.story; return; } } } } // 操作按钮 if (this.actionBtnRect && this.isInRect(x, scrolledY, this.actionBtnRect)) { this.handleAction(this.actionBtnRect.type); return; } // 创作按钮 if (this.currentTab === 2 && this.createBtnRect && this.isInRect(x, scrolledY, this.createBtnRect)) { this.handleCreate(); return; } } isInRect(x, y, rect) { return x >= rect.x && x <= rect.x + rect.width && y >= rect.y && y <= rect.y + rect.height; } handleTagSelect(type, tag) { if (type === 'genre') { this.createForm.genre = tag.value; } else if (type === 'rewrite') { this.selectedRewriteTag = tag.index; } else if (type === 'continue') { this.selectedContinueTag = tag.index; } } showInputModal(field) { const titles = { keywords: '输入故事关键词', protagonist: '输入主角设定', conflict: '输入核心冲突' }; wx.showModal({ title: titles[field], editable: true, placeholderText: '请输入...', content: this.createForm[field] || '', success: (res) => { if (res.confirm && res.content) { this.createForm[field] = res.content; } } }); } showQuotaModal() { wx.showModal({ title: 'AI创作次数', content: `今日剩余${this.quota.daily - this.quota.used}次\n购买次数${this.quota.purchased}次\n\n观看广告可获得1次`, confirmText: '看广告', success: (res) => { if (res.confirm) { this.watchAdForQuota(); } } }); } watchAdForQuota() { wx.showToast({ title: '获得1次AI次数', icon: 'success' }); this.quota.purchased += 1; } handleAction(type) { if (!this.selectedStory) { wx.showToast({ title: '请先选择故事', icon: 'none' }); return; } const remaining = this.quota.daily - this.quota.used + this.quota.purchased; if (remaining <= 0) { this.showQuotaModal(); return; } if (type === 'rewrite') { this.startRewrite(); } else { this.startContinue(); } } startRewrite() { const tag = this.selectedRewriteTag !== undefined ? this.rewriteTags[this.selectedRewriteTag] : ''; wx.showModal({ title: 'AI改写', content: '确定要改写这个故事的结局吗?', editable: true, placeholderText: tag || '输入改写方向(可选)', success: async (res) => { if (res.confirm) { wx.showLoading({ title: 'AI创作中...' }); try { const result = await this.main.storyManager.rewriteEnding( this.selectedStory.id, { name: '当前结局', content: '' }, res.content || tag || '改写结局' ); wx.hideLoading(); if (result) { this.quota.used += 1; this.main.sceneManager.switchScene('story', { storyId: this.selectedStory.id, aiContent: result }); } } catch (e) { wx.hideLoading(); wx.showToast({ title: '创作失败', icon: 'none' }); } } } }); } startContinue() { const tag = this.selectedContinueTag !== undefined ? this.continueTags[this.selectedContinueTag] : ''; wx.showModal({ title: 'AI续写', content: '确定要让AI续写这个故事吗?', editable: true, placeholderText: tag || '输入续写方向(可选)', success: async (res) => { if (res.confirm) { wx.showLoading({ title: 'AI创作中...' }); try { // TODO: 实现续写API const result = await this.main.storyManager.continueStory( this.selectedStory.id, res.content || tag || '续写剧情' ); wx.hideLoading(); if (result) { this.quota.used += 1; this.main.sceneManager.switchScene('story', { storyId: this.selectedStory.id, aiContent: result }); } } catch (e) { wx.hideLoading(); wx.showToast({ title: '创作失败', icon: 'none' }); } } } }); } handleCreate() { if (!this.createForm.genre) { wx.showToast({ title: '请选择题材', icon: 'none' }); return; } if (!this.createForm.keywords) { wx.showToast({ title: '请输入关键词', icon: 'none' }); return; } const remaining = this.quota.daily - this.quota.used + this.quota.purchased; if (remaining < 5) { wx.showModal({ title: '次数不足', content: 'AI创作需要5次配额,当前剩余' + remaining + '次', confirmText: '获取更多', success: (res) => { if (res.confirm) this.showQuotaModal(); } }); return; } wx.showModal({ title: '确认创作', content: `题材:${this.createForm.genre}\n关键词:${this.createForm.keywords}\n\n将消耗5次AI次数`, success: async (res) => { if (res.confirm) { wx.showLoading({ title: 'AI创作中...', mask: true }); try { // TODO: 实现完整创作API const result = await this.main.storyManager.createStory(this.createForm); wx.hideLoading(); if (result) { this.quota.used += 5; wx.showToast({ title: '创作成功!', icon: 'success' }); // 跳转到新故事 setTimeout(() => { this.main.sceneManager.switchScene('story', { storyId: result.storyId }); }, 1500); } } catch (e) { wx.hideLoading(); wx.showToast({ title: '创作失败', icon: 'none' }); } } } }); } }