import NumberToImage from "../../Script/NumberToImage"; import CareerList2 from "./CareerList2"; import CareerItem2 from "./CareerItem2"; const { ccclass, property } = cc._decorator; //排行榜 @ccclass export default class CareerManager2 extends cc.Component { /** rankGame 按钮轻振动;振动异常不能阻断排行榜点击逻辑。 */ private vibrateButtonClick(event?: cc.Event) { if (!event) { return; } try { cc.fx.GameTool.setVibrate("light", 1); } catch (error) { try { console.warn("rankGame 按钮轻振动调用失败:", error); } catch (e) { } } } @property(cc.Sprite) //用户头像换图 phone: cc.Sprite = null; @property(cc.Node) //用户头像换图 bg: cc.Node = null; private rankList: CareerList2; //排行榜 @property(cc.Prefab) Information: cc.Prefab = null; private informationNode: cc.Node = null; private informationDefaultIconFrame: cc.SpriteFrame = null; private informationRemoteIconFrame: cc.SpriteFrame = null; private informationAvatarRequestId: number = 0; private informationTween: cc.Tween = null; private informationBaseScaleX: number = 1; private informationBaseScaleY: number = 1; private readonly informationAutoDestroy = () => { this.playInformationOut(); }; listData: any; //总列表信息 topRankData: any[] = []; //topRank 中 careerRankItem 对应的数据 selfData: any; //自己信息 rankNumber: number; //用户自己排名 有可能不在排行榜内99+ rankTotal: number; //获取排行榜用户数量 现在为100 JiaZai: any; onLoad() { this.node.opacity = 0; // this.init(); } //初始化数据 init(data, topData) { this.destroyInformation(); this.applyCurrentUserCat(data, topData); this.topRankData = Array.isArray(topData) ? topData : []; this.rankList = cc.find("ScrollView", this.node).getComponent(CareerList2); this.listData = data; this.selfData = null; this.rankNumber = 100; this.rankTotal = 100; this.rankList.setData(data, topData); // 获取当前日期 let currentDate = new Date(); let month = currentDate.getMonth() + 1; let day = currentDate.getDate(); if (this.bg.getChildByName("month")) this.bg.getChildByName("month").getComponent(cc.Label).string = month.toString(); if (this.bg.getChildByName("day")) this.bg.getChildByName("day").getComponent(cc.Label).string = day.toString(); // const canvasTemp = cc.find("Canvas"); // 假设 Canvas 节点 // if (canvasTemp) { // this.JiaZai = canvasTemp.getComponent("JiaZai"); // } } /** * 排行榜数据中的 userCat 是上次提交关卡时的快照。 * 当前用户在 career2 的顶部榜和城市桌位,都使用本地实时选择的猫。 */ private applyCurrentUserCat(data: any, topData: any) { const currentUid = cc.fx.GameConfig.GM_INFO.uid; const catMatch = String( cc.fx.GameConfig.GM_INFO.useravatarCat || "" ).match(/\d+/); if (!currentUid || !catMatch) { return; } const currentCat = catMatch[0]; const applyToUsers = (users: any[]) => { if (!Array.isArray(users)) { return; } users.forEach(user => { if (user && user.id == currentUid) { user.userCat = currentCat; } }); }; applyToUsers(topData); if (Array.isArray(data)) { data.forEach(cityData => { applyToUsers(cityData && cityData.rankingData); }); } } start() { } //实际设置排行数据 // public getRankData(data) { // if (data) { // // console.log(data); // cc.fx.GameTool.getRankData(data, this, 6); // this.setPic(this.selfData.pic); // } // } //返回按钮 public backClick() { cc.director.loadScene("LoadScene"); } //最上方用户动画 //设置头像 处理的逻辑比较多,不用公共类的了 // public setPic(pic) { // this.phone.node.parent.getChildByName("icon").active = false; // this.phone.node.active = false; // fetch(pic) // .then(response => { // return response.headers.get('Content-Length'); // }) // .then(errNo => { // if (errNo == "5093") { // this.phone.node.parent.getChildByName("icon").active = true; // } // }) // .catch(error => { // // console.error('Error fetching X-Info:', error); // }); // var self = this; // cc.assetManager.loadRemote(pic, { ext: '.png' }, (err, texture: cc.Texture2D) => { // if (texture) { // self.phone.node.active = true; // self.phone.spriteFrame = new cc.SpriteFrame(texture); // } // else { // } // }) // } public openInformation(event: cc.Event, customEventData?: string) { this.vibrateButtonClick(event); console.log("点击个人信息", event); const clickedItem = this.getInformationTarget(event && event.target as cc.Node); const rankData = this.getInformationRankData(clickedItem); if (!clickedItem || !rankData || !this.Information) { return; } // Information 只实例化一次。点击其他排行项时复用节点,避免频繁创建/销毁。 let informationNode = this.informationNode; if (!informationNode || !cc.isValid(informationNode, true)) { informationNode = cc.instantiate(this.Information); this.informationNode = informationNode; const defaultIconNode = cc.find("mask/icon", informationNode); const defaultIconSprite = defaultIconNode && defaultIconNode.getComponent(cc.Sprite); this.informationDefaultIconFrame = defaultIconSprite ? defaultIconSprite.spriteFrame : null; this.informationBaseScaleX = informationNode.scaleX; this.informationBaseScaleY = informationNode.scaleY; } else { this.stopInformationTween(); informationNode.active = false; } // topRank 本身跟随 ScrollView content 移动,同时可通过 zIndex 覆盖其他排行项。 informationNode.parent = clickedItem.parent; informationNode.zIndex = 9999; this.renderInformationAvatar(rankData, informationNode); this.renderInformationText(rankData, informationNode); this.setInformationPosition(clickedItem, informationNode); // Information 预制体默认是 inactive,数据和位置设置好后播放气泡弹出动画。 this.playInformationIn(informationNode); // 每次点击都重新计算 3 秒显示时间,始终只有一个有效定时任务。 this.unschedule(this.informationAutoDestroy); this.scheduleOnce(this.informationAutoDestroy, 3); } /** 从按钮事件节点向上查找对应的顶部或城市排行用户节点。 */ private getInformationTarget(target: cc.Node): cc.Node { let current = target; while (current) { if (current.name === "careerRankItem" || current.name === "careerCityItem") { return current; } if (current === this.node) { break; } current = current.parent; } return null; } /** 获取顶部排行或城市排行中,被点击用户节点所对应的数据。 */ private getInformationRankData(clickedItem: cc.Node): any { if (!clickedItem || !clickedItem.parent) { return null; } if (clickedItem.name === "careerRankItem") { // topRank 前三个子节点是 gun1、gun2、gun3,careerRankItem 从下标 3 开始。 const dataIndex = clickedItem.getSiblingIndex() - 3; return dataIndex >= 0 && dataIndex < this.topRankData.length ? this.topRankData[dataIndex] : null; } if (clickedItem.name === "careerCityItem") { const cityItem = clickedItem.parent.getComponent(CareerItem2); return cityItem ? cityItem.getRankingDataByNode(clickedItem) : null; } return null; } /** 根据排行数据重新动态创建 Information 中的头像或 Spine。 */ private renderInformationAvatar(rankData: any, informationNode: cc.Node) { const targetMask = informationNode.getChildByName("mask"); if (!targetMask) { return; } const targetIcon = targetMask.getChildByName("icon"); const targetSp = targetMask.getChildByName("sp"); const targetSprite = targetIcon && targetIcon.getComponent(cc.Sprite); const useravatar = rankData && rankData.useravatar; const avatarValue = useravatar == null ? "" : String(useravatar); const useSpine = /^([0-9]|1[0-9]|2[0-6])$/.test(avatarValue); const requestId = ++this.informationAvatarRequestId; this.clearInformationRemoteIconFrame(); if (targetIcon) { targetIcon.active = !useSpine; targetIcon.opacity = 255; targetIcon.color = cc.Color.WHITE; if (targetSprite) { targetSprite.spriteFrame = this.informationDefaultIconFrame; } } if (targetSp) { targetSp.active = useSpine; targetSp.opacity = 255; targetSp.color = cc.Color.WHITE; } if (useSpine && targetSp) { this.loadInformationSpine(targetSp, avatarValue, informationNode, requestId); } else if (avatarValue && targetIcon && targetSprite) { this.loadInformationRemoteIcon( avatarValue, targetIcon, targetSprite, informationNode, requestId ); } } /** 与 CareerList2 一致,根据 useravatar 动态加载 Spine。 */ private loadInformationSpine( spineNode: cc.Node, useravatar: string, informationNode: cc.Node, requestId: number ) { const spineName = cc.fx.GameTool.getActionName(useravatar); const spinePath = `spine/${spineName}/skeleton`; cc.resources.load(spinePath, sp.SkeletonData, (err: Error, skeletonData: sp.SkeletonData) => { if (err) { console.error(`Information加载Spine失败: ${spinePath}`, err); return; } if (!this.isCurrentInformationRequest(informationNode, requestId) || !cc.isValid(spineNode, true)) { return; } let skeleton = spineNode.getComponent(sp.Skeleton); if (!skeleton) { skeleton = spineNode.addComponent(sp.Skeleton); } skeleton.skeletonData = skeletonData; skeleton.setAnimation(0, this.getInformationSpineAnimation(spineName), true); skeleton.premultipliedAlpha = true; skeleton.timeScale = 1; }); } /** 与 GameTool.loadSpineSimple 使用相同的动画名称转换规则。 */ private getInformationSpineAnimation(spineName: string): string { let actionNumber = spineName.indexOf("action") === 0 ? spineName.substring(6) : ""; const specialAnimationNames = { "19": "009", "20": "007", "21": "003", "22": "005", "23": "006", "24": "008", "25": "002", "26": "001" }; return specialAnimationNames[actionNumber] || actionNumber; } /** 与 CareerList2.setPic 一致,远程加载图片并创建新的 SpriteFrame。 */ private loadInformationRemoteIcon( url: string, iconNode: cc.Node, sprite: cc.Sprite, informationNode: cc.Node, requestId: number ) { cc.assetManager.loadRemote(url, { ext: ".png" }, (err, texture: cc.Texture2D) => { if (err || !texture) { return; } if (!this.isCurrentInformationRequest(informationNode, requestId) || !cc.isValid(iconNode, true)) { return; } this.clearInformationRemoteIconFrame(); const spriteFrame = new cc.SpriteFrame(texture); this.informationRemoteIconFrame = spriteFrame; sprite.spriteFrame = spriteFrame; }); } private isCurrentInformationRequest(informationNode: cc.Node, requestId: number): boolean { return this.informationNode === informationNode && this.informationAvatarRequestId === requestId && cc.isValid(informationNode, true); } private clearInformationRemoteIconFrame() { const oldSpriteFrame = this.informationRemoteIconFrame; this.informationRemoteIconFrame = null; if (oldSpriteFrame && cc.isValid(oldSpriteFrame, true)) { oldSpriteFrame.destroy(); } } /** 气泡式出现:缩放和透明度弹出,再轻微回落到原始大小。 */ private playInformationIn(informationNode: cc.Node) { this.stopInformationTween(); informationNode.active = true; informationNode.opacity = 0; informationNode.scaleX = this.informationBaseScaleX * 0.25; informationNode.scaleY = this.informationBaseScaleY * 0.25; const tween = cc.tween(informationNode) .to(0.14, { opacity: 255, scaleX: this.informationBaseScaleX * 1.22, scaleY: this.informationBaseScaleY * 1.22 }, { easing: "backOut" }) .to(0.06, { scaleX: this.informationBaseScaleX, scaleY: this.informationBaseScaleY }, { easing: "sineOut" }) .call(() => { if (this.informationTween === tween) { this.informationTween = null; } }); this.informationTween = tween; tween.start(); } /** 气泡式消失:先轻微鼓起,再缩小淡出,完成后销毁节点。 */ private playInformationOut() { const informationNode = this.informationNode; if (!informationNode || !cc.isValid(informationNode, true)) { this.destroyInformation(); return; } this.unschedule(this.informationAutoDestroy); this.informationAvatarRequestId++; this.stopInformationTween(); const tween = cc.tween(informationNode) .to(0.05, { scaleX: this.informationBaseScaleX * 1.15, scaleY: this.informationBaseScaleY * 1.15 }, { easing: "sineOut" }) .to(0.12, { opacity: 0, scaleX: this.informationBaseScaleX * 0.20, scaleY: this.informationBaseScaleY * 0.20 }, { easing: "backIn" }) .call(() => { if (this.informationTween === tween) { this.informationTween = null; this.destroyInformation(); } }); this.informationTween = tween; tween.start(); } private stopInformationTween() { const tween = this.informationTween; this.informationTween = null; if (tween) { tween.stop(); } } /** 使用与 topRank 相同的用户名长度规则,并显示玩家关卡数据。 */ private renderInformationText(rankData: any, informationNode: cc.Node) { const nameNode = informationNode.getChildByName("name"); const levelNode = informationNode.getChildByName("level"); const nameLabel = nameNode && nameNode.getComponent(cc.Label); const levelLabel = levelNode && levelNode.getComponent(cc.Label); if (nameLabel) { let username = cc.fx.GameTool.subName(rankData.username, 5); if (username === "user") { username = "匿名玩家"; } nameLabel.string = username == null ? "" : String(username); } if (levelLabel) { levelLabel.string = rankData.addLevel == null ? "" : String(rankData.addLevel); } } /** 使用世界坐标换算,把 Information 放在点击项视觉区域的正上方。 */ private setInformationPosition(clickedItem: cc.Node, informationNode: cc.Node) { const itemTopLocal = cc.v2( (0.5 - clickedItem.anchorX) * clickedItem.width, (1 - clickedItem.anchorY) * clickedItem.height ); const itemTopWorld = clickedItem.convertToWorldSpaceAR(itemTopLocal); const informationParent = informationNode.parent; if (!informationParent) { return; } const itemTopInParent = informationParent.convertToNodeSpaceAR(itemTopWorld); const bgNode = informationNode.getChildByName("bg"); const informationHeight = bgNode ? bgNode.height * Math.abs(bgNode.scaleY) : 212; const gap = 10; const isCityItem = clickedItem.name === "careerCityItem"; const offsetX = isCityItem ? 3 : 7; const offsetY = isCityItem ? -20 : -140; informationNode.setPosition( itemTopInParent.x + offsetX, itemTopInParent.y + informationHeight / 2 + gap + offsetY ); } private destroyInformation() { this.unschedule(this.informationAutoDestroy); this.stopInformationTween(); this.informationAvatarRequestId++; this.clearInformationRemoteIconFrame(); const oldInformation = this.informationNode; this.informationNode = null; this.informationDefaultIconFrame = null; this.informationBaseScaleX = 1; this.informationBaseScaleY = 1; if (oldInformation && cc.isValid(oldInformation, true)) { oldInformation.active = false; oldInformation.destroy(); } } public setData(data, topData) { this.applyCurrentUserCat(this.listData, topData); this.selfData = data; this.topRankData = Array.isArray(topData) ? topData : []; this.rankList.setData(this.listData, topData); if (data) { if (data.length) { if (data.length > 0) { this.rankNumber = data[0].rank; this.rankTotal = data.length; } } } } onDestroy() { this.destroyInformation(); } }