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/BattleGameDailyShopSkillVie...

472 lines
17 KiB

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<number, string> = {
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;
}
}