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.
366 lines
14 KiB
366 lines
14 KiB
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';
|
|
|
|
/** 空装饰格下标(grid17):预制体底图保持关闭,逻辑不处理 */
|
|
const BLANK_INDEX = 17;
|
|
|
|
/** 一次待开启推进:新生待解锁格,以及本次要累加到这些格子数字上的等级 */
|
|
export interface TextListPromoteResult {
|
|
/** 新变成待开启、需要出字的格子 */
|
|
readonly born: readonly number[];
|
|
/** 本次解锁对应的显示等级加值(Const 39);无加值为 0 */
|
|
readonly levelAdd: number;
|
|
}
|
|
|
|
/**
|
|
* 按关卡 normalland / tolockland 管理 ui_textList 格子状态与拖入解锁。
|
|
* normalland 为开启且开局不放字;tolockland 为待开启(有字、不能拖、靠同类型同等级合成打开)。
|
|
* 待开启被合成后自身变为开启,上下左右的未解锁格变为待开启。其余为未解锁。
|
|
* 每局记录解锁次数,bind 时清零。第 1 次读 Const 39 的 index 0,依次类推;超出则用最后一项。
|
|
* 该加值累加到本次上下左右新待解锁格的数字等级上。
|
|
* grid17 是空装饰格,不刷底、不出字、不可拖放。名为 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 清零。下次解锁用这个下标读 Const 39 */
|
|
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<ITableLevel>(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');
|
|
this._stripBlank(this._open, 'normalland');
|
|
this._stripBlank(this._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-23),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 index === BLANK_INDEX;
|
|
}
|
|
|
|
/**
|
|
* 待解锁格与空装饰格禁止把字拖走。
|
|
* @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;
|
|
}
|
|
|
|
/**
|
|
* 配表若写了空装饰格,去掉并打日志。
|
|
* @param arr 下标数组
|
|
* @param label 字段名
|
|
*/
|
|
private _stripBlank(arr: number[], label: string): void {
|
|
const idx = arr.indexOf(BLANK_INDEX);
|
|
if (idx < 0) return;
|
|
console.log('[TextListLand] 空装饰格不参与地块配置', label, BLANK_INDEX);
|
|
arr.splice(idx, 1);
|
|
}
|
|
|
|
/** 数组是否包含 */
|
|
private _has(arr: readonly number[], value: number): boolean {
|
|
return arr.indexOf(value) >= 0;
|
|
}
|
|
}
|
|
|