Files
mp-xieyingeng/assets/prefabs/PageHome.ts
2026-04-27 10:11:36 +08:00

352 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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';
import { StaminaManager } from 'db://assets/scripts/utils/StaminaManager';
import { ToastManager } from 'db://assets/scripts/utils/ToastManager';
import { StaminaInfo } from 'db://assets/scripts/types/ApiTypes';
const { ccclass, property } = _decorator;
/**
* 首页组件
* 继承 BaseView实现页面生命周期
*/
@ccclass('PageHome')
export class PageHome extends BaseView {
/** 默认体力上限 */
private static readonly DEFAULT_STAMINA_MAX = 50;
@property({ type: Node, tooltip: '开始游戏按钮' })
startGameBtn: Node | null = null;
@property({ type: Node, tooltip: 'PK按钮' })
pkBtn: Node | null = null;
/** 体力值显示标签 */
@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;
/**
* 页面首次加载时调用
*/
onViewLoad(): void {
console.log('[PageHome] onViewLoad');
this._initButtons();
this._initWxShare();
// 检查隐私授权
this._checkPrivacyAuthorization();
}
/**
* 初始化微信分享功能
*/
private _initWxShare(): void {
WxSDK.initShare({
title: '写英语',
imageUrl: '',
query: ''
});
}
/**
* 检查隐私授权状态
*/
private async _checkPrivacyAuthorization(): Promise<void> {
if (!WxSDK.isWechat()) {
console.log('[PageHome] 非微信环境,跳过隐私授权检查');
return;
}
try {
const { needAuthorization } = await checkPrivacySetting();
if (needAuthorization) {
console.log('[PageHome] 用户未授权隐私,引导授权');
await requirePrivacyAuthorize();
} else {
console.log('[PageHome] 用户已授权隐私');
}
} catch (err) {
console.warn('[PageHome] 隐私授权检查异常:', err);
}
}
/**
* 初始化按钮事件
*/
private _initButtons(): void {
if (this.startGameBtn) {
this.startGameBtn.on(Button.EventType.CLICK, this._onStartGameClick, this);
}
if (this.pkBtn) {
this.pkBtn.on(Button.EventType.CLICK, this._onPkClick, this);
}
}
/**
* 开始游戏按钮点击回调
*/
private _onStartGameClick(): void {
if (this._isAnimating) return;
console.log('[PageHome] 开始游戏按钮点击');
// 体力检查
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;
});
}
/**
* PK按钮点击回调
*/
private _onPkClick(): void {
console.log('[PageHome] PK按钮点击');
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 {
console.log('[PageHome] onViewShow');
this.updateStaminaLabel();
// 保险恢复:防止动画中途被打断导致 IconLive 隐藏残留
const iconLive = this._findIconLive();
if (iconLive && !iconLive.active) {
iconLive.active = true;
}
}
/**
* 获取体力上限
*/
private _getStaminaMax(stamina: StaminaInfo): number {
return typeof stamina.max === 'number' ? stamina.max : PageHome.DEFAULT_STAMINA_MAX;
}
/**
* 更新体力值显示
*/
private updateStaminaLabel(): void {
if (this.liveLabel) {
const stamina = StaminaManager.instance.getStamina();
const maxStamina = this._getStaminaMax(stamina);
this.liveLabel.string = `${stamina.current}/${maxStamina}`;
}
}
/**
* 页面隐藏时调用
*/
onViewHide(): void {
console.log('[PageHome] onViewHide');
}
/**
* 页面销毁时调用
*/
onViewDestroy(): void {
console.log('[PageHome] onViewDestroy');
// 移除按钮事件监听
if (this.startGameBtn) {
this.startGameBtn.off(Button.EventType.CLICK, this._onStartGameClick, this);
}
if (this.pkBtn) {
this.pkBtn.off(Button.EventType.CLICK, this._onPkClick, this);
}
}
}