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.
407 lines
15 KiB
407 lines
15 KiB
|
3 weeks ago
|
import { Color, instantiate, Label, Layout, Node, Sprite } from 'cc';
|
||
|
|
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',
|
||
|
|
};
|
||
|
|
|
||
|
|
/** 敌方被动格:仅展示,不触发玩家侧 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(被动)。
|
||
|
|
* 仅展示与同步 AI CD,不可拖拽。
|
||
|
|
*/
|
||
|
|
export class BattleGameOtherSkillView {
|
||
|
|
private readonly _active1: BattleGameDailyShopSkillSlot | null;
|
||
|
|
private readonly _active2: BattleGameDailyShopSkillSlot | null;
|
||
|
|
private readonly _passiveRoot: Node | null;
|
||
|
|
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;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 重建被动列表。
|
||
|
|
* @param passives 携带列表
|
||
|
|
*/
|
||
|
|
private _rebuildPassives(passives: DailyShopParsed[]): void {
|
||
|
|
this._clearPassives();
|
||
|
|
const root = this._passiveRoot;
|
||
|
|
const template = this._passiveTemplate;
|
||
|
|
if (!root?.isValid || !template?.isValid) return;
|
||
|
|
for (let i = 0; i < passives.length; i++) {
|
||
|
|
const item = instantiate(template);
|
||
|
|
item.active = true;
|
||
|
|
item.setParent(root);
|
||
|
|
const slot = new BattleGameOtherPassiveSlot(item);
|
||
|
|
slot.bind(passives[i]);
|
||
|
|
this._passiveNodes.push(item);
|
||
|
|
this._passiveSlots.push(slot);
|
||
|
|
}
|
||
|
|
root.getComponent(Layout)?.updateLayout();
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @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;
|
||
|
|
}
|
||
|
|
}
|