feat: 优化通关效果
This commit is contained in:
@@ -5471,7 +5471,7 @@
|
||||
},
|
||||
"_contentSize": {
|
||||
"__type__": "cc.Size",
|
||||
"width": 442.03125,
|
||||
"width": 461.40625,
|
||||
"height": 75.6
|
||||
},
|
||||
"_anchorPoint": {
|
||||
|
||||
@@ -9346,8 +9346,8 @@
|
||||
},
|
||||
"_lpos": {
|
||||
"__type__": "cc.Vec3",
|
||||
"x": -8.80499999999995,
|
||||
"y": 748.5280000000002,
|
||||
"x": -8.805,
|
||||
"y": 167.297,
|
||||
"z": 0
|
||||
},
|
||||
"_lrot": {
|
||||
|
||||
@@ -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<typeof setTimeout> | null = null;
|
||||
|
||||
/** pose 赞美延迟播放定时器;等待通关动画与成功音效结束后再触发 */
|
||||
private _posePraiseDelayTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
/** pose 赞美延迟播放序号,用于取消 resources.load 异步回调里的过期播放 */
|
||||
private _posePraiseSequenceId: number = 0;
|
||||
|
||||
/** good.mp3 加载缓存 */
|
||||
private _goodAudioClip: AudioClip | null = null;
|
||||
|
||||
/** good.mp3 加载中的 Promise,避免重复请求 resources */
|
||||
private _goodAudioLoadPromise: Promise<AudioClip | null> | 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<void> {
|
||||
private _hidePassNode(restoreBottomLayers: boolean = true): Promise<void> {
|
||||
return new Promise<void>((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<void> {
|
||||
const goodAudio = await this._loadGoodAudioClip();
|
||||
if (sequenceId !== this._posePraiseSequenceId || !this._isPassNodeShown || !this.node.isValid) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._playPosePraise(count);
|
||||
this.playSound(goodAudio);
|
||||
}
|
||||
|
||||
private _loadGoodAudioClip(): Promise<AudioClip | null> {
|
||||
if (this._goodAudioClip) {
|
||||
return Promise.resolve(this._goodAudioClip);
|
||||
}
|
||||
if (this._goodAudioLoadPromise) {
|
||||
return this._goodAudioLoadPromise;
|
||||
}
|
||||
|
||||
this._goodAudioLoadPromise = new Promise<AudioClip | null>((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;
|
||||
|
||||
@@ -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
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
]
|
||||
|
||||
@@ -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<void> {
|
||||
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;
|
||||
|
||||
BIN
assets/resources/audios/good.mp3
Normal file
BIN
assets/resources/audios/good.mp3
Normal file
Binary file not shown.
14
assets/resources/audios/good.mp3.meta
Normal file
14
assets/resources/audios/good.mp3.meta
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user