@@ -20,6 +20,11 @@ export default class EndingScene extends BaseScene {
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 ;
@@ -85,6 +90,10 @@ export default class EndingScene extends BaseScene {
if ( this . showRewritePanel ) {
this . renderRewritePanel ( ctx ) ;
}
// AI续写面板
if ( this . showContinuePanel ) {
this . renderContinuePanel ( ctx ) ;
}
}
renderBackground ( ctx ) {
@@ -257,15 +266,17 @@ export default class EndingScene extends BaseScene {
const buttonHeight = 38 ;
const buttonMargin = 8 ;
const startY = this . screenHeight - 220 ;
const buttonWidth = ( this . screenWidth - padding * 2 - buttonMargin ) / 2 ;
// AI改写按钮(带配额提示 )
// AI改写按钮和AI续写按钮( 第一行 )
const remaining = this . aiQuota . daily - this . aiQuota . used + this . aiQuota . purchased ;
const ai BtnText = remaining > 0 ? '✨ AI改写结局 ' : '⚠️ 次数不足' ;
this . renderGradientButton ( ctx , padding , startY , this . screenWidth - padd ing * 2 , buttonHeight , aiBtnText , [ '#a855f7' , '#ec4899' ] ) ;
const rewrite BtnText = remaining > 0 ? '✨ AI改写' : '⚠️ 次数不足' ;
const continueBtnText = remain ing > 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 ;
const buttonWidth = ( this . screenWidth - padding * 2 - buttonMargin ) / 2 ;
this . renderGradientButton ( ctx , padding , row2Y , buttonWidth , buttonHeight , '分享结局' , [ '#ff6b6b' , '#ffd700' ] ) ;
// 章节选择按钮
@@ -503,6 +514,183 @@ export default class EndingScene extends BaseScene {
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 ( ) ;
@@ -610,6 +798,12 @@ export default class EndingScene extends BaseScene {
return ;
}
// 如果续写面板打开,优先处理
if ( this . showContinuePanel ) {
this . handleContinuePanelTouch ( x , y ) ;
return ;
}
if ( ! this . showButtons ) return ;
const padding = 15 ;
@@ -618,12 +812,18 @@ export default class EndingScene extends BaseScene {
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 ) ) {
// 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 ) ) {
@@ -696,6 +896,20 @@ export default class EndingScene extends BaseScene {
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 ) {
@@ -734,6 +948,44 @@ export default class EndingScene extends BaseScene {
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 : '输入改写想法' ,
@@ -749,6 +1001,21 @@ export default class EndingScene extends BaseScene {
} ) ;
}
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 ;
@@ -758,68 +1025,94 @@ export default class EndingScene extends BaseScene {
}
this . isRewriting = true ;
this . rewriteProgress = 0 ;
// 显示加载动画
wx . showLoading ( {
title : 'AI创作 中...' ,
title : '提交 中...' ,
mask : true
} ) ;
// 模拟进度条效果
const progressInterval = setInterval ( ( ) => {
this . rewriteProgress += Math . random ( ) * 20 ;
if ( this . rewriteProgress > 90 ) this . rewriteProgress = 90 ;
} , 500 ) ;
try {
const result = await this . main . story Manager. rewriteEnding (
const userId = this . main . user Manager. userId || 1 ;
const result = await this . main . storyManager . rewriteEndingAsync (
this . storyId ,
this . ending ,
prompt
prompt ,
userId
) ;
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 ;
if ( result && result . draftId ) {
// 扣除配额
this . aiQuota . used += 1 ;
// 成功提示
wx . showToast ( {
title : '改写 成功! ' ,
i con: 'success ' ,
duration : 1500
// 提交 成功提示
wx . showModal ( {
title : '提交 成功' ,
content : 'AI正在后台生成新结局, 完成后会通知您。\n您可以在草稿箱中查看。 ' ,
showCancel : false ,
confirmText : '知道了'
} ) ;
// 延迟跳转到故事场景播放新内容
setTimeout ( ( ) => {
this . main . sceneManager . switchScene ( 'story' , {
storyId : this . storyId ,
aiContent : result
} ) ;
} , 1500 ) ;
} else {
wx . showToast ( { title : '改写 失败,请重试' , icon : 'none' } ) ;
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 ;
}
}
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 : '知道了'
} ) ;
} else {
wx . showToast ( { title : '提交失败,请重试' , icon : 'none' } ) ;
}
} catch ( error ) {
wx . hideLoading ( ) ;
console . error ( '续写失败:' , error ) ;
wx . showToast ( { title : error . message || '网络错误' , icon : 'none' } ) ;
} finally {
this . isRewriting = false ;
}
}