feat: 优化关卡以及选关 UI

This commit is contained in:
richarjiang
2026-05-30 22:37:07 +08:00
parent 332ec0081d
commit 7355cb9c2e
14 changed files with 1944 additions and 330 deletions

View File

@@ -41,7 +41,7 @@ export class PageLevel extends BaseView {
private static readonly DEFAULT_STAMINA_MAX = 50;
/** 答案正确后到弹出通关弹窗之间的停留时间(不论是否有谐音梗都保持一致) */
private static readonly PASS_MODAL_DELAY_MS = 2000;
private static readonly PASS_MODAL_DELAY_MS = 1000;
/** 图片2描述默认文案 */
private static readonly DEFAULT_IMAGE2_DESCRIPTION = '这是什么?';
@@ -91,6 +91,17 @@ export class PageLevel extends BaseView {
/** 彩带 spine 动画名(一次播放) */
private static readonly CAIDAI_ANIMATION_NAME = 'open';
/**
* 通关赞美 spinepose 节点)档位:[最小通关数, 动画名],倒序匹配。
* 1-5 关 → "1"6-10 关 → "2"11+ 关 → "3"。
* count <= 0 不会匹配任何档位,自然不播放。
*/
private static readonly POSE_TIERS: ReadonlyArray<readonly [number, string]> = [
[11, '3'],
[6, '2'],
[1, '1'],
];
// ========== 节点引用 ==========
@property(Node)
inputLayout: Node | null = null;
@@ -236,6 +247,14 @@ export class PageLevel extends BaseView {
@property(sp.Skeleton)
caidaiSkeleton: sp.Skeleton | null = null;
/** 通关赞美 spine 节点PassNode 子节点,根据本场通关数选择 1/2/3 动画) */
@property(Node)
poseNode: Node | null = null;
/** 通关赞美 sp.Skeleton 组件,动画名 "1" / "2" / "3"loop=false */
@property(sp.Skeleton)
poseSkeleton: sp.Skeleton | null = null;
// ========== 配置属性 ==========
@property(AudioClip)
clickAudio: AudioClip | null = null;
@@ -330,6 +349,15 @@ export class PageLevel extends BaseView {
/** 通关页所用「已通关数量」(业务数据,给成就体系展示用) */
private _passCompletedLevelCount: number | null = null;
/**
* 本次进入 PageLevel 后累计通关数onViewLoad / _reinitLevelSession 时归零)。
* 普通模式 / 分享模式都计入;用于驱动 pose 赞美动画档位。
*/
private _sessionPassCount: number = 0;
/** pose Spine 隐藏延时定时器setTimeout 句柄);切关 / 关页时需清理避免穿屏触发 */
private _poseHideTimer: ReturnType<typeof setTimeout> | null = null;
/** 通关页动画起点(通关前)的已通关数量;为 null 表示不播跨称号过渡 */
private _passPreviousCompletedLevelCount: number | null = null;
@@ -388,6 +416,9 @@ export class PageLevel extends BaseView {
onViewLoad(): void {
console.log('[PageLevel] onViewLoad');
// 本次进入答题页的会话通关数归零;普通 / 分享模式都重新开始计数
this._sessionPassCount = 0;
// 必须在任何可能改动 InputLayout/punchLayout 位置的逻辑之前记录原始位置
this._captureActionOriginalPositions();
@@ -489,6 +520,9 @@ export class PageLevel extends BaseView {
private _reinitLevelSession(shareMode: boolean): void {
this._isShareMode = shareMode;
// 跨模式切换视为新的"本次进入"会话,赞美动画从最低档位重新开始
this._sessionPassCount = 0;
// 上一场可能遗留的弹窗 / 倒计时一并清掉,避免主线模式还看到分享态弹窗
this._resetPassNode();
this._closeWrongModal();
@@ -1726,13 +1760,6 @@ export class PageLevel extends BaseView {
this.playSound(this.successAudio);
}
/**
* 播放通关成功音效
*/
private playSuccessSound(): void {
this.playSound(this.successAudio);
}
// ========== 倒计时相关方法 ==========
/**
@@ -2099,12 +2126,18 @@ export class PageLevel extends BaseView {
this._isPassNodeShown = true;
// 记录本次进入答题页的累计通关数(普通 / 分享模式都计入)。
// 放在 _showPassNode 而不是 reportLevelCompleted分享模式不会经过 reportLevelCompleted
// 但两种模式都经过 _showPassNode且 _isPassNodeShown 防重入保证只 +1 一次。
this._sessionPassCount++;
// 配置成就体系数据 / 按钮文案 / 事件
this._setupPassNodeContent();
this._bindPassNodeEvents();
// 启动彩带 + 滑入动画 + 淡出底部UI + 通关音效
// 启动彩带 + 滑入动画 + 淡出底部UI + 通关音效 + 赞美 spine
this._playCaidai();
this._playPosePraise(this._sessionPassCount);
this._playPassNodeShowAnimation();
this.playSuccessSound();
@@ -2185,6 +2218,14 @@ export class PageLevel extends BaseView {
if (this.caidaiNode) {
this.caidaiNode.active = false;
}
// pose 节点 prefab 默认 active=true 且 defaultAnimation="1",运行时必须先关掉,
// 否则进入 PageLevel 时 pose 会立即显示并自动跑一次动画。
if (this.poseNode) {
this.poseNode.active = false;
}
this._clearPoseHideTimer();
this._isPassNodeShown = false;
this._isPassNodeAnimating = false;
}
@@ -2274,6 +2315,12 @@ export class PageLevel extends BaseView {
this.caidaiNode.active = false;
}
// 关掉赞美 spinePassNode 一关 pose 自然不可见,仍显式置 false 防御下次显示残留帧)
if (this.poseNode) {
this.poseNode.active = false;
}
this._clearPoseHideTimer();
this._unbindPassNodeEvents();
this._titleAnimator?.stop();
this._isPassNodeShown = false;
@@ -2298,6 +2345,12 @@ export class PageLevel extends BaseView {
this.caidaiNode.active = false;
}
// 同步清掉赞美 spine防止跨模式 / 切关 / 销毁时残留
if (this.poseNode) {
this.poseNode.active = false;
}
this._clearPoseHideTimer();
this._restoreBottomLayersImmediate();
this._unbindPassNodeEvents();
this._titleAnimator?.stop();
@@ -2365,6 +2418,35 @@ export class PageLevel extends BaseView {
this.caidaiSkeleton.setAnimation(0, PageLevel.CAIDAI_ANIMATION_NAME, false);
}
/**
* 根据本次会话累计通关数选择并播放赞美 spine 动画pose 节点)。
* 档位匹配规则见 PageLevel.POSE_TIERS1-5 → "1"6-10 → "2"11+ → "3"。
* 单次 spine 动画太短,这里用 loop=true 持续播放,配合 POSE_DISPLAY_DURATION
* 定时强制隐藏 poseNode。切关 / 销毁会通过 _clearPoseHideTimer 取消定时器。
*/
private _playPosePraise(count: number): void {
if (!this.poseNode || !this.poseSkeleton) return;
const tier = PageLevel.POSE_TIERS.find(([min]) => count >= min);
if (!tier) return;
const [, animName] = tier;
// 重置上一次可能仍在排队的隐藏定时器(极端情况下连击通关)
this._clearPoseHideTimer();
this.poseNode.active = true;
this.poseSkeleton.setAnimation(0, animName, false);
}
/** 清理 pose 隐藏定时器(用于切关 / 销毁 / 跨模式切换前的清场)。 */
private _clearPoseHideTimer(): void {
if (this._poseHideTimer !== null) {
clearTimeout(this._poseHideTimer);
this._poseHideTimer = null;
}
}
/** 底部两层淡出(透明度 → 0完成后 active = false */
private _fadeOutBottomLayers(): void {
for (const layer of [this.bottomLayoutNode, this.tipsLayout]) {