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.
268 lines
9.2 KiB
268 lines
9.2 KiB
import { membershipAdd, mapKeysToArray, type MembershipSet } from '../../../mx/tools/MembershipSet';
|
|
import type { MapManager } from '../map/legacy/MapManager';
|
|
import { TextGrid } from './TextGrid';
|
|
|
|
/** 字块起飞前的原归属格 */
|
|
export type GridDetachSource =
|
|
| { kind: 'map'; gridIndex: number }
|
|
| { kind: 'bag'; slotIndex: number };
|
|
|
|
/**
|
|
* 背包槽 / 地图格 / 飞行中字块的唯一归属索引。
|
|
* 视图层不得再直接维护多份 Map;所有挂接变更经此类提交。
|
|
*/
|
|
export class BoardGridRegistry {
|
|
private _slotGrids = new Map<number, TextGrid>();
|
|
private _mapGrids = new Map<number, TextGrid>();
|
|
private _flightGrids: TextGrid[] = [];
|
|
|
|
/** 背包槽索引表(可变引用,供 BagSlotEntranceAnimator 等直接读写) */
|
|
get slotGrids(): Map<number, TextGrid> {
|
|
return this._slotGrids;
|
|
}
|
|
|
|
/** 地图格索引表(可变引用) */
|
|
get mapGrids(): Map<number, TextGrid> {
|
|
return this._mapGrids;
|
|
}
|
|
|
|
/** 飞行中字块列表 */
|
|
get flightGrids(): readonly TextGrid[] {
|
|
return this._flightGrids;
|
|
}
|
|
|
|
/** 全量替换背包索引(rebuild 批次切换) */
|
|
replaceSlotGrids(next: Map<number, TextGrid>): void {
|
|
this._slotGrids = next;
|
|
}
|
|
|
|
/** 全量替换地图索引(rebuild 批次切换) */
|
|
replaceMapGrids(next: Map<number, TextGrid>): void {
|
|
this._mapGrids = next;
|
|
}
|
|
|
|
/** 取出并清空飞行列表(全量 rebuild 纳入 prev 复用) */
|
|
takeFlightGrids(): TextGrid[] {
|
|
const out = this._flightGrids;
|
|
this._flightGrids = [];
|
|
return out;
|
|
}
|
|
|
|
/** 背包槽字块 */
|
|
getBagGrid(slotIndex: number): TextGrid | undefined {
|
|
return this._slotGrids.get(slotIndex);
|
|
}
|
|
|
|
/** 地图格字块 */
|
|
getMapGrid(gridIndex: number): TextGrid | undefined {
|
|
return this._mapGrids.get(gridIndex);
|
|
}
|
|
|
|
/** 登记背包槽归属 */
|
|
setBagGrid(slotIndex: number, grid: TextGrid): void {
|
|
this._purgeGridFromIndices(grid);
|
|
this._slotGrids.set(slotIndex, grid);
|
|
this._removeFromFlight(grid);
|
|
}
|
|
|
|
/** 登记地图格归属 */
|
|
setMapGrid(gridIndex: number, grid: TextGrid): void {
|
|
this._purgeGridFromIndices(grid);
|
|
this._mapGrids.set(gridIndex, grid);
|
|
this._removeFromFlight(grid);
|
|
}
|
|
|
|
/** 删除背包槽登记 */
|
|
deleteBagSlot(slotIndex: number): void {
|
|
this._slotGrids.delete(slotIndex);
|
|
}
|
|
|
|
/** 删除地图格登记 */
|
|
deleteMapCell(gridIndex: number): void {
|
|
this._mapGrids.delete(gridIndex);
|
|
}
|
|
|
|
/** 交换飞行:从槽/地图摘下并记入飞行列表 */
|
|
detachForFlight(grid: TextGrid, from: GridDetachSource): void {
|
|
if (from.kind === 'map') {
|
|
if (this._mapGrids.get(from.gridIndex) === grid) {
|
|
this._mapGrids.delete(from.gridIndex);
|
|
}
|
|
} else if (this._slotGrids.get(from.slotIndex) === grid) {
|
|
this._slotGrids.delete(from.slotIndex);
|
|
}
|
|
if (this._flightGrids.indexOf(grid) < 0) {
|
|
this._flightGrids.push(grid);
|
|
}
|
|
}
|
|
|
|
/** 从飞行列表移除 */
|
|
removeFromFlight(grid: TextGrid): void {
|
|
const i = this._flightGrids.indexOf(grid);
|
|
if (i >= 0) this._flightGrids.splice(i, 1);
|
|
}
|
|
|
|
/** 从所有索引与飞行列表移除(回收前) */
|
|
purgeGridFromIndices(grid: TextGrid): void {
|
|
this._purgeGridFromIndices(grid);
|
|
this._removeFromFlight(grid);
|
|
}
|
|
|
|
/** 是否已登记槽/格/飞行(不含父节点兜底) */
|
|
isIndexed(grid: TextGrid): boolean {
|
|
for (const g of this._slotGrids.values()) {
|
|
if (g === grid) return true;
|
|
}
|
|
for (const g of this._mapGrids.values()) {
|
|
if (g === grid) return true;
|
|
}
|
|
return this._flightGrids.indexOf(grid) >= 0;
|
|
}
|
|
|
|
/**
|
|
* 字块是否已被本注册表跟踪。
|
|
* @param mapHostedCheck 父节点在地图格上但索引未登记时的兜底
|
|
*/
|
|
isTracked(grid: TextGrid, mapHostedCheck: (g: TextGrid) => boolean): boolean {
|
|
if (this.isIndexed(grid)) return true;
|
|
return mapHostedCheck(grid);
|
|
}
|
|
|
|
/**
|
|
* 背包重建前:收集所有须保护的地图字块(含专用层与格子残留)。
|
|
* @param textLayer ui_textGrids
|
|
*/
|
|
collectMapProtected(
|
|
out: MembershipSet<TextGrid>,
|
|
map: MapManager | null,
|
|
prevMap?: ReadonlyMap<number, TextGrid>,
|
|
hasTile?: (gridIndex: number) => boolean,
|
|
textLayer?: import('cc').Node | null,
|
|
): void {
|
|
for (const g of this._mapGrids.values()) {
|
|
if (g?.isValid) membershipAdd(out, g);
|
|
}
|
|
if (prevMap) {
|
|
for (const [idx, g] of prevMap) {
|
|
if (g?.isValid && (!hasTile || hasTile(idx))) membershipAdd(out, g);
|
|
}
|
|
}
|
|
this._collectTextGridsOnNode(textLayer, out);
|
|
const cells = map?.mapGrids;
|
|
if (!cells?.length) return;
|
|
for (let i = 0; i < cells.length; i++) {
|
|
this._collectTextGridsOnNode(cells[i]?.node, out);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 背包重建后:按 session 地图数据 + 专用层/格子对齐 _mapGrids。
|
|
* @param textLayer ui_textGrids
|
|
*/
|
|
reconcileMapFromScene(
|
|
map: MapManager,
|
|
hasTile: (gridIndex: number) => boolean,
|
|
isNodeOnMapLayer: (node: import('cc').Node | null | undefined) => boolean,
|
|
textLayer?: import('cc').Node | null,
|
|
): void {
|
|
const mapIdx = mapKeysToArray(this._mapGrids);
|
|
for (let i = 0; i < mapIdx.length; i++) {
|
|
const idx = mapIdx[i];
|
|
const g = this._mapGrids.get(idx);
|
|
if (!hasTile(idx)) {
|
|
this._mapGrids.delete(idx);
|
|
} else if (!g?.node?.isValid || !isNodeOnMapLayer(g.node)) {
|
|
this._mapGrids.delete(idx);
|
|
}
|
|
}
|
|
this._indexLayerMapGrids(textLayer, hasTile);
|
|
const cells = map.mapGrids;
|
|
if (!cells?.length) return;
|
|
for (let i = 0; i < cells.length; i++) {
|
|
if (!hasTile(i)) continue;
|
|
if (this._mapGrids.get(i)?.node?.isValid) continue;
|
|
const parent = cells[i]?.node;
|
|
if (!parent?.isValid) continue;
|
|
for (const child of parent.children) {
|
|
if (child.name === '__bagDragGhost') continue;
|
|
const tg = child.getComponent(TextGrid);
|
|
if (tg?.isValid) {
|
|
this._mapGrids.set(i, tg);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/** 在场景父节点下查找地图格上的字块 */
|
|
findMapGridOnCell(parent: import('cc').Node | null | undefined): TextGrid | null {
|
|
if (!parent?.isValid) return null;
|
|
for (const child of parent.children) {
|
|
if (child.name === '__bagDragGhost') continue;
|
|
const tg = child.getComponent(TextGrid);
|
|
if (tg?.isValid) return tg;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/** 父节点上除 exclude 外的第一块 TextGrid */
|
|
findOtherGridOnCell(parent: import('cc').Node | null | undefined, exclude: TextGrid): TextGrid | null {
|
|
if (!parent?.isValid) return null;
|
|
for (const child of parent.children) {
|
|
if (child.name === '__bagDragGhost') continue;
|
|
const tg = child.getComponent(TextGrid);
|
|
if (tg?.isValid && tg !== exclude) return tg;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/** 清空全部登记 */
|
|
clear(): void {
|
|
this._slotGrids.clear();
|
|
this._mapGrids.clear();
|
|
this._flightGrids = [];
|
|
}
|
|
|
|
private _collectTextGridsOnNode(host: import('cc').Node | null | undefined, out: MembershipSet<TextGrid>): void {
|
|
if (!host?.isValid) return;
|
|
for (const child of host.children) {
|
|
if (child.name === '__bagDragGhost') continue;
|
|
const tg = child.getComponent(TextGrid);
|
|
if (tg?.isValid) membershipAdd(out, tg);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 按 location 把专用层上的地图字写入索引。
|
|
* @param textLayer ui_textGrids
|
|
* @param hasTile 该格是否仍有字
|
|
*/
|
|
private _indexLayerMapGrids(textLayer: import('cc').Node | null | undefined, hasTile: (gridIndex: number) => boolean): void {
|
|
if (!textLayer?.isValid) return;
|
|
for (const child of textLayer.children) {
|
|
if (child.name === '__bagDragGhost') continue;
|
|
const tg = child.getComponent(TextGrid);
|
|
if (!tg?.isValid) continue;
|
|
const loc = tg.location;
|
|
if (loc?.kind !== 'map') continue;
|
|
if (!hasTile(loc.gridIndex)) continue;
|
|
if (!this._mapGrids.get(loc.gridIndex)?.node?.isValid) {
|
|
this._mapGrids.set(loc.gridIndex, tg);
|
|
}
|
|
}
|
|
}
|
|
|
|
private _purgeGridFromIndices(grid: TextGrid): void {
|
|
for (const [idx, g] of this._slotGrids) {
|
|
if (g === grid) this._slotGrids.delete(idx);
|
|
}
|
|
for (const [idx, g] of this._mapGrids) {
|
|
if (g === grid) this._mapGrids.delete(idx);
|
|
}
|
|
}
|
|
|
|
private _removeFromFlight(grid: TextGrid): void {
|
|
const i = this._flightGrids.indexOf(grid);
|
|
if (i >= 0) this._flightGrids.splice(i, 1);
|
|
}
|
|
}
|
|
|