import { _decorator, Asset, BoxCollider2D, Contact2DType, EventTouch, Game, game, ImageAsset, Input, input, KeyCode, Node, NodeEventType, Skeleton, sp, Sprite, SpriteFrame, Texture2D, tween, UIOpacity, UITransform, Vec3 } from 'cc'; import BaseView from '../../../../../../extensions/app/assets/base/BaseView'; import { Tools } from 'db://assets/res-native/tools/Tools'; import { app } from 'db://assets/app/app'; const { ccclass, property } = _decorator; /** 游戏时间 */ const GAME_TIME = 30; /** 游戏边界 */ const MOVE_BOUNDARY = 500; @ccclass('PageMain') export class PageMain extends BaseView { /** 掉落红包预制件 */ @property(Node) redPacket: Node = null!; /** 时间 */ @property(Node) lab_time: Node = null!; /** 分数 */ @property(Node) lab_score: Node = null!; /** 开始按钮 */ @property(Node) btn_start: Node = null!; /** 红包天空 */ @property(Node) red_sky: Node = null!; /** 财神 */ @property(Node) caiShen: Node = null!; /** 财神上位节点,用来防止文字旋转 */ @property(Node) caiShen_up: Node = null!; /** 背景 */ @property(Node) background: Node = null!; /** 需要浮动 */ @property(Node) needFlow: Node[] = []; /** 需要闪烁 */ @property(Node) needFlash: Node[] = []; /** 获奖文字 */ @property(Node) lab_add: Node = null!; /** 准备页面 */ @property(Node) readyPage: Node[] = []; /** 游戏页面 */ @property(Node) gamePage: Node[] = []; /** 去充值按钮 */ @property(Node) btn_to_charge: Node = null!; /** 去游戏按钮 */ @property(Node) btn_to_game: Node = null!; /** 奖励界面 */ @property(Node) rw: Node = null!; /** 倒计时 */ @property(Node) count: Node = null!; /** 音乐按钮 */ @property(Node) btn_music: Node = null!; /** 关闭音乐按钮 */ @property(Node) btn_no_music: Node = null!; /** logo展示 */ @property(Sprite) logo: Sprite = null!; /** 金币雨 */ @property(Node) gold_rain: Node = null!; /** 是否正在游戏 */ private _gaming = false; /** 剩余游戏时间 */ private _time = 0; /** 红包管理器 */ private _redMgr : Node[] = []; /** 红包掉落间隔时间 秒 */ private _redTime: number = 1/2; /** 当前游戏id */ private _game_id = ""; /** 当前分数 */ private _score = 0; /** 最大分数 */ private _maxScore = 0; /** 剩余红包个数 */ private _redRemain = 0; /** 财神移动速度 */ private readonly caiShenMoveSpeed: number = 600; /** 移动边界 */ private readonly leftBoundary: number = -MOVE_BOUNDARY; private readonly rightBoundary: number = MOVE_BOUNDARY; /** 当前移动方向 */ private _currentDirection: number = 0; // -1:左, 0:无, 1:右 /** 倒计时 */ private countdownInterval: any; /** 游戏开始需要的时间和充值 */ private _money_need : boolean = false; private _time_need : boolean = false; private _chance_balance : number = 0; /** 记录切换到后台时间 */ private _hideTime: number = 0; // 初始化的相关逻辑写在这 onLoad() {} // 界面打开时的相关逻辑写在这(onShow可被多次调用-它与onHide不成对) onShow(params: any) { input.on(Input.EventType.KEY_DOWN, this.onKeyDown, this); input.on(Input.EventType.KEY_UP, this.onKeyUp, this); // 注册触摸事件 this.background.on(NodeEventType.TOUCH_MOVE, this.onTouchMove, this); this.background.on(NodeEventType.TOUCH_START, this.onTouchStart, this); this.background.on(NodeEventType.TOUCH_END, this.onTouchEnd, this); // 监听隐藏事件(切换到后台) game.on(Game.EVENT_HIDE, this.onAppHide, this); // 监听显示事件(从前台返回) game.on(Game.EVENT_SHOW, this.onAppShow, this); // 开始更新循环以处理持续移动 this.schedule(this.updateMovement, 1/60); // 每帧更新 this.startFlow() this.goToReadyPage() // Tools.httpGetReq("mini-games/angpau/assets", (res: any) => { // Tools.remoteLoadSprite(res.logo, this.logo) // }) } // 界面关闭时的相关逻辑写在这(已经关闭的界面不会触发onHide) onHide(result: undefined) { // 注销键盘事件 input.off(Input.EventType.KEY_DOWN, this.onKeyDown, this); input.off(Input.EventType.KEY_UP, this.onKeyUp, this); // 注销触摸事件 this.background.off(NodeEventType.TOUCH_MOVE, this.onTouchMove, this); this.background.off(NodeEventType.TOUCH_START, this.onTouchStart, this); this.background.off(NodeEventType.TOUCH_END, this.onTouchEnd, this); // 移除事件监听 game.off(Game.EVENT_HIDE, this.onAppHide, this); game.off(Game.EVENT_SHOW, this.onAppShow, this); // 取消更新循环 this.unschedule(this.updateMovement); return result; } initReadyPage(d:any){ // d.progress.current = 500 // d.event.start_at = "2026-01-22T23:59:59+08:00" Tools.SetChildText(this.readyPage[0], "lab_title", d.title) Tools.SetChildText(this.readyPage[0], "lab_act_time", d.footer1.title) Tools.SetChildText(this.readyPage[0], "lab_act_time/time", d.footer1.desc) Tools.SetChildText(this.readyPage[0], "lab_act_qua", d.footer2.title) Tools.SetChildText(this.readyPage[0], "lab_act_qua/time", d.footer2.desc) Tools.SetChildText(this.readyPage[0], "lab_charge_pro/now", d.progress.current) Tools.SetChildText(this.readyPage[0], "lab_charge_pro/all", d.progress.max) Tools.SetChildText(this.readyPage[0], "lab_charge_goal/num", (d.progress.max - d.progress.current).toString()) Tools.ActChild(this.readyPage[0], "btn_save", d.progress.max > d.progress.current) Tools.ActChild(this.readyPage[0], "btn_start", d.progress.max <= d.progress.current) Tools.ActChild(this.readyPage[0], "lab_charge", d.progress.max > d.progress.current) Tools.ActChild(this.readyPage[0], "lab_charge_goal", d.progress.max > d.progress.current) Tools.ActChild(this.readyPage[0], "lab_get_res", d.progress.max <= d.progress.current) const fill = Tools.MathClampZeroToOne(d.progress.current / d.progress.max); const bar = this.readyPage[0].getChildByName("pro_bar") Tools.GetChildComp(bar, "bar", Sprite).fillRange = fill; bar.getChildByName("point").setPosition(fill * bar.getComponent(UITransform).width, 0, 0) this.startCountdown(d.event.start_at) this._money_need = d.progress.max <= d.progress.current this._time_need = false this._chance_balance = d.angpau_chance_balance } /** * 计算倒计时剩余时间 * @param targetDate 目标日期 * @returns 包含天、小时、分钟、秒的对象 */ calculateCountdown(targetDate: string | Date): { days: number; hours: number; minutes: number; seconds: number } { const targetTime = new Date(targetDate).getTime(); const currentTime = new Date().getTime(); // 如果目标时间已过,则返回全零 if (targetTime <= currentTime) { return { days: 0, hours: 0, minutes: 0, seconds: 0 }; } const diffTime = targetTime - currentTime; const days = Math.floor(diffTime / (1000 * 60 * 60 * 24)); const hours = Math.floor((diffTime % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)); const minutes = Math.floor((diffTime % (1000 * 60 * 60)) / (1000 * 60)); const seconds = Math.floor((diffTime % (1000 * 60)) / 1000); return { days, hours, minutes, seconds }; } /** * 开始倒计时显示 */ startCountdown(targetDate:string) { // 立即更新一次 this.updateCountdownDisplay(targetDate); // 每秒更新一次倒计时 this.countdownInterval = setInterval(() => { this.updateCountdownDisplay(targetDate); }, 1000); } /** * 停止倒计时 */ stopCountdown() { if (this.countdownInterval) { clearInterval(this.countdownInterval); this.countdownInterval = null; } } /** * 更新倒计时显示 */ updateCountdownDisplay(targetDate: string) { const countdown = this.calculateCountdown(targetDate); // 更新UI显示 if (this.readyPage && this.readyPage[0]) { Tools.SetChildText(this.readyPage[0], "lab_time/day", countdown.days.toString()); Tools.SetChildText(this.readyPage[0], "lab_time/hour", countdown.hours.toString()); Tools.SetChildText(this.readyPage[0], "lab_time/minute", countdown.minutes.toString()); Tools.SetChildText(this.readyPage[0], "lab_time/second", countdown.seconds.toString()); } if (countdown.days == 0 && countdown.hours == 0 && countdown.minutes == 0 && countdown.seconds == 0){ this.stopCountdown() this._time_need = true } } /** 去游戏界面 */ btnToGamePage(){ if (!this._money_need){ app.manager.ui.showToast("存款金额不够") return } if (!this._time_need){ app.manager.ui.showToast("游戏时间未到") return } if (this._chance_balance <= 0){ app.manager.ui.showToast("抽奖次数已用完") return } for (let i = 0; i < this.readyPage.length; i++) { this.readyPage[i].active = false; } for (let i = 0; i < this.gamePage.length; i++) { this.gamePage[i].active = true; } this.caiShen.getComponent(sp.Skeleton).paused = true } /** 去准备页面 */ goToReadyPage(){ for (let i = 0; i < this.gamePage.length; i++) { this.gamePage[i].active = false; } Tools.httpGetReq("mini-games/angpau/event", (res: any) => { for (let i = 0; i < this.readyPage.length; i++) { this.readyPage[i].active = true; } this.initReadyPage(res) }) } btnGameStart(){ this.btn_start.active = false; this._game_id = ""; Tools.httpReq("mini-games/angpau/reward", {}, (res: any) => { this.count.active = true; this._maxScore = res.max_reward; this._game_id = res.game_id; this._redRemain = Math.floor(GAME_TIME / this._redTime) - 1; //开始动画 this.loadSprite("main_count_3", this.count); app.manager.sound.playEffect({name : "effect/start"}); let countNum = 3; let fun = () => { countNum--; if (countNum <= -1) { this.unschedule(fun); this.count.active = false; this._gaming = true; this._time = GAME_TIME; this._score = 0; this.updateScore(); this.resetRedMgr(); Tools.SetText(this.lab_time, this._time + "s"); this.schedule(this.countTime, 1); this.schedule(this.dropRedPacket, this._redTime); return } this.loadSprite(`main_count_${countNum}`, this.count); } this.schedule(fun, 1); app.manager.sound.playEffect({name : "effect/start"}); },()=>{ this.btn_start.active = true; }) } startFlow(){ for (let i = 0; i < this.needFlow.length; i++) { this.aniFlow(this.needFlow[i]) } for (let i = 0; i < this.needFlash.length; i++) { this.aniFlash(this.needFlash[i]) } } aniFlash(node: Node){ let light = Math.random() * 255; let time = (255 - light) / 100; let opacity = node.getComponent(UIOpacity); if (opacity) { tween(opacity) .to(time, {opacity: light}) .to(time, {opacity: 255}) .call(() => { this.aniFlash(node) }) .start(); } } aniFlow(node: Node){ let dis = Math.random() * 20 + 50; tween(node) .by(dis / 12, {position: new Vec3(0, -dis, 0)}) .by(dis / 12, {position: new Vec3(0, dis, 0)}) .call(() => { this.aniFlow(node) }) .start(); } resetRedMgr(){ this.desAllRed(); this._redMgr = []; } desAllRed(){ // 创建副本以避免在遍历时修改数组 const redPacketsToDestroy = [...this._redMgr]; for (let i = 0; i < redPacketsToDestroy.length; i++) { if (redPacketsToDestroy[i] && redPacketsToDestroy[i].isValid) { const collider = redPacketsToDestroy[i].getComponent(BoxCollider2D); if (collider) { try { collider.off(Contact2DType.BEGIN_CONTACT, this.onBeginContact, this); } catch (error) { console.warn('移除碰撞监听器时出错:', error); } } // 延迟销毁 this.scheduleOnce(() => { if (redPacketsToDestroy[i] && redPacketsToDestroy[i].isValid) { redPacketsToDestroy[i].destroy(); } }, 0.01); } } // 清空管理器 this._redMgr = []; } countTime(){ if (!this._gaming) return; this._time--; Tools.SetText(this.lab_time, this._time + "s"); if (this._time <= 0) { this.gameOver(); } } gameOver(){ Tools.SetText(this.lab_time, "0s"); this._gaming = false; this.caiShen.getComponent(sp.Skeleton).paused = true; this.btn_start.active = true; this.unschedule(this.countTime); this.unschedule(this.dropRedPacket); this.desAllRed(); Tools.httpReq("mini-games/angpau/finalize", {game_id:this._game_id, final_amount:this._score}, (res: any) => { Tools.SetChildText(this.rw, "bg/lab_layout/num", res.credited_amount); Tools.SetChildText(this.rw, "bg/lab_layout/lab_num", res.currency); this.rw.active = true; let rain = Tools.AddChild(this.rw, this.gold_rain, "gold_rain"); rain.setPosition(0, 420, 0); rain.active = true; this.scheduleOnce(()=>{ rain?.destroy(); }, 5) app.manager.sound.playEffect({name : "effect/gold_rain"}); }) } btnCloseRw(){ this.rw.active = false; } dropRedPacket(){ let n = this._redMgr.length this._redMgr[n] = Tools.AddChild(this.red_sky, this.redPacket, "redPacket"); this._redMgr[n].active = true; this._redMgr[n].setPosition(Math.random() * MOVE_BOUNDARY * 2 - MOVE_BOUNDARY, 0); const collider = this._redMgr[n].getComponent(BoxCollider2D); if (collider) { collider.on(Contact2DType.BEGIN_CONTACT, this.onBeginContact, this); } const amounts = (this._maxScore / this._redRemain ).toFixed(1); this._maxScore -= parseFloat(amounts); this._redRemain-- //@ts-ignore this._redMgr[n].amount = amounts; // 存储金额 } /** * 碰撞开始时的回调 * @param contact 碰撞信息 * @param selfCollider 自身碰撞器 * @param otherCollider 另一个碰撞器 */ onBeginContact(selfCollider: any, otherCollider: any) { if (otherCollider.node.name === 'red_floor') { this.removeRedPacket(selfCollider.node); } else if (otherCollider.node.name === 'CaiShen') { app.manager.sound.playEffect({name : "effect/catch"}); const redPacketNode = selfCollider.node; const amount = redPacketNode.amount || 0; // 默认为0 this.removeRedPacket(selfCollider.node); this._score = this._score + parseFloat(amount) let lab = Tools.AddChild(this.caiShen_up, this.lab_add) Tools.SetText(lab, "+" + amount) this.aniScoreUp(lab) this.updateScore(); if (navigator.vibrate) { navigator.vibrate(200); } } } aniScoreUp(node: Node){ node.active = true node.setPosition(0, 400, 0) let opacity = node.getComponent(UIOpacity) tween(opacity) .to(0.5, {opacity: 0}) .call(() => { node.destroy() }) .start(); tween(node) .by(0.5, {position: new Vec3(0, 50, 0)}) .start(); } /** * 更新分数 */ updateScore() { let score = this._score == 0 ? "0" : this._score.toFixed(1); Tools.SetText(this.lab_score, score); } /** * 移除红包 */ removeRedPacket(redPacketNode: Node) { // 检查节点是否有效 if (!redPacketNode || redPacketNode.isValid === false) { return; } // 获取碰撞体组件并移除监听器 const collider = redPacketNode.getComponent(BoxCollider2D); if (collider) { try { collider.off(Contact2DType.BEGIN_CONTACT, this.onBeginContact, this); } catch (error) { console.warn('移除碰撞监听器时出错:', error); } } //从管理器中移除 const index = this._redMgr.findIndex(item => item === redPacketNode); if (index > -1) { this._redMgr.splice(index, 1); } this.scheduleOnce(()=>{ tween(redPacketNode.getComponent(UIOpacity)) .to(0.1, { opacity: 0 }) .call(() => { try { redPacketNode.destroy(); } catch (error) { console.warn('销毁节点时出错:', error); } }) .start(); }, 0) } /** * 键盘按下处理 */ private onKeyDown(event: any) { if (!this._gaming) return this.caiShen.getComponent(sp.Skeleton).paused = false switch (event.keyCode) { case KeyCode.ARROW_LEFT: case KeyCode.KEY_A: this._currentDirection = -1; this.caiShen.setScale(new Vec3(-1, 1, 1)) break; case KeyCode.ARROW_RIGHT: case KeyCode.KEY_D: this._currentDirection = 1; this.caiShen.setScale(new Vec3(1, 1, 1)) break; } } /** * 更新财神移动 */ private updateMovement(dt: number) { if (this._currentDirection === 0 || !this._gaming) { return; } const currentPosition = this.caiShen_up.position; let newX = currentPosition.x + this._currentDirection * this.caiShenMoveSpeed * dt; // 边界限制 newX = Math.max(this.leftBoundary, Math.min(this.rightBoundary, newX)); this.caiShen_up.setPosition(newX, currentPosition.y, currentPosition.z); } /** * 键盘释放处理 */ private onKeyUp(event: any) { switch (event.keyCode) { case KeyCode.ARROW_LEFT: case KeyCode.KEY_A: if (this._currentDirection === -1) { this._currentDirection = 0; this.caiShen.getComponent(sp.Skeleton).paused = true } break; case KeyCode.ARROW_RIGHT: case KeyCode.KEY_D: if (this._currentDirection === 1) { this._currentDirection = 0; this.caiShen.getComponent(sp.Skeleton).paused = true } break; } } /** * 触摸移动处理 */ private onTouchMove(touch: EventTouch) { if (!this._gaming) return const delta = touch.getDelta(); const currentPosition = this.caiShen_up.position; let newX = currentPosition.x + delta.x; this.caiShen.setScale(new Vec3(delta.x > 0 ? 1 : -1, 1, 1)) // 边界限制 newX = Math.max(this.leftBoundary, Math.min(this.rightBoundary, newX)); this.caiShen_up.setPosition(newX, currentPosition.y, currentPosition.z); } private onTouchStart(touch: EventTouch) { if (!this._gaming) return this.caiShen.getComponent(sp.Skeleton).paused = false; } private onTouchEnd(touch: EventTouch) { if (!this._gaming) return this.caiShen.getComponent(sp.Skeleton).paused = true; } loadSprite(pic: string, node: Node){ this.loadRes(pic, Asset, (res: ImageAsset)=>{ let sp = new SpriteFrame() let tex = new Texture2D(); tex.image = res; sp.texture = tex node.getComponent(Sprite).spriteFrame = sp }); } // 应用隐藏时的处理(切换到其他应用) private onAppHide() { // 暂停游戏逻辑 this._hideTime = Date.now(); } // 应用显示时的处理(返回应用) private onAppShow() { // 计算在后台停留的时间(单位:毫秒) const currentTime = Date.now(); const timeInBackground = currentTime - this._hideTime; const timeInBackgroundSeconds = timeInBackground / 1000; // 转换为秒 // 如果在后台停留时间超过10秒,结束游戏 if (this._gaming && timeInBackgroundSeconds > 10) { app.manager.ui.showToast("离开时间过长,游戏已结束"); this.gameOver(); } } closeMusic(){ this.btn_music.active = false this.btn_no_music.active = true app.manager.sound.stopMusic() } openMusic(){ this.btn_music.active = true this.btn_no_music.active = false app.manager.sound.playDefaultMusic() } }