import { isValid, Node, sp, UITransform, Vec3 } from 'cc'; import { GBundle } from '../../game/ConfigRes'; import { ConstNumType } from '../../game/GameDataEnums'; import { BattleSpinePlayback } from '../presentation/BattleSpinePlayback'; import { schedulePresentation } from '../skill/BattlePresentationClock'; import type { MapManager } from './legacy/MapManager'; import type { MapGrid } from './legacy/MapGrid'; /** 宝箱预制路径 */ const CHEST_TIP_PATH = 'prefab/'; /** 宝箱预制名 */ const CHEST_TIP_NAME = '宝箱'; /** 揭示待机 */ const ANIM_IDLE = 'idle'; /** 铲开播放 */ const ANIM_PLAY = 'play'; /** 盖子骨(xiangzi2:跳起落稳后视为已打开) */ const OPEN_LID_BONE = 'z2'; /** 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; } /** * 地图开挖宝箱:开战埋点、八邻揭示(idle)、铲开播 play 后弹三选一。 */ export class MapDigChestHost { private _map: MapManager | null = null; private readonly _slots: DigChestSlot[] = []; private _dugChestCount = 0; /** 开箱完成回调世代(clear / 重复开箱作废) */ private _openGen = 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); } /** * 铲子开挖:铲到宝箱则计入;tip 留在格上待播 play(勿提前回收)。 * @param index 被铲格 Index */ onDigged(index: number): DigChestOutcome { const outcome: DigChestOutcome = { dugChest: false, dugChestCount: this._dugChestCount, }; const dugSlot = this._slotOf(index); if (dugSlot && !dugSlot.dug) { dugSlot.dug = true; dugSlot.revealed = true; this._dugChestCount++; outcome.dugChest = true; outcome.dugChestCount = this._dugChestCount; const grid = this._map?.getMapGridByIndex(index); if (grid) { grid.chestRevealed = false; grid.updateGrid(); if (!dugSlot.tip?.isValid) { this._ensureTip(dugSlot, grid); } } } this._revealNeighbors(index); return outcome; } /** * 立刻回收 tip(AI 跳过开箱 Spine)。 * @param index 被铲格 */ releaseTipAt(index: number): void { const slot = this._slotOf(index); if (slot) this._releaseTip(slot); } /** * 铲开后播 play:盖子打开落稳后回收 tip 并回调三选一(弹窗后不再继续播开箱)。 * xiangzi2:`z2` translate 约 0.67 起跳、1.2 落稳 → 落稳后弹三选一。 * @param index 被铲格 * @param onOpen 打开之后回调(弹三选一) */ playOpen(index: number, onOpen: () => void): void { const slot = this._slotOf(index); const tip = slot?.tip ?? null; if (!slot || !tip?.isValid) { console.warn('[MapDigChestHost] 开箱缺 tip', index); onOpen(); return; } const sk = this._tipSkeleton(tip); if (!sk) { console.warn('[MapDigChestHost] 开箱缺 Spine', index); this._releaseTip(slot); onOpen(); return; } const gen = ++this._openGen; const openedAt = BattleSpinePlayback.findBoneMotionEndTime(sk, ANIM_PLAY, OPEN_LID_BONE, 'translate') ?? BattleSpinePlayback.findBoneMotionEndTime(sk, ANIM_PLAY, OPEN_LID_BONE, 'rotate'); if (!(openedAt != null && openedAt > 0)) { console.warn('[MapDigChestHost] play 无盖开落稳关键帧,立刻弹三选一', index); } const openDelay = openedAt != null && openedAt > 0 ? openedAt : 0; const ok = BattleSpinePlayback.play(sk, ANIM_PLAY, { loop: false }); if (!ok) { this._releaseTip(slot); onOpen(); return; } schedulePresentation(openDelay, () => { if (gen !== this._openGen) return; this._releaseTip(slot); onOpen(); }); } /** 退局:归还 tip、清标记 */ clear(): void { this._openGen++; 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); } /** 对象池取出后复位,并播 idle */ private _resetTipVisual(tip: Node): void { tip.setScale(1, 1, 1); const sk = this._tipSkeleton(tip); if (!sk) { console.warn('[MapDigChestHost] 宝箱缺 anim/Spine'); return; } BattleSpinePlayback.play(sk, ANIM_IDLE, { loop: true }); } /** * @param tip 宝箱根节点 */ private _tipSkeleton(tip: Node): sp.Skeleton | null { const anim = tip.getChildByName('anim'); return anim?.getComponent(sp.Skeleton) ?? tip.getComponentInChildren(sp.Skeleton) ?? null; } /** * @param slot 宝箱槽 */ private _releaseTip(slot: DigChestSlot): void { const tip = slot.tip; slot.tip = null; if (!tip || !isValid(tip)) return; const sk = this._tipSkeleton(tip); if (sk) BattleSpinePlayback.clearPendingComplete(sk); tip.active = false; if (typeof gg !== 'undefined' && tip['resKey'] && gg.res?.putNode) { gg.res.putNode(tip); } else { tip.destroy(); } } }