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/BattleGame/BattleGameOtherSkillView.ts

489 lines
18 KiB

11 hours ago
import { Color, instantiate, Label, Layout, Node, Sprite, UITransform } from 'cc';
3 weeks ago
import { SpriteLoad } from 'db://assets/mx/components/SpriteLoad';
import { GBundle, GPath } from '../../game/ConfigRes';
import { BattleBuffCarryStore } from '../../manager/battleBuff/BattleBuffCarryStore';
import { isBattleBuffPassiveBaseThunder, passiveBuffUsesBattleGameCd } from '../../manager/battleBuff/BattleBuffPassiveSkillUtil';
import { DailyShopKind, type DailyShopParsed } from '../../manager/DailyShopTypes';
import { DailyShopLotteryIcon, DailyShopOwnedDesc } from '../PopDailyShopLottery/DailyShopLotteryIcon';
import { AiHeadlessBagHost } from '../../battle/ai/runtime/AiHeadlessBagHost';
import { getBattleBuffBaseThunderHost } from '../../battle/combat/BattleBuffBaseThunderHost';
import type { AiActiveSkillUiSlot } from '../../battle/ai/runtime/AiActiveSkillHost';
import type { AiPassiveCdUiSlot } from '../../battle/ai/runtime/AiPassiveCdHost';
import { AiBattleBridge } from '../../battle/ai/bridge/AiBattleBridge';
import { applySkillCdMaskFillRange, BattleGameDailyShopSkillSlot, setupSkillCdMaskSprite } from './BattleGameDailyShopSkillView';
/** 被动名称描边(与主动同品质色) */
const PASSIVE_NAME_OUTLINE_HEX: Record<number, string> = {
1: '#386c42',
2: '#285c6d',
3: '#623073',
4: '#734930',
};
11 hours ago
/** 敌方被动:左右各最多展示数 */
const PASSIVE_PER_SIDE = 3;
/** 右侧被动列表节点名(运行时创建) */
const PASSIVE_RIGHT_NAME = 'ui_otherskill2ListR';
/** 左右列表贴向屏幕中线的间距(父节点本地像素) */
const PASSIVE_CENTER_GAP = 16;
3 weeks ago
/** 敌方被动格:仅展示,不触发玩家侧 CD 产字 */
class BattleGameOtherPassiveSlot {
private readonly _root: Node;
private readonly _quality: Node | null;
private readonly _icon: Node | null;
private readonly _mask: Node | null;
private readonly _maskSprite: Sprite | null;
private readonly _name: Label | null;
private _buffId = 0;
private _usesCd = false;
private _cdTotalSec = 0;
private _cdLeftSec = 0;
private _parsed: DailyShopParsed | null = null;
/** @param root skillGrid 实例 */
constructor(root: Node) {
this._root = root;
this._quality = root.getChildByName('quality');
this._icon = root.getChildByName('icon') ?? this._quality?.getChildByName('icon');
this._mask = root.getChildByName('mask');
this._maskSprite = this._mask?.getComponent(Sprite) ?? null;
if (this._maskSprite?.isValid) setupSkillCdMaskSprite(this._maskSprite);
this._name = root.getChildByName('name')?.getComponent(Label) ?? null;
}
/**
* 绑定被动展示。
* @param parsed 被动解析
*/
bind(parsed: DailyShopParsed | null): void {
if (!parsed || parsed.kind !== DailyShopKind.Passive) {
this._root.active = false;
this._buffId = 0;
this._usesCd = false;
this._cdTotalSec = 0;
this._cdLeftSec = 0;
this._parsed = null;
return;
}
const row = parsed.row as ITableBattlebuff;
const ft = Math.floor(Number(row?.functiontype) || 0);
this._buffId = parsed.id;
this._parsed = parsed;
this._usesCd = passiveBuffUsesBattleGameCd(ft);
this._root.active = true;
const quality = Math.max(1, Math.floor(Number(parsed.quality) || 0));
this._setQualitySprite(quality);
this._setIconSprite(DailyShopLotteryIcon.itemIconName(parsed));
this._setName(parsed.name, quality);
if (this._usesCd) {
this._cdTotalSec = Math.max(0, parsed.cd | 0);
this._cdLeftSec = this._cdTotalSec;
} else {
this._cdTotalSec = 0;
this._cdLeftSec = 0;
}
this._refreshCdVisual();
this.syncBaseThunderMask();
}
/** 当前 buff id */
get buffId(): number {
return this._buffId;
}
/** 被动图标节点(飞行动画复制影子) */
getIconNode(): Node | null {
return this._icon?.isValid ? this._icon : null;
}
/** 配表 functiontype */
getFunctionType(): number {
const row = this._parsed?.row as ITableBattlebuff | undefined;
return Math.floor(Number(row?.functiontype) || 0);
}
/** 根节点 */
get root(): Node {
return this._root;
}
/** 绑定的被动解析 */
getParsed(): DailyShopParsed | null {
return this._parsed;
}
/**
* 同步 AI 侧 CD。
* @param cdLeftSec 剩余
* @param cdTotalSec 总长
*/
syncCd(cdLeftSec: number, cdTotalSec: number): void {
if (!this._usesCd) return;
this._cdTotalSec = Math.max(0, cdTotalSec);
this._cdLeftSec = Math.max(0, cdLeftSec);
this._refreshCdVisual();
this.syncBaseThunderMask();
}
/** 九霄神雷次数用尽:直接盖满 mask,不刷新 CD */
syncBaseThunderMask(): void {
const row = this._parsed?.row as ITableBattlebuff | undefined;
if (!isBattleBuffPassiveBaseThunder(row)) return;
if (!getBattleBuffBaseThunderHost().isTriggersExhausted(AiHeadlessBagHost.AI_PLAYER_ID)) return;
if (this._mask?.isValid) this._mask.active = true;
if (this._maskSprite?.isValid) applySkillCdMaskFillRange(this._maskSprite, 1);
}
private _setQualitySprite(quality: number): void {
if (!this._quality?.isValid) return;
const sl = this._quality.getComponent(SpriteLoad);
if (!sl) {
console.log('[BattleGame] 敌方被动 quality 缺少 SpriteLoad', this._root.name);
return;
}
sl.setSprite(GPath.BattleGamePassiveSkillBg(quality), GBundle.BattleGame);
}
private _setIconSprite(iconName: string): void {
if (!this._icon?.isValid) return;
const sl = this._icon.getComponent(SpriteLoad);
if (!sl) {
console.log('[BattleGame] 敌方被动 icon 缺少 SpriteLoad', this._root.name);
return;
}
sl.setSprite(GPath.ShopSkill(iconName), GBundle.Items);
}
private _setName(name: string, quality: number): void {
if (!this._name?.isValid) return;
this._name.string = name;
const hex = PASSIVE_NAME_OUTLINE_HEX[quality];
if (!hex) {
console.log('[BattleGame] 敌方被动未知品质描边', quality);
return;
}
this._name.enableOutline = true;
this._name.outlineColor = Color.fromHEX(new Color(), hex);
}
private _refreshCdVisual(): void {
if (!this._usesCd) {
if (this._mask?.isValid) this._mask.active = false;
return;
}
const onCd = this._cdLeftSec > 0;
if (this._mask?.isValid) this._mask.active = onCd;
if (onCd && this._maskSprite?.isValid && this._cdTotalSec > 0) {
applySkillCdMaskFillRange(this._maskSprite, this._cdLeftSec / this._cdTotalSec);
}
}
}
/**
* 战斗界面敌方 buff:ui_otherskill1List(主动)/ ui_otherskill2List(被动)。
11 hours ago
* 被动左右最多各 3 个;右侧列表运行时镜像创建。
3 weeks ago
* 仅展示与同步 AI CD,不可拖拽。
*/
export class BattleGameOtherSkillView {
private readonly _active1: BattleGameDailyShopSkillSlot | null;
private readonly _active2: BattleGameDailyShopSkillSlot | null;
private readonly _passiveRoot: Node | null;
11 hours ago
private _passiveRightRoot: Node | null = null;
3 weeks ago
private readonly _passiveTemplate: Node | null;
private readonly _passiveSlots: BattleGameOtherPassiveSlot[] = [];
private readonly _passiveNodes: Node[] = [];
private _activeBuffIds: number[] = [];
private _ownedDesc: DailyShopOwnedDesc | null = null;
/**
* @param activeList ui_otherskill1List
* @param passiveList ui_otherskill2List
*/
constructor(activeList: Node | null | undefined, passiveList: Node | null | undefined) {
const skill1 = activeList?.getChildByName('skill1') ?? null;
const skill2 = activeList?.getChildByName('skill2') ?? null;
this._active1 = skill1?.isValid ? new BattleGameDailyShopSkillSlot(skill1, 0) : null;
this._active2 = skill2?.isValid ? new BattleGameDailyShopSkillSlot(skill2, 1) : null;
this._passiveRoot = passiveList?.isValid ? passiveList : null;
this._passiveTemplate = this._findTemplate(this._passiveRoot);
if (this._passiveTemplate?.isValid) {
this._passiveTemplate.active = false;
}
}
/** 按 CarryStore(AI) 重建展示;随后 syncFromAi 对齐 CD */
refresh(): void {
this._ownedDesc?.hide();
const aiPid = AiHeadlessBagHost.AI_PLAYER_ID;
const actives = BattleBuffCarryStore.getActives(aiPid);
this._activeBuffIds = [];
for (let i = 0; i < actives.length; i++) {
this._activeBuffIds.push(actives[i].id);
}
this._bindActiveSlot(this._active1, actives[0] ?? null);
this._bindActiveSlot(this._active2, actives[1] ?? null);
this._rebuildPassives(BattleBuffCarryStore.getPassives(aiPid));
this.bindSkillDesc();
this.syncFromAi();
}
/** 绑定 ui_otherDesc 描述浮层 */
setOwnedDesc(desc: DailyShopOwnedDesc | null): void {
this._ownedDesc = desc;
}
/** 刷新敌方主动/被动 skill 点击描述 */
bindSkillDesc(): void {
const desc = this._ownedDesc;
if (!desc) return;
for (let slotIndex = 0; slotIndex < 2; slotIndex++) {
const slot = slotIndex === 0 ? this._active1 : this._active2;
const root = slot?.root;
const parsed = slot?.getParsed();
if (!root?.isValid || !root.active || !parsed) {
if (root?.isValid) desc.unbindBattleSkillItem(root);
continue;
}
desc.bindBattleSkillItem(root, parsed, DailyShopKind.Active);
}
for (let i = 0; i < this._passiveSlots.length; i++) {
const slot = this._passiveSlots[i];
const root = slot.root;
const parsed = slot.getParsed();
if (!root?.isValid || !root.active || !parsed) {
if (root?.isValid) desc.unbindBattleSkillItem(root);
continue;
}
desc.bindBattleSkillItem(root, parsed, DailyShopKind.Passive);
}
}
/** 从 AiBattleBridge 同步 CD / 七十二变展示 */
syncFromAi(): void {
const bridge = AiBattleBridge.inst;
this._syncActives(bridge.snapshotActiveSkillUi());
this._syncPassives(bridge.snapshotPassiveCdUi());
this._syncBaseThunderMasks();
}
/**
* 主动槽 icon 节点(供施放飞图标复制)。
* @param slotIndex 0/1
*/
getActiveIconNode(slotIndex: number): Node | null {
const slot = slotIndex === 0 ? this._active1 : this._active2;
return slot?.getIconNode() ?? null;
}
/**
* 按 functiontype 取敌方被动 icon 节点。
* @param functionType Battlebuff.functiontype
*/
getPassiveIconNode(functionType: number): Node | null {
const ft = Math.floor(Number(functionType) || 0);
if (ft <= 0) return null;
for (let i = 0; i < this._passiveSlots.length; i++) {
const slot = this._passiveSlots[i];
if (slot.getFunctionType() === ft) {
return slot.getIconNode();
}
}
return null;
}
/**
* 绑定主动格。
* @param slot 槽
* @param parsed buff
*/
private _bindActiveSlot(slot: BattleGameDailyShopSkillSlot | null, parsed: DailyShopParsed | null): void {
if (!slot) return;
if (!parsed) {
slot.bind(null);
return;
}
slot.bind(parsed);
}
/**
* 同步主动 CD 与展示。
* @param uiSlots AI 快照
*/
private _syncActives(uiSlots: AiActiveSkillUiSlot[]): void {
for (let i = 0; i < 2; i++) {
const slot = i === 0 ? this._active1 : this._active2;
if (!slot) continue;
const buffId = this._activeBuffIds[i] ?? 0;
if (buffId <= 0) continue;
const snap = this._findActiveSnap(uiSlots, buffId);
if (!snap) continue;
if (slot.getBoundBuffId() !== snap.display.id) {
slot.bind(snap.display, {
cdLeftSec: snap.cdLeftSec,
cdTotalSec: snap.cdTotalSec,
});
} else {
slot.syncCooldown(snap.cdLeftSec, snap.cdTotalSec);
}
}
}
/**
* 同步被动 CD。
* @param uiSlots AI CD 快照
*/
private _syncPassives(uiSlots: AiPassiveCdUiSlot[]): void {
for (let i = 0; i < this._passiveSlots.length; i++) {
const slot = this._passiveSlots[i];
const snap = this._findPassiveSnap(uiSlots, slot.buffId);
if (!snap) continue;
slot.syncCd(snap.cdLeftSec, snap.cdTotalSec);
}
}
/** 同步 AI 九霄神雷次数用尽遮罩 */
private _syncBaseThunderMasks(): void {
for (let i = 0; i < this._passiveSlots.length; i++) {
this._passiveSlots[i].syncBaseThunderMask();
}
}
/**
* @param uiSlots 快照
* @param buffId id
*/
private _findActiveSnap(uiSlots: AiActiveSkillUiSlot[], buffId: number): AiActiveSkillUiSlot | null {
for (let i = 0; i < uiSlots.length; i++) {
if (uiSlots[i].buffId === buffId) return uiSlots[i];
}
return null;
}
/**
* @param uiSlots 快照
* @param buffId id
*/
private _findPassiveSnap(uiSlots: AiPassiveCdUiSlot[], buffId: number): AiPassiveCdUiSlot | null {
for (let i = 0; i < uiSlots.length; i++) {
if (uiSlots[i].buffId === buffId) return uiSlots[i];
}
return null;
}
/**
11 hours ago
* 重建被动列表:前 3 个左、后 3 个右;超出截断并 warn。
3 weeks ago
* @param passives 携带列表
*/
private _rebuildPassives(passives: DailyShopParsed[]): void {
this._clearPassives();
11 hours ago
const left = this._passiveRoot;
3 weeks ago
const template = this._passiveTemplate;
11 hours ago
if (!left?.isValid || !template?.isValid) return;
const maxTotal = PASSIVE_PER_SIDE * 2;
if (passives.length > maxTotal) {
console.warn('[BattleGame] 敌方被动超过左右各3,截断', passives.length, maxTotal);
}
const count = Math.min(passives.length, maxTotal);
const needRight = count > PASSIVE_PER_SIDE;
const right = needRight ? this._ensurePassiveRightRoot() : null;
if (this._passiveRightRoot?.isValid) {
this._passiveRightRoot.active = needRight;
}
for (let i = 0; i < count; i++) {
const parent = i < PASSIVE_PER_SIDE ? left : right;
if (!parent?.isValid) {
console.warn('[BattleGame] 敌方被动父节点无效', i);
continue;
}
3 weeks ago
const item = instantiate(template);
item.active = true;
11 hours ago
item.setParent(parent);
3 weeks ago
const slot = new BattleGameOtherPassiveSlot(item);
slot.bind(passives[i]);
this._passiveNodes.push(item);
this._passiveSlots.push(slot);
}
11 hours ago
this._layoutPassiveSide(left, true);
if (right?.isValid) this._layoutPassiveSide(right, false);
left.getComponent(Layout)?.updateLayout();
right?.getComponent(Layout)?.updateLayout();
}
/**
* 左右都从左往右排,并贴向中线:左组右缘、右组左缘靠近中间。
* @param node 列表根
* @param isLeft 是否左侧
*/
private _layoutPassiveSide(node: Node, isLeft: boolean): void {
const ut = node.getComponent(UITransform);
if (ut) ut.setAnchorPoint(isLeft ? 1 : 0, ut.anchorPoint.y);
const layout = node.getComponent(Layout);
if (layout) layout.horizontalDirection = Layout.HorizontalDirection.LEFT_TO_RIGHT;
const p = node.position;
node.setPosition(isLeft ? -PASSIVE_CENTER_GAP : PASSIVE_CENTER_GAP, p.y, p.z);
}
/**
* 右侧被动列表:与左侧同高、同缩放,从左往右排。
*/
private _ensurePassiveRightRoot(): Node | null {
if (this._passiveRightRoot?.isValid) return this._passiveRightRoot;
const left = this._passiveRoot;
const parent = left?.parent;
if (!left?.isValid || !parent?.isValid) {
console.warn('[BattleGame] 无法创建敌方被动右侧列表');
return null;
}
let right = parent.getChildByName(PASSIVE_RIGHT_NAME);
if (!right?.isValid) {
right = new Node(PASSIVE_RIGHT_NAME);
right.parent = parent;
const lut = left.getComponent(UITransform);
const rut = right.addComponent(UITransform);
if (lut) {
rut.setContentSize(lut.contentSize);
rut.setAnchorPoint(0, lut.anchorPoint.y);
} else {
rut.setAnchorPoint(0, 0.5);
}
const ll = left.getComponent(Layout);
const rl = right.addComponent(Layout);
rl.type = Layout.Type.HORIZONTAL;
rl.resizeMode = ll?.resizeMode ?? Layout.ResizeMode.CONTAINER;
rl.spacingX = ll?.spacingX ?? 10;
rl.paddingLeft = ll?.paddingLeft ?? 0;
rl.paddingRight = ll?.paddingRight ?? 0;
rl.horizontalDirection = Layout.HorizontalDirection.LEFT_TO_RIGHT;
const ls = left.scale;
right.setScale(ls.x, ls.y, ls.z);
right.setPosition(PASSIVE_CENTER_GAP, left.position.y, left.position.z);
}
this._passiveRightRoot = right;
return right;
3 weeks ago
}
/** @param listRoot 被动列表根 */
private _findTemplate(listRoot: Node | null): Node | null {
if (!listRoot?.isValid) return null;
const named = listRoot.getChildByName('template');
if (named?.isValid) return named;
if (listRoot.children.length > 0) return listRoot.children[0];
return null;
}
private _clearPassives(): void {
const desc = this._ownedDesc;
for (let i = 0; i < this._passiveNodes.length; i++) {
const node = this._passiveNodes[i];
if (desc && node?.isValid) desc.unbindBattleSkillItem(node);
if (node?.isValid) node.destroy();
}
this._passiveNodes.length = 0;
this._passiveSlots.length = 0;
11 hours ago
if (this._passiveRightRoot?.isValid) {
this._passiveRightRoot.active = false;
}
3 weeks ago
}
}