MatchMaster/assets/career2/script/CareerManager2.ts
2026-08-03 15:53:17 +08:00

527 lines
19 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 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、gun3careerRankItem 从下标 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();
}
}