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/legacy/MapAreaUnlock.ts

521 lines
18 KiB

import { Label, Layout, Node, UITransform, Vec3, isValid } from 'cc';
import { TableNames } from '../../../game/ConfigTableData';
import { GBundle } from '../../../game/ConfigRes';
import { MapGrid } from './MapGrid';
/** 解锁条件气泡预制体路径 */
const LOCK_TIP_PATH = 'prefab/';
/** 解锁条件气泡预制体名 */
const LOCK_TIP_NAME = '格子锁';
/** 预制体上显示数字的子节点名 */
const LOCK_TIP_NUM = 'num';
/** 气泡世界坐标转本地时复用 */
const _tipWorld = new Vec3();
/** 气泡本地坐标复用 */
const _tipLocal = new Vec3();
/**
* 拖英雄推进区域解锁的结果。
* skip:不是解锁格,走原落子。
* blocked:还没轮到、条件没随出来,或英雄类型/等级不符。
* unlocked:英雄符合当前条件,整区可放置文字。
*/
export type AreaUnlockDrag = 'skip' | 'blocked' | 'unlocked';
/** 拖上格子的英雄:类型与表等级 */
export interface AreaUnlockHero {
/** Hero.herotype */
heroType: number;
/** Hero.level */
level: number;
}
/**
* 地图解锁区域。
* 区域 ID 从小到大依次解锁。当前区域从关卡 unlockland1-6 按权重随机一条条件,
* 等级加上段位 levelvalue。把符合类型和等级的英雄拖上去后,整区可放置文字。
*/
export class MapAreaUnlock {
private _ids: number[] = [];
private _done: boolean[] = [];
private _rolled: boolean[] = [];
private _heroTypes: number[] = [];
private _needLevels: number[] = [];
private _grids: MapGrid[] = [];
private _levelId = 0;
private _levelValue = 0;
private _levelReady = false;
private _warnedSprite = false;
/** 按阵营各挂一个条件气泡(镜像图不能把两边格子平均到地图中心) */
private _tips: Node[] = [];
/**
* 本局关卡与段位等级加值。加值来自 Rank.levelvalue。
* @param levelId 关卡表 id
* @param levelValue 段位等级加值
*/
setLevel(levelId: number, levelValue: number): void {
this._levelId = levelId;
this._levelValue = levelValue;
this._levelReady = true;
this._rollCurrent();
this.applyVisual();
}
/** 归还条件气泡(卸地图前调用) */
clear(): void {
this._releaseTips();
}
/**
* 按当前格子重建区域,并刷新等待解锁的显示。已解锁和已随机的条件按区域 ID 保留。
* @param grids 地图格子
*/
sync(grids: MapGrid[]): void {
const prevIds = this._ids;
const prevDone = this._done;
const prevRolled = this._rolled;
const prevTypes = this._heroTypes;
const prevLevels = this._needLevels;
this._grids = grids;
this._ids = [];
this._done = [];
this._rolled = [];
this._heroTypes = [];
this._needLevels = [];
this._warnedSprite = false;
for (let i = 0; i < grids.length; i++) {
const grid = grids[i];
if (!grid?.isCanBeDig || grid.unlockAreaId <= 0) continue;
this._touchArea(grid.unlockAreaId, prevIds, prevDone, prevRolled, prevTypes, prevLevels);
}
this._sortAreas();
this._rollCurrent();
this.applyVisual();
}
/** 当前正在解锁的区域 ID;没有则 0 */
currentId(): number {
for (let i = 0; i < this._ids.length; i++) {
if (!this._done[i]) return this._ids[i];
}
return 0;
}
/**
* 拖到当前解锁区域是否满足类型与等级(不改状态)。
* 类型 0 表示任意 herotype,等级须达到配置等级加段位加值。
*/
canDrag(grid: MapGrid, hero: AreaUnlockHero | null): boolean {
return this._probe(grid, hero) === 'unlocked';
}
/**
* 把英雄拖到当前解锁区域。条件与 canDrag 相同,成功则整区解锁。
* @param grid 落点格
* @param hero 拖动的英雄;没有对应英雄时传 null
*/
tryDrag(grid: MapGrid, hero: AreaUnlockHero | null): AreaUnlockDrag {
const hit = this._probe(grid, hero);
if (hit !== 'unlocked') return hit;
const index = this._indexOf(this.currentId());
this._done[index] = true;
this._rollCurrent();
return 'unlocked';
}
/**
* 解锁落点探测:skip 非解锁格;blocked 未轮到或类型/等级不符;unlocked 条件满足。
*/
private _probe(grid: MapGrid, hero: AreaUnlockHero | null): AreaUnlockDrag {
if (!grid?.isAreaUnlock) return 'skip';
const current = this.currentId();
if (current <= 0 || grid.unlockAreaId !== current) return 'blocked';
const index = this._indexOf(current);
if (index < 0 || !this._rolled[index]) return 'blocked';
if (!hero) return 'blocked';
const typeOk = this._heroTypes[index] === 0 || hero.heroType === this._heroTypes[index];
const levelOk = hero.level >= this._needLevels[index];
if (!typeOk || !levelOk) return 'blocked';
return 'unlocked';
}
/**
* 某区域下挂着该 ID 的格子。
* @param areaId 解锁区域 ID
*/
gridsOf(areaId: number): MapGrid[] {
const out: MapGrid[] = [];
for (let i = 0; i < this._grids.length; i++) {
const grid = this._grids[i];
if (grid?.unlockAreaId === areaId) out.push(grid);
}
return out;
}
/**
* 刷新解锁区域皮:当前区域 Wait Unlocked,未轮到 Put,已解锁 Dig(由 MapGrid 按状态选图)。
* 同时把「格子锁」气泡放到当前待解锁区域所有格子的正中间。
*/
applyVisual(): void {
const current = this.currentId();
for (let i = 0; i < this._grids.length; i++) {
const grid = this._grids[i];
if (!grid) continue;
const wait = grid.isAreaUnlock && grid.unlockAreaId === current;
if (wait && !grid.waitUnlockedSprite && !this._warnedSprite) {
console.warn('[MapAreaUnlock] 当前解锁区域缺少 Wait Unlocked', current, grid.Index);
this._warnedSprite = true;
}
grid.waitingUnlock = wait;
if (!wait && grid.isAreaUnlock) grid.updateGrid();
}
this._refreshTip(current);
}
/**
* 按阵营分别在待解锁格子中心显示条件数字(Hero.number)。
* @param current 当前区域 ID;0 表示没有待解锁区
*/
private _refreshTip(current: number): void {
if (current <= 0) {
this._hideTips();
return;
}
const index = this._indexOf(current);
if (index < 0 || !this._rolled[index]) {
this._hideTips();
return;
}
const groups = this._waitingGroups(current);
if (groups.length === 0) {
this._hideTips();
return;
}
const num = this._heroNumber(this._needLevels[index], this._heroTypes[index]);
if (!(num > 0)) {
this._hideTips();
return;
}
if (!this._ensureTips(groups.length, groups[0][0])) {
this._hideTips();
return;
}
for (let i = 0; i < groups.length; i++) {
const tip = this._tips[i];
if (!tip?.isValid) continue;
const numNode = tip.getChildByName(LOCK_TIP_NUM);
const label = numNode?.getComponent(Label);
if (!label) {
console.warn('[MapAreaUnlock] 格子锁缺少 num Label');
tip.active = false;
continue;
}
label.string = String(num);
tip.getComponent(Layout)?.updateLayout();
this._placeTipCenter(tip, groups[i]);
tip.active = true;
}
for (let i = groups.length; i < this._tips.length; i++) {
const tip = this._tips[i];
if (tip?.isValid) tip.active = false;
}
}
/**
* 当前区域待解锁格,按 CampAreaID 分组(每组各自取中心)。
* @param current 当前区域 ID
*/
private _waitingGroups(current: number): MapGrid[][] {
const camps: number[] = [];
const groups: MapGrid[][] = [];
for (let i = 0; i < this._grids.length; i++) {
const grid = this._grids[i];
if (!grid?.isAreaUnlock || grid.unlockAreaId !== current) continue;
const camp = grid.CampAreaID;
let g = -1;
for (let j = 0; j < camps.length; j++) {
if (camps[j] === camp) {
g = j;
break;
}
}
if (g < 0) {
camps.push(camp);
groups.push([grid]);
} else {
groups[g].push(grid);
}
}
return groups;
}
/**
* 取解锁等级对应的 Hero.number(界面展示用)。
* @param level 需求等级(已含段位加值)
* @param heroType 需求英雄类型;0 为任意类型
*/
private _heroNumber(level: number, heroType: number): number {
const heroes = gg.data.table.getTableList<ITableHero>(TableNames.Hero) ?? [];
for (let i = 0; i < heroes.length; i++) {
const hero = heroes[i];
if (Math.floor(Number(hero.level)) !== level) continue;
if (heroType > 0 && Math.floor(Number(hero.herotype)) !== heroType) continue;
const num = Math.floor(Number(hero.number));
if (num > 0) return num;
}
console.warn('[MapAreaUnlock] 找不到解锁等级对应 Hero.number', level, heroType);
return 0;
}
/**
* 确保气泡数量够用,并挂到地图根节点。
* @param count 需要的气泡数
* @param anchor 用于定位父节点的格子
*/
private _ensureTips(count: number, anchor: MapGrid): boolean {
const mapRoot = anchor.node?.parent?.parent;
if (!mapRoot?.isValid) {
console.warn('[MapAreaUnlock] 找不到地图根节点');
return false;
}
if (typeof gg === 'undefined' || !gg.res?.getNode) {
console.warn('[MapAreaUnlock] gg.res 未就绪,无法加载格子锁');
return false;
}
while (this._tips.length < count) {
const tip = gg.res.getNode(LOCK_TIP_PATH, LOCK_TIP_NAME, GBundle.BattleGame);
if (!tip?.isValid) {
console.warn('[MapAreaUnlock] 缺少格子锁预制体', LOCK_TIP_NAME);
return this._tips.length > 0;
}
tip.parent = mapRoot;
tip.setSiblingIndex(mapRoot.children.length - 1);
this._tips.push(tip);
}
for (let i = 0; i < this._tips.length; i++) {
const tip = this._tips[i];
if (!tip?.isValid) continue;
if (tip.parent !== mapRoot) tip.parent = mapRoot;
tip.setSiblingIndex(mapRoot.children.length - 1);
}
return true;
}
/**
* 气泡放到该组格子世界坐标中心。
* @param tip 气泡节点
* @param grids 同阵营待解锁格子
*/
private _placeTipCenter(tip: Node, grids: MapGrid[]): void {
let sx = 0;
let sy = 0;
let sz = 0;
let count = 0;
for (let i = 0; i < grids.length; i++) {
const node = grids[i]?.node;
if (!node?.isValid) continue;
const wp = node.worldPosition;
sx += wp.x;
sy += wp.y;
sz += wp.z;
count++;
}
if (count <= 0) {
console.warn('[MapAreaUnlock] 待解锁格子节点无效,无法定位气泡');
return;
}
_tipWorld.set(sx / count, sy / count, sz / count);
const parent = tip.parent;
const ut = parent?.getComponent(UITransform);
if (ut) {
ut.convertToNodeSpaceAR(_tipWorld, _tipLocal);
tip.setPosition(_tipLocal);
return;
}
tip.setWorldPosition(_tipWorld);
}
/** 隐藏全部气泡(本局内复用节点) */
private _hideTips(): void {
for (let i = 0; i < this._tips.length; i++) {
const tip = this._tips[i];
if (!tip?.isValid) continue;
tip.active = false;
}
}
/** 归还全部气泡到对象池 */
private _releaseTips(): void {
for (let i = 0; i < this._tips.length; i++) {
const tip = this._tips[i];
if (!tip || !isValid(tip)) continue;
if (tip['resKey']) gg.res.putNode(tip);
else tip.destroy();
}
this._tips.length = 0;
}
/** 给当前区域随机一条 unlockland 条件 */
private _rollCurrent(): void {
if (!this._levelReady) return;
const id = this.currentId();
if (id <= 0) return;
const index = this._indexOf(id);
if (index < 0 || this._rolled[index]) return;
const level = gg.data.table.getTableData<ITableLevel>(TableNames.Level, this._levelId);
if (!level) {
console.warn('[MapAreaUnlock] 找不到关卡', this._levelId);
return;
}
const raw = this._landRaw(level, id);
if (raw == null) return;
const picked = this._pickLand(raw, id);
if (!picked) return;
this._heroTypes[index] = picked.heroType;
this._needLevels[index] = picked.level + this._levelValue;
this._rolled[index] = true;
}
/**
* 按区域 ID 取 unlockland1-6。超出 1-6 时警告。
* @param level 关卡行
* @param areaId 解锁区域 ID
*/
private _landRaw(level: ITableLevel, areaId: number): string | null {
switch (areaId) {
case 1: return level.unlockland1;
case 2: return level.unlockland2;
case 3: return level.unlockland3;
case 4: return level.unlockland4;
case 5: return level.unlockland5;
case 6: return level.unlockland6;
default:
console.warn('[MapAreaUnlock] 解锁区域ID只支持 1-6', areaId);
return null;
}
}
/**
* 按权重随机一条。格式:属性类型,等级,权重|属性类型,等级,权重。
* @param raw 配表原文
* @param areaId 解锁区域 ID
*/
private _pickLand(raw: string, areaId: number): { heroType: number; level: number } | null {
const text = String(raw ?? '').trim();
if (!text) {
console.warn('[MapAreaUnlock] 关卡未配置解锁条件', this._levelId, areaId);
return null;
}
const types: number[] = [];
const levels: number[] = [];
const weights: number[] = [];
const parts = text.split('|');
for (let i = 0; i < parts.length; i++) {
const seg = parts[i].trim().split(',');
if (seg.length !== 3) {
console.warn('[MapAreaUnlock] 解锁条件格式应为 类型,等级,权重', areaId, parts[i]);
continue;
}
const heroType = Number(seg[0]);
const level = Number(seg[1]);
const weight = Number(seg[2]);
if (!Number.isFinite(heroType) || !Number.isFinite(level) || !Number.isFinite(weight)) {
console.warn('[MapAreaUnlock] 解锁条件含非数字', areaId, parts[i]);
continue;
}
if (weight <= 0) continue;
types.push(heroType);
levels.push(level);
weights.push(weight);
}
if (types.length === 0) {
console.warn('[MapAreaUnlock] 解锁条件没有有效权重', this._levelId, areaId, text);
return null;
}
let total = 0;
for (let i = 0; i < weights.length; i++) total += weights[i];
let roll = Math.random() * total;
let pick = types.length - 1;
for (let i = 0; i < weights.length; i++) {
roll -= weights[i];
if (roll < 0) {
pick = i;
break;
}
}
return { heroType: types[pick], level: levels[pick] };
}
/**
* @param id 区域 ID
* @param prevIds 同步前的区域 ID
* @param prevDone 同步前是否已解锁
* @param prevRolled 同步前是否已随机条件
* @param prevTypes 同步前的英雄类型
* @param prevLevels 同步前的需求等级
*/
private _touchArea(
id: number,
prevIds: number[],
prevDone: boolean[],
prevRolled: boolean[],
prevTypes: number[],
prevLevels: number[],
): void {
if (this._indexOf(id) >= 0) return;
const old = this._oldIndex(id, prevIds);
this._ids.push(id);
this._done.push(old >= 0 ? prevDone[old] : false);
this._rolled.push(old >= 0 ? prevRolled[old] : false);
this._heroTypes.push(old >= 0 ? prevTypes[old] : 0);
this._needLevels.push(old >= 0 ? prevLevels[old] : 0);
}
/** 区域 ID 从小到大 */
private _sortAreas(): void {
for (let i = 1; i < this._ids.length; i++) {
const id = this._ids[i];
const done = this._done[i];
const rolled = this._rolled[i];
const heroType = this._heroTypes[i];
const needLevel = this._needLevels[i];
let j = i - 1;
while (j >= 0 && this._ids[j] > id) {
this._ids[j + 1] = this._ids[j];
this._done[j + 1] = this._done[j];
this._rolled[j + 1] = this._rolled[j];
this._heroTypes[j + 1] = this._heroTypes[j];
this._needLevels[j + 1] = this._needLevels[j];
j--;
}
this._ids[j + 1] = id;
this._done[j + 1] = done;
this._rolled[j + 1] = rolled;
this._heroTypes[j + 1] = heroType;
this._needLevels[j + 1] = needLevel;
}
}
/**
* @param id 区域 ID
*/
private _indexOf(id: number): number {
for (let i = 0; i < this._ids.length; i++) {
if (this._ids[i] === id) return i;
}
return -1;
}
/**
* @param id 区域 ID
* @param ids 旧列表
*/
private _oldIndex(id: number, ids: number[]): number {
for (let i = 0; i < ids.length; i++) {
if (ids[i] === id) return i;
}
return -1;
}
}