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/mechanism/CharmController.ts

314 lines
11 KiB

import type { EntityId } from 'db://assets/mx/module/ecs';
import { createMembershipSet, membershipAdd, membershipFromArray, membershipDedupeArray, mapKeysToArray, type MembershipSet, type ReadonlyMembershipSet } from '../../../mx/tools/MembershipSet';
import {
MergeEventType,
type ComposedUnit,
type MergeEvent,
type MergeEventSubscription,
type MergeSession,
} from 'db://assets/mx/module/merge';
import { composeKey } from '../runtime/ComposeUnitKey';
import { Node, Sprite, Tween } from 'cc';
import type { BattleRuntime } from '../runtime/BattleRuntime';
import type { CocosBagBoardView } from '../view/CocosBagBoardView';
import type { TextGrid } from '../view/TextGrid';
import type { CharmTarget } from './MechanismServices';
interface CharmEntry {
composeKey: string;
entityId: EntityId;
gridIndices: readonly number[];
durationSec: number;
remainingSec: number;
/** 魅惑进度/飞入表现根;幻魂镜等纯动画技能可为 null */
fxRoot: Node | null;
progressSprite: Sprite | null;
}
/** 魅惑状态:禁攻、禁拖、进度条;升级或计时结束解除 */
export class CharmController {
private _runtime: BattleRuntime | null = null;
private _bagView: CocosBagBoardView | null = null;
private _mergeSession: MergeSession | null = null;
private _entries = new Map<string, CharmEntry>();
private _entityToKey = new Map<EntityId, string>();
private _mergeListener: ((e: MergeEvent) => void) | null = null;
/** 魅惑条目被释放时(合成变阵 / 升级 / 超时等),供幻魂镜表现收尾 */
private _onEntryReleased: ((composeKey: string) => void) | null = null;
bindRuntime(runtime: BattleRuntime): void {
this.unbind();
this._runtime = runtime;
}
bindBagView(view: CocosBagBoardView | null): void {
this._bagView = view;
this._syncDragState();
}
/** 弹窗暂停等打断:收尾背包/地图拖动(遮罩常吞掉 DRAG_END) */
cancelBagDrag(): void {
this._bagView?.cancelActiveDrag();
}
/** 暂停恢复后:按当前相位重开拖拽,并覆盖魅惑禁拖格 */
refreshBagDragEnabled(): void {
this._bagView?.refreshDragEnabled();
this._syncDragState();
}
/**
* 魅惑被释放时回调(composeKey);用于幻魂镜等与合成生命周期对齐。
* @param handler 回调;null 清除
*/
setEntryReleasedHandler(handler: ((composeKey: string) => void) | null): void {
this._onEntryReleased = handler;
}
bindMergeSession(session: MergeSession | null): void {
this._detachMergeListeners();
this._mergeSession = session;
if (!session) {
this._mergeListener = null;
return;
}
this._mergeListener = (event) => {
if (event.type === MergeEventType.MapUpgraded) {
this.releaseByGridIndex(event.gridIndex, 'upgraded');
return;
}
if (event.type === MergeEventType.ComposeChanged) {
this.reconcileComposeChanged(event.compositions);
}
};
const listener = this._mergeListener as (e: MergeEvent) => void;
session.on(MergeEventType.MapUpgraded as unknown as MergeEventSubscription, listener);
session.on(MergeEventType.ComposeChanged as unknown as MergeEventSubscription, listener);
}
/**
* 合成变阵后:composeKey 失效或实体已死的魅惑须解除(成将/换位不发 MapUpgraded)。
* @param compositions 当前合成体列表
*/
reconcileComposeChanged(compositions: readonly ComposedUnit[]): void {
const validKeys = createMembershipSet<string>();
for (let i = 0; i < compositions.length; i++) {
const c = compositions[i];
if (c.gridIndices.length >= 2) {
membershipAdd(validKeys, composeKey(c));
}
}
const runtime = this._runtime;
const keys = mapKeysToArray(this._entries);
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
const entry = this._entries.get(key);
if (!entry) continue;
if (!validKeys.has(key)) {
this._releaseEntry(key, 'compose');
continue;
}
if (runtime && !runtime.session.world.isAlive(entry.entityId)) {
this._releaseEntry(key, 'compose');
}
}
}
unbind(): void {
this.clear();
this.bindMergeSession(null);
this._onEntryReleased = null;
this._runtime = null;
this._bagView = null;
}
private _detachMergeListeners(): void {
if (!this._mergeSession || !this._mergeListener) return;
const listener = this._mergeListener as (e: MergeEvent) => void;
this._mergeSession.off(MergeEventType.MapUpgraded as unknown as MergeEventSubscription, listener);
this._mergeSession.off(MergeEventType.ComposeChanged as unknown as MergeEventSubscription, listener);
}
clear(): void {
const keys = mapKeysToArray(this._entries);
for (let i = 0; i < keys.length; i++) {
this._releaseEntry(keys[i], 'clear');
}
}
tick(dt: number): void {
if (this._entries.size === 0) return;
const expired: string[] = [];
for (const [key, entry] of this._entries) {
entry.remainingSec -= dt;
this._syncCharmProgress(entry);
if (entry.remainingSec <= 0) {
expired.push(key);
}
}
for (const key of expired) {
this._releaseEntry(key, 'timeout');
}
}
isEntityCharmed(entityId: EntityId): boolean {
return this._entityToKey.has(entityId);
}
isGridCharmed(gridIndex: number): boolean {
for (const entry of this._entries.values()) {
if (entry.gridIndices.includes(gridIndex)) return true;
}
return false;
}
isComposeKeyCharmed(composeKey: string): boolean {
return this._entries.has(composeKey);
}
getCharmedComposeKeys(): ReadonlyMembershipSet<string> {
return membershipFromArray(mapKeysToArray(this._entries));
}
applyCharm(target: CharmTarget, durationSec: number, fxRoot: Node | null): void {
if (this.isComposeKeyCharmed(target.composeKey)) {
this._releaseEntry(target.composeKey, 'reapply');
}
const runtime = this._runtime;
if (!runtime) return;
const session = runtime.session;
session.presentation.cancelByCaster(target.entityId);
if (fxRoot?.isValid) {
fxRoot.name = 'charmFx';
fxRoot.active = true;
this._prepareCharmFx(fxRoot);
const anchorGrid = this._resolveAnchorGrid(target.anchorGridIndex);
const world = fxRoot.worldPosition.clone();
if (anchorGrid?.node?.isValid) {
fxRoot.parent = anchorGrid.node;
fxRoot.setWorldPosition(world);
}
const hitNode = fxRoot.getChildByName('击中效果');
if (hitNode?.isValid) {
hitNode.active = true;
}
}
const entry: CharmEntry = {
composeKey: target.composeKey,
entityId: target.entityId,
gridIndices: target.gridIndices,
durationSec: Math.max(0.001, durationSec),
remainingSec: Math.max(0.001, durationSec),
fxRoot: fxRoot?.isValid ? fxRoot : null,
progressSprite: fxRoot?.isValid ? _resolveCharmProgressSprite(fxRoot) : null,
};
if (entry.progressSprite) {
_ensureCharmProgressSprite(entry.progressSprite);
Tween.stopAllByTarget(entry.progressSprite);
}
this._entries.set(target.composeKey, entry);
this._entityToKey.set(target.entityId, target.composeKey);
this._syncCharmProgress(entry);
this._syncDragState();
}
releaseByGridIndex(gridIndex: number, reason: 'upgraded' | 'timeout' | 'clear' | 'reapply' = 'upgraded'): void {
for (const [key, entry] of this._entries) {
if (entry.gridIndices.includes(gridIndex)) {
this._releaseEntry(key, reason);
return;
}
}
}
private _prepareCharmFx(fxRoot: Node): void {
const flyNode = fxRoot.getChildByName('fly');
if (flyNode?.isValid) flyNode.active = false;
const progressNode = fxRoot.getChildByName('progressTemplate');
if (progressNode?.isValid) progressNode.active = true;
}
private _resolveAnchorGrid(gridIndex: number): TextGrid | null {
return this._bagView?.getMapGrids().get(gridIndex) ?? null;
}
private _releaseEntry(key: string, _reason: 'upgraded' | 'timeout' | 'clear' | 'reapply' | 'compose'): void {
const entry = this._entries.get(key);
if (!entry) return;
this._entries.delete(key);
this._entityToKey.delete(entry.entityId);
if (entry.progressSprite?.isValid) {
Tween.stopAllByTarget(entry.progressSprite);
}
if (entry.fxRoot?.isValid) {
entry.fxRoot.destroy();
}
this._syncDragState();
try {
this._onEntryReleased?.(key);
} catch (e) {
console.warn('[CharmController] entryReleased handler', String(e));
}
}
private _syncDragState(): void {
const view = this._bagView;
if (!view) return;
view.refreshMapDragEnabled();
for (const grid of view.getMapGrids().values()) {
if (!grid?.isValid || grid.location?.kind !== 'map') continue;
if (this.isGridCharmed(grid.location.gridIndex)) {
grid.enableDrag(false);
}
}
}
/** 战斗 tick 驱动:remainingSec 递减 + fillRange 同步(不依赖 Component.update) */
private _syncCharmProgress(entry: CharmEntry): void {
if (!entry.fxRoot?.isValid || entry.durationSec <= 0) return;
let sprite = entry.progressSprite;
if (!sprite?.isValid) {
sprite = _resolveCharmProgressSprite(entry.fxRoot);
entry.progressSprite = sprite;
}
if (!sprite?.isValid) return;
_ensureCharmProgressSprite(sprite);
sprite.fillRange = Math.max(0, Math.min(1, entry.remainingSec / entry.durationSec));
}
}
function _ensureCharmProgressSprite(sprite: Sprite): void {
sprite.type = Sprite.Type.FILLED;
sprite.fillType = Sprite.FillType.RADIAL;
sprite.fillCenter.set(0.5, 0.5);
sprite.fillStart = 0;
}
function _resolveCharmProgressSprite(fxRoot: Node): Sprite | null {
const tpl = fxRoot.getChildByName('progressTemplate');
if (!tpl?.isValid) return null;
const progressNode = _findNamedNode(tpl, 'progress');
const named = progressNode?.getComponent(Sprite) ?? null;
if (named) return named;
for (const sp of tpl.getComponentsInChildren(Sprite)) {
if (sp.node.name === 'progress') return sp;
}
for (const sp of tpl.getComponentsInChildren(Sprite)) {
if (sp.type === Sprite.Type.FILLED) return sp;
}
return null;
}
function _findNamedNode(root: Node, name: string): Node | null {
if (root.name === name) return root;
for (const child of root.children) {
const found = _findNamedNode(child, name);
if (found) return found;
}
return null;
}