import { Color, Label, Node, Sprite, Tween, tween } 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 { isBattleBuffDragUnitLevelUp, resolveBattleGameSkillDragKind, resolveBattleGameSkillClickKind, BattleGameSkillDragKind, isBattleBuffDragRecycleToPeach, } from '../../manager/battleBuff/BattleBuffActiveSkillUtil'; import type { ProjectBattleHandle } from '../../battle/runtime/BattleRuntime'; import { getBattleGameRandomActiveSkillHost } from './BattleGameRandomActiveSkillHost'; /** 径向 CD 遮罩起点:12 点钟(Cocos fillStart 0=3 点,逆时针计量) */ export const SKILL_CD_MASK_FILL_START = 0.25; /** * 初始化技能 CD 径向遮罩(12 点起顺时针填充,fillRange 取正)。 * @param sprite mask 节点 Sprite */ export function setupSkillCdMaskSprite(sprite: Sprite): void { sprite.type = Sprite.Type.FILLED; sprite.fillType = Sprite.FillType.RADIAL; sprite.fillCenter.set(0.5, 0.5); sprite.fillStart = SKILL_CD_MASK_FILL_START; } /** * 刷新 CD 遮罩比例(1=满 CD,0=无遮罩)。 * @param sprite mask 节点 Sprite * @param cdRatio 剩余 CD 占比 */ export function applySkillCdMaskFillRange(sprite: Sprite, cdRatio: number): void { sprite.fillRange = Math.max(0, Math.min(1, cdRatio)); } /** 战斗界面 buff 名称描边色(按品质) */ const SKILL_NAME_OUTLINE_HEX: Record = { 1: '#386c42', 2: '#285c6d', 3: '#623073', 4: '#734930', }; /** 就绪态 light 旋转一圈耗时(秒) */ const SKILL_LIGHT_ROTATE_SEC = 2; /** skill 槽 bind 选项 */ export type BattleGameSkillSlotBindOpts = { /** 剩余 CD 秒;省略则满 CD */ cdLeftSec?: number; /** CD 总长;省略则用 parsed.cd */ cdTotalSec?: number; /** 槽位携带 buff(七十二变 roll 时与 display 不同) */ carriedParsed?: DailyShopParsed | null; /** 当前应施放 buff(与 AI castParsed 一致;roll 后与 display 相同) */ castParsed?: DailyShopParsed | null; }; /** 单个 skill 格:品质 / 图标 / 名称 / CD 遮罩 */ export class BattleGameDailyShopSkillSlot { private readonly _root: Node; private readonly _light: Node | null; private readonly _quality: Node | null; private readonly _icon: Node | null; private readonly _name: Label | null; private readonly _mask: Node | null; private readonly _maskSprite: Sprite | null; private _cdTotalSec = 0; private _cdLeftSec = 0; /** 展示用 buff(图标/名称) */ private _parsed: DailyShopParsed | null = null; /** 槽位携带 buff(七十二变本体) */ private _carriedParsed: DailyShopParsed | null = null; /** 当前应施放 buff(七十二变 roll 后为随机 buff;与 AI castParsed 同源) */ private _castParsed: DailyShopParsed | null = null; /** light 旋转 tween 是否已启动 */ private _lightRotating = false; /** @param root skill1 / skill2 根节点 */ constructor(root: Node, private readonly _slotIndex: number) { this._root = root; this._light = root.getChildByName('light'); const qualityNode = root.getChildByName('quality'); this._quality = qualityNode; this._icon = qualityNode?.getChildByName('icon') ?? root.getChildByName('icon'); this._name = root.getChildByName('name')?.getComponent(Label) ?? null; this._mask = root.getChildByName('mask'); this._maskSprite = this._mask?.getComponent(Sprite) ?? null; if (this._maskSprite?.isValid) setupSkillCdMaskSprite(this._maskSprite); } /** 绑定槽位 buff;无 buff 时隐藏根节点 */ bind(parsed: DailyShopParsed | null, opts?: BattleGameSkillSlotBindOpts): void { if (!parsed || parsed.kind !== DailyShopKind.Active) { this._root.active = false; this._cdTotalSec = 0; this._cdLeftSec = 0; this._parsed = null; this._carriedParsed = null; this._castParsed = null; this._stopLightRotate(); return; } this._parsed = parsed; this._carriedParsed = opts?.carriedParsed ?? parsed; this._castParsed = opts?.castParsed ?? parsed; this._root.active = true; const cdBaseParsed = this._carriedParsed ?? parsed; this._cdTotalSec = Math.max(0, opts?.cdTotalSec ?? (cdBaseParsed.cd | 0)); this._cdLeftSec = opts?.cdLeftSec != null ? Math.max(0, opts.cdLeftSec) : this._cdTotalSec; const quality = Math.max(1, Math.floor(Number(parsed.quality) || 0)); this._setQualitySprite(quality); this._setIconSprite(DailyShopLotteryIcon.itemIconName(parsed)); this._setName(parsed.name, quality); this._refreshCdVisual(); } /** 帧更新 CD 遮罩 */ tick(dt: number): void { if (this._cdLeftSec <= 0) return; this._cdLeftSec = Math.max(0, this._cdLeftSec - dt); this._refreshCdVisual(); } /** 槽位索引(0=skill1,1=skill2) */ get slotIndex(): number { return this._slotIndex; } /** 根节点 */ get root(): Node { return this._root; } /** 图标节点(拖拽影子) */ getIconNode(): Node | null { return this._icon?.isValid ? this._icon : null; } /** 是否可施放(未在 CD) */ isReady(): boolean { return this._root.active && this._cdLeftSec <= 0; } /** 槽位携带 buff 解析(七十二变本体) */ getCarriedParsed(): DailyShopParsed | null { return this._carriedParsed; } /** 当前应施放/交互的 buff(七十二变 roll 后为随机 buff) */ getCastParsed(): DailyShopParsed | null { return this._castParsed; } /** 当前应施放 buff 表行 */ getBuffRow(): ITableBattlebuff | null { return (this.getCastParsed()?.row as ITableBattlebuff) ?? null; } /** 当前应施放 buff 解析结果 */ getParsed(): DailyShopParsed | null { return this.getCastParsed(); } /** 展示 buff id(roll 后与携带 id 可能不同) */ getBoundBuffId(): number { return this._parsed?.id ?? 0; } /** 槽位携带 buff id;未绑定为 0 */ getCarriedBuffId(): number { return this._carriedParsed?.id ?? 0; } /** 槽位是否已携带 buff(七十二变 CD 中也为 true) */ hasCarriedBuff(): boolean { return this.getCarriedBuffId() > 0; } /** 当前应走的 skill 拖拽类型(七十二变 roll 后按变化 buff) */ getCastDragKind(): BattleGameSkillDragKind | null { const row = (this._castParsed?.row as ITableBattlebuff) ?? null; return resolveBattleGameSkillDragKind(row); } /** * 仅同步 CD 遮罩(不改图标/名称)。 * @param cdLeftSec 剩余秒 * @param cdTotalSec 总秒 */ syncCooldown(cdLeftSec: number, cdTotalSec: number): void { if (!this._root.active) return; this._cdTotalSec = Math.max(0, cdTotalSec); this._cdLeftSec = Math.max(0, cdLeftSec); this._refreshCdVisual(); } /** 是否为主动拖放升级 buff */ isDragUnitLevelUpBuff(): boolean { return isBattleBuffDragUnitLevelUp(this.getBuffRow()); } /** 是否为主动缚仙网 buff(Phase1 无) */ isDragBindNetBuff(): boolean { return false; } /** 是否可拖拽施放(Phase1 无商店主动拖拽) */ isDraggableSkill(): boolean { return false; } /** 是否乾坤袋回收 buff */ isRecycleToPeachSkill(): boolean { return isBattleBuffDragRecycleToPeach(this.getBuffRow()); } /** 是否有主动 skill 交互(拖拽 / 点击) */ hasBattleSkillInteraction(): boolean { return this._root.active && ( this.isDraggableSkill() || this.isClickSkill() ); } /** 是否可点击施放(万神幡等) */ isClickSkill(): boolean { return resolveBattleGameSkillClickKind(this.getBuffRow()) != null; } /** 施放后开始冷却 */ startCooldown(): void { if (this._cdTotalSec <= 0) return; this._cdLeftSec = this._cdTotalSec; this._refreshCdVisual(); } /** 品质底图 */ 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.BattleGameSkillBg(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 = SKILL_NAME_OUTLINE_HEX[quality]; if (!hex) { console.log('[BattleGame] 未知 buff 品质描边', quality, this._root.name); return; } this._name.enableOutline = true; this._name.outlineColor = Color.fromHEX(new Color(), hex); } /** light / mask 与 fillRange */ private _refreshCdVisual(): void { const onCd = this._cdLeftSec > 0; const showLight = !onCd; if (this._light?.isValid) { this._light.active = showLight; } this._syncLightRotate(showLight && this._root.active); if (this._mask?.isValid) { this._mask.active = onCd; } if (onCd && this._maskSprite?.isValid && this._cdTotalSec > 0) { applySkillCdMaskFillRange(this._maskSprite, this._cdLeftSec / this._cdTotalSec); } } /** 就绪态 light 持续旋转;CD 中停止并归零 */ private _syncLightRotate(active: boolean): void { const light = this._light; if (!light?.isValid) return; if (active) { if (this._lightRotating) return; this._lightRotating = true; Tween.stopAllByTarget(light); light.angle = 0; tween(light) .by(SKILL_LIGHT_ROTATE_SEC, { angle: -360 }) .repeatForever() .start(); return; } this._stopLightRotate(); } /** 停止 light 旋转 tween */ private _stopLightRotate(): void { this._lightRotating = false; const light = this._light; if (!light?.isValid) return; Tween.stopAllByTarget(light); light.angle = 0; } } /** * 战斗界面 ui_selfskill1List:skill1 / skill2 展示每日商店主动 buff。 */ export class BattleGameDailyShopSkillView { private readonly _skill1: BattleGameDailyShopSkillSlot | null; private readonly _skill2: BattleGameDailyShopSkillSlot | null; private _battle: ProjectBattleHandle | null = null; private _onDisplayChanged: (() => void) | null = null; private _ownedDesc: DailyShopOwnedDesc | null = null; /** @param skillList ui_selfskill1List 节点 */ constructor(skillList: Node | null | undefined) { const skill1Node = skillList?.getChildByName('skill1') ?? null; const skill2Node = skillList?.getChildByName('skill2') ?? null; this._skill1 = skill1Node?.isValid ? new BattleGameDailyShopSkillSlot(skill1Node, 0) : null; this._skill2 = skill2Node?.isValid ? new BattleGameDailyShopSkillSlot(skill2Node, 1) : null; } /** 取槽位展示器 */ getSlot(slotIndex: number): BattleGameDailyShopSkillSlot | null { if (slotIndex === 0) return this._skill1; if (slotIndex === 1) return this._skill2; return null; } /** 绑定 battle(七十二变 roll 用 rng) */ bindBattle(battle: ProjectBattleHandle | null): void { this._battle = battle; } /** skill 展示变化时回调(重新绑定拖拽/点击) */ setOnDisplayChanged(handler: (() => void) | null): void { this._onDisplayChanged = handler; } /** 绑定战斗 skill 描述浮层 */ setOwnedDesc(desc: DailyShopOwnedDesc | null): void { this._ownedDesc = desc; } /** 刷新 skill1 / skill2 描述点击(拖拽槽由 Drag 处理轻触) */ bindSkillDesc(): void { const desc = this._ownedDesc; if (!desc) return; for (let slotIndex = 0; slotIndex < 2; slotIndex++) { const slot = this.getSlot(slotIndex); const root = slot?.root; if (!root?.isValid || !root.active) continue; const parsed = slot.getCastParsed(); if (!parsed) continue; if (slot.hasBattleSkillInteraction() && slot.isDraggableSkill()) { desc.unbindBattleSkillItem(root); continue; } desc.bindBattleSkillItem(root, parsed, DailyShopKind.Active); } } /** 按主动槽位顺序刷新展示 */ refresh(): void { this._ownedDesc?.hide(); const host = getBattleGameRandomActiveSkillHost(); const slotIds = DailyShopDataManager.getActiveSlotIds(); for (let slotIndex = 0; slotIndex < 2; slotIndex++) { const baseId = Number(slotIds[slotIndex]) || 0; const baseParsed = this._resolveActiveBuff(baseId); const row = (baseParsed?.row as ITableBattlebuff) ?? null; host.syncSlot(slotIndex, baseId, row); this._applySlotDisplay(slotIndex, baseParsed); } this.bindSkillDesc(); } /** 帧更新 CD;七十二变 CD 结束自动 roll */ tick(dt: number): void { const host = getBattleGameRandomActiveSkillHost(); for (let slotIndex = 0; slotIndex < 2; slotIndex++) { const slot = this.getSlot(slotIndex); if (!slot) continue; const wasOnCd = !slot.isReady(); slot.tick(dt); if (!host.isRandomActiveSlot(slotIndex) || host.isShowingRolledBuff(slotIndex)) continue; if (!wasOnCd || !slot.isReady()) continue; if (!host.tryRollOnCooldownReady(slotIndex, this._battle)) continue; const baseId = Number(DailyShopDataManager.getActiveSlotIds()[slotIndex]) || 0; this._applySlotDisplay(slotIndex, this._resolveActiveBuff(baseId), { ready: true }); this.bindSkillDesc(); this._onDisplayChanged?.(); } } /** 指定槽位开始冷却(0=skill1,1=skill2) */ startCooldown(slotIndex: number): void { const host = getBattleGameRandomActiveSkillHost(); if (host.isShowingRolledBuff(slotIndex)) { host.onRolledBuffConsumed(slotIndex); const baseId = Number(DailyShopDataManager.getActiveSlotIds()[slotIndex]) || 0; const baseParsed = this._resolveActiveBuff(baseId); this._applySlotDisplay(slotIndex, baseParsed); this.getSlot(slotIndex)?.startCooldown(); this._onDisplayChanged?.(); return; } if (slotIndex === 0) { this._skill1?.startCooldown(); } else if (slotIndex === 1) { this._skill2?.startCooldown(); } } /** 刷新单槽展示 */ private _applySlotDisplay( slotIndex: number, baseParsed: DailyShopParsed | null, opts?: { ready?: boolean }, ): void { const slot = this.getSlot(slotIndex); if (!slot) return; const host = getBattleGameRandomActiveSkillHost(); const castParsed = host.resolveDisplayParsed(slotIndex, baseParsed); if (!castParsed) { slot.bind(null); return; } const bindOpts: BattleGameSkillSlotBindOpts = { carriedParsed: baseParsed, castParsed, }; if (host.isShowingRolledBuff(slotIndex) || opts?.ready) { bindOpts.cdLeftSec = 0; bindOpts.cdTotalSec = 0; } slot.bind(castParsed, bindOpts); } /** 槽位 id → 主动 buff 解析结果 */ private _resolveActiveBuff(slotId: number): DailyShopParsed | null { const id = Number(slotId) || 0; if (id <= 0) return null; const parsed = DailyShopDataManager.getParsed(id); if (!parsed) return null; if (parsed.kind !== DailyShopKind.Active) { console.log('[BattleGame] 主动槽非主动 buff', id, parsed.kind); return null; } return parsed; } }