feat: 接入通关弹窗
This commit is contained in:
31
assets/scripts/config/AchievementTitleConfig.ts
Normal file
31
assets/scripts/config/AchievementTitleConfig.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
export interface AchievementTitleConfigItem {
|
||||
readonly seriesName: string;
|
||||
readonly levelName: string;
|
||||
readonly clearsToNext: number;
|
||||
}
|
||||
|
||||
const createSeries = (seriesName: string, levelCount: number, clearsToNext: number): AchievementTitleConfigItem[] => {
|
||||
return Array.from({ length: levelCount }, (_, index) => ({
|
||||
seriesName,
|
||||
levelName: `${seriesName}${index + 1}级`,
|
||||
clearsToNext
|
||||
}));
|
||||
};
|
||||
|
||||
export const ACHIEVEMENT_TITLE_CONFIG: readonly AchievementTitleConfigItem[] = [
|
||||
...createSeries('冷场小白', 2, 3),
|
||||
...createSeries('尬笑学生', 2, 3),
|
||||
...createSeries('浅梗游民', 3, 4),
|
||||
...createSeries('热梗新秀', 6, 5),
|
||||
...createSeries('笑点刺客', 6, 5),
|
||||
...createSeries('爆梗高手', 8, 6),
|
||||
...createSeries('幽默大师', 10, 8),
|
||||
...createSeries('爆笑领主', 10, 8),
|
||||
...createSeries('梗王之王', 20, 8)
|
||||
];
|
||||
|
||||
export const INFINITE_ACHIEVEMENT_TITLE = {
|
||||
seriesName: '幽默始祖',
|
||||
levelNamePrefix: '幽默始祖',
|
||||
clearsToNext: 8
|
||||
} as const;
|
||||
9
assets/scripts/config/AchievementTitleConfig.ts.meta
Normal file
9
assets/scripts/config/AchievementTitleConfig.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "eb65203d-ee3d-4169-806d-1de88d9702eb",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
102
assets/scripts/core/BaseModal.ts
Normal file
102
assets/scripts/core/BaseModal.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import { _decorator, Node, Tween, UIOpacity, Vec3, tween } from 'cc';
|
||||
import { BaseView } from './BaseView';
|
||||
const { ccclass, property } = _decorator;
|
||||
|
||||
@ccclass('BaseModal')
|
||||
export class BaseModal extends BaseView {
|
||||
@property([Node])
|
||||
protected animationNodes: Node[] = [];
|
||||
|
||||
@property(Node)
|
||||
protected backdropNode: Node | null = null;
|
||||
|
||||
@property
|
||||
protected openAnimationEnabled: boolean = true;
|
||||
|
||||
@property
|
||||
protected openAnimationDuration: number = 0.36;
|
||||
|
||||
private readonly _originalScales: Map<Node, Vec3> = new Map();
|
||||
|
||||
onViewShow(): void {
|
||||
this.playOpenAnimation();
|
||||
}
|
||||
|
||||
onViewHide(): void {
|
||||
this.stopOpenAnimation();
|
||||
}
|
||||
|
||||
protected playOpenAnimation(): void {
|
||||
if (!this.openAnimationEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.playBackdropFadeIn();
|
||||
|
||||
const targets = this.getAnimationTargets();
|
||||
targets.forEach((target, index) => {
|
||||
this.playBounceIn(target, index * 0.035);
|
||||
});
|
||||
}
|
||||
|
||||
protected stopOpenAnimation(): void {
|
||||
this.getAnimationTargets().forEach((target) => {
|
||||
Tween.stopAllByTarget(target);
|
||||
});
|
||||
|
||||
const opacity = this.backdropNode?.getComponent(UIOpacity);
|
||||
if (opacity) {
|
||||
Tween.stopAllByTarget(opacity);
|
||||
}
|
||||
}
|
||||
|
||||
private getAnimationTargets(): Node[] {
|
||||
const configuredTargets = this.animationNodes.filter((node) => node?.isValid);
|
||||
return configuredTargets.length > 0 ? configuredTargets : [this.node];
|
||||
}
|
||||
|
||||
private getOriginalScale(target: Node): Vec3 {
|
||||
const cachedScale = this._originalScales.get(target);
|
||||
if (cachedScale) {
|
||||
return cachedScale;
|
||||
}
|
||||
|
||||
const originalScale = target.scale.clone();
|
||||
this._originalScales.set(target, originalScale);
|
||||
return originalScale;
|
||||
}
|
||||
|
||||
private playBounceIn(target: Node, delay: number): void {
|
||||
const originalScale = this.getOriginalScale(target);
|
||||
const startScale = this.multiplyScale(originalScale, 0.82);
|
||||
const peakScale = this.multiplyScale(originalScale, 1.045);
|
||||
|
||||
Tween.stopAllByTarget(target);
|
||||
target.setScale(startScale);
|
||||
|
||||
tween(target)
|
||||
.delay(delay)
|
||||
.to(this.openAnimationDuration * 0.58, { scale: peakScale }, { easing: 'backOut' })
|
||||
.to(this.openAnimationDuration * 0.24, { scale: this.multiplyScale(originalScale, 0.985) }, { easing: 'sineOut' })
|
||||
.to(this.openAnimationDuration * 0.18, { scale: originalScale }, { easing: 'sineOut' })
|
||||
.start();
|
||||
}
|
||||
|
||||
private playBackdropFadeIn(): void {
|
||||
if (!this.backdropNode?.isValid) {
|
||||
return;
|
||||
}
|
||||
|
||||
const opacity = this.backdropNode.getComponent(UIOpacity) ?? this.backdropNode.addComponent(UIOpacity);
|
||||
Tween.stopAllByTarget(opacity);
|
||||
opacity.opacity = 0;
|
||||
|
||||
tween(opacity)
|
||||
.to(0.18, { opacity: 255 }, { easing: 'sineOut' })
|
||||
.start();
|
||||
}
|
||||
|
||||
private multiplyScale(scale: Vec3, factor: number): Vec3 {
|
||||
return new Vec3(scale.x * factor, scale.y * factor, scale.z);
|
||||
}
|
||||
}
|
||||
9
assets/scripts/core/BaseModal.ts.meta
Normal file
9
assets/scripts/core/BaseModal.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "981df4d4-68c1-4651-aa5b-44633c119e93",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -44,6 +44,7 @@ export interface GameData {
|
||||
stamina: StaminaInfo;
|
||||
};
|
||||
completedLevelIds: string[];
|
||||
completedLevelCount?: number;
|
||||
}
|
||||
|
||||
/** 关卡列表项 */
|
||||
|
||||
111
assets/scripts/utils/AchievementTitleManager.ts
Normal file
111
assets/scripts/utils/AchievementTitleManager.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import { ACHIEVEMENT_TITLE_CONFIG, AchievementTitleConfigItem, INFINITE_ACHIEVEMENT_TITLE } from '../config/AchievementTitleConfig';
|
||||
|
||||
export interface AchievementTitleInfo {
|
||||
readonly titleText: string;
|
||||
readonly nextTitleText: string;
|
||||
readonly nextTitleProgress: number;
|
||||
readonly progressText: string;
|
||||
readonly completedLevelCount: number;
|
||||
readonly currentTitleStartCount: number;
|
||||
readonly nextTitleRequiredCount: number;
|
||||
readonly remainingToNextTitle: number;
|
||||
}
|
||||
|
||||
interface AchievementTitleStage {
|
||||
readonly titleText: string;
|
||||
readonly clearsToNext: number;
|
||||
readonly startCount: number;
|
||||
}
|
||||
|
||||
export class AchievementTitleManager {
|
||||
public static getTitleInfo(completedLevelCount: number): AchievementTitleInfo {
|
||||
const safeCompletedCount = AchievementTitleManager.normalizeCompletedCount(completedLevelCount);
|
||||
const stages = AchievementTitleManager.buildFiniteStages();
|
||||
const finiteResult = AchievementTitleManager.findFiniteStage(safeCompletedCount, stages);
|
||||
|
||||
if (finiteResult) {
|
||||
return AchievementTitleManager.createTitleInfo(safeCompletedCount, finiteResult.currentStage, finiteResult.nextStage);
|
||||
}
|
||||
|
||||
return AchievementTitleManager.createInfiniteTitleInfo(safeCompletedCount, stages[stages.length - 1]);
|
||||
}
|
||||
|
||||
private static normalizeCompletedCount(completedLevelCount: number): number {
|
||||
if (!Number.isFinite(completedLevelCount) || completedLevelCount < 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return Math.floor(completedLevelCount);
|
||||
}
|
||||
|
||||
private static buildFiniteStages(): AchievementTitleStage[] {
|
||||
let startCount = 0;
|
||||
|
||||
return ACHIEVEMENT_TITLE_CONFIG.map((item: AchievementTitleConfigItem) => {
|
||||
const stage: AchievementTitleStage = {
|
||||
titleText: item.levelName,
|
||||
clearsToNext: item.clearsToNext,
|
||||
startCount
|
||||
};
|
||||
startCount += item.clearsToNext;
|
||||
return stage;
|
||||
});
|
||||
}
|
||||
|
||||
private static findFiniteStage(completedLevelCount: number, stages: AchievementTitleStage[]): { currentStage: AchievementTitleStage; nextStage: AchievementTitleStage } | null {
|
||||
for (let index = 0; index < stages.length; index++) {
|
||||
const currentStage = stages[index];
|
||||
const nextStage = stages[index + 1] ?? {
|
||||
titleText: `${INFINITE_ACHIEVEMENT_TITLE.levelNamePrefix}1级`,
|
||||
clearsToNext: INFINITE_ACHIEVEMENT_TITLE.clearsToNext,
|
||||
startCount: currentStage.startCount + currentStage.clearsToNext
|
||||
};
|
||||
|
||||
if (completedLevelCount < nextStage.startCount) {
|
||||
return { currentStage, nextStage };
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static createInfiniteTitleInfo(completedLevelCount: number, lastFiniteStage: AchievementTitleStage): AchievementTitleInfo {
|
||||
const infiniteStartCount = lastFiniteStage.startCount + lastFiniteStage.clearsToNext;
|
||||
const completedAfterInfiniteStart = Math.max(0, completedLevelCount - infiniteStartCount);
|
||||
const currentInfiniteLevel = Math.floor(completedAfterInfiniteStart / INFINITE_ACHIEVEMENT_TITLE.clearsToNext) + 1;
|
||||
const currentStageStartCount = infiniteStartCount + (currentInfiniteLevel - 1) * INFINITE_ACHIEVEMENT_TITLE.clearsToNext;
|
||||
const nextStage: AchievementTitleStage = {
|
||||
titleText: `${INFINITE_ACHIEVEMENT_TITLE.levelNamePrefix}${currentInfiniteLevel + 1}级`,
|
||||
clearsToNext: INFINITE_ACHIEVEMENT_TITLE.clearsToNext,
|
||||
startCount: currentStageStartCount + INFINITE_ACHIEVEMENT_TITLE.clearsToNext
|
||||
};
|
||||
|
||||
return AchievementTitleManager.createTitleInfo(
|
||||
completedLevelCount,
|
||||
{
|
||||
titleText: `${INFINITE_ACHIEVEMENT_TITLE.levelNamePrefix}${currentInfiniteLevel}级`,
|
||||
clearsToNext: INFINITE_ACHIEVEMENT_TITLE.clearsToNext,
|
||||
startCount: currentStageStartCount
|
||||
},
|
||||
nextStage
|
||||
);
|
||||
}
|
||||
|
||||
private static createTitleInfo(completedLevelCount: number, currentStage: AchievementTitleStage, nextStage: AchievementTitleStage): AchievementTitleInfo {
|
||||
const interval = Math.max(1, nextStage.startCount - currentStage.startCount);
|
||||
const completedInCurrentTitle = Math.max(0, completedLevelCount - currentStage.startCount);
|
||||
const remainingToNextTitle = Math.max(0, nextStage.startCount - completedLevelCount);
|
||||
const nextTitleProgress = Math.max(0, Math.min(1, completedInCurrentTitle / interval));
|
||||
|
||||
return {
|
||||
titleText: currentStage.titleText,
|
||||
nextTitleText: nextStage.titleText,
|
||||
nextTitleProgress,
|
||||
progressText: `还差${remainingToNextTitle}题获得${nextStage.titleText}`,
|
||||
completedLevelCount,
|
||||
currentTitleStartCount: currentStage.startCount,
|
||||
nextTitleRequiredCount: nextStage.startCount,
|
||||
remainingToNextTitle
|
||||
};
|
||||
}
|
||||
}
|
||||
9
assets/scripts/utils/AchievementTitleManager.ts.meta
Normal file
9
assets/scripts/utils/AchievementTitleManager.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "a97f1641-1123-4458-9fb5-3b3f74e314a9",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -15,6 +15,8 @@ export class AuthManager {
|
||||
private _isLoggedIn: boolean = false;
|
||||
/** 服务端返回的已完成关卡 ID(登录后暂存,等 LevelDataManager 就绪后同步) */
|
||||
private _completedLevelIds: string[] = [];
|
||||
/** 服务端返回的已完成关卡数量,用于称号体系计算 */
|
||||
private _completedLevelCount: number = 0;
|
||||
|
||||
static get instance(): AuthManager {
|
||||
if (!this._instance) {
|
||||
@@ -37,6 +39,14 @@ export class AuthManager {
|
||||
return this._completedLevelIds;
|
||||
}
|
||||
|
||||
get completedLevelCount(): number {
|
||||
return this._completedLevelCount;
|
||||
}
|
||||
|
||||
addCompletedLevelCount(delta: number = 1): void {
|
||||
this._completedLevelCount = Math.max(0, this._completedLevelCount + delta);
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化认证:尝试恢复 token 或执行微信登录
|
||||
*/
|
||||
@@ -109,8 +119,9 @@ export class AuthManager {
|
||||
this._isLoggedIn = true;
|
||||
StorageManager.setStamina(gameData.user.stamina);
|
||||
this._completedLevelIds = gameData.completedLevelIds;
|
||||
this._completedLevelCount = this._resolveCompletedLevelCount(gameData);
|
||||
|
||||
console.log(`[AuthManager] Token 验证成功,体力: ${gameData.user.stamina.current}/${gameData.user.stamina.max},已完成: ${this._completedLevelIds.length} 关`);
|
||||
console.log(`[AuthManager] Token 验证成功,体力: ${gameData.user.stamina.current}/${gameData.user.stamina.max},已完成: ${this._completedLevelCount} 关`);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -121,6 +132,7 @@ export class AuthManager {
|
||||
const gameData = await this._fetchGameData();
|
||||
if (gameData) {
|
||||
this._completedLevelIds = gameData.completedLevelIds;
|
||||
this._completedLevelCount = this._resolveCompletedLevelCount(gameData);
|
||||
StorageManager.setStamina(gameData.user.stamina);
|
||||
}
|
||||
}
|
||||
@@ -140,4 +152,8 @@ export class AuthManager {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private _resolveCompletedLevelCount(gameData: GameData): number {
|
||||
return gameData.completedLevelCount ?? gameData.completedLevelIds.length;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user