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 { 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按钮点击'); ViewManager.instance.open('PageWriteLevels'); } // ========== 体力消耗动画 ========== /** * 通过节点路径查找 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; } } /** * 获取体力上限 */ 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); } } }