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/view/TextListLandHost.ts

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