527 lines
19 KiB
TypeScript
527 lines
19 KiB
TypeScript
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();
|
||
}
|
||
|
||
}
|