import { Node } from 'cc'; import { TableNames } from '../../game/ConfigTableData'; import { ConstNumType } from '../../game/GameDataEnums'; /** ui_textList 格子状态 */ export enum TextListCellState { /** 开启:可拖动、可合成;开局不放字 */ Open = 1, /** 待开启:不可拖动、可合成;开局或被邻格解锁时放字 */ OpenNoDrag = 2, /** 未解锁:不可拖、不可落字 */ Locked = 3, /** 空装饰格:不刷底、不出字、不可拖放。当前没有这种格子 */ Blank = 4, } /** 格子节点上的开启底 */ const OPEN_NODE = '背包格开启'; /** 格子节点上的未开启底 */ const LOCKED_NODE = '背包格未开启'; /** 格子节点上的待开启底 */ const GATE_NODE = '待开启'; /** 模板节点,不参与状态 */ const TEMP_NODE = 'temp'; /** 一次待开启推进:新生待解锁格,以及本次要累加到这些格子数字上的等级 */ export interface TextListPromoteResult { /** 新变成待开启、需要出字的格子 */ readonly born: readonly number[]; /** 本次解锁对应的显示等级加值(Const 39);无加值为 0 */ readonly levelAdd: number; } /** * 按关卡 normalland / tolockland 管理 ui_textList 格子状态与拖入解锁。 * normalland 为开启且开局不放字;tolockland 为待开启(有字、不能拖、靠同类型同等级合成打开)。 * 待开启被合成后自身变为开启,上下左右的未解锁格变为待开启。其余为未解锁。 * 每局记录解锁次数,bind 时清零。第 1 次读 Const26(UnlockLandLevelAdd)的 index 0,依次类推;超出则用最后一项。 * 该加值累加到本次上下左右新待解锁格的数字等级上。 * grid1-24 都是普通地块(含 grid17、grid23、grid24)。名为 temp 的节点跳过。 * 格子上的字块是背包槽位数据(BattleBagSession),由视图按槽位记录生成标准 TextGrid; * 拖拽预览、合成解锁走既有背包管线,本类只负责格子底图状态与解锁规则。 */ export class TextListLandHost { /** ui_textList(玩家刷底图;AI 无头仅作邻格几何) */ private _list: Node | null = null; /** 已按关卡刷过;未配置时全部视为开启 */ private _useTable = false; /** 无头:只维护状态,不改节点底图(AI 独立地块进度) */ private _headless = false; /** 开启下标(含拖入后新解锁) */ private readonly _open: number[] = []; /** 待解锁下标(开启但不能拖) */ private readonly _gate: number[] = []; /** 本局已解锁次数;bind / clear 清零。下次解锁用这个下标读 Const26 */ private _unlockCount = 0; /** 最近一次解锁的邻格等级加值,供本条解锁日志使用 */ private _lastLevelAdd = 0; /** * 读取关卡并刷新格子底图。 * @param levelId Level.id * @param list ui_textList */ bind(levelId: number, list: Node | null): void { this._bindLand(levelId, list, false); } /** * AI 无头地块:与玩家同读 Level.normalland/tolockland,进度独立;layout 只用于邻格几何。 * @param levelId Level.id * @param layoutList 玩家 ui_textList(可读坐标;不改其底图) */ bindHeadless(levelId: number, layoutList: Node | null): void { this._bindLand(levelId, layoutList, true); } /** * @param levelId 关卡 * @param list 布局节点 * @param headless 是否无头 */ private _bindLand(levelId: number, list: Node | null, headless: boolean): void { this.clear(); this._headless = headless; if (!list?.isValid) { console.log('[TextListLand] ui_textList 缺失', headless ? 'headless' : ''); return; } this._list = list; const row = gg.data.table.getTableData(TableNames.Level, levelId); if (!row) { console.log('[TextListLand] Level 缺失', levelId); if (!headless) this._applyAll(TextListCellState.Open); else this._useTable = false; return; } const normal = row.normalland; const gate = row.tolockland; if (!normal?.length && !gate?.length) { console.log('[TextListLand] 关卡未配置地块,全部开启', levelId); if (!headless) this._applyAll(TextListCellState.Open); else this._useTable = false; return; } this._useTable = true; this._fill(this._open, normal, 'normalland'); this._fill(this._gate, gate, 'tolockland'); for (let i = this._gate.length - 1; i >= 0; i--) { if (!this._has(this._open, this._gate[i])) continue; console.log('[TextListLand] 下标同时在开启与待解锁,按开启', this._gate[i]); this._gate.splice(i, 1); } if (!headless) this._applyFromTable(); } /** 解除引用 */ clear(): void { this._list = null; this._useTable = false; this._headless = false; this._open.length = 0; this._gate.length = 0; this._unlockCount = 0; this._lastLevelAdd = 0; } /** * 节点对应的格子下标。命名 gridN → N(grid1-24),temp 跳过,其余名字打日志返回 null。 * @param node ui_textList 子节点 */ static indexOfNode(node: Node | null | undefined): number | null { if (!node?.isValid) return null; if (node.name === TEMP_NODE) return null; const matched = /^grid(\d+)$/.exec(node.name); if (!matched) { console.log('[TextListLand] 无法解析格子下标', node.name); return null; } const index = parseInt(matched[1], 10); if (!Number.isFinite(index) || index <= 0) { console.log('[TextListLand] 格子下标非法', node.name); return null; } return index; } /** * 当前下标状态。未走配表时一律开启。 * @param index 格子下标 */ stateOf(index: number): TextListCellState { if (this.isBlank(index)) return TextListCellState.Blank; if (!this._useTable) return TextListCellState.Open; if (this._has(this._open, index)) return TextListCellState.Open; if (this._has(this._gate, index)) return TextListCellState.OpenNoDrag; return TextListCellState.Locked; } /** * 是否空装饰格。当前没有这种格子,grid17 起都按普通地块。 * @param index 格子下标 */ isBlank(_index: number): boolean { return false; } /** * 待解锁格与空装饰格禁止把字拖走。 * @param index 格子下标 */ isNoDrag(index: number): boolean { const state = this.stateOf(index); return state === TextListCellState.OpenNoDrag || state === TextListCellState.Blank; } /** * 待开启格被同类型同等级合成:自身改为开启,上下左右的未解锁格改为待开启。 * 成功解锁计一次,并按本局次数取 Const 39 等级加值。 * @param gateIndex 待开启格下标 */ promoteAround(gateIndex: number): TextListPromoteResult { if (this.isBlank(gateIndex) || this.stateOf(gateIndex) !== TextListCellState.OpenNoDrag) { console.log('[TextListLand] 非待开启格', gateIndex); return { born: [], levelAdd: 0 }; } const gateAt = this._gate.indexOf(gateIndex); if (gateAt >= 0) this._gate.splice(gateAt, 1); if (!this._has(this._open, gateIndex)) this._open.push(gateIndex); if (!this._headless) { const self = this._nodeAt(gateIndex); if (self) this._applyNode(self, TextListCellState.Open); } const born: number[] = []; const neighbors = this._orthogonalNeighbors(gateIndex); for (let i = 0; i < neighbors.length; i++) { const index = neighbors[i]; if (this.stateOf(index) !== TextListCellState.Locked) continue; this._gate.push(index); if (!this._headless) { const node = this._nodeAt(index); if (node) this._applyNode(node, TextListCellState.OpenNoDrag); } born.push(index); } const levelAdd = this._unlockLevelAdd(this._unlockCount); this._lastLevelAdd = levelAdd; this._unlockCount++; return { born, levelAdd }; } /** * 玩家本次解锁只打一条:解锁次数、上下左右累加等级、各新生格随机等级。AI 不打印。 * @param rolls 各格随机等级,形如「槽:等级」;没有新生格传空数组 */ printUnlock(rolls: readonly string[]): void { if (this._headless) return; const randomText = rolls.length > 0 ? rolls.join(',') : '无'; console.log('[TextListLand] 玩家 解锁次数', this._unlockCount, '上下左右累加等级', this._lastLevelAdd, '随机等级', randomText); } /** * Const 39:第 1 次解锁读 index 0,依次类推;下标超出数组则读最后一项。 * @param unlockIndex 本次解锁前已解锁次数 */ private _unlockLevelAdd(unlockIndex: number): number { const list = gg.data.table.getConstArray(ConstNumType.UnlockLandLevelAdd); if (!list.length) { console.log('[TextListLand] Const 解锁格子等级加值为空', ConstNumType.UnlockLandLevelAdd); return 0; } const index = unlockIndex >= 0 && unlockIndex < list.length ? unlockIndex : list.length - 1; const value = Math.floor(Number(list[index])); if (!Number.isFinite(value) || value < 0) { console.log('[TextListLand] 解锁格子等级加值非法', unlockIndex, list[index]); return 0; } return value; } /** * 上下左右相邻格(按节点坐标,间距约一格)。空装饰格不算。 * @param index 中心格下标 */ private _orthogonalNeighbors(index: number): number[] { const origin = this._nodeAt(index); const list = this._list; if (!origin?.isValid || !list?.isValid) return []; const ox = origin.position.x; const oy = origin.position.y; const out: number[] = []; const children = list.children; for (let i = 0; i < children.length; i++) { const other = TextListLandHost.indexOfNode(children[i]); if (other == null || other === index || this.isBlank(other)) continue; const dx = Math.abs(children[i].position.x - ox); const dy = Math.abs(children[i].position.y - oy); const horizontal = dy < 30 && dx > 50 && dx < 120; const vertical = dx < 30 && dy > 50 && dy < 120; if (horizontal || vertical) out.push(other); } return out; } /** * 写入一组下标,非法项打日志。 * @param out 目标数组 * @param raw 配表数组 * @param label 字段名 */ private _fill(out: number[], raw: readonly number[] | undefined, label: string): void { if (!raw?.length) return; for (let i = 0; i < raw.length; i++) { const index = Math.floor(Number(raw[i])); if (!Number.isFinite(index) || index < 0) { console.log('[TextListLand] 非法下标', label, raw[i]); continue; } if (this._has(out, index)) continue; out.push(index); } } /** 全部格子按状态刷底图 */ private _applyAll(state: TextListCellState): void { const list = this._list; if (!list?.isValid) return; const children = list.children; for (let i = 0; i < children.length; i++) { const index = TextListLandHost.indexOfNode(children[i]); if (index == null || this.isBlank(index)) continue; this._applyNode(children[i], state); } } /** 按配表状态刷底图 */ private _applyFromTable(): void { const list = this._list; if (!list?.isValid) return; const children = list.children; for (let i = 0; i < children.length; i++) { const cell = children[i]; const index = TextListLandHost.indexOfNode(cell); if (index == null || this.isBlank(index)) continue; this._applyNode(cell, this.stateOf(index)); } } /** * 切换单格底图:开启显示「背包格开启」,待开启显示「待开启」,未解锁显示「背包格未开启」。 * @param cell 格子节点 * @param state 目标状态 */ private _applyNode(cell: Node, state: TextListCellState): void { const openNode = cell.getChildByName(OPEN_NODE); const lockedNode = cell.getChildByName(LOCKED_NODE); const gateNode = cell.getChildByName(GATE_NODE); if (!openNode && !lockedNode && !gateNode) { console.log('[TextListLand] 格子缺少底图节点', cell.name); return; } if (state === TextListCellState.OpenNoDrag && !gateNode) { console.log('[TextListLand] 格子缺少待开启节点', cell.name); } if (openNode) openNode.active = state === TextListCellState.Open; if (lockedNode) lockedNode.active = state === TextListCellState.Locked; if (gateNode) gateNode.active = state === TextListCellState.OpenNoDrag; } /** * 按下标找格子节点。 * @param index 格子下标 */ private _nodeAt(index: number): Node | null { const list = this._list; if (!list?.isValid) return null; const children = list.children; for (let i = 0; i < children.length; i++) { if (TextListLandHost.indexOfNode(children[i]) === index) return children[i]; } return null; } /** 数组是否包含 */ private _has(arr: readonly number[], value: number): boolean { return arr.indexOf(value) >= 0; } }