diff --git a/assets/prefabs/PageHome.prefab b/assets/prefabs/PageHome.prefab index 827a972..974dc1e 100644 --- a/assets/prefabs/PageHome.prefab +++ b/assets/prefabs/PageHome.prefab @@ -5471,7 +5471,7 @@ }, "_contentSize": { "__type__": "cc.Size", - "width": 442.03125, + "width": 461.40625, "height": 75.6 }, "_anchorPoint": { diff --git a/assets/prefabs/PageLevel.prefab b/assets/prefabs/PageLevel.prefab index 148fde1..dcf7fb2 100644 --- a/assets/prefabs/PageLevel.prefab +++ b/assets/prefabs/PageLevel.prefab @@ -9346,8 +9346,8 @@ }, "_lpos": { "__type__": "cc.Vec3", - "x": -8.80499999999995, - "y": 748.5280000000002, + "x": -8.805, + "y": 167.297, "z": 0 }, "_lrot": { diff --git a/assets/prefabs/PageLevel.ts b/assets/prefabs/PageLevel.ts index fa785a3..d4a35ad 100644 --- a/assets/prefabs/PageLevel.ts +++ b/assets/prefabs/PageLevel.ts @@ -1,4 +1,4 @@ -import { _decorator, Node, EditBox, instantiate, Vec3, Button, Label, Sprite, SpriteFrame, AudioClip, AudioSource, Prefab, EffectAsset, UITransform, UIOpacity, ProgressBar, tween, Tween, Color, Layout, sp, view } from 'cc'; +import { _decorator, Node, EditBox, instantiate, Vec3, Button, Label, Sprite, SpriteFrame, AudioClip, AudioSource, Prefab, EffectAsset, UITransform, UIOpacity, ProgressBar, tween, Tween, Color, Layout, Widget, sp, view, resources } from 'cc'; import { BaseView } from 'db://assets/scripts/core/BaseView'; import { ViewManager } from 'db://assets/scripts/core/ViewManager'; import { StaminaManager } from 'db://assets/scripts/utils/StaminaManager'; @@ -41,7 +41,7 @@ export class PageLevel extends BaseView { private static readonly DEFAULT_STAMINA_MAX = 50; /** 答案正确后到弹出通关弹窗之间的停留时间(不论是否有谐音梗都保持一致) */ - private static readonly PASS_MODAL_DELAY_MS = 1000; + private static readonly PASS_MODAL_DELAY_MS = 800; /** 图片2描述默认文案 */ private static readonly DEFAULT_IMAGE2_DESCRIPTION = '这是什么?'; @@ -91,6 +91,12 @@ export class PageLevel extends BaseView { /** 彩带 spine 动画名(一次播放) */ private static readonly CAIDAI_ANIMATION_NAME = 'open'; + /** pose 赞美音效 resources 路径(assets/resources/audios/good.mp3) */ + private static readonly GOOD_AUDIO_RESOURCE_PATH = 'audios/good'; + + /** pose 赞美段提前量:比通关动效 / 成功音效结束点提前 1 秒启动 */ + private static readonly POSE_PRAISE_ADVANCE_SECONDS = 1.5; + /** * 通关赞美 spine(pose 节点)档位:[最小通关数, 动画名],倒序匹配。 * 1-5 关 → "1",6-10 关 → "2",11+ 关 → "3"。 @@ -346,6 +352,9 @@ export class PageLevel extends BaseView { /** PassNode 原始 local position(prefab 摆放位),动画结束后用来回归 */ private _passNodeOriginalPos: Vec3 | null = null; + /** PassNode Widget 初始启用状态;进出场动画期间临时关闭,避免激活首帧回写位置 */ + private _passNodeWidgetOriginalEnabled: boolean | null = null; + /** 通关页所用「已通关数量」(业务数据,给成就体系展示用) */ private _passCompletedLevelCount: number | null = null; @@ -358,6 +367,18 @@ export class PageLevel extends BaseView { /** pose Spine 隐藏延时定时器(setTimeout 句柄);切关 / 关页时需清理避免穿屏触发 */ private _poseHideTimer: ReturnType | null = null; + /** pose 赞美延迟播放定时器;等待通关动画与成功音效结束后再触发 */ + private _posePraiseDelayTimer: ReturnType | null = null; + + /** pose 赞美延迟播放序号,用于取消 resources.load 异步回调里的过期播放 */ + private _posePraiseSequenceId: number = 0; + + /** good.mp3 加载缓存 */ + private _goodAudioClip: AudioClip | null = null; + + /** good.mp3 加载中的 Promise,避免重复请求 resources */ + private _goodAudioLoadPromise: Promise | null = null; + /** 通关页动画起点(通关前)的已通关数量;为 null 表示不播跨称号过渡 */ private _passPreviousCompletedLevelCount: number | null = null; @@ -703,10 +724,8 @@ export class PageLevel extends BaseView { // 设置图片描述 this.setImageDescriptions(config.image1Description, config.image2Description); - // 设置关卡标题 - this.updateTitleLevelLabel(); - this.updatePkLevelProgressLabel(); - this.updatePkNextLevelButtonText(); + // 设置关卡标题与主线 / 分享模式常驻 UI 可见性 + this._refreshModeUI(); // 隐藏包袱答案,通关后再按 punchline 展示 this.hidePunchline(); @@ -1178,10 +1197,36 @@ export class PageLevel extends BaseView { if (this._isSubmittingShareResult) { return; } + + // 通关页已展示:分享模式下 passNextLevelButton 被整体隐藏(见 _setupPassNodeContent), + // pkNextLevelButton 同时承担「通关后下一题」的入口。 + // 已经答对的题不需要二次确认(确认弹窗是「未答对就跳过」时的兜底),直接切关。 + // 注意切关顺序:先在 PassNode 仍覆盖屏幕时切换到下一关内容(分享模式题目均预加载, + // _enterAndInitLevel 几乎瞬时完成),再播放 PassNode 滑出动画。否则 _hidePassNode 先滑出 → + // 异步加载下一关 → _applyLevelConfig,期间玩家会看到上一题的提示 / 谐音梗 / 已填答案闪现。 + // 用 _isPassNodeAnimating 整段加锁防止异步过程中再次点击。 + if (this._isPassNodeShown) { + if (this._isPassNodeAnimating) return; + + this._isPassNodeAnimating = true; + AudioManager.instance.playButtonClick(); + void (async () => { + try { + await this.goToNextLevel(); + await this._hidePassNode(); + } catch (err) { + console.error('[PageLevel] 分享模式切换下一题失败:', err); + this._isPassNodeAnimating = false; + } + })(); + return; + } + if (this._isTransitioning) { return; } + // 未通关就点下一题 = 跳过当前题,弹二次确认避免误触 this.playClickSound(); this._showShareNextConfirmModal(() => { this._recordCurrentShareSubmission(); @@ -2135,11 +2180,17 @@ export class PageLevel extends BaseView { this._setupPassNodeContent(); this._bindPassNodeEvents(); - // 启动彩带 + 滑入动画 + 淡出底部UI + 通关音效 + 赞美 spine - this._playCaidai(); - this._playPosePraise(this._sessionPassCount); - this._playPassNodeShowAnimation(); - this.playSuccessSound(); + // 先让 PassNode 完整入场,再启动通关动效和成功音效;pose 赞美段延后到它们结束后播放。 + this._playPassNodeShowAnimation(() => { + const caidaiDuration = this._playCaidai(); + this.playSuccessSound(); + if (this._isShareMode) { + this._playPosePraise(this._sessionPassCount); + return; + } + + this._schedulePosePraiseAfterPassEffects(this._sessionPassCount, caidaiDuration); + }); console.log('[PageLevel] 显示通关页 PassNode'); } @@ -2186,6 +2237,16 @@ export class PageLevel extends BaseView { } // NextLevelButton 文案 + // 分享模式下「下一题 / 提交答案」入口由 pkNextLevelButton(黄色 PK 按钮)承担, + // PassNode 内部的 passNextLevelButton / passShareButton 整体隐藏避免出现重复入口, + // 但通关页的滑入 / 彩带 / 赞美等动画样式保持一致。 + if (this.passNextLevelButton) { + this.passNextLevelButton.active = !this._isShareMode; + } + if (this.passShareButton) { + this.passShareButton.active = !this._isShareMode; + } + const isFinalShareSubmit = this._isShareMode && this._isFinalShareLevel(); const nextLabel = this.passNextLevelButton?.getChildByName('Label')?.getComponent(Label); if (nextLabel) { @@ -2193,6 +2254,10 @@ export class PageLevel extends BaseView { ? PageLevel.SHARE_SUBMIT_BUTTON_TEXT : '下一关'; } + + // 分享模式下 PassNode 展示后,让 pkNextLevelButton 文案与最终态保持同步 + // (SHARE_SUBMIT_BUTTON_TEXT vs SHARE_NEXT_BUTTON_TEXT),由 updatePkNextLevelButtonText 统一处理 + this.updatePkNextLevelButtonText(); } private _ensureTitleAnimator(): AchievementTitleAnimator { @@ -2213,6 +2278,8 @@ export class PageLevel extends BaseView { private _initPassNodeState(): void { if (this.passNode) { this._passNodeOriginalPos = this.passNode.position.clone(); + const widget = this.passNode.getComponent(Widget); + this._passNodeWidgetOriginalEnabled = widget?.enabled ?? null; this.passNode.active = false; } if (this.caidaiNode) { @@ -2224,6 +2291,7 @@ export class PageLevel extends BaseView { if (this.poseNode) { this.poseNode.active = false; } + this._clearPosePraiseDelayTimer(); this._clearPoseHideTimer(); this._isPassNodeShown = false; @@ -2231,7 +2299,7 @@ export class PageLevel extends BaseView { } /** PassNode 进入动画:底部两层淡出 + PassNode 从屏幕左侧滑入 */ - private _playPassNodeShowAnimation(): void { + private _playPassNodeShowAnimation(onShown?: () => void): void { if (!this.passNode) return; this._isPassNodeAnimating = true; @@ -2250,8 +2318,9 @@ export class PageLevel extends BaseView { const startX = originalPos.x - screenWidth; Tween.stopAllByTarget(this.passNode); - this.passNode.active = true; + this._setPassNodeWidgetEnabledForAnimation(false); this.passNode.setPosition(startX, originalPos.y, originalPos.z); + this.passNode.active = true; tween(this.passNode) .to( @@ -2260,7 +2329,9 @@ export class PageLevel extends BaseView { { easing: 'cubicOut' }, ) .call(() => { + this._setPassNodeWidgetEnabledForAnimation(true); this._isPassNodeAnimating = false; + onShown?.(); }) .start(); } @@ -2269,7 +2340,7 @@ export class PageLevel extends BaseView { * PassNode 退出(用户点下一关时调用):滑出 PassNode + 底部两层淡入 * 返回 Promise,外部链路通常 await 后再调 goToNextLevel */ - private _hidePassNode(): Promise { + private _hidePassNode(restoreBottomLayers: boolean = true): Promise { return new Promise((resolve) => { if (!this.passNode || !this._isPassNodeShown) { resolve(); @@ -2279,7 +2350,9 @@ export class PageLevel extends BaseView { this._isPassNodeAnimating = true; // 底部两层淡入恢复 - this._fadeInBottomLayers(); + if (restoreBottomLayers) { + this._fadeInBottomLayers(); + } // 滑出 PassNode(向左滑出屏幕) const screenWidth = view.getVisibleSize().width; @@ -2287,6 +2360,7 @@ export class PageLevel extends BaseView { const exitX = originalPos.x - screenWidth; Tween.stopAllByTarget(this.passNode); + this._setPassNodeWidgetEnabledForAnimation(false); tween(this.passNode) .to( PageLevel.PASS_NODE_SLIDE_DURATION, @@ -2308,6 +2382,7 @@ export class PageLevel extends BaseView { if (this._passNodeOriginalPos) { this.passNode.setPosition(this._passNodeOriginalPos); } + this._setPassNodeWidgetEnabledForAnimation(true); } // 关掉彩带 @@ -2319,6 +2394,7 @@ export class PageLevel extends BaseView { if (this.poseNode) { this.poseNode.active = false; } + this._clearPosePraiseDelayTimer(); this._clearPoseHideTimer(); this._unbindPassNodeEvents(); @@ -2339,6 +2415,7 @@ export class PageLevel extends BaseView { if (this._passNodeOriginalPos) { this.passNode.setPosition(this._passNodeOriginalPos); } + this._setPassNodeWidgetEnabledForAnimation(true); } if (this.caidaiNode) { @@ -2349,6 +2426,7 @@ export class PageLevel extends BaseView { if (this.poseNode) { this.poseNode.active = false; } + this._clearPosePraiseDelayTimer(); this._clearPoseHideTimer(); this._restoreBottomLayersImmediate(); @@ -2358,6 +2436,19 @@ export class PageLevel extends BaseView { this._isPassNodeAnimating = false; } + private _setPassNodeWidgetEnabledForAnimation(enabled: boolean): void { + const widget = this.passNode?.getComponent(Widget); + if (!widget) { + return; + } + + if (this._passNodeWidgetOriginalEnabled === null) { + this._passNodeWidgetOriginalEnabled = widget.enabled; + } + + widget.enabled = enabled ? this._passNodeWidgetOriginalEnabled : false; + } + /** PassNode 事件绑定(NextLevel + Share) */ private _bindPassNodeEvents(): void { // 防御:先解绑,避免重复绑定 @@ -2390,11 +2481,38 @@ export class PageLevel extends BaseView { AudioManager.instance.playButtonClick(); - // 普通模式 _showShareNextConfirmModal 内部首行就 onConfirm(),所以两条路统一 - this._showShareNextConfirmModal(async () => { - await this._hidePassNode(); - void this.goToNextLevel(); - }); + if (this._isShareMode) { + // 分享模式题目已预加载,继续保持“先换题、再退场”,避免露出上一题内容。 + this._showShareNextConfirmModal(() => { + if (this._isPassNodeAnimating) return; + + this._isPassNodeAnimating = true; + void (async () => { + try { + await this.goToNextLevel(); + await this._hidePassNode(); + } catch (err) { + console.error('[PageLevel] 分享模式切换下一题失败:', err); + this._isPassNodeAnimating = false; + } + })(); + }); + return; + } + + void (async () => { + try { + // 主线模式需要等 PassNode 退场动画结束后再进入下一关; + // 下一关会走 enter 接口和图片刷新,提前切换会在动画过程中露出新题。 + await this._hidePassNode(false); + await this.goToNextLevel(); + this._fadeInBottomLayers(); + } catch (err) { + console.error('[PageLevel] 主线模式切换下一关失败:', err); + this._isPassNodeAnimating = false; + this._fadeInBottomLayers(); + } + })(); } /** @@ -2409,13 +2527,73 @@ export class PageLevel extends BaseView { }); } - /** 启动彩带 spine 动画 "open",单次播放 */ - private _playCaidai(): void { - if (!this.caidaiNode || !this.caidaiSkeleton) return; + /** 启动彩带 spine 动画 "open",单次播放,返回动画时长(秒)用于串联后续赞美段 */ + private _playCaidai(): number { + if (!this.caidaiNode || !this.caidaiSkeleton) return 0; this.caidaiNode.active = true; // setAnimation 三参:trackIndex, name, loop - this.caidaiSkeleton.setAnimation(0, PageLevel.CAIDAI_ANIMATION_NAME, false); + const trackEntry = this.caidaiSkeleton.setAnimation(0, PageLevel.CAIDAI_ANIMATION_NAME, false); + if (!trackEntry) return 0; + + return Math.max(0, trackEntry.animationEnd - trackEntry.animationStart); + } + + /** + * 通关页主氛围顺序: + * 1) PassNode 入场完成后播放彩带 + successAudio + * 2) 等彩带动画和 successAudio 都结束 + * 3) 再播放 pose 赞美 Spine + good.mp3 + */ + private _schedulePosePraiseAfterPassEffects(count: number, caidaiDuration: number): void { + this._clearPosePraiseDelayTimer(); + + const successDuration = this.successAudio?.getDuration() ?? 0; + const delaySeconds = Math.max(0, Math.max(caidaiDuration, successDuration) - PageLevel.POSE_PRAISE_ADVANCE_SECONDS); + const delayMs = delaySeconds * 1000; + const sequenceId = ++this._posePraiseSequenceId; + + void this._loadGoodAudioClip(); + + this._posePraiseDelayTimer = setTimeout(() => { + this._posePraiseDelayTimer = null; + void this._playPosePraiseWithGoodAudio(count, sequenceId); + }, delayMs); + } + + private async _playPosePraiseWithGoodAudio(count: number, sequenceId: number): Promise { + const goodAudio = await this._loadGoodAudioClip(); + if (sequenceId !== this._posePraiseSequenceId || !this._isPassNodeShown || !this.node.isValid) { + return; + } + + this._playPosePraise(count); + this.playSound(goodAudio); + } + + private _loadGoodAudioClip(): Promise { + if (this._goodAudioClip) { + return Promise.resolve(this._goodAudioClip); + } + if (this._goodAudioLoadPromise) { + return this._goodAudioLoadPromise; + } + + this._goodAudioLoadPromise = new Promise((resolve) => { + resources.load(PageLevel.GOOD_AUDIO_RESOURCE_PATH, AudioClip, (err, clip) => { + if (err) { + console.warn('[PageLevel] good.mp3 音效加载失败:', err); + this._goodAudioLoadPromise = null; + resolve(null); + return; + } + + this._goodAudioClip = clip; + resolve(clip); + }); + }); + + return this._goodAudioLoadPromise; } /** @@ -2447,6 +2625,15 @@ export class PageLevel extends BaseView { } } + /** 清理尚未触发的 pose 赞美延迟播放,避免快速切关后串到下一题。 */ + private _clearPosePraiseDelayTimer(): void { + this._posePraiseSequenceId++; + if (this._posePraiseDelayTimer !== null) { + clearTimeout(this._posePraiseDelayTimer); + this._posePraiseDelayTimer = null; + } + } + /** 底部两层淡出(透明度 → 0),完成后 active = false */ private _fadeOutBottomLayers(): void { for (const layer of [this.bottomLayoutNode, this.tipsLayout]) { @@ -2476,6 +2663,13 @@ export class PageLevel extends BaseView { for (const layer of [this.bottomLayoutNode, this.tipsLayout]) { if (!layer) continue; + // 分享模式下 bottomLayoutNode 由 _refreshModeUI 强制隐藏(pkNextLevelButton 接管该位置), + // 不能在 _fadeInBottomLayers 里偷偷打开它,否则两个「下一题」入口又会同时出现。 + // tipsLayout 在两种模式下都需要恢复,这里只跳过 bottomLayoutNode。 + if (layer === this.bottomLayoutNode && this._isShareMode) { + continue; + } + const opacity = this._ensureUIOpacity(layer); Tween.stopAllByTarget(opacity); layer.active = true; diff --git a/assets/prefabs/PagePKEnd.prefab b/assets/prefabs/PagePKEnd.prefab index 508c2c9..c04fda8 100644 --- a/assets/prefabs/PagePKEnd.prefab +++ b/assets/prefabs/PagePKEnd.prefab @@ -2241,6 +2241,8 @@ "__id__": 0 }, "fileId": "90w8HRdbBPLYFqBkhPhWfM", + "instance": null, + "targetOverrides": null, "nestedPrefabInstanceRoots": null }, { @@ -2254,9 +2256,6 @@ "_children": [ { "__id__": 93 - }, - { - "__id__": 147 } ], "_active": true, @@ -2274,7 +2273,7 @@ "_lpos": { "__type__": "cc.Vec3", "x": -8.201, - "y": 492.399, + "y": -183.147, "z": 0 }, "_lrot": { @@ -2311,27 +2310,30 @@ "_children": [ { "__id__": 94 + }, + { + "__id__": 140 } ], "_active": true, "_components": [ { - "__id__": 140 + "__id__": 148 }, { - "__id__": 142 + "__id__": 150 }, { - "__id__": 144 + "__id__": 152 } ], "_prefab": { - "__id__": 146 + "__id__": 154 }, "_lpos": { "__type__": "cc.Vec3", "x": 0, - "y": -80.522, + "y": 46.995, "z": 0 }, "_lrot": { @@ -2382,7 +2384,7 @@ "_lpos": { "__type__": "cc.Vec3", "x": -10, - "y": 125, + "y": 540, "z": 0 }, "_lrot": { @@ -3459,7 +3461,7 @@ "_contentSize": { "__type__": "cc.Size", "width": 780, - "height": 400 + "height": 1080 }, "_anchorPoint": { "__type__": "cc.Vec2", @@ -3485,6 +3487,181 @@ "targetOverrides": null, "nestedPrefabInstanceRoots": null }, + { + "__type__": "cc.Node", + "_name": "ScrolViewMask", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 93 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 141 + }, + { + "__id__": 143 + }, + { + "__id__": 145 + } + ], + "_prefab": { + "__id__": 147 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 9.242999999999938, + "y": -517.9897285399853, + "z": 0 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0, + "y": 0, + "z": 1, + "w": 6.123233995736766e-17 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 0.7336757153338225, + "y": 0.7336757153338225, + "z": 0.7336757153338225 + }, + "_mobility": 0, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 180, + "y": 180, + "z": 7.016709298534876e-15 + }, + "_id": "" + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 140 + }, + "_enabled": true, + "__prefab": { + "__id__": 142 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 1444.78, + "height": 60 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "57cb61nstNg7YGomw8vXcp" + }, + { + "__type__": "cc.Sprite", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 140 + }, + "_enabled": true, + "__prefab": { + "__id__": 144 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 225, + "g": 245, + "b": 197, + "a": 255 + }, + "_spriteFrame": { + "__uuid__": "faab3d46-e885-4c46-8f19-9f872e7d6973@f9941", + "__expectedType__": "cc.SpriteFrame" + }, + "_type": 0, + "_fillType": 0, + "_sizeMode": 0, + "_fillCenter": { + "__type__": "cc.Vec2", + "x": 0, + "y": 0 + }, + "_fillStart": 0, + "_fillRange": 0, + "_isTrimmedMode": true, + "_useGrayscale": false, + "_atlas": null, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "d5kG67QW9Nqplq9kKPqg4X" + }, + { + "__type__": "cc.Widget", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 140 + }, + "_enabled": true, + "__prefab": { + "__id__": 146 + }, + "_alignFlags": 44, + "_target": null, + "_left": -130.757, + "_right": -149.243, + "_top": 0, + "_bottom": 0, + "_horizontalCenter": 0, + "_verticalCenter": 0, + "_isAbsLeft": true, + "_isAbsRight": true, + "_isAbsTop": true, + "_isAbsBottom": true, + "_isAbsHorizontalCenter": true, + "_isAbsVerticalCenter": true, + "_originalWidth": 1080, + "_originalHeight": 0, + "_alignMode": 2, + "_lockFlags": 4, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "60ik9u5ftG4oQNCowOqvJi" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "36lE6eex1I0Jo9+fLrBGoc", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, { "__type__": "cc.UITransform", "_name": "", @@ -3495,12 +3672,12 @@ }, "_enabled": true, "__prefab": { - "__id__": 141 + "__id__": 149 }, "_contentSize": { "__type__": "cc.Size", "width": 780, - "height": 400 + "height": 1080 }, "_anchorPoint": { "__type__": "cc.Vec2", @@ -3523,7 +3700,7 @@ }, "_enabled": true, "__prefab": { - "__id__": 143 + "__id__": 151 }, "_type": 0, "_inverted": false, @@ -3545,7 +3722,7 @@ }, "_enabled": true, "__prefab": { - "__id__": 145 + "__id__": 153 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -3594,181 +3771,6 @@ "targetOverrides": null, "nestedPrefabInstanceRoots": null }, - { - "__type__": "cc.Node", - "_name": "ScrolViewMask", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 92 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 148 - }, - { - "__id__": 150 - }, - { - "__id__": 152 - } - ], - "_prefab": { - "__id__": 154 - }, - "_lpos": { - "__type__": "cc.Vec3", - "x": 9.242999999999938, - "y": -173.667, - "z": 0 - }, - "_lrot": { - "__type__": "cc.Quat", - "x": 0, - "y": 0, - "z": 1, - "w": 6.123233995736766e-17 - }, - "_lscale": { - "__type__": "cc.Vec3", - "x": 1, - "y": 1, - "z": 1 - }, - "_mobility": 0, - "_layer": 1073741824, - "_euler": { - "__type__": "cc.Vec3", - "x": 0, - "y": 0, - "z": 180 - }, - "_id": "" - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 147 - }, - "_enabled": true, - "__prefab": { - "__id__": 149 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 1080, - "height": 60 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "57cb61nstNg7YGomw8vXcp" - }, - { - "__type__": "cc.Sprite", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 147 - }, - "_enabled": true, - "__prefab": { - "__id__": 151 - }, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 225, - "g": 245, - "b": 197, - "a": 255 - }, - "_spriteFrame": { - "__uuid__": "faab3d46-e885-4c46-8f19-9f872e7d6973@f9941", - "__expectedType__": "cc.SpriteFrame" - }, - "_type": 0, - "_fillType": 0, - "_sizeMode": 0, - "_fillCenter": { - "__type__": "cc.Vec2", - "x": 0, - "y": 0 - }, - "_fillStart": 0, - "_fillRange": 0, - "_isTrimmedMode": true, - "_useGrayscale": false, - "_atlas": null, - "_id": "" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "d5kG67QW9Nqplq9kKPqg4X" - }, - { - "__type__": "cc.Widget", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 147 - }, - "_enabled": true, - "__prefab": { - "__id__": 153 - }, - "_alignFlags": 40, - "_target": null, - "_left": -130.757, - "_right": -149.243, - "_top": 0, - "_bottom": 0, - "_horizontalCenter": 0, - "_verticalCenter": 0, - "_isAbsLeft": true, - "_isAbsRight": true, - "_isAbsTop": true, - "_isAbsBottom": true, - "_isAbsHorizontalCenter": true, - "_isAbsVerticalCenter": true, - "_originalWidth": 1080, - "_originalHeight": 0, - "_alignMode": 2, - "_lockFlags": 0, - "_id": "" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "60ik9u5ftG4oQNCowOqvJi" - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "36lE6eex1I0Jo9+fLrBGoc", - "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null - }, { "__type__": "cc.UITransform", "_name": "", @@ -3784,7 +3786,7 @@ "_contentSize": { "__type__": "cc.Size", "width": 800, - "height": 400 + "height": 1600 }, "_anchorPoint": { "__type__": "cc.Vec2", @@ -3837,6 +3839,8 @@ "__id__": 0 }, "fileId": "bdqvu61fVFTIU1TQqs/qES", + "instance": null, + "targetOverrides": null, "nestedPrefabInstanceRoots": null }, { diff --git a/assets/prefabs/PageWriteLevels.prefab b/assets/prefabs/PageWriteLevels.prefab index f5548d2..5ad1128 100644 --- a/assets/prefabs/PageWriteLevels.prefab +++ b/assets/prefabs/PageWriteLevels.prefab @@ -5396,6 +5396,10 @@ "dataBtn": { "__id__": 34 }, + "commonModalPrefab": { + "__uuid__": "5379669e-7cd7-45b6-9dd3-4a021730b23e", + "__expectedType__": "cc.Prefab" + }, "roundedSpriteEffect": { "__uuid__": "f0080a34-1786-4547-8d81-d89cc517b63e", "__expectedType__": "cc.EffectAsset" @@ -5483,4 +5487,4 @@ "instance": null, "targetOverrides": null } -] \ No newline at end of file +] diff --git a/assets/prefabs/PageWriteLevels.ts b/assets/prefabs/PageWriteLevels.ts index 5ea46b4..b21917a 100644 --- a/assets/prefabs/PageWriteLevels.ts +++ b/assets/prefabs/PageWriteLevels.ts @@ -1,6 +1,7 @@ -import { _decorator, Node, Button, Sprite, Label, Toggle, ScrollView, EditBox, instantiate, UITransform, Vec2, EventTouch, EffectAsset } from 'cc'; +import { _decorator, Node, Button, Sprite, Label, Toggle, ScrollView, EditBox, instantiate, UITransform, Vec2, EventTouch, EffectAsset, Prefab } from 'cc'; import { BaseView } from 'db://assets/scripts/core/BaseView'; import { ViewManager } from 'db://assets/scripts/core/ViewManager'; +import { CommonModal } from 'db://assets/prefabs/CommonModal'; import { CompletedLevelsManager } from 'db://assets/scripts/utils/CompletedLevelsManager'; import { ToastManager } from 'db://assets/scripts/utils/ToastManager'; import { ShareManager } from 'db://assets/scripts/utils/ShareManager'; @@ -30,6 +31,7 @@ const LAYOUT_CONFIG = { CENTER_ROWS: 2, VIEW_WIDTH: 900, VIEW_HEIGHT: 1300, + LIST_BOTTOM_GAP_TO_TITLE: 54, }; /** 必须选择的关卡数量 */ @@ -67,6 +69,9 @@ export class PageWriteLevels extends BaseView { @property({ type: EffectAsset, tooltip: '关卡封面圆角材质 EffectAsset' }) roundedSpriteEffect: EffectAsset | null = null; + @property({ type: Prefab, tooltip: '通用弹窗预制体' }) + commonModalPrefab: Prefab | null = null; + @property({ tooltip: '关卡封面圆角半径比例(相对于短边,0-0.5)' }) coverCornerRadius: number = 0.1; @@ -86,6 +91,7 @@ export class PageWriteLevels extends BaseView { console.log('[PageWriteLevels] onViewLoad'); this._initButtons(); this._initScrollView(); + this._resizeScrollViewport(); this._updateSelectionUI(); } @@ -132,12 +138,51 @@ export class PageWriteLevels extends BaseView { onViewShow(): void { console.log('[PageWriteLevels] onViewShow'); + this._resizeScrollViewport(); + this._updateContentSize(); + // 仅首次初始化列表,从预览页返回时保留选中状态 if (this._itemNodes.length === 0) { void this._initLevelList(); } } + private _resizeScrollViewport(): void { + if (!this.scrollView || !this._viewTransform || !this.shareTitleEditBox) { + return; + } + + const rootTransform = this.node.getComponent(UITransform); + const scrollTransform = this.scrollView.getComponent(UITransform); + const scrollWidget = this.scrollView.getComponent('cc.Widget') as any; + const shareTitleTransform = this.shareTitleEditBox.getComponent(UITransform); + const shareTitleWidget = this.shareTitleEditBox.getComponent('cc.Widget') as any; + const bottomMaskNode = this.scrollView.getChildByName('ScrolViewMask'); + + if (!rootTransform || !scrollTransform || !shareTitleTransform || !scrollWidget || !shareTitleWidget) { + return; + } + + const topInset = Number(scrollWidget.top ?? 0); + const shareBottomInset = Number(shareTitleWidget.bottom ?? 0); + const shareTitleHeight = shareTitleTransform.height * Math.abs(this.shareTitleEditBox.scale.y); + const nextHeight = Math.max( + LAYOUT_CONFIG.VIEW_HEIGHT, + rootTransform.height - topInset - shareBottomInset - shareTitleHeight - LAYOUT_CONFIG.LIST_BOTTOM_GAP_TO_TITLE, + ); + + scrollTransform.setContentSize(scrollTransform.width, nextHeight); + this._viewTransform.setContentSize(this._viewTransform.width, nextHeight); + + if (bottomMaskNode) { + bottomMaskNode.setPosition( + bottomMaskNode.position.x, + -(nextHeight / 2) + 30, + bottomMaskNode.position.z, + ); + } + } + private async _initLevelList(): Promise { this._clearList(); @@ -474,11 +519,11 @@ export class PageWriteLevels extends BaseView { } } - // 更新 CompleteButton 和 PreviewButton 的可用状态 + // 预览与分享数量不足时也要允许点击,统一弹出提示弹窗。 if (this.completeBtn) { const btn = this.completeBtn.getComponent(Button); if (btn) { - btn.interactable = isFull; + btn.interactable = true; } } if (this.previewBtn) { @@ -501,18 +546,31 @@ export class PageWriteLevels extends BaseView { } /** - * 校验是否已选满关卡,未满则 Toast 提示 + * 校验是否已选满关卡,未满则弹出统一提示弹窗 * @returns true 表示校验通过 */ private _validateSelection(): boolean { if (this._selectedIndices.size < MAX_SELECTION) { - const remaining = MAX_SELECTION - this._selectedIndices.size; - ToastManager.instance.show(`还需选择${remaining}个关卡`); + this._showSelectionRequiredModal(); return false; } return true; } + private _showSelectionRequiredModal(): void { + if (!this.commonModalPrefab) { + console.warn('[PageWriteLevels] commonModalPrefab 未设置,回退为 Toast 提示'); + ToastManager.instance.show(`请选择${MAX_SELECTION}个关卡后再预览或分享`); + return; + } + + CommonModal.show(this.commonModalPrefab, { + title: '提示', + content: `要选择${MAX_SELECTION}个关卡才能分享和预览`, + buttonConfirm: '知道了', + }); + } + private _onPreviewClick(): void { AudioManager.instance.playButtonClick(); if (!this._validateSelection()) return; diff --git a/assets/resources/audios/good.mp3 b/assets/resources/audios/good.mp3 new file mode 100644 index 0000000..3df944b Binary files /dev/null and b/assets/resources/audios/good.mp3 differ diff --git a/assets/resources/audios/good.mp3.meta b/assets/resources/audios/good.mp3.meta new file mode 100644 index 0000000..1a44ce2 --- /dev/null +++ b/assets/resources/audios/good.mp3.meta @@ -0,0 +1,14 @@ +{ + "ver": "1.0.0", + "importer": "audio-clip", + "imported": true, + "uuid": "f1b26185-7493-4c10-b45a-e35ecc86c507", + "files": [ + ".json", + ".mp3" + ], + "subMetas": {}, + "userData": { + "downloadMode": 0 + } +}