You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
AdLike2048/assets/script/ui/PopDailyShopLottery/DailyShopLotteryIcon.ts

474 lines
19 KiB

import { Color, EventTouch, Label, Node, Sorting2D, UIRenderer, UITransform, Vec3 } from 'cc';
import { SpriteLoad } from 'db://assets/mx/components/SpriteLoad';
import { GBundle, GPath } from '../../game/ConfigRes';
import { DailyShopKind, DailyShopParsed } from '../../manager/DailyShopTypes';
/** 道具名称描边色(按品质) */
const NAME_OUTLINE_HEX: Record<number, string> = {
1: '#386c42',
2: '#285c6d',
3: '#623073',
4: '#734930',
};
/** 抽奖弹窗:shopSkill 品质框与图标 */
export class DailyShopLotteryIcon {
/** 抽奖格品质框:skill_lottery{quality} */
static lotteryQualityFrame(quality: number): string {
const q = Math.max(1, Math.floor(Number(quality) || 0));
return `skill_lottery${q}`;
}
/** 被动槽品质框:skill_square_small_{quality} */
static passiveQualityFrame(quality: number): string {
const q = Math.max(1, Math.floor(Number(quality) || 0));
return `skill_square_small_${q}`;
}
/** 主动槽品质框:skill_circle_small{quality} */
static activeQualityFrame(quality: number): string {
const q = Math.max(1, Math.floor(Number(quality) || 0));
return `skill_circle_small${q}`;
}
/** 槽位品质框 */
static slotQualityFrame(kind: DailyShopKind, quality: number): string {
return kind === DailyShopKind.Active
? this.activeQualityFrame(quality)
: this.passiveQualityFrame(quality);
}
/** 货架品质框:主动 skill_circle_big{q},被动 skill_square_big_{q} */
static shopQualityFrame(kind: DailyShopKind, quality: number): string {
const q = Math.max(1, Math.floor(Number(quality) || 0));
return kind === DailyShopKind.Active
? `skill_circle_big${q}`
: `skill_square_big_${q}`;
}
/** 货架名称品质底:nameQuality{q} */
static shopNameQualityFrame(quality: number): string {
const q = Math.max(1, Math.floor(Number(quality) || 0));
return `nameQuality${q}`;
}
/** 替换弹窗新道具品质框:skill_square_big_{q} */
static replaceNewQualityFrame(quality: number): string {
const q = Math.max(1, Math.floor(Number(quality) || 0));
return `skill_square_big_${q}`;
}
/** 替换弹窗列表项品质框:skill_square_small_{q} */
static replaceListQualityFrame(quality: number): string {
const q = Math.max(1, Math.floor(Number(quality) || 0));
return `skill_square_small_${q}`;
}
/** Battlebuff 表描述 */
static battleBuffDesc(parsed: DailyShopParsed): string {
const row = parsed.row as ITableBattlebuff;
return String(row?.desc ?? '');
}
/** 名称 Label 按品质设置描边色 */
static applyNameOutline(label: Label | null | undefined, quality: number): void {
if (!label) return;
const hex = NAME_OUTLINE_HEX[Math.max(1, Math.floor(Number(quality) || 0))];
if (!hex) return;
label.enableOutline = true;
label.outlineColor = Color.fromHEX(new Color(), hex);
}
/** 道具图标名(优先 parsed.icon,其次表 icon,最后 name) */
static itemIconName(parsed: DailyShopParsed): string {
if (parsed.icon) return String(parsed.icon);
const row = parsed.row as { icon?: string; name?: string } | null | undefined;
const tableIcon = row?.icon != null ? String(row.icon).trim() : '';
if (tableIcon) return tableIcon;
return String(parsed.name ?? row?.name ?? '');
}
/** 对已挂载 SpriteLoad 的节点设图;缺失组件时 warn */
private static _setNodeSprite(node: Node | null | undefined, assetName: string): void {
if (!node?.isValid) return;
const sl = node.getComponent(SpriteLoad);
if (!sl) {
console.log('[DailyShop] 节点缺少 SpriteLoad', node.name);
return;
}
sl.setSprite(GPath.ShopSkill(assetName), GBundle.Items);
}
/**
* 绑定品质框 + 图标
* @param root 根节点(含 quality / icon 子节点)
* @param frameName 品质框图名
* @param iconName 图标图名
*/
static bindFrameAndIcon(root: Node, frameName: string, iconName: string): void {
const qualityNode = root.getChildByName('quality') ?? root;
this._setNodeSprite(qualityNode, frameName);
const icon = qualityNode.getChildByName('icon') ?? root.getChildByName('icon');
this._setNodeSprite(icon, iconName);
}
/** 绑定携带槽道具;品质框按槽位类型(ui_skillMain 圆框 / ui_skillPassive 方框) */
static bindSlotItem(root: Node, parsed: DailyShopParsed, slotKind: DailyShopKind): void {
const q = Math.max(1, Math.floor(Number(parsed.quality) || 0));
this.bindFrameAndIcon(
root,
this.slotQualityFrame(slotKind, q),
this.itemIconName(parsed),
);
}
/** 绑定抽奖格道具 */
static bindLotteryItem(root: Node, parsed: DailyShopParsed): void {
const q = Math.max(1, Math.floor(Number(parsed.quality) || 0));
this.bindFrameAndIcon(
root,
this.lotteryQualityFrame(q),
this.itemIconName(parsed),
);
}
/** 货架 icon 节点(quality/icon) */
static resolveShopIcon(root: Node): Node | null {
const qualityNode = root.getChildByName('quality');
const icon = qualityNode?.getChildByName('icon') ?? root.getChildByName('icon');
return icon?.isValid ? icon : null;
}
/** 绑定货架商品格(quality / quality.icon / nameQuality / main) */
static bindShopItem(root: Node, parsed: DailyShopParsed): void {
const q = Math.max(1, Math.floor(Number(parsed.quality) || 0));
const iconName = this.itemIconName(parsed);
const qualityNode = root.getChildByName('quality');
if (qualityNode?.isValid) {
this._setNodeSprite(qualityNode, this.shopQualityFrame(parsed.kind, q));
this._setNodeSprite(qualityNode.getChildByName('icon'), iconName);
} else {
this.bindFrameAndIcon(
root,
this.shopQualityFrame(parsed.kind, q),
iconName,
);
}
this._setNodeSprite(root.getChildByName('nameQuality'), this.shopNameQualityFrame(q));
const main = root.getChildByName('main');
if (main?.isValid) {
main.active = parsed.kind === DailyShopKind.Active;
}
}
/** 绑定替换弹窗新道具 ui_getQuality(品质框在根节点) */
static bindReplaceNewItem(root: Node, parsed: DailyShopParsed): void {
const q = Math.max(1, Math.floor(Number(parsed.quality) || 0));
const iconName = this.itemIconName(parsed);
this._setNodeSprite(root, this.replaceNewQualityFrame(q));
this._setNodeSprite(root.getChildByName('icon'), iconName);
const main = root.getChildByName('main');
if (main?.isValid) {
main.active = parsed.kind === DailyShopKind.Active;
}
}
/** 绑定替换弹窗列表项 ui_item */
static bindReplaceListItem(root: Node, parsed: DailyShopParsed): void {
const q = Math.max(1, Math.floor(Number(parsed.quality) || 0));
const iconName = this.itemIconName(parsed);
const qualityNode = root.getChildByName('quality') ?? root;
this._setNodeSprite(qualityNode, this.replaceListQualityFrame(q));
const icon = qualityNode.getChildByName('icon') ?? root.getChildByName('icon');
this._setNodeSprite(icon, iconName);
}
}
/** 描述浮层 Y 偏移配置 */
export type DailyShopOwnedDescOpts = {
/** 主动槽:相对技能格中心的世界 Y 偏移 */
activeYOffset?: number;
/** 被动槽:相对技能格中心的世界 Y 偏移 */
passiveYOffset?: number;
};
/** 携带槽描述浮层(PopDailyShopBuy / PopDailyShopLottery 共用) */
export class DailyShopOwnedDesc {
/** 默认主动槽描述框 Y 偏移(我方 ui_desc) */
private static readonly DEFAULT_ACTIVE_Y_OFFSET = 40;
/** 默认被动槽描述框 Y 偏移(我方 ui_desc) */
private static readonly DEFAULT_PASSIVE_Y_OFFSET = 30;
/** 与字块 canUpEff 同 Sorting2D 层 */
private static readonly DESC_SORT_LAYER = 3;
/** 高于 canUpEff(4) / buff 特效(5) / 营地血条(20) */
private static readonly DESC_SORT_ORDER = 30;
private readonly _uiDescLeft: Node | null;
private readonly _uiDescRight: Node | null;
private readonly _activeYOffset: number;
private readonly _passiveYOffset: number;
private _peerDesc: DailyShopOwnedDesc | null = null;
/**
* @param uiDescLeft 点击 X<0 时显示的描述根节点
* @param uiDescRight 点击 X>=0 时显示的描述根节点
* @param opts Y 偏移;敌方 ui_otherDesc 使用 -66 / -35
*/
constructor(uiDescLeft: Node | null, uiDescRight: Node | null, opts?: DailyShopOwnedDescOpts) {
this._uiDescLeft = uiDescLeft;
this._uiDescRight = uiDescRight;
this._activeYOffset = opts?.activeYOffset ?? DailyShopOwnedDesc.DEFAULT_ACTIVE_Y_OFFSET;
this._passiveYOffset = opts?.passiveYOffset ?? DailyShopOwnedDesc.DEFAULT_PASSIVE_Y_OFFSET;
}
/** 互斥显示:打开本浮层前先关闭对方(我方/敌方 skill 描述) */
setPeerDesc(peer: DailyShopOwnedDesc | null): void {
this._peerDesc = peer;
}
/** 初始化:默认隐藏,并置于父节点最上层 */
init(): void {
if (this._uiDescLeft?.isValid) this._uiDescLeft.active = false;
if (this._uiDescRight?.isValid) this._uiDescRight.active = false;
this._raiseDescLayer(this._uiDescLeft);
this._raiseDescLayer(this._uiDescRight);
}
/** 注册点击描述层关闭 */
bindDismiss(): void {
this._bindDismissOnRoot(this._uiDescLeft);
this._bindDismissOnRoot(this._uiDescRight);
}
/** 注销关闭监听并隐藏 */
unbindDismiss(): void {
this._unbindDismissOnRoot(this._uiDescLeft);
this._unbindDismissOnRoot(this._uiDescRight);
this.hide();
}
/** 绑定携带槽点击显示描述 */
bindSlotItem(item: Node, parsed: DailyShopParsed, slotKind: DailyShopKind): void {
if (!item?.isValid) return;
item['dailyShopOwnedParsed'] = parsed;
item['dailyShopOwnedSlotKind'] = slotKind;
item.targetOff(Node.EventType.TOUCH_END);
item.on(Node.EventType.TOUCH_END, this._onClickOwnedSlot, this);
}
/**
* 绑定战斗 skill 格点击描述(仅移除本类监听,不影响拖拽等其它 TOUCH_END)。
* @param item skill 格根节点
* @param parsed buff
* @param slotKind 主动/被动
*/
bindBattleSkillItem(item: Node, parsed: DailyShopParsed, slotKind: DailyShopKind): void {
if (!item?.isValid) return;
item['dailyShopOwnedParsed'] = parsed;
item['dailyShopOwnedSlotKind'] = slotKind;
item.off(Node.EventType.TOUCH_END, this._onClickOwnedSlot, this);
item.on(Node.EventType.TOUCH_END, this._onClickOwnedSlot, this);
}
/** 注销战斗 skill 格描述点击 */
unbindBattleSkillItem(item: Node): void {
if (!item?.isValid) return;
item.off(Node.EventType.TOUCH_END, this._onClickOwnedSlot, this);
}
/** 绑定抽奖格点击显示描述(锚点为格子中心) */
bindLotteryItem(item: Node, parsed: DailyShopParsed): void {
if (!item?.isValid) return;
item['dailyShopLotteryParsed'] = parsed;
item.targetOff(Node.EventType.TOUCH_END);
item.on(Node.EventType.TOUCH_END, this._onClickLotteryItem, this);
}
/** 绑定货架商品点击显示描述(锚点为商品格中心;含售罄) */
bindShopItem(item: Node, parsed: DailyShopParsed): void {
if (!item?.isValid) return;
item['dailyShopGoodsParsed'] = parsed;
item.targetOff(Node.EventType.TOUCH_END);
item.on(Node.EventType.TOUCH_END, this._onClickShopItem, this);
}
/** 隐藏描述浮层 */
hide(): void {
if (this._uiDescLeft?.isValid) this._uiDescLeft.active = false;
if (this._uiDescRight?.isValid) this._uiDescRight.active = false;
}
/** 点击描述层关闭 */
private _onClickUiDesc(): void {
this.hide();
}
/** 点击携带槽显示描述 */
private _onClickOwnedSlot(event: EventTouch): void {
const target = event.target as Node;
if (this._isDeleteBtnTouch(target)) return;
const item = event.currentTarget as Node;
const parsed = item?.['dailyShopOwnedParsed'] as DailyShopParsed | undefined;
const slotKind = Number(item?.['dailyShopOwnedSlotKind']) as DailyShopKind | undefined;
if (!parsed || !item?.isValid) return;
this.show(item, parsed, slotKind ?? parsed.kind);
}
/** 点击抽奖格:描述框锚在格子中心 */
private _onClickLotteryItem(event: EventTouch): void {
const item = event.currentTarget as Node;
const parsed = item?.['dailyShopLotteryParsed'] as DailyShopParsed | undefined;
if (!parsed || !item?.isValid) return;
this.showAtWorld(this._resolveNodeCenterWorld(item), parsed);
}
/** 点击货架商品:描述框锚在商品格中心(跳过购买/售罄标记) */
private _onClickShopItem(event: EventTouch): void {
const target = event.target as Node;
if (this._isShopActionTouch(target)) return;
const item = event.currentTarget as Node;
const parsed = item?.['dailyShopGoodsParsed'] as DailyShopParsed | undefined;
if (!parsed || !item?.isValid) return;
this.showAtWorld(this._resolveNodeCenterWorld(item), parsed);
}
/** 在锚点上方显示描述浮层(按 X 正负选左/右模板,避免超出屏幕) */
show(anchor: Node, parsed: DailyShopParsed, slotKind?: DailyShopKind): void {
if (!anchor?.isValid) return;
const world = anchor.getWorldPosition();
const kind = slotKind ?? parsed.kind;
const yOffset = kind === DailyShopKind.Active
? this._activeYOffset
: this._passiveYOffset;
this.showAtWorld(new Vec3(world.x, world.y + yOffset, world.z), parsed);
}
/**
* 在指定世界坐标显示描述(按 X 正负选左/右模板)。
* @param world 锚点世界坐标(通常为点击中心)
* @param parsed 商品
*/
showAtWorld(world: Readonly<Vec3>, parsed: DailyShopParsed): void {
this._peerDesc?.hide();
const refUt = this._resolveRefTransform();
if (!refUt) {
console.log('[DailyShop] ui_descLeft/ui_descRight 缺少 UITransform');
return;
}
const anchorPos = refUt.convertToNodeSpaceAR(new Vec3(world.x, world.y, world.z));
const useLeft = anchorPos.x < 0;
const descRoot = useLeft ? this._uiDescLeft : this._uiDescRight;
if (!descRoot?.isValid) {
console.log('[DailyShop] ui_desc 缺失', useLeft ? 'Left' : 'Right');
return;
}
const descBg = descRoot.getChildByName('descBg');
if (!descBg?.isValid) {
console.log('[DailyShop] ui_desc 缺少 descBg', descRoot.name);
return;
}
const descUt = descRoot.getComponent(UITransform);
if (!descUt) {
console.log('[DailyShop] ui_desc 缺少 UITransform', descRoot.name);
return;
}
this.hide();
const bgPos = descUt.convertToNodeSpaceAR(new Vec3(world.x, world.y, world.z));
descBg.setPosition(bgPos);
const nameNode = descBg.getChildByName('name');
this._setLabel(nameNode, parsed.name);
DailyShopLotteryIcon.applyNameOutline(nameNode?.getComponent(Label), parsed.quality);
this._setLabel(descBg.getChildByName('desc'), DailyShopLotteryIcon.battleBuffDesc(parsed));
this._raiseDescLayer(descRoot);
descRoot.active = true;
}
/** 描述层 sibling + Sorting2D 置顶(战斗字块 canUpEff 走 Sorting2D,仅 sibling 不够) */
private _raiseDescLayer(descRoot: Node | null): void {
if (!descRoot?.isValid) return;
const parent = descRoot.parent;
if (parent?.isValid) {
descRoot.setSiblingIndex(parent.children.length - 1);
}
this._applyDescSortingTree(descRoot);
}
/** 为描述层所有 UIRenderer 节点设置更高 Sorting2D */
private _applyDescSortingTree(root: Node): void {
if (!root?.isValid) return;
if (root.getComponent(UIRenderer)) {
let sorting = root.getComponent(Sorting2D);
if (!sorting) sorting = root.addComponent(Sorting2D);
sorting.sortingLayer = DailyShopOwnedDesc.DESC_SORT_LAYER;
sorting.sortingOrder = DailyShopOwnedDesc.DESC_SORT_ORDER;
}
const kids = root.children;
for (let i = 0; i < kids.length; i++) {
this._applyDescSortingTree(kids[i]);
}
}
/** 取左/右描述层共用的坐标参照 UITransform */
private _resolveRefTransform(): UITransform | null {
if (this._uiDescLeft?.isValid) {
const ut = this._uiDescLeft.getComponent(UITransform);
if (ut) return ut;
}
if (this._uiDescRight?.isValid) {
return this._uiDescRight.getComponent(UITransform) ?? null;
}
return null;
}
/**
* 节点中心世界坐标。
* @param node 目标节点
*/
private _resolveNodeCenterWorld(node: Node): Vec3 {
const ut = node.getComponent(UITransform);
if (ut) return ut.convertToWorldSpaceAR(new Vec3(0, 0, 0));
return node.getWorldPosition();
}
/** 注册单个描述根节点关闭 */
private _bindDismissOnRoot(root: Node | null): void {
if (!root?.isValid) return;
root.on(Node.EventType.TOUCH_END, this._onClickUiDesc, this);
}
/** 注销单个描述根节点关闭 */
private _unbindDismissOnRoot(root: Node | null): void {
if (!root?.isValid) return;
root.off(Node.EventType.TOUCH_END, this._onClickUiDesc, this);
}
/** 是否点在 deleteBtn 上 */
private _isDeleteBtnTouch(node: Node | null): boolean {
let cur = node;
while (cur?.isValid) {
if (cur.name === 'deleteBtn') return true;
cur = cur.parent;
}
return false;
}
/** 是否点在货架购买/售罄控件上 */
private _isShopActionTouch(node: Node | null): boolean {
let cur = node;
while (cur?.isValid) {
const name = cur.name;
if (name === 'buyBtn' || name === 'buyBtnAd' || name === 'complete') return true;
cur = cur.parent;
}
return false;
}
/** 设置 Label 文本 */
private _setLabel(node: Node | null | undefined, text: string): void {
if (!node?.isValid) return;
const lb = node.getComponent(Label);
if (lb) lb.string = text;
}
}