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/map/MapDigChestHost.ts

405 lines
14 KiB

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();
}
}
}