perf: 首页支持扣减体力动画
This commit is contained in:
@@ -620,7 +620,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"__type__": "cc.Node",
|
"__type__": "cc.Node",
|
||||||
"_name": "IconStam2",
|
"_name": "IconLive",
|
||||||
"_objFlags": 0,
|
"_objFlags": 0,
|
||||||
"__editorExtras__": {},
|
"__editorExtras__": {},
|
||||||
"_parent": {
|
"_parent": {
|
||||||
|
|||||||
@@ -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 { BaseView } from 'db://assets/scripts/core/BaseView';
|
||||||
import { ViewManager } from 'db://assets/scripts/core/ViewManager';
|
import { ViewManager } from 'db://assets/scripts/core/ViewManager';
|
||||||
import { WxSDK, checkPrivacySetting, requirePrivacyAuthorize } from 'db://assets/scripts/utils/WxSDK';
|
import { WxSDK, checkPrivacySetting, requirePrivacyAuthorize } from 'db://assets/scripts/utils/WxSDK';
|
||||||
@@ -26,6 +26,21 @@ export class PageHome extends BaseView {
|
|||||||
@property(Label)
|
@property(Label)
|
||||||
liveLabel: Label | null = null;
|
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 {
|
private _onStartGameClick(): void {
|
||||||
|
if (this._isAnimating) return;
|
||||||
|
|
||||||
console.log('[PageHome] 开始游戏按钮点击');
|
console.log('[PageHome] 开始游戏按钮点击');
|
||||||
|
|
||||||
|
// 体力检查
|
||||||
|
if (!StaminaManager.instance.hasStamina()) {
|
||||||
|
ToastManager.show('体力不足,请等待恢复');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._isAnimating = true;
|
||||||
|
this._playStaminaCostAnimation()
|
||||||
|
.then(() => {
|
||||||
ViewManager.instance.open('PageLevel');
|
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('功能正在开发中,敬请期待吧!');
|
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<void> {
|
||||||
|
return new Promise<void>((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 {
|
onViewShow(): void {
|
||||||
console.log('[PageHome] onViewShow');
|
console.log('[PageHome] onViewShow');
|
||||||
this.updateStaminaLabel();
|
this.updateStaminaLabel();
|
||||||
|
|
||||||
|
// 保险恢复:防止动画中途被打断导致 IconLive 隐藏残留
|
||||||
|
const iconLive = this._findIconLive();
|
||||||
|
if (iconLive && !iconLive.active) {
|
||||||
|
iconLive.active = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user