From 16efb1bb25f8634d040d18a3f6630bbd0a542c26 Mon Sep 17 00:00:00 2001 From: richarjiang Date: Mon, 27 Apr 2026 10:11:36 +0800 Subject: [PATCH] =?UTF-8?q?perf:=20=E9=A6=96=E9=A1=B5=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E6=89=A3=E5=87=8F=E4=BD=93=E5=8A=9B=E5=8A=A8=E7=94=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/prefabs/PageHome.prefab | 2 +- assets/prefabs/PageHome.ts | 208 ++++++++++++++++++++++++++++++++- 2 files changed, 207 insertions(+), 3 deletions(-) diff --git a/assets/prefabs/PageHome.prefab b/assets/prefabs/PageHome.prefab index a381ef2..177c1bf 100644 --- a/assets/prefabs/PageHome.prefab +++ b/assets/prefabs/PageHome.prefab @@ -620,7 +620,7 @@ }, { "__type__": "cc.Node", - "_name": "IconStam2", + "_name": "IconLive", "_objFlags": 0, "__editorExtras__": {}, "_parent": { diff --git a/assets/prefabs/PageHome.ts b/assets/prefabs/PageHome.ts index 558c3ca..8f89230 100644 --- a/assets/prefabs/PageHome.ts +++ b/assets/prefabs/PageHome.ts @@ -1,4 +1,4 @@ -import { _decorator, Node, Button, Label } from 'cc'; +import { _decorator, Node, Button, Label, tween, Vec3, UIOpacity, UITransform, Color, instantiate } from 'cc'; import { BaseView } from 'db://assets/scripts/core/BaseView'; import { ViewManager } from 'db://assets/scripts/core/ViewManager'; import { WxSDK, checkPrivacySetting, requirePrivacyAuthorize } from 'db://assets/scripts/utils/WxSDK'; @@ -26,6 +26,21 @@ export class PageHome extends BaseView { @property(Label) liveLabel: Label | null = null; + /** 飞行动画持续时间(秒) */ + private static readonly FLY_DURATION = 0.5; + + /** 到达后弹跳持续时间(秒) */ + private static readonly BOUNCE_DURATION = 0.15; + + /** 浮动文本动画持续时间(秒) */ + private static readonly FLOAT_DURATION = 0.8; + + /** 浮动文本上移距离 */ + private static readonly FLOAT_OFFSET_Y = 120; + + /** 是否正在播放体力消耗动画 */ + private _isAnimating: boolean = false; + /** * 页面首次加载时调用 */ @@ -86,8 +101,29 @@ export class PageHome extends BaseView { * 开始游戏按钮点击回调 */ private _onStartGameClick(): void { + if (this._isAnimating) return; + console.log('[PageHome] 开始游戏按钮点击'); - ViewManager.instance.open('PageLevel'); + + // 体力检查 + if (!StaminaManager.instance.hasStamina()) { + ToastManager.show('体力不足,请等待恢复'); + return; + } + + this._isAnimating = true; + this._playStaminaCostAnimation() + .then(() => { + ViewManager.instance.open('PageLevel'); + }) + .catch(err => { + console.error('[PageHome] 体力消耗动画异常:', err); + // 异常兜底:直接进入关卡 + ViewManager.instance.open('PageLevel'); + }) + .finally(() => { + this._isAnimating = false; + }); } /** @@ -98,12 +134,180 @@ export class PageHome extends BaseView { ToastManager.show('功能正在开发中,敬请期待吧!'); } + // ========== 体力消耗动画 ========== + + /** + * 通过节点路径查找 IconLive + */ + private _findIconLive(): Node | null { + return this.node + .getChildByName('TopLayout') + ?.getChildByName('Live') + ?.getChildByName('IconLive') ?? null; + } + + /** + * 将节点的世界坐标转换为目标父节点的本地坐标 + */ + private _worldToLocal(worldPos: Vec3, parent: Node): Vec3 { + const parentTransform = parent.getComponent(UITransform); + if (!parentTransform) return worldPos; + return parentTransform.convertToNodeSpaceAR(worldPos); + } + + /** + * 获取节点的世界坐标 + */ + private _getWorldPos(node: Node): Vec3 { + const transform = node.getComponent(UITransform); + if (!transform) return node.worldPosition.clone(); + return transform.convertToWorldSpaceAR(Vec3.ZERO); + } + + /** + * 播放体力消耗动画 + * 1. 克隆 IconLive 飞向 StarGame 按钮 + * 2. 到达后弹跳 + * 3. "体力值-1" 浮动文本上移渐隐 + * 4. 同步更新体力数字 + */ + private _playStaminaCostAnimation(): Promise { + return new Promise((resolve) => { + const iconLive = this._findIconLive(); + const targetBtn = this.startGameBtn; + + if (!iconLive || !targetBtn) { + console.warn('[PageHome] 动画节点未找到,跳过动画'); + resolve(); + return; + } + + // --- 坐标计算 --- + const iconWorldPos = this._getWorldPos(iconLive); + const targetWorldPos = this._getWorldPos(targetBtn); + const rootNode = this.node; + + // 起始和终点在 root 本地空间的坐标 + const startLocal = this._worldToLocal(iconWorldPos, rootNode); + const endLocal = this._worldToLocal(targetWorldPos, rootNode); + + // --- 克隆飞行节点 --- + const flyNode = instantiate(iconLive); + flyNode.name = '_flyIcon'; + flyNode.setPosition(startLocal); + // 保持与原始 IconLive 相同的缩放 + flyNode.setScale(iconLive.worldScale.clone()); + rootNode.addChild(flyNode); + + // 隐藏原始 IconLive + iconLive.active = false; + + // --- 飞行路径(带弧度的抛物线效果) --- + // 中间控制点:x 取中点,y 取较高值 + 偏移形成弧线 + const midX = (startLocal.x + endLocal.x) / 2; + const midY = Math.max(startLocal.y, endLocal.y) + 150; + const midPoint = new Vec3(midX, midY, 0); + + // 阶段1:飞到弧线顶点 + const halfDuration = PageHome.FLY_DURATION / 2; + + tween(flyNode) + .to(halfDuration, { position: midPoint }, { easing: 'quadOut' }) + .to(halfDuration, { position: endLocal }, { easing: 'quadIn' }) + // 阶段2:到达弹跳 + .to(PageHome.BOUNCE_DURATION / 2, { scale: new Vec3(0.4, 0.4, 1) }, { easing: 'quadOut' }) + .to(PageHome.BOUNCE_DURATION / 2, { scale: new Vec3(0.3, 0.3, 1) }, { easing: 'quadIn' }) + .call(() => { + // 飞行完成 — flyNode 使命结束,立即清理 + flyNode.destroy(); + iconLive.active = true; + + // 创建浮动文本 + this._showFloatText(targetBtn, rootNode); + + // 乐观更新体力数字 + this._optimisticUpdateStamina(); + + // 等浮动文本播完再 resolve(用 setTimeout,不依赖已销毁节点的 tween) + setTimeout(() => resolve(), PageHome.FLOAT_DURATION * 1000); + }) + .start(); + }); + } + + /** + * 显示浮动提示文本 "体力值-1" + * 从按钮位置向上漂移并渐隐 + */ + private _showFloatText(anchorNode: Node, parentNode: Node): void { + // 创建文本节点 + const textNode = new Node('_floatText'); + textNode.addComponent(UITransform); + + const label = textNode.addComponent(Label); + label.string = '体力值-1'; + label.fontSize = 36; + label.lineHeight = 40; + label.color = new Color(255, 80, 80, 255); + label.isBold = true; + + // 复用 liveLabel 的字体(如果有) + if (this.liveLabel?.font) { + label.font = this.liveLabel.font; + } + + // 添加 UIOpacity 用于渐隐 + const opacity = textNode.addComponent(UIOpacity); + opacity.opacity = 255; + + // 定位到按钮上方 + const anchorWorldPos = this._getWorldPos(anchorNode); + const localPos = this._worldToLocal(anchorWorldPos, parentNode); + // 起始位置在按钮上方偏移 + localPos.y += 120; + textNode.setPosition(localPos); + + parentNode.addChild(textNode); + + // 向上漂移 + 渐隐 + const floatTarget = new Vec3(localPos.x, localPos.y + PageHome.FLOAT_OFFSET_Y, 0); + tween(textNode) + .to(PageHome.FLOAT_DURATION, { position: floatTarget }, { easing: 'quadOut' }) + .call(() => { + textNode.destroy(); + }) + .start(); + + tween(opacity) + .delay(PageHome.FLOAT_DURATION * 0.3) + .to(PageHome.FLOAT_DURATION * 0.7, { opacity: 0 }, { easing: 'quadIn' }) + .start(); + } + + /** + * 乐观更新体力标签(本地 -1 预扣显示) + */ + private _optimisticUpdateStamina(): void { + if (!this.liveLabel) return; + + const stamina = StaminaManager.instance.getStamina(); + const maxStamina = this._getStaminaMax(stamina); + const displayCurrent = Math.max(0, stamina.current - 1); + this.liveLabel.string = `${displayCurrent}/${maxStamina}`; + } + /** * 页面每次显示时调用 */ onViewShow(): void { console.log('[PageHome] onViewShow'); this.updateStaminaLabel(); + + // 保险恢复:防止动画中途被打断导致 IconLive 隐藏残留 + const iconLive = this._findIconLive(); + if (iconLive && !iconLive.active) { + iconLive.active = true; + } } /**