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.
755 lines
27 KiB
755 lines
27 KiB
import { Label, Node, UITransform, Vec3, isValid } from 'cc';
|
|
import { SpriteLoad } from 'db://assets/mx/components/SpriteLoad';
|
|
import { TableNames } from '../../../game/ConfigTableData';
|
|
import { GBundle, GPath } from '../../../game/ConfigRes';
|
|
import { ConstNumType } from '../../../game/GameDataEnums';
|
|
import { MapGrid } from './MapGrid';
|
|
|
|
/** 解锁条件气泡预制体路径 */
|
|
const LOCK_TIP_PATH = 'prefab/';
|
|
/** 解锁条件气泡预制体名 */
|
|
const LOCK_TIP_NAME = '格子锁';
|
|
/** 锁与数字容器 */
|
|
const LOCK_TIP_LOCK = 'lock';
|
|
/** 预制体上显示数字的子节点名(在 lock 下) */
|
|
const LOCK_TIP_NUM = 'num';
|
|
/** 三选一宝箱提示子节点 */
|
|
const LOCK_TIP_BOX = 'box';
|
|
/** 五行属性图标子节点 */
|
|
const LOCK_TIP_FIVE = 'five';
|
|
/** 无宝箱时 lock 的本地 Y */
|
|
const LOCK_Y_NO_BOX = 0;
|
|
/** 有宝箱时 lock 的本地 Y */
|
|
const LOCK_Y_WITH_BOX = -15;
|
|
/** 气泡世界坐标转本地时复用 */
|
|
const _tipWorld = new Vec3();
|
|
/** 气泡本地坐标复用 */
|
|
const _tipLocal = new Vec3();
|
|
/** herotype → 五行名(1金 2木 3水 4火 5土) */
|
|
const HERO_TYPE_ICON: string[] = ['', '金', '木', '水', '火', '土'];
|
|
|
|
/**
|
|
* herotype 对应五行名;非法或 0 返回空串。
|
|
* @param heroType Hero.herotype / unlockland 属性类型
|
|
*/
|
|
export function unlockHeroTypeName(heroType: number): string {
|
|
const type = Math.floor(Number(heroType) || 0);
|
|
if (type <= 0 || type >= HERO_TYPE_ICON.length) return '';
|
|
return HERO_TYPE_ICON[type];
|
|
}
|
|
|
|
/**
|
|
* 拖英雄推进区域解锁的结果。
|
|
* skip:不是解锁格,走原落子。
|
|
* blocked:还没轮到、条件没随出来,或英雄类型/等级不符。
|
|
* unlocked:英雄符合当前条件,整区可放置文字。
|
|
*/
|
|
export type AreaUnlockDrag = 'skip' | 'blocked' | 'unlocked';
|
|
|
|
/** 拖上格子的英雄:类型与表等级 */
|
|
export interface AreaUnlockHero {
|
|
/** Hero.herotype */
|
|
heroType: number;
|
|
/** Hero.level */
|
|
level: number;
|
|
}
|
|
|
|
/** 单阵营解锁进度(与共享区域 ID 列表对齐) */
|
|
interface CampUnlockTrack {
|
|
/** CampAreaID */
|
|
camp: number;
|
|
/** 各区域是否已解锁 */
|
|
done: boolean[];
|
|
/** 各区域是否已随机条件 */
|
|
rolled: boolean[];
|
|
/** 各区域需求英雄类型 */
|
|
heroTypes: number[];
|
|
/** 各区域需求等级(含段位加值) */
|
|
needLevels: number[];
|
|
/** 段位等级加值 Rank.levelvalue */
|
|
levelValue: number;
|
|
/** 是否已绑定关卡加值 */
|
|
levelReady: boolean;
|
|
}
|
|
|
|
/**
|
|
* 地图解锁区域。
|
|
* 区域 ID 从小到大依次解锁;进度与条件按 CampAreaID 隔离(镜像图双方互不影响)。
|
|
* 当前区域从关卡 unlockland1-11 按权重随机一条条件,等级加上该阵营段位 levelvalue。
|
|
*/
|
|
export class MapAreaUnlock {
|
|
/** 地图上出现过的区域 ID(双方共享序号,进度按阵营分轨) */
|
|
private _ids: number[] = [];
|
|
/** 各阵营解锁轨 */
|
|
private _tracks: CampUnlockTrack[] = [];
|
|
private _grids: MapGrid[] = [];
|
|
private _levelId = 0;
|
|
private _warnedSprite = false;
|
|
/** 按阵营各挂一个条件气泡 */
|
|
private _tips: Node[] = [];
|
|
/** 与当前展示中的 _tips 对齐的阵营 CampAreaID */
|
|
private _tipCamps: number[] = [];
|
|
|
|
/**
|
|
* 绑定本局关卡与某阵营段位等级加值。
|
|
* @param levelId 关卡表 id
|
|
* @param levelValue 段位等级加值
|
|
* @param campAreaId 阵营 CampAreaID(1 本机 / 2 AI)
|
|
*/
|
|
setLevel(levelId: number, levelValue: number, campAreaId: number): void {
|
|
const camp = Math.floor(Number(campAreaId) || 0);
|
|
if (camp <= 0) {
|
|
console.warn('[MapAreaUnlock] setLevel 需要有效 CampAreaID', campAreaId);
|
|
return;
|
|
}
|
|
this._levelId = levelId;
|
|
const track = this._ensureTrack(camp);
|
|
track.levelValue = levelValue;
|
|
track.levelReady = true;
|
|
this._rollCurrent(track);
|
|
// 镜像图另一阵营若尚未绑定段位,先用同一加值出条件气泡(AI bind 后会覆盖 camp=2)
|
|
for (let i = 0; i < this._tracks.length; i++) {
|
|
const other = this._tracks[i];
|
|
if (other.camp === camp || other.levelReady) continue;
|
|
other.levelValue = levelValue;
|
|
other.levelReady = true;
|
|
this._rollCurrent(other);
|
|
}
|
|
this.applyVisual();
|
|
}
|
|
|
|
/** 归还条件气泡(卸地图前调用) */
|
|
clear(): void {
|
|
this._releaseTips();
|
|
}
|
|
|
|
/**
|
|
* 按当前格子重建区域,并刷新等待解锁的显示。已解锁和已随机的条件按「阵营+区域 ID」保留。
|
|
* @param grids 地图格子
|
|
*/
|
|
sync(grids: MapGrid[]): void {
|
|
const prevIds = this._ids;
|
|
const prevTracks = this._tracks;
|
|
this._grids = grids;
|
|
this._ids = [];
|
|
this._tracks = [];
|
|
this._warnedSprite = false;
|
|
|
|
for (let i = 0; i < grids.length; i++) {
|
|
const grid = grids[i];
|
|
if (!grid?.isCanBeDig || grid.unlockAreaId <= 0) continue;
|
|
this._touchAreaId(grid.unlockAreaId);
|
|
this._ensureTrack(grid.CampAreaID | 0);
|
|
}
|
|
this._sortAreas();
|
|
this._restoreTracks(prevIds, prevTracks);
|
|
for (let i = 0; i < this._tracks.length; i++) {
|
|
this._rollCurrent(this._tracks[i]);
|
|
}
|
|
this.applyVisual();
|
|
}
|
|
|
|
/**
|
|
* 某阵营当前正在解锁的区域 ID;没有则 0。
|
|
* @param campAreaId 阵营 CampAreaID
|
|
*/
|
|
currentId(campAreaId: number): number {
|
|
const track = this._trackOf(campAreaId);
|
|
if (!track) return 0;
|
|
for (let i = 0; i < this._ids.length; i++) {
|
|
if (!track.done[i]) return this._ids[i];
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* 某阵营已完成解锁次数。
|
|
* @param campAreaId 阵营 CampAreaID
|
|
*/
|
|
completedCount(campAreaId: number): number {
|
|
const track = this._trackOf(campAreaId);
|
|
if (!track) return 0;
|
|
let n = 0;
|
|
for (let i = 0; i < track.done.length; i++) {
|
|
if (track.done[i]) n++;
|
|
}
|
|
return n;
|
|
}
|
|
|
|
/**
|
|
* 某阵营当前待解锁区需求 herotype;0=任意属性或无可解锁区。
|
|
* @param campAreaId 阵营 CampAreaID
|
|
*/
|
|
currentNeedHeroType(campAreaId: number): number {
|
|
const track = this._trackOf(campAreaId);
|
|
if (!track) return 0;
|
|
const current = this.currentId(campAreaId);
|
|
if (current <= 0) return 0;
|
|
const index = this._indexOf(current);
|
|
if (index < 0 || !track.rolled[index]) return 0;
|
|
return Math.floor(Number(track.heroTypes[index]) || 0);
|
|
}
|
|
|
|
/**
|
|
* 拖到当前解锁区域是否满足类型与等级(不改状态)。
|
|
* 类型 0 表示任意 herotype,等级须达到配置等级加段位加值。
|
|
*/
|
|
canDrag(grid: MapGrid, hero: AreaUnlockHero | null): boolean {
|
|
return this._probe(grid, hero) === 'unlocked';
|
|
}
|
|
|
|
/**
|
|
* 某阵营当前格子锁气泡(用于解锁引导连线终点)。
|
|
* @param campAreaId 阵营 CampAreaID
|
|
*/
|
|
tipOf(campAreaId: number): Node | null {
|
|
const camp = Math.floor(Number(campAreaId) || 0);
|
|
if (camp <= 0) return null;
|
|
for (let i = 0; i < this._tipCamps.length; i++) {
|
|
if (this._tipCamps[i] !== camp) continue;
|
|
const tip = this._tips[i];
|
|
if (tip?.isValid && tip.active) return tip;
|
|
return null;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* 把英雄拖到当前解锁区域。条件与 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 camp = grid.CampAreaID | 0;
|
|
const track = this._trackOf(camp);
|
|
if (!track) {
|
|
console.warn('[MapAreaUnlock] tryDrag 无阵营轨', camp);
|
|
return 'blocked';
|
|
}
|
|
const index = this._indexOf(this.currentId(camp));
|
|
if (index < 0) {
|
|
console.warn('[MapAreaUnlock] tryDrag 找不到当前区域', camp);
|
|
return 'blocked';
|
|
}
|
|
track.done[index] = true;
|
|
this._rollCurrent(track);
|
|
return 'unlocked';
|
|
}
|
|
|
|
/**
|
|
* 某阵营下某区域的格子(不含对侧同 areaId)。
|
|
* @param areaId 解锁区域 ID
|
|
* @param campAreaId 阵营 CampAreaID
|
|
*/
|
|
gridsOf(areaId: number, campAreaId: number): MapGrid[] {
|
|
const camp = Math.floor(Number(campAreaId) || 0);
|
|
const out: MapGrid[] = [];
|
|
for (let i = 0; i < this._grids.length; i++) {
|
|
const grid = this._grids[i];
|
|
if (!grid) continue;
|
|
if (grid.unlockAreaId !== areaId) continue;
|
|
if ((grid.CampAreaID | 0) !== camp) continue;
|
|
out.push(grid);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/**
|
|
* 刷新解锁区域皮:各阵营当前区域 Wait Unlocked;气泡按阵营分开放。
|
|
*/
|
|
applyVisual(): void {
|
|
for (let i = 0; i < this._grids.length; i++) {
|
|
const grid = this._grids[i];
|
|
if (!grid) continue;
|
|
const current = this.currentId(grid.CampAreaID | 0);
|
|
const wait = grid.isAreaUnlock && current > 0 && 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._refreshTips();
|
|
}
|
|
|
|
/**
|
|
* 解锁落点探测:skip 非解锁格;blocked 未轮到或类型/等级不符;unlocked 条件满足。
|
|
*/
|
|
private _probe(grid: MapGrid, hero: AreaUnlockHero | null): AreaUnlockDrag {
|
|
if (!grid?.isAreaUnlock) return 'skip';
|
|
const camp = grid.CampAreaID | 0;
|
|
const track = this._trackOf(camp);
|
|
if (!track) return 'blocked';
|
|
const current = this.currentId(camp);
|
|
if (current <= 0 || grid.unlockAreaId !== current) return 'blocked';
|
|
const index = this._indexOf(current);
|
|
if (index < 0 || !track.rolled[index]) return 'blocked';
|
|
if (!hero) return 'blocked';
|
|
const typeOk = track.heroTypes[index] === 0 || hero.heroType === track.heroTypes[index];
|
|
const levelOk = hero.level >= track.needLevels[index];
|
|
if (!typeOk || !levelOk) return 'blocked';
|
|
return 'unlocked';
|
|
}
|
|
|
|
/** 各阵营当前待解锁区各自挂气泡 */
|
|
private _refreshTips(): void {
|
|
const jobs: { camp: number; grids: MapGrid[]; num: number; heroType: number }[] = [];
|
|
for (let t = 0; t < this._tracks.length; t++) {
|
|
const track = this._tracks[t];
|
|
const current = this.currentId(track.camp);
|
|
if (current <= 0) continue;
|
|
const index = this._indexOf(current);
|
|
if (index < 0 || !track.rolled[index]) continue;
|
|
const group = this._waitingOfCamp(current, track.camp);
|
|
if (group.length <= 0) continue;
|
|
const heroType = track.heroTypes[index] | 0;
|
|
const num = this._heroNumber(track.needLevels[index], heroType);
|
|
if (!(num > 0)) continue;
|
|
jobs.push({ camp: track.camp, grids: group, num, heroType });
|
|
}
|
|
this._tipCamps.length = 0;
|
|
if (jobs.length <= 0) {
|
|
this._hideTips();
|
|
return;
|
|
}
|
|
if (!this._ensureTips(jobs.length, jobs[0].grids[0])) {
|
|
this._hideTips();
|
|
return;
|
|
}
|
|
for (let i = 0; i < jobs.length; i++) {
|
|
const tip = this._tips[i];
|
|
if (!tip?.isValid) {
|
|
this._tipCamps.push(0);
|
|
continue;
|
|
}
|
|
const lock = tip.getChildByName(LOCK_TIP_LOCK);
|
|
const label = lock?.getChildByName(LOCK_TIP_NUM)?.getComponent(Label);
|
|
if (!label) {
|
|
console.warn('[MapAreaUnlock] 格子锁缺少 lock/num Label');
|
|
tip.active = false;
|
|
this._tipCamps.push(0);
|
|
continue;
|
|
}
|
|
label.string = String(jobs[i].num);
|
|
this._syncFiveIcon(tip, jobs[i].heroType);
|
|
const nextCount = this.completedCount(jobs[i].camp) + 1;
|
|
this._syncBoxVisible(tip, this._isSelectTriggerCount(nextCount));
|
|
this._placeTipCenter(tip, jobs[i].grids);
|
|
tip.active = true;
|
|
this._tipCamps.push(jobs[i].camp);
|
|
}
|
|
for (let i = jobs.length; i < this._tips.length; i++) {
|
|
const tip = this._tips[i];
|
|
if (tip?.isValid) tip.active = false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 下次解锁是否触发三选一时显示 box,并调整 lock 的 Y。
|
|
* @param tip 格子锁气泡
|
|
* @param show 是否显示
|
|
*/
|
|
private _syncBoxVisible(tip: Node, show: boolean): void {
|
|
const box = tip.getChildByName(LOCK_TIP_BOX);
|
|
if (!box) {
|
|
console.warn('[MapAreaUnlock] 格子锁缺少 box');
|
|
return;
|
|
}
|
|
box.active = show;
|
|
const lock = tip.getChildByName(LOCK_TIP_LOCK);
|
|
if (!lock) {
|
|
console.warn('[MapAreaUnlock] 格子锁缺少 lock');
|
|
return;
|
|
}
|
|
const pos = lock.position;
|
|
lock.setPosition(pos.x, show ? LOCK_Y_WITH_BOX : LOCK_Y_NO_BOX, pos.z);
|
|
}
|
|
|
|
/**
|
|
* Const27 是否包含该解锁次数(本文件内判断,避免依赖 SelectService 造成循环)。
|
|
* @param unlockCount 次数
|
|
*/
|
|
private _isSelectTriggerCount(unlockCount: number): boolean {
|
|
const n = Math.floor(Number(unlockCount) || 0);
|
|
if (n <= 0) return false;
|
|
const list = gg.data.table.getConstArray(ConstNumType.BattleSelectTriggerWaves);
|
|
for (let i = 0; i < list.length; i++) {
|
|
if (Math.floor(list[i]) === n) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* 同步 five 五行图标:类型 0 隐藏;1–5 显示并加载对应图标。
|
|
* @param tip 格子锁气泡
|
|
* @param heroType 当前随机到的 Hero.herotype;0 为无属性限制
|
|
*/
|
|
private _syncFiveIcon(tip: Node, heroType: number): void {
|
|
const five = tip.getChildByName(LOCK_TIP_FIVE);
|
|
if (!five) {
|
|
console.warn('[MapAreaUnlock] 格子锁缺少 five');
|
|
return;
|
|
}
|
|
const type = Math.floor(Number(heroType) || 0);
|
|
if (type <= 0) {
|
|
five.active = false;
|
|
return;
|
|
}
|
|
const name = HERO_TYPE_ICON[type];
|
|
if (!name) {
|
|
console.warn('[MapAreaUnlock] 未知解锁 herotype,无法加载属性图标', type);
|
|
five.active = false;
|
|
return;
|
|
}
|
|
five.active = true;
|
|
let sl = five.getComponent(SpriteLoad);
|
|
if (!sl) sl = five.addComponent(SpriteLoad);
|
|
sl.setSprite(GPath.ElementAttr(name), GBundle.Items);
|
|
}
|
|
|
|
/**
|
|
* 某阵营当前待解锁格。
|
|
* @param current 当前区域 ID
|
|
* @param camp 阵营
|
|
*/
|
|
private _waitingOfCamp(current: number, camp: number): MapGrid[] {
|
|
const out: MapGrid[] = [];
|
|
for (let i = 0; i < this._grids.length; i++) {
|
|
const grid = this._grids[i];
|
|
if (!grid?.isAreaUnlock || grid.unlockAreaId !== current) continue;
|
|
if ((grid.CampAreaID | 0) !== camp) continue;
|
|
out.push(grid);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/**
|
|
* 取解锁等级对应的 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 {
|
|
this._tipCamps.length = 0;
|
|
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;
|
|
const five = tip.getChildByName(LOCK_TIP_FIVE);
|
|
if (five?.isValid) five.active = false;
|
|
if (tip['resKey']) gg.res.putNode(tip);
|
|
else tip.destroy();
|
|
}
|
|
this._tips.length = 0;
|
|
this._tipCamps.length = 0;
|
|
}
|
|
|
|
/**
|
|
* 给某阵营当前区域随机一条 unlockland 条件。
|
|
* @param track 阵营轨
|
|
*/
|
|
private _rollCurrent(track: CampUnlockTrack): void {
|
|
if (!track.levelReady) return;
|
|
const id = this.currentId(track.camp);
|
|
if (id <= 0) return;
|
|
const index = this._indexOf(id);
|
|
if (index < 0 || track.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;
|
|
track.heroTypes[index] = picked.heroType;
|
|
track.needLevels[index] = picked.level + track.levelValue;
|
|
track.rolled[index] = true;
|
|
}
|
|
|
|
/**
|
|
* 按区域 ID 取 unlockland1-11。超出 1-11 时警告。
|
|
* @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;
|
|
case 7: return level.unlockland7;
|
|
case 8: return level.unlockland8;
|
|
case 9: return level.unlockland9;
|
|
case 10: return level.unlockland10;
|
|
case 11: return level.unlockland11;
|
|
default:
|
|
console.warn('[MapAreaUnlock] 解锁区域ID只支持 1-11', 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] };
|
|
}
|
|
|
|
/**
|
|
* 登记区域 ID(去重)。
|
|
* @param id 区域 ID
|
|
*/
|
|
private _touchAreaId(id: number): void {
|
|
if (this._indexOf(id) >= 0) return;
|
|
this._ids.push(id);
|
|
}
|
|
|
|
/**
|
|
* 确保阵营轨存在;新建时按当前 _ids 长度填默认进度。
|
|
* @param camp CampAreaID
|
|
*/
|
|
private _ensureTrack(camp: number): CampUnlockTrack {
|
|
const existing = this._trackOf(camp);
|
|
if (existing) return existing;
|
|
if (camp <= 0) {
|
|
console.warn('[MapAreaUnlock] 无效 CampAreaID', camp);
|
|
}
|
|
const track: CampUnlockTrack = {
|
|
camp,
|
|
done: [],
|
|
rolled: [],
|
|
heroTypes: [],
|
|
needLevels: [],
|
|
levelValue: 0,
|
|
levelReady: false,
|
|
};
|
|
this._padTrack(track);
|
|
this._tracks.push(track);
|
|
return track;
|
|
}
|
|
|
|
/**
|
|
* sync 后按旧轨恢复进度,并补齐数组长度。
|
|
* @param prevIds 同步前区域 ID
|
|
* @param prevTracks 同步前阵营轨
|
|
*/
|
|
private _restoreTracks(prevIds: number[], prevTracks: CampUnlockTrack[]): void {
|
|
for (let t = 0; t < this._tracks.length; t++) {
|
|
const track = this._tracks[t];
|
|
this._padTrack(track);
|
|
let prev: CampUnlockTrack | null = null;
|
|
for (let i = 0; i < prevTracks.length; i++) {
|
|
if (prevTracks[i].camp === track.camp) {
|
|
prev = prevTracks[i];
|
|
break;
|
|
}
|
|
}
|
|
if (!prev) continue;
|
|
track.levelValue = prev.levelValue;
|
|
track.levelReady = prev.levelReady;
|
|
for (let i = 0; i < this._ids.length; i++) {
|
|
const old = this._oldIndex(this._ids[i], prevIds);
|
|
if (old < 0) continue;
|
|
track.done[i] = !!prev.done[old];
|
|
track.rolled[i] = !!prev.rolled[old];
|
|
track.heroTypes[i] = prev.heroTypes[old] ?? 0;
|
|
track.needLevels[i] = prev.needLevels[old] ?? 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 把轨内数组补到与 _ids 同长。
|
|
* @param track 阵营轨
|
|
*/
|
|
private _padTrack(track: CampUnlockTrack): void {
|
|
while (track.done.length < this._ids.length) track.done.push(false);
|
|
while (track.rolled.length < this._ids.length) track.rolled.push(false);
|
|
while (track.heroTypes.length < this._ids.length) track.heroTypes.push(0);
|
|
while (track.needLevels.length < this._ids.length) track.needLevels.push(0);
|
|
}
|
|
|
|
/** 区域 ID 从小到大(同步重排轨内数组) */
|
|
private _sortAreas(): void {
|
|
for (let i = 1; i < this._ids.length; i++) {
|
|
const id = this._ids[i];
|
|
let j = i - 1;
|
|
while (j >= 0 && this._ids[j] > id) {
|
|
this._ids[j + 1] = this._ids[j];
|
|
j--;
|
|
}
|
|
this._ids[j + 1] = id;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param camp CampAreaID
|
|
*/
|
|
private _trackOf(camp: number): CampUnlockTrack | null {
|
|
for (let i = 0; i < this._tracks.length; i++) {
|
|
if (this._tracks[i].camp === camp) return this._tracks[i];
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* @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;
|
|
}
|
|
}
|
|
|