import { instantiate, Layout, Node, Sprite, Vec3 } from 'cc'; import { SpriteLoad } from 'db://assets/mx/components/SpriteLoad'; import { GBundle, GPath } from '../../game/ConfigRes'; import { DailyShopDataManager } from '../../manager/DailyShopDataManager'; import { DailyShopKind, DailyShopParsed } from '../../manager/DailyShopTypes'; import { DailyShopLotteryIcon, DailyShopOwnedDesc } from '../PopDailyShopLottery/DailyShopLotteryIcon'; import { isBattleBuffPassiveBaseThunder, passiveBuffUsesBattleGameCd } from '../../manager/battleBuff/BattleBuffPassiveSkillUtil'; import { getBattleBuffBaseThunderHost } from '../../battle/combat/BattleBuffBaseThunderHost'; import type { ProjectBattleHandle } from '../../battle/runtime/BattleRuntime'; import type { CocosBagBoardView } from '../../battle/view/CocosBagBoardView'; import { BattleGamePassiveSkillRuntime } from './BattleGamePassiveSkillRuntime'; import type { BattleGamePassiveSkillHost } from './BattleGamePassiveSkillHost'; import { applySkillCdMaskFillRange, setupSkillCdMaskSprite } from './BattleGameDailyShopSkillView'; /** 单个被动 skill 格:品质 / 图标 / CD 遮罩 */ class BattleGamePassiveSkillSlot { 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 _cdTotalSec = 0; private _cdLeftSec = 0; private _usesCd = false; private _parsed: DailyShopParsed | null = null; private _spawnInflight = false; /** 本轮 CD 已触发过(防止 cd=0 时每帧重复产字) */ private _cdTriggerConsumed = false; /** ui_selfskill2List 槽位下标 */ private readonly _slotIndex: number; /** @param root skillGrid2 实例根节点 */ /** @param slotIndex 槽位下标 */ constructor(root: Node, slotIndex: number) { this._root = root; this._slotIndex = slotIndex; 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); } /** 绑定被动 buff */ bind(parsed: DailyShopParsed | null): void { if (!parsed || parsed.kind !== DailyShopKind.Passive) { this._root.active = false; this._parsed = null; this._cdTotalSec = 0; this._cdLeftSec = 0; this._usesCd = false; this._spawnInflight = false; this._cdTriggerConsumed = false; return; } const row = parsed.row as ITableBattlebuff; const ft = Number(row?.functiontype) || 0; this._usesCd = passiveBuffUsesBattleGameCd(ft); this._parsed = parsed; this._spawnInflight = false; this._cdTriggerConsumed = false; this._root.active = true; const quality = Math.max(1, Math.floor(Number(parsed.quality) || 0)); this._setQualitySprite(quality); this._setIconSprite(DailyShopLotteryIcon.itemIconName(parsed)); 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(); } /** 帧更新 CD */ tick(dt: number, battle: ProjectBattleHandle | null, host: BattleGamePassiveSkillHost | null): void { this._syncBaseThunderMask(); if (!this._usesCd || this._spawnInflight) return; if (this._cdLeftSec > 0) { this._cdLeftSec = Math.max(0, this._cdLeftSec - dt); this._refreshCdVisual(); return; } if (this._cdTriggerConsumed) return; const row = this._parsed?.row as ITableBattlebuff | undefined; if (!row) return; const ft = Math.floor(Number(row.functiontype) || 0); if ((ft === 3 || ft === 9) && (!host?.bagView || !host.battle?.bagSession)) { return; } this._cdTriggerConsumed = true; const handled = BattleGamePassiveSkillRuntime.onPassiveCdReady(row, battle, host, this._slotIndex); if (!handled) { this.restartCd(); } } /** ui_selfskill2List 槽位下标 */ get slotIndex(): number { return this._slotIndex; } /** 被动图标世界坐标 */ getIconWorldPosition(): Readonly | null { const icon = this._icon; if (!icon?.isValid) return null; return icon.worldPosition.clone(); } /** 被动图标节点(飞行动画复制影子) */ 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); } /** 产字成功后重启 CD */ restartCd(): void { if (!this._usesCd) return; this._cdLeftSec = this._cdTotalSec; this._cdTriggerConsumed = false; this._refreshCdVisual(); } /** 异步产字进行中 */ setSpawnInflight(inflight: boolean): void { this._spawnInflight = inflight; } 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 _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); } } /** 九霄神雷次数用尽:直接盖满 mask,不刷新 CD */ private _syncBaseThunderMask(): void { const row = this._parsed?.row as ITableBattlebuff | undefined; if (!isBattleBuffPassiveBaseThunder(row)) return; if (!getBattleBuffBaseThunderHost().isTriggersExhausted(0)) return; if (this._mask?.isValid) this._mask.active = true; if (this._maskSprite?.isValid) applySkillCdMaskFillRange(this._maskSprite, 1); } } /** * 战斗界面 ui_selfskill2List:按当前携带被动数量动态展示 skillGrid2。 */ export class BattleGamePassiveSkillView implements BattleGamePassiveSkillHost { private readonly _listRoot: Node | null; private readonly _template: Node | null; private _battle: ProjectBattleHandle | null = null; private _bagView: CocosBagBoardView | null = null; private _ownedDesc: DailyShopOwnedDesc | null = null; private readonly _slots: BattleGamePassiveSkillSlot[] = []; private readonly _itemNodes: Node[] = []; /** 金矿飞币:图标世界坐标 → ui_money */ private _goldMineCoinFly: ((fromWorld: Readonly) => void) | null = null; /** @param listRoot ui_selfskill2List 节点 */ constructor(listRoot: Node | null | undefined) { this._listRoot = listRoot?.isValid ? listRoot : null; this._template = this._findTemplate(this._listRoot); if (this._template?.isValid) { this._template.active = false; } } /** * 绑定金矿产币飞币表现。 * @param handler 起点世界坐标回调;null 清除 */ setGoldMineCoinFlyHandler(handler: ((fromWorld: Readonly) => void) | null): void { this._goldMineCoinFly = handler; } /** @inheritdoc */ get battle(): ProjectBattleHandle | null { return this._battle; } /** @inheritdoc */ get bagView(): CocosBagBoardView | null { return this._bagView; } bindBattle(battle: ProjectBattleHandle | null): void { this._battle = battle; } bindBagView(bagView: CocosBagBoardView | null): void { this._bagView = bagView; } /** 绑定战斗 skill 描述浮层 */ setOwnedDesc(desc: DailyShopOwnedDesc | null): void { this._ownedDesc = desc; } /** @inheritdoc */ getIconWorldPosition(slotIndex: number): Readonly | null { const slot = this._slots[slotIndex]; return slot?.getIconWorldPosition() ?? 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._slots.length; i++) { const slot = this._slots[i]; if (slot.getFunctionType() === ft) { return slot.getIconNode(); } } return null; } /** @inheritdoc */ notifyGoldMineCoinFly(slotIndex: number): void { const from = this.getIconWorldPosition(slotIndex); if (!from) { console.log('[PassiveSkill] 金矿飞币无图标坐标', slotIndex); return; } this._goldMineCoinFly?.(from); } /** @inheritdoc */ restartPassiveCd(slotIndex: number): void { this._slots[slotIndex]?.restartCd(); } /** @inheritdoc */ setPassiveSpawnInflight(slotIndex: number, inflight: boolean): void { this._slots[slotIndex]?.setSpawnInflight(inflight); } /** 按被动槽刷新展示 */ refresh(): void { this._ownedDesc?.hide(); this._clearItems(); const root = this._listRoot; const template = this._template; if (!root?.isValid || !template?.isValid) return; const carried = this._collectCarriedPassive(); for (let i = 0; i < carried.length; i++) { const item = instantiate(template); item.active = true; item.setParent(root); const slot = new BattleGamePassiveSkillSlot(item, i); slot.bind(carried[i]); this._ownedDesc?.bindBattleSkillItem(item, carried[i], DailyShopKind.Passive); this._itemNodes.push(item); this._slots.push(slot); } root.getComponent(Layout)?.updateLayout(); } /** 帧更新 CD */ tick(dt: number): void { const battle = this._battle; for (let i = 0; i < this._slots.length; i++) { this._slots[i].tick(dt, battle, this); } } 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 _collectCarriedPassive(): DailyShopParsed[] { const out: DailyShopParsed[] = []; const ids = DailyShopDataManager.getPassiveSlotIds(); for (let i = 0; i < ids.length; i++) { const id = Number(ids[i]) || 0; if (id <= 0) continue; const parsed = DailyShopDataManager.getParsed(id); if (!parsed) { console.log('[BattleGame] 被动槽 buff 解析失败', id); continue; } if (parsed.kind !== DailyShopKind.Passive) { console.log('[BattleGame] 被动槽非被动 buff', id, parsed.kind); continue; } out.push(parsed); } return out; } private _clearItems(): void { for (let i = 0; i < this._itemNodes.length; i++) { const node = this._itemNodes[i]; if (node?.isValid) node.destroy(); } this._itemNodes.length = 0; this._slots.length = 0; } }