import { isValid, Node, UIOpacity, UITransform, Vec3 } from 'cc'; import { GBundle } from '../../game/ConfigRes'; import { ConstNumType } from '../../game/GameDataEnums'; import type { MapManager } from './legacy/MapManager'; import type { MapGrid } from './legacy/MapGrid'; /** 宝箱预制路径 */ const CHEST_TIP_PATH = 'prefab/'; /** 宝箱预制名(原格子锁) */ const CHEST_TIP_NAME = '宝箱'; /** tip 世界坐标复用 */ const _tipWorld = new Vec3(); /** tip 本地坐标复用 */ const _tipLocal = new Vec3(); /** 单格宝箱运行时 */ interface DigChestSlot { /** 格子 Index */ index: number; /** 是否已八邻揭示 */ revealed: boolean; /** 是否已被铲开 */ dug: boolean; /** 宝箱 tip */ tip: Node | null; } /** 铲成功后的宝箱处理结果 */ export interface DigChestOutcome { /** 本次是否铲到宝箱 */ dugChest: boolean; /** 本局已铲开宝箱数(含本次) */ dugChestCount: number; /** 飞入起点世界坐标;未铲到宝箱为 null */ flyWorld: Vec3 | null; } /** * 地图开挖宝箱:开战埋点、八邻揭示宝箱、铲开计数。 */ export class MapDigChestHost { private _map: MapManager | null = null; private readonly _slots: DigChestSlot[] = []; private _dugChestCount = 0; /** * 开战埋宝箱:Const38 为**每个阵营**固定个数,只在各自 CampArea 的 Dig 格内随机位置。 * @param map 地图 */ init(map: MapManager): void { this.clear(); this._map = map; const digsByCamp: { camp: number; digs: MapGrid[] }[] = []; for (let i = 0; i < map.mapGrids.length; i++) { const g = map.mapGrids[i]; if (!g) continue; g.chestRevealed = false; if (!(g.isCanBeDig && !g.isCanPutText && !g.isCanWalk)) continue; g.updateGrid(); const camp = (g.CampAreaID | 0); let bucket: { camp: number; digs: MapGrid[] } | null = null; for (let b = 0; b < digsByCamp.length; b++) { if (digsByCamp[b].camp === camp) { bucket = digsByCamp[b]; break; } } if (!bucket) { bucket = { camp, digs: [] }; digsByCamp.push(bucket); } bucket.digs.push(g); } if (digsByCamp.length <= 0) { console.warn('[MapDigChestHost] 无可开挖格,未埋宝箱'); return; } const want = this._resolveChestCount(); if (want <= 0) { console.warn('[MapDigChestHost] Const38 宝箱个数无效,未埋宝箱', want); return; } for (let c = 0; c < digsByCamp.length; c++) { this._buryInCamp(digsByCamp[c].camp, digsByCamp[c].digs, want); } console.log('[MapDigChestHost] 埋宝箱合计', this._slots.length, this._slots.map((s) => s.index)); } /** * 在单个阵营 Dig 池内埋固定个数(只随机位置)。 * @param camp CampAreaID * @param digs 该阵营可开挖格 * @param want Const38 个数 */ private _buryInCamp(camp: number, digs: MapGrid[], want: number): void { if (digs.length <= 0) return; const shuffled = digs.slice(); for (let i = shuffled.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); const tmp = shuffled[i]; shuffled[i] = shuffled[j]; shuffled[j] = tmp; } const n = Math.min(want, shuffled.length); if (shuffled.length < want) { console.warn('[MapDigChestHost] 阵营可开挖格不足', camp, shuffled.length, '目标', want); } for (let i = 0; i < n; i++) { this._slots.push({ index: shuffled[i].Index, revealed: false, dug: false, tip: null, }); } console.log('[MapDigChestHost] 阵营埋宝箱', camp, n, shuffled.slice(0, n).map((g) => g.Index)); } /** Const38 关卡技能宝箱个数 */ private _resolveChestCount(): number { if (typeof gg === 'undefined' || !gg.data?.table?.getConst) { console.warn('[MapDigChestHost] gg.data.table 未就绪,无法读 Const38'); return 0; } return Math.floor(Number(gg.data.table.getConst(ConstNumType.BattleDigChestCount)) || 0); } /** * @param index 格 Index * @returns 是否仍有未铲宝箱 */ hasChest(index: number): boolean { const s = this._slotOf(index); return !!(s && !s.dug); } /** * @param index 格 Index * @returns 是否已揭示且未铲 */ isRevealed(index: number): boolean { const s = this._slotOf(index); return !!(s && s.revealed && !s.dug); } /** * 铲子逻辑开挖成功后立刻调用:只做八邻揭示(不等动画结束)。 * @param index 被铲格 Index */ revealAround(index: number): void { this._revealNeighbors(index); } /** * 铲子开挖动画收尾:铲宝箱则计入并返回飞入信息;并补一次八邻揭示。 * @param index 被铲格 Index */ onDigged(index: number): DigChestOutcome { const outcome: DigChestOutcome = { dugChest: false, dugChestCount: this._dugChestCount, flyWorld: null, }; const dugSlot = this._slotOf(index); if (dugSlot && !dugSlot.dug) { dugSlot.dug = true; this._dugChestCount++; outcome.dugChest = true; outcome.dugChestCount = this._dugChestCount; outcome.flyWorld = this._takeFlyWorld(dugSlot); this._releaseTip(dugSlot); const grid = this._map?.getMapGridByIndex(index); if (grid) { grid.chestRevealed = false; grid.updateGrid(); } } // 补揭示:防止动画中途回调丢失时邻箱未亮 this._revealNeighbors(index); return outcome; } /** 退局:归还 tip、清标记 */ clear(): void { for (let i = 0; i < this._slots.length; i++) { const s = this._slots[i]; this._releaseTip(s); const g = this._map?.getMapGridByIndex(s.index); if (g) { g.chestRevealed = false; } } this._slots.length = 0; this._dugChestCount = 0; this._map = null; } private _slotOf(index: number): DigChestSlot | null { for (let i = 0; i < this._slots.length; i++) { if (this._slots[i].index === index) return this._slots[i]; } return null; } /** * 八邻未挖宝箱揭示。 * @param centerIndex 刚铲开的格 */ private _revealNeighbors(centerIndex: number): void { const map = this._map; if (!map) return; const center = map.getMapGridByIndex(centerIndex); if (!center) return; for (let dr = -1; dr <= 1; dr++) { for (let dc = -1; dc <= 1; dc++) { if (dr === 0 && dc === 0) continue; const nb = map.getMapGrid(center.Row + dr, center.Col + dc); if (!nb) continue; const slot = this._slotOf(nb.Index); if (!slot || slot.dug || slot.revealed) continue; this._revealSlot(slot, nb); } } } /** * @param slot 宝箱槽 * @param grid 格子 */ private _revealSlot(slot: DigChestSlot, grid: MapGrid): void { slot.revealed = true; grid.chestRevealed = true; if (!grid.waitUnlockedSprite) { console.warn('[MapDigChestHost] 揭示缺 waitUnlockedSprite', grid.Index); } grid.updateGrid(); this._ensureTip(slot, grid); } /** * 挂宝箱到 MapManager 根(勿挂「格子」:该节点有 Layout,多一个子节点会整图重排偏移)。 * @param slot 宝箱槽 * @param grid 格子 */ private _ensureTip(slot: DigChestSlot, grid: MapGrid): void { const cell = grid.node; if (!cell?.isValid) { console.warn('[MapDigChestHost] 格子节点无效,无法挂宝箱', grid.Index); return; } const tipLayer = this._tipLayer(); if (!tipLayer?.isValid) { console.warn('[MapDigChestHost] 宝箱挂点无效', grid.Index); return; } if (slot.tip?.isValid) { this._placeTipOnMap(slot.tip, cell, tipLayer); this._resetTipVisual(slot.tip); slot.tip.active = true; return; } if (typeof gg === 'undefined' || !gg.res?.getNode) { console.warn('[MapDigChestHost] gg.res 未就绪,无法加载宝箱'); return; } const tip = gg.res.getNode(CHEST_TIP_PATH, CHEST_TIP_NAME, GBundle.BattleGame); if (!tip?.isValid) { console.warn('[MapDigChestHost] 缺少宝箱预制体', CHEST_TIP_NAME); return; } this._placeTipOnMap(tip, cell, tipLayer); this._resetTipVisual(tip); tip.active = true; slot.tip = tip; } /** * 宝箱 tip 层:MapManager 根下独立节点,压在「格子」之上、不受 Layout 影响。 */ private _tipLayer(): Node | null { const map = this._map; if (!map?.node?.isValid) return null; let layer = map.node.getChildByName('宝箱提示层'); if (layer?.isValid) return layer; layer = new Node('宝箱提示层'); layer.layer = map.node.layer; layer.parent = map.node; const grids = map.mapGridParent; if (grids?.isValid) { layer.setSiblingIndex(grids.getSiblingIndex() + 1); } else { layer.setSiblingIndex(map.node.children.length - 1); } return layer; } /** * @param tip 宝箱节点 * @param cell 格子节点 * @param tipLayer 宝箱提示层 */ private _placeTipOnMap(tip: Node, cell: Node, tipLayer: Node): void { tip.parent = tipLayer; tip.setSiblingIndex(tipLayer.children.length - 1); cell.getWorldPosition(_tipWorld); const ut = tipLayer.getComponent(UITransform) ?? tipLayer.addComponent(UITransform); ut.convertToNodeSpaceAR(_tipWorld, _tipLocal); tip.setPosition(_tipLocal); } /** 对象池取出后复位显隐(飞入曾改 scale / opacity) */ private _resetTipVisual(tip: Node): void { tip.setScale(1, 1, 1); const box = tip.getChildByName('box'); if (box?.isValid) box.active = true; const op = tip.getComponent(UIOpacity); if (op) op.opacity = 255; } /** * 取飞入起点世界坐标。 * @param slot 宝箱槽 */ private _takeFlyWorld(slot: DigChestSlot): Vec3 { const tip = slot.tip; if (tip?.isValid) { tip.getWorldPosition(_tipWorld); return _tipWorld.clone(); } const grid = this._map?.getMapGridByIndex(slot.index); if (grid?.node?.isValid) { grid.node.getWorldPosition(_tipWorld); return _tipWorld.clone(); } console.warn('[MapDigChestHost] 铲宝箱无飞入坐标', slot.index); return new Vec3(); } /** * @param slot 宝箱槽 */ private _releaseTip(slot: DigChestSlot): void { const tip = slot.tip; slot.tip = null; if (!tip || !isValid(tip)) return; tip.active = false; if (typeof gg !== 'undefined' && tip['resKey'] && gg.res?.putNode) { gg.res.putNode(tip); } else { tip.destroy(); } } }