import { Node } from 'cc'; export type RedpointCallback = (count: number, action: string) => void; export type RedpointListener = Node | RedpointCallback; interface ListenerRecord { listener: RedpointListener; target?: unknown; } /** * 红点数据及红点间聚合关系管理器。 * * 未分组的红点数量由 update 直接设置;分组后的父红点数量始终等于 * 所有直接子红点的数量之和,并会沿父级关系自动向上传播。 */ export class RedpointManager { /** update 写入的原始数量。父红点一旦分组,其显示数量不再使用该值。 */ private readonly sourceCounts = new Map(); /** 对外可见的最终数量(包含分组汇总结果)。 */ private readonly counts = new Map(); private readonly children = new Map(); private readonly parents = new Map(); private readonly listeners = new Map(); private readonly refreshQueue: string[] = []; private refreshing = false; /** * 更新某个红点的数量。 * 负数及非有限数会按 0 处理;父红点的最终数量仍由其子红点决定。 */ update(action: string, count: number): void { const normalizedCount = Number.isFinite(count) ? Math.max(0, count) : 0; this.sourceCounts.set(action, normalizedCount); this.refresh(action); } /** 获取红点当前的最终数量。 */ getCount(action: string): number { return this.counts.get(action) ?? 0; } /** * 绑定红点组关系。重复调用会在已有关系上追加(重复项会被忽略)。 * main 的数量等于其所有 sub 的数量之和,支持多级分组。 */ group(main: string, ...sub: string[]): void { let mainChildren = this.children.get(main); let changed = false; for (const child of sub) { if (child === main || this.hasDescendant(child, main)) { console.warn(`[RedpointManager] ignored circular group relation: ${main} -> ${child}`); continue; } if (!mainChildren) { mainChildren = []; this.children.set(main, mainChildren); } if (mainChildren.includes(child)) continue; mainChildren.push(child); let childParents = this.parents.get(child); if (!childParents) { childParents = []; this.parents.set(child, childParents); } childParents.push(main); changed = true; } if (changed) this.refresh(main); } /** * 添加红点监听。 * - 绑定 Node 时,数量大于 0 显示,等于 0 隐藏。 * - 绑定回调时,回调参数为 (count, action),并使用 target 作为 this。 * * 监听添加后会立即同步一次当前状态;相同 listener + target 不会重复添加。 */ on(action: string, listener: Node, target?: unknown): void; on(action: string, listener: RedpointCallback, target?: unknown): void; on(action: string, listener: RedpointListener, target?: unknown): void { if (!(listener instanceof Node) && typeof listener !== 'function') { console.error(`[RedpointManager] ignored invalid listener: ${action}`); return; } if (listener instanceof Node && !listener.isValid) return; // Node 未显式传 owner 时以自身作为 target,便于 offTarget(node) 清理。 const owner = arguments.length >= 3 ? target : (listener instanceof Node ? listener : undefined); let bucket = this.listeners.get(action); if (!bucket) { bucket = []; this.listeners.set(action, bucket); } for (const record of bucket) { if (record.listener === listener && record.target === owner) return; } const record: ListenerRecord = { listener, target: owner }; bucket.push(record); this.dispatch(action, record, this.getCount(action)); } /** * 移除监听。 * - off(action):移除该红点的全部监听。 * - off(action, listener):移除该 listener 在该红点上的全部监听。 * - off(action, listener, target):只移除完全匹配的监听。 * - off(action, undefined, target):移除 target 在该红点上的全部监听。 * - off(target):移除该 target 在所有红点上的全部监听。 */ off(target: object): void; off(action: string, listener?: RedpointListener, target?: unknown): void; off(actionOrTarget: string | object, listener?: RedpointListener, target?: unknown): void { if (typeof actionOrTarget !== 'string') { this.offTarget(actionOrTarget); return; } const action = actionOrTarget; const bucket = this.listeners.get(action); if (!bucket) return; const filterByListener = listener !== undefined; const filterByTarget = arguments.length >= 3; if (!filterByListener && !filterByTarget) { this.listeners.delete(action); return; } for (let index = bucket.length - 1; index >= 0; --index) { const record = bucket[index]; if (filterByListener && record.listener !== listener) continue; if (filterByTarget && record.target !== target) continue; bucket.splice(index, 1); } if (bucket.length === 0) this.listeners.delete(action); } /** 移除 on 时使用同一 target 添加的所有监听(跨所有 action)。 */ offTarget(target: unknown): void { for (const [action, bucket] of this.listeners) { for (let index = bucket.length - 1; index >= 0; --index) { if (bucket[index].target === target) bucket.splice(index, 1); } if (bucket.length === 0) this.listeners.delete(action); } } /** 与 Cocos EventTarget 命名保持兼容。 */ targetOff(target: unknown): void { this.offTarget(target); } /** 清空所有数量、分组关系和监听。 */ clear(): void { this.sourceCounts.clear(); this.counts.clear(); this.children.clear(); this.parents.clear(); this.listeners.clear(); this.refreshQueue.length = 0; } /** 刷新 action 及其所有祖先;所有数量先稳定,再统一派发通知。 */ private refresh(action: string): void { this.refreshQueue.push(action); if (this.refreshing) return; this.refreshing = true; try { while (this.refreshQueue.length > 0) { this.performRefresh(this.refreshQueue.shift()!); } } finally { this.refreshing = false; } } private performRefresh(action: string): void { const affected = this.collectAffected(action); const memo = new Map(); const changed: string[] = []; for (const current of affected) { const nextCount = this.resolveCount(current, memo, []); const previousCount = this.counts.get(current) ?? 0; this.counts.set(current, nextCount); if (nextCount !== previousCount) changed.push(current); } for (const current of changed) { this.emit(current, this.counts.get(current) ?? 0); } } /** 按 action -> 直接父级 -> 更高父级的顺序收集,保证通知顺序可预期。 */ private collectAffected(action: string): string[] { const result: string[] = []; const visited: string[] = []; const pending = [action]; while (pending.length > 0) { const current = pending.shift()!; if (visited.includes(current)) continue; visited.push(current); result.push(current); for (const parent of this.parents.get(current) ?? []) pending.push(parent); } return result; } private resolveCount(action: string, memo: Map, visiting: string[]): number { const cached = memo.get(action); if (cached !== undefined) return cached; const actionChildren = this.children.get(action); if (!actionChildren || actionChildren.length === 0) { const count = this.sourceCounts.get(action) ?? 0; memo.set(action, count); return count; } // group 已阻止循环;这里保留保护,避免异常数据导致无限递归。 if (visiting.includes(action)) return 0; visiting.push(action); let count = 0; for (const child of actionChildren) { count += this.resolveCount(child, memo, visiting); } visiting.pop(); memo.set(action, count); return count; } private hasDescendant(action: string, expected: string): boolean { const visited: string[] = []; const pending = [action]; while (pending.length > 0) { const current = pending.pop()!; if (current === expected) return true; if (visited.includes(current)) continue; visited.push(current); for (const child of this.children.get(current) ?? []) pending.push(child); } return false; } private emit(action: string, count: number): void { const bucket = this.listeners.get(action); if (!bucket) return; for (const record of [...bucket]) { if (!this.dispatch(action, record, count)) { const index = bucket.indexOf(record); if (index >= 0) bucket.splice(index, 1); } } if (bucket.length === 0) this.listeners.delete(action); } /** 返回 false 表示监听对象已失效,应自动清理。 */ private dispatch(action: string, record: ListenerRecord, count: number): boolean { if (record.listener instanceof Node) { if (!record.listener.isValid) return false; record.listener.active = count > 0; return true; } try { record.listener.call(record.target, count, action); } catch (error) { console.error(`[RedpointManager] listener failed: ${action}`, error, record); } return true; } }