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/battle/skill/SkillController.ts

456 lines
18 KiB

3 weeks ago
import { _decorator, Component, Node } from 'cc';
import {
BATTLE_EVENT_ALL,
BattleEventType,
TeamComp,
type BattleEvent,
type EntityId,
} from 'db://assets/mx/module/battle';
import type { Player } from '../../game/Player';
import type { IBattleRuntimeHandle } from '../types/ProjectBattleTypes';
import type { MapManager } from '../map/legacy/MapManager';
import { TextGrid } from '../view/TextGrid';
import { Skill } from './Skill';
import { Gong } from '../heros/Gong';
import { GONG_WEAPON1_ID } from '../heros/HeroActorRes';
import { getTowerLoadoutService } from '../combat/TowerLoadoutService';
import { buildSkillPresentationContext } from './SkillPresentationBridge';
import { isActiveAbilityId, isMonsterAbilityId, isWeaponAbilityId } from '../config/weapon1/Weapon1BattleIds';
import { findCompositionForEntity } from '../runtime/DeployEntityQuery';
import { SkillPresentationEnv } from './SkillPresentationEnv';
import { BattleAudio } from '../audio/BattleAudio';
import { SkillNameFloater, ULT_SKILL_RELEASE_DELAY_SEC } from './SkillNameFloater';
import { HeroActorHost } from '../heros/HeroActorHost';
import { isHeroActorWeapon1 } from '../heros/HeroActorRes';
2 weeks ago
import { isElementCombatHeroId } from '../config/BattleHeroTable';
3 weeks ago
import { ensureSpecialSkillPresenters } from '../presentation/SpecialSkillPresenters';
import { getNezhaChargeHost } from '../presentation/NezhaChargePresenter';
1 week ago
import { getEarthChargeHost } from '../presentation/EarthChargePresenter';
3 weeks ago
import { getPagodaSmashHost } from '../presentation/PagodaSmashPresenter';
import { getStaffSmashHost } from '../presentation/StaffSmashPresenter';
const { ccclass } = _decorator;
/** 皮肤未就绪时 PresentationCue 最多延迟帧数(约 2s@30fps) */
const PRESENTATION_SKILL_BIND_MAX_RETRIES = 60;
const CONFUSION_BUFF_ID = 'debuff_8';
/** 单个玩家的技能表现控制器(监听战斗事件 → 驱动 HeroActor Skill) */
@ccclass('SkillController')
export class SkillController extends Component {
private _player: Player | null = null;
private _match: IBattleRuntimeHandle | null = null;
private _map: MapManager | null = null;
private _mapGrids: ReadonlyMap<number, TextGrid> | undefined;
private _skills = new Map<EntityId, Skill>();
/** 上次 bind 时的地图格 index,用于检测搬家后 stale Skill 缓存 */
private _entityGridIndex = new Map<EntityId, number>();
private _listener: ((e: BattleEvent) => void) | null = null;
/** 是否已 bind 到 Player/Match;unbind 幂等,避免 UIBattleGame 与 onDestroy 重复解绑 */
private _bound = false;
get player(): Player | null {
return this._player;
}
bind(player: Player, match: IBattleRuntimeHandle): void {
this.unbind();
ensureSpecialSkillPresenters();
this._player = player;
this._match = match;
player.skillController = this;
this._listener = (e) => this._onBattleEvent(e);
match.on(BATTLE_EVENT_ALL, this._listener);
this._bound = true;
}
unbind(): void {
if (!this._bound) return;
this._bound = false;
if (this._player) {
this._player.skillController = null;
this._player = null;
}
if (this._match && this._listener) {
this._match.off(BATTLE_EVENT_ALL, this._listener);
}
this._listener = null;
this._match = null;
this._map = null;
this._mapGrids = undefined;
this._skills.clear();
this._entityGridIndex.clear();
getNezhaChargeHost().abortAll();
1 week ago
getEarthChargeHost().abortAll();
3 weeks ago
getPagodaSmashHost().abortAll();
getStaffSmashHost().abortAll();
HeroActorHost.instance.destroyAll();
}
/** 地图布局变更前打断指定实体的表现(含 ctx.cancel) */
abortEntityPresentation(entityId: EntityId): void {
if (!this._ownsEntity(entityId)) return;
const skill = this._skills.get(entityId);
if (skill?.isValid) skill.abortPresentation();
}
registerEntitySkill(entityId: EntityId, skillRoot: Node): void {
const skill = skillRoot.getComponent(Skill);
if (!skill?.isValid) return;
const prev = this._skills.get(entityId);
this._skills.set(entityId, skill);
if (prev === skill) return;
2 weeks ago
if (!skill.isPresentationBusy() && !skill.visualChannel.isCastOrRestHold) skill.onIdle();
3 weeks ago
}
unregisterEntity(entityId: EntityId): void {
this._skills.delete(entityId);
HeroActorHost.instance.destroy(entityId);
}
/** 按 Player.deployedUnits + 地图字块刷新 entity → Skill 映射(不触发词条物化) */
refreshEntitySkillBindings(map: MapManager, mapGrids?: ReadonlyMap<number, TextGrid>): void {
this._map = map;
this._mapGrids = mapGrids;
const prev = this._skills;
const prevGridByEntity = this._entityGridIndex;
this._skills = new Map();
this._entityGridIndex = new Map();
const player = this._player;
if (!player) return;
for (const entityId of player.deployedUnits) {
const gridIndex = this._resolveGridIndex(entityId);
const prevGrid = prevGridByEntity.get(entityId);
const gridMoved = prevGrid != null && gridIndex != null && prevGrid !== gridIndex;
const old = prev.get(entityId);
if (gridMoved && old?.isValid) {
old.abortPresentation();
} else if (old) {
this._skills.set(entityId, old);
}
this._tryRegisterSkill(entityId);
if (gridIndex != null) this._entityGridIndex.set(entityId, gridIndex);
}
}
/** 词条物化后刷新表现(遗留钩子;特殊 Presenter 自读 loadout) */
refreshEntityLoadoutPresentation(entityId: EntityId): void {
if (!this._ownsEntity(entityId)) return;
void entityId;
}
/** 局内 UI 暂停:冻结各塔 Skill 的 Spine / schedule(不 cancel 逻辑) */
freezeAllPresentations(): void {
for (const skill of this._skills.values()) {
if (skill?.isValid) skill.freezePresentation();
}
}
/** 恢复 UI 暂停冻结的表现 */
unfreezeAllPresentations(): void {
for (const skill of this._skills.values()) {
if (skill?.isValid) skill.unfreezePresentation();
}
}
/** 移动/拆塔等打断未 finish 的表现 */
abortAllPresentations(): void {
for (const skill of this._skills.values()) {
if (skill?.isValid) skill.abortPresentation();
}
}
/** @deprecated 使用 refreshEntitySkillBindings */
syncFromDeploy(
_deploySync: unknown,
map: MapManager,
mapGrids?: ReadonlyMap<number, TextGrid>,
): void {
this.refreshEntitySkillBindings(map, mapGrids);
}
protected onDestroy(): void {
this.unbind();
}
private _ownsEntity(entityId: EntityId): boolean {
const player = this._player;
if (!player) return false;
if (player.deployedUnits.includes(entityId)) return true;
const team = this._match?.session.world.getComponent(entityId, TeamComp);
return team?.ownerId === player.playerId;
}
private _onBattleEvent(event: BattleEvent): void {
switch (event.type) {
case BattleEventType.PresentationCue:
if (isMonsterAbilityId(event.abilityId) || event.params?.skillKind === 'monster') {
return;
}
if (!this._ownsEntity(event.caster)) return;
this._playPresentationCue(event);
break;
case BattleEventType.UnitPlaced:
if (!this._ownsEntity(event.entity)) return;
break;
case BattleEventType.UnitSold:
if (!this._ownsEntity(event.entity)) return;
this.unregisterEntity(event.entity);
break;
case BattleEventType.BuffApplied:
if (event.buffId === CONFUSION_BUFF_ID) {
this._setTowerConfusionFx(event.target, true);
}
break;
case BattleEventType.BuffRemoved:
if (event.buffId === CONFUSION_BUFF_ID) {
this._setTowerConfusionFx(event.target, false);
}
break;
case BattleEventType.EntityKilled:
case BattleEventType.UnitDespawned:
if (event.type === BattleEventType.EntityKilled) {
this._setTowerConfusionFx(event.victim, false);
} else {
this._setTowerConfusionFx(event.entity, false);
}
break;
}
}
/** 混乱:塔格挂「混乱效果」,禁攻由 AbilitySystem debuff_8 判定 */
private _setTowerConfusionFx(entityId: EntityId, on: boolean): void {
if (!this._ownsEntity(entityId)) return;
const gridIndex = this._resolveGridIndex(entityId);
if (gridIndex == null) return;
2 weeks ago
this._textGridAt(gridIndex)?.setConfusionFx(on);
3 weeks ago
}
private _playPresentationCue(event: Extract<BattleEvent, { type: BattleEventType.PresentationCue }>): void {
const session = this._match?.session;
if (!session) return;
const ctx = buildSkillPresentationContext(session, {
caster: event.caster,
abilityId: event.abilityId,
cueId: event.cueId,
castInstanceId: event.castInstanceId,
targetEntityIds: event.targetEntityIds,
params: event.params,
});
this._dispatchPresentationCue(event, ctx, 0);
}
/**
* HeroActor 未就绪时延迟重试,避免首刀 finish() 丢伤害。
*/
private _dispatchPresentationCue(
event: Extract<BattleEvent, { type: BattleEventType.PresentationCue }>,
ctx: ReturnType<typeof buildSkillPresentationContext>,
retried: number,
): void {
if (!ctx.isAlive) {
ctx.cancel('caster_dead');
return;
}
const skill = this._tryRegisterSkill(event.caster);
if (!skill) {
if (retried < PRESENTATION_SKILL_BIND_MAX_RETRIES && this._shouldRetrySkillBind(event.caster)) {
SkillPresentationEnv.scheduleBattle(0, () => {
this._dispatchPresentationCue(event, ctx, retried + 1);
});
return;
}
// 无 HeroActor(桃等)或重试耗尽:先 hit 再 finish,避免丢 deferred 产桃/伤害
this._drainThenFinish(ctx);
return;
}
const cue = event.cueId.toLowerCase();
const isWeaponSk = isWeaponAbilityId(event.abilityId) || cue === 'weaponskill';
const isUlt = !isWeaponSk && (
isActiveAbilityId(event.abilityId) || cue === 'skill' || cue.includes('ult')
);
const isComboBurst = Number(event.params?.comboAttack) > 0
|| (ctx.flow.vars.get('__comboAttack') ?? 0) > 0;
const extraHead = this._extraSkillHead(event.params);
const skipUltPresentationDelay = isComboBurst || extraHead === 1;
if (isWeaponSk) {
const fireWeapon = () => {
if (!ctx.isAlive) {
ctx.cancel('caster_dead');
return;
}
if (!skill.isValid) {
this._drainThenFinish(ctx);
return;
}
try {
BattleAudio.playWeaponSkill(event.caster, true);
skill.onWeaponSkill(ctx);
} catch (err) {
console.error('[SkillController] onWeaponSkill 异常,释放施法锁', event.abilityId, String(err));
if (ctx.isAlive) ctx.finish();
}
};
if (skipUltPresentationDelay) fireWeapon();
else SkillPresentationEnv.scheduleBattle(ULT_SKILL_RELEASE_DELAY_SEC, fireWeapon);
} else if (isUlt) {
const fireUlt = () => {
if (!ctx.isAlive) {
ctx.cancel('caster_dead');
return;
}
if (!skill.isValid) {
this._drainThenFinish(ctx);
return;
}
try {
BattleAudio.playWeaponSkill(event.caster);
skill.onSkill(ctx);
} catch (err) {
console.error('[SkillController] onSkill 异常,释放施法锁', event.abilityId, String(err));
if (ctx.isAlive) ctx.finish();
}
};
if (skipUltPresentationDelay) {
SkillNameFloater.play(event.caster, event.abilityId);
fireUlt();
} else {
SkillNameFloater.play(event.caster, event.abilityId);
SkillPresentationEnv.scheduleBattle(ULT_SKILL_RELEASE_DELAY_SEC, fireUlt);
}
} else {
this._fireNormAttack(event, ctx, skill);
}
}
/**
* Skill.skill 头类型。
* @param params PlayPresentation 参数
*/
private _extraSkillHead(params: Record<string, unknown> | undefined): number {
const raw = String(params?.extraSkill ?? params?.skillRaw ?? '').trim();
return Math.floor(Number(raw.split(',')[0]) || 0);
}
/**
* 先结算剩余 deferred,再解锁施法。
* @param ctx 表现上下文
*/
private _drainThenFinish(ctx: ReturnType<typeof buildSkillPresentationContext>): void {
while (ctx.isAlive && ctx.hit()) {
/* drain deferred slices */
}
ctx.finish();
}
/** 普攻 cue:异常必须 finish,避免施法锁卡住后续 OnTick */
private _fireNormAttack(event: Extract<BattleEvent, { type: BattleEventType.PresentationCue }>, ctx: ReturnType<typeof buildSkillPresentationContext>, skill: Skill): void {
if (!ctx.isAlive) {
ctx.cancel('caster_dead');
return;
}
if (!skill.isValid) {
this._drainThenFinish(ctx);
return;
}
try {
const heroId = getTowerLoadoutService().getLoadout(event.caster)?.weapon1Id;
if (heroId !== GONG_WEAPON1_ID && !(skill instanceof Gong)) {
BattleAudio.playWeaponAttack(event.caster);
}
skill.onAttack(ctx);
this._syncPartnerAttackVisual(event.caster, skill);
} catch (err) {
console.error('[SkillController] onAttack 异常,释放施法锁', event.abilityId, String(err));
if (ctx.isAlive) ctx.finish();
}
}
/** 组合体伴侣字块同步攻击动画(HeroActor 由子类 playPartnerAttackVisual 处理) */
private _syncPartnerAttackVisual(entityId: EntityId, mainSkill: Skill): void {
if (HeroActorHost.instance.getActor(entityId) === mainSkill) {
const timeScale = Math.max(0.01, SkillPresentationEnv.getFinalAttackSpeed(entityId));
mainSkill.playPartnerAttackVisual(timeScale);
}
}
2 weeks ago
/**
* 地图格上的字块:先查绑定表,缺则从格节点现找(AI 对账晚于 refresh 时表会空)。
* @param gridIndex 地图格
*/
private _textGridAt(gridIndex: number): TextGrid | null {
const tracked = this._mapGrids?.get(gridIndex);
if (tracked?.isValid) return tracked;
const parent = this._map?.getMapGridByIndex(gridIndex)?.node;
if (!parent?.isValid) return null;
const live = parent.getComponentInChildren(TextGrid);
return live?.isValid ? live : null;
}
3 weeks ago
private _resolveGridIndex(entityId: EntityId): number | null {
const session = this._match?.session;
const map = this._map;
const player = this._player;
if (!session || !map || !player) return null;
const comp = findCompositionForEntity(entityId, player, session, map);
if (!comp?.gridIndices.length) return null;
return SkillPresentationEnv.leftmostGridIndex(comp.gridIndices);
}
/**
* 仅 HeroActor 未就绪时重试绑定;桃等无 Actor 塔立即结算。
* @param entityId 施法实体
*/
private _shouldRetrySkillBind(entityId: EntityId): boolean {
const loadout = getTowerLoadoutService().getLoadout(entityId);
2 weeks ago
if (loadout && !isHeroActorWeapon1(loadout.weapon1Id) && !isElementCombatHeroId(loadout.weapon1Id)) return false;
3 weeks ago
const session = this._match?.session;
const player = this._player;
const map = this._map;
if (!session || !player || !map) return true;
const comp = findCompositionForEntity(entityId, player, session, map);
2 weeks ago
if (comp && !isHeroActorWeapon1(comp.weapon1Id) && !isElementCombatHeroId(comp.weapon1Id)) return false;
3 weeks ago
return true;
}
/** 绑定 HeroActor Skill;全部 Weapon1 英雄已迁新方案,不再从 grid 皮肤找 Skill* */
private _tryRegisterSkill(entityId: EntityId): Skill | null {
const actor = HeroActorHost.instance.getActor(entityId);
if (actor?.isValid) {
this.registerEntitySkill(entityId, actor.node);
return this._skills.get(entityId) ?? actor;
}
const session = this._match?.session;
const player = this._player;
const map = this._map;
if (!session || !player || !map) return null;
const comp = findCompositionForEntity(entityId, player, session, map);
2 weeks ago
if (!comp) return null;
if (isElementCombatHeroId(comp.weapon1Id)) {
const gridIndex = this._resolveGridIndex(entityId);
2 weeks ago
const grid = gridIndex != null ? this._textGridAt(gridIndex) : null;
2 weeks ago
const skill = grid?.ensureNumberSkill(comp.weapon1Id, entityId) ?? null;
if (!skill?.isValid) return null;
this.registerEntitySkill(entityId, skill.node);
return this._skills.get(entityId) ?? skill;
}
if (!isHeroActorWeapon1(comp.weapon1Id)) return null;
3 weeks ago
HeroActorHost.instance.syncFromMap(entityId, comp, map, !player.isLocal);
const created = HeroActorHost.instance.getActor(entityId);
if (!created?.isValid) return null;
this.registerEntitySkill(entityId, created.node);
return this._skills.get(entityId) ?? created;
}
}