|
|
import { color } from "cc";
|
|
|
import { Singleton } from "../../mx/tools/Singleton";
|
|
|
import { LevelFieldResolver } from "../manager/LevelFieldResolver";
|
|
|
import { SkillMultiplyType, TableNames } from "./ConfigTableData";
|
|
|
import { isBasicHero } from '../battle/config/BattleHeroTable';
|
|
|
import { deriveTowerSetupStats } from '../battle/combat/TowerStatResolver';
|
|
|
import type { IBattleRuntimeHandle } from '../battle/types/ProjectBattleTypes';
|
|
|
import MTools from "../../mx/tools/MTools";
|
|
|
import { DailyWinStats } from '../manager/DailyWinStats';
|
|
|
import { MatchWinStats } from '../manager/MatchWinStats';
|
|
|
import {
|
|
|
createMembershipSet,
|
|
|
membershipAdd,
|
|
|
membershipFromArray,
|
|
|
membershipToArray,
|
|
|
mapKeysToArray,
|
|
|
type MembershipSet,
|
|
|
} from "../../mx/tools/MembershipSet";
|
|
|
import { ChapterNewPlayTypeEnum, ChapterPlayData, IAccelerateGateProperty, IBloodBagAddHpProperty, IBloodMoonProperty, IColdFireProperty, ICharmProperty, IFogAndWindProperty, IMagicEyeProperty, INightAndLightProperty, IParatroopsMonsterOption, IParatroopsProperty, IWitchProperty } from "./ChapterPlayTypes";
|
|
|
import { ConstNumType, ItemNumType, OtherDataType, StatisticsType, StatusType } from "./GameDataEnums";
|
|
|
import type { ComposedUnit } from 'db://assets/mx/module/merge';
|
|
|
|
|
|
export { ConstNumType };
|
|
|
|
|
|
|
|
|
|
|
|
///////////////////////----------------新机制-------------------///////////////////////
|
|
|
/**是否有章节新机制 */
|
|
|
type FieldExtractor = (rawData: any, isPropMode: boolean) => any;
|
|
|
interface PlayConfigDescriptor {
|
|
|
constKey: number;
|
|
|
needsFilter?: boolean;
|
|
|
// 定义该玩法需要的参数映射关系
|
|
|
// key: 传给 parser 的参数名,value: 如何从 excel 行数据中提取该参数
|
|
|
paramMapping: Record<string, FieldExtractor>;
|
|
|
// 具体的业务解析逻辑
|
|
|
parser: (params: any) => ChapterPlayData;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 项目动态数据
|
|
|
* 大部分通用的动态数据在gg.data中已经实现了
|
|
|
* 根据项目自定义数据获取实现方式在这个脚本里写,方便统一管理和调用
|
|
|
*/
|
|
|
export class ConfigProjectData extends Singleton {
|
|
|
|
|
|
///////////////////////----------------道具相关-------------------///////////////////////
|
|
|
//#region 道具
|
|
|
/**将id相同的道具合并 */
|
|
|
mergeSameItem(items: ItemData[]) {
|
|
|
let itemMap = new Map<number, ItemData>();
|
|
|
for (let i = 0; i < items.length; i++) {
|
|
|
let item = items[i];
|
|
|
let itemData = itemMap.get(item.id);
|
|
|
if (itemData) {
|
|
|
itemData.num += item.num;
|
|
|
} else {
|
|
|
itemMap.set(item.id, item);
|
|
|
}
|
|
|
}
|
|
|
items = [];
|
|
|
itemMap.forEach((value, key) => {
|
|
|
items.push(value);
|
|
|
});
|
|
|
return items;
|
|
|
}
|
|
|
|
|
|
//#endregion
|
|
|
|
|
|
///////////////////////----------------体力相关-------------------///////////////////////
|
|
|
//#region 体力
|
|
|
/**
|
|
|
* 扣除体力后同步自动回复计时锚点:从「达到上限」首次降到「未满」时记录 AutoAddVigourTime。
|
|
|
* 实际扣体力走 gg.data.subVigour,须在其 subItemNum 之后调用本方法(project.subVigour 也会调用)。
|
|
|
*/
|
|
|
syncAutoAddVigourTimeAfterConsume(count: number) {
|
|
|
let curentVigour = gg.data.getItemNum(ItemNumType.vigour);
|
|
|
let maxVigour = gg.data.table.getConst(ConstNumType.MaxVigour);
|
|
|
if (curentVigour < maxVigour && curentVigour + count >= maxVigour) {
|
|
|
gg.data.setStatistics(StatisticsType.AutoAddVigourTime, gg.data.getCurentTime());
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/**消耗体力(仅扣道具;任务/事件请用 gg.data.subVigour) */
|
|
|
subVigour(count: number) {
|
|
|
gg.data.subItemNum(ItemNumType.vigour, count);
|
|
|
this.syncAutoAddVigourTimeAfterConsume(count);
|
|
|
// gg.data.saveToServer();
|
|
|
}
|
|
|
|
|
|
/**获取下一次自动增加体力的剩余时间(单位秒) */
|
|
|
getAddVigourTime() {
|
|
|
let lastAddTime = gg.data.getStatistics(StatisticsType.AutoAddVigourTime);
|
|
|
let curTime = gg.data.getCurentTime();
|
|
|
let maxTime = gg.data.table.getConst(ConstNumType.VigourRecoverSec);
|
|
|
let maxVigour = gg.data.table.getConst(ConstNumType.MaxVigour);
|
|
|
let curentVigour = gg.data.getItemNum(ItemNumType.vigour);
|
|
|
if (!(maxTime > 0)) return 0;
|
|
|
// 已满:不进入倒计时
|
|
|
if (curentVigour >= maxVigour) {
|
|
|
return maxTime;
|
|
|
}
|
|
|
// 未满但锚点未初始化:仅首次写入当前时刻(避免 curTime-0 炸成天文负数)
|
|
|
// 离线补发依赖扣体力时 syncAutoAddVigourTimeAfterConsume 写入的真实锚点
|
|
|
const anchorOk = lastAddTime > 1e11 && lastAddTime <= curTime + 60000;
|
|
|
if (!anchorOk) {
|
|
|
gg.data.setStatistics(StatisticsType.AutoAddVigourTime, curTime);
|
|
|
return maxTime;
|
|
|
}
|
|
|
const elapsedSec = Math.floor((curTime - lastAddTime) / 1000);
|
|
|
if (elapsedSec <= 0) return maxTime;
|
|
|
const rem = elapsedSec % maxTime;
|
|
|
// 刚好整周期:剩余 0(本帧应被 autoAddVigour 补上)
|
|
|
if (rem === 0) return 0;
|
|
|
return maxTime - rem;
|
|
|
}
|
|
|
|
|
|
/**验证是否显示体力恢复时间 */
|
|
|
checkShowAddVigourTime() {
|
|
|
let time = this.getAddVigourTime();
|
|
|
let maxVigour = gg.data.table.getConst(ConstNumType.MaxVigour);
|
|
|
let curentVigour = gg.data.getItemNum(ItemNumType.vigour);
|
|
|
return time <= 0 && curentVigour < maxVigour;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 自动恢复体力(含离线追补:按锚点经过的整周期数一次加满缺口)。
|
|
|
* 登录 startTimer 会立刻调一次,之后每秒一次。
|
|
|
*/
|
|
|
autoAddVigour() {
|
|
|
let maxVigour = gg.data.table.getConst(ConstNumType.MaxVigour);
|
|
|
let maxTime = gg.data.table.getConst(ConstNumType.VigourRecoverSec);
|
|
|
if (!(maxTime > 0) || !(maxVigour > 0)) return;
|
|
|
|
|
|
let curentVigour = gg.data.getItemNum(ItemNumType.vigour);
|
|
|
if (curentVigour >= maxVigour) return;
|
|
|
|
|
|
let lastAddTime = gg.data.getStatistics(StatisticsType.AutoAddVigourTime);
|
|
|
let curTime = gg.data.getCurentTime();
|
|
|
const anchorOk = lastAddTime > 1e11 && lastAddTime <= curTime + 60000;
|
|
|
if (!anchorOk) {
|
|
|
// 无有效锚点:不能凭空按 epoch 狂补;等扣体力写锚点后再计时
|
|
|
gg.data.setStatistics(StatisticsType.AutoAddVigourTime, curTime);
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
const elapsedSec = Math.floor((curTime - lastAddTime) / 1000);
|
|
|
if (elapsedSec < maxTime) return;
|
|
|
|
|
|
let count = Math.floor(elapsedSec / maxTime);
|
|
|
if (count + curentVigour > maxVigour) count = maxVigour - curentVigour;
|
|
|
if (count <= 0) return;
|
|
|
|
|
|
gg.data.addVigour(count);
|
|
|
// 保留未满一个周期的余量,避免离线追补后倒计时被抹成满间隔
|
|
|
const consumedMs = count * maxTime * 1000;
|
|
|
const nextAnchor = lastAddTime + consumedMs;
|
|
|
gg.data.setStatistics(
|
|
|
StatisticsType.AutoAddVigourTime,
|
|
|
curentVigour + count >= maxVigour ? curTime : nextAnchor,
|
|
|
);
|
|
|
}
|
|
|
//#endregion
|
|
|
|
|
|
///////////////////////----------------武器相关-------------------///////////////////////
|
|
|
//#region 武器(局外:Weapon1 = 旧 Weapon2)
|
|
|
/** 局外武器配置 map(Weapon1.id) */
|
|
|
private _weaponDataMap: Map<number, ITableHero> = new Map();
|
|
|
private _weaponDataList: ITableHero[] = [];
|
|
|
private _weaponIds: number[] = [];
|
|
|
|
|
|
/** 获取所有局外武器数据(Weapon1) */
|
|
|
getWeaponDataList(): ITableHero[] {
|
|
|
if (this._weaponDataList.length <= 0) {
|
|
|
this._weaponDataList = gg.data.table.getTableList<ITableHero>(TableNames.Hero);
|
|
|
}
|
|
|
return this._weaponDataList;
|
|
|
}
|
|
|
/** 按武器元素类型筛选(旧表 type 字段;Weapon1 配表需自行维护 type) */
|
|
|
getWeaponListByType(type: number) {
|
|
|
return this.getWeaponDataList().filter(
|
|
|
(item) => (item as ITableHero & { type?: number }).type === type,
|
|
|
);
|
|
|
}
|
|
|
|
|
|
/** 获取所有局外武器 id(Weapon1.id) */
|
|
|
getWeaponIds() {
|
|
|
if (this._weaponIds.length <= 0) {
|
|
|
this._weaponIds = this.getWeaponDataList()
|
|
|
.map((item) => item.id)
|
|
|
.filter((id) => Number.isFinite(id));
|
|
|
}
|
|
|
return this._weaponIds;
|
|
|
}
|
|
|
/** 获取局外武器配置(Weapon1) */
|
|
|
getWeaponData(weaponId: number): ITableHero | undefined {
|
|
|
if (this._weaponDataMap.size <= 0) {
|
|
|
const arr = this.getWeaponDataList();
|
|
|
for (let i = 0; i < arr.length; i++) {
|
|
|
const item = arr[i];
|
|
|
if (!Number.isFinite(item.id)) continue;
|
|
|
this._weaponDataMap.set(item.id, item);
|
|
|
}
|
|
|
}
|
|
|
return this._weaponDataMap.get(Number(weaponId));
|
|
|
}
|
|
|
|
|
|
/**获取已经拥有的武器id数组 */
|
|
|
getHaveWeaponIds() {
|
|
|
let weaponIds = gg.data.getArrayData(OtherDataType.HaveWeapon);
|
|
|
return weaponIds.map(item => Number(item));
|
|
|
}
|
|
|
/**设置已拥有的武器id数组 */
|
|
|
setHaveWeaponIds(weaponIds: any[]) {
|
|
|
gg.data.setArrayData(OtherDataType.HaveWeapon, weaponIds);
|
|
|
}
|
|
|
/**获取已经拥有的武器数据列表 */
|
|
|
getHaveWeaponDataList(): ITableHero[] {
|
|
|
const haveWeaponIds = this.getHaveWeaponIds();
|
|
|
return haveWeaponIds
|
|
|
.map((item) => this.getWeaponData(Number(item)))
|
|
|
.filter((item): item is ITableHero => item != null);
|
|
|
}
|
|
|
|
|
|
/** 已拥有英雄 → 可随机到的碎片道具 id(Weapon.itemid) */
|
|
|
getHaveWeaponScrapItemIds(): number[] {
|
|
|
const result: number[] = [];
|
|
|
for (const w of this.getHaveWeaponDataList()) {
|
|
|
const scrapId = this.getWeaponScrapId(w.id);
|
|
|
if (scrapId > 0) result.push(scrapId);
|
|
|
}
|
|
|
return result;
|
|
|
}
|
|
|
/** 获取未拥有的武器数据列表 */
|
|
|
getNoHaveWeaponDataList(): ITableHero[] {
|
|
|
let haveWeaponIds = this.getHaveWeaponIds();
|
|
|
let weaponIds = this.getWeaponIds();
|
|
|
let noHaveWeaponIds = weaponIds.filter(item => haveWeaponIds.indexOf(item) < 0);
|
|
|
return noHaveWeaponIds
|
|
|
.map((item) => this.getWeaponData(Number(item)))
|
|
|
.filter((item): item is ITableHero => item != null);
|
|
|
}
|
|
|
/**解锁武器 */
|
|
|
unlockWeapon(weaponId: number) {
|
|
|
let haveWeaponIds = this.getHaveWeaponIds();
|
|
|
if (haveWeaponIds.indexOf(weaponId) < 0) {
|
|
|
haveWeaponIds.push(weaponId);
|
|
|
}
|
|
|
this.setHaveWeaponIds(haveWeaponIds);
|
|
|
}
|
|
|
/**解除拥有武器 */
|
|
|
removeHaveWeapon(weaponId: number) {
|
|
|
let haveWeaponIds = this.getHaveWeaponIds();
|
|
|
let index = haveWeaponIds.indexOf(weaponId);
|
|
|
if (index >= 0) {
|
|
|
haveWeaponIds.splice(index, 1);
|
|
|
}
|
|
|
this.setHaveWeaponIds(haveWeaponIds);
|
|
|
}
|
|
|
/**判断武器是否解锁 */
|
|
|
isWeaponUnlocked(weaponId: number) {
|
|
|
let haveWeaponIds = this.getHaveWeaponIds();
|
|
|
return haveWeaponIds.indexOf(weaponId) >= 0;
|
|
|
}
|
|
|
|
|
|
/**获取武器等级 */
|
|
|
getWeaponLevel(weaponId: number) {
|
|
|
let level = 1;
|
|
|
let weaponLevel = gg.data.getkeyKeyValue(OtherDataType.WeaponLevel, weaponId);
|
|
|
level = weaponLevel && weaponLevel != "" ? parseInt(weaponLevel) : 1;
|
|
|
return level;
|
|
|
}
|
|
|
/**设置武器等级 */
|
|
|
setWeaponLevel(weaponId: number, level: number) {
|
|
|
gg.data.setkeyKeyValue(OtherDataType.WeaponLevel, weaponId, level);
|
|
|
}
|
|
|
/**武器升级 */
|
|
|
upgradeWeapon(weaponId: number) {
|
|
|
let level = this.getWeaponLevel(weaponId);
|
|
|
level++;
|
|
|
this.setWeaponLevel(weaponId, level);
|
|
|
}
|
|
|
|
|
|
/**获取上阵武器状态 */
|
|
|
getEquipWeaponStatus() {
|
|
|
let arr = gg.data.getArrayData(OtherDataType.EquipWeapon);
|
|
|
return arr.map((item) => Number(item));
|
|
|
}
|
|
|
/**设置上阵武器状态,目前8个状态位,例如:[1,2,3,4,5,6,7,8] */
|
|
|
setEquipWeaponStatus(weaponIds: number[]) {
|
|
|
gg.data.setArrayData(OtherDataType.EquipWeapon, weaponIds);
|
|
|
}
|
|
|
/**上阵武器 */
|
|
|
equipWeapon(weaponId: number, index: number) {
|
|
|
let equipWeapons = this.getEquipWeaponStatus();
|
|
|
if (equipWeapons.length <= index) {
|
|
|
equipWeapons.push(weaponId);
|
|
|
} else {
|
|
|
equipWeapons[index] = weaponId;
|
|
|
}
|
|
|
this.setEquipWeaponStatus(equipWeapons);
|
|
|
}
|
|
|
/**下阵武器 */
|
|
|
unequipWeapon(index: number) {
|
|
|
let equipWeapons = this.getEquipWeaponStatus();
|
|
|
if (equipWeapons.length > index) {
|
|
|
equipWeapons[index] = 0;
|
|
|
}
|
|
|
this.setEquipWeaponStatus(equipWeapons);
|
|
|
}
|
|
|
/**获取上阵武器id */
|
|
|
getEquipWeaponId(index: number) {
|
|
|
let equipWeapons = this.getEquipWeaponStatus();
|
|
|
if (equipWeapons.length > index) {
|
|
|
return equipWeapons[index];
|
|
|
}
|
|
|
return 0;
|
|
|
}
|
|
|
/**判断武器是否已经上阵
|
|
|
* 返回上阵索引位置
|
|
|
*/
|
|
|
isWeaponEquipped(weaponId: number) {
|
|
|
let equipWeapons = this.getEquipWeaponStatus();
|
|
|
return equipWeapons.indexOf(weaponId);
|
|
|
}
|
|
|
/**获取所有已经上阵的武器 */
|
|
|
getEquipWeaponIds() {
|
|
|
let equipWeapons = this.getEquipWeaponStatus();
|
|
|
return equipWeapons.filter((item) => item > 0);
|
|
|
}
|
|
|
/**获取所有已经上阵的武器数据列表 */
|
|
|
getEquippedWeaponDataList(): ITableHero[] {
|
|
|
let equipWeapons = this.getEquipWeaponIds();
|
|
|
return equipWeapons
|
|
|
.map((item) => this.getWeaponData(item))
|
|
|
.filter((item): item is ITableHero => item != null);
|
|
|
}
|
|
|
/** 局外已上阵且 herotype === 1 的基础英雄(三选1 基础池等共用口径) */
|
|
|
getEquippedBasicWeaponDataList(): ITableHero[] {
|
|
|
return this.getEquippedWeaponDataList().filter((w) => isBasicHero(w));
|
|
|
}
|
|
|
/**获取空位(任意槽) */
|
|
|
getEmptyPos() {
|
|
|
let equipWeapons = this.getEquipWeaponStatus();
|
|
|
let index = equipWeapons.indexOf(0);
|
|
|
return index;
|
|
|
}
|
|
|
/**
|
|
|
* 按 herotype 取空位:1 占用 0–3 槽,2 占用 4–7 槽。
|
|
|
* @returns 空位索引;无空位返回 -1
|
|
|
*/
|
|
|
getEmptyPosByNormal(herotype: number) {
|
|
|
const equipWeapons = this.getEquipWeaponStatus();
|
|
|
const start = herotype === 2 ? 4 : 0;
|
|
|
const end = start + 4;
|
|
|
for (let i = start; i < end; i++) {
|
|
|
if ((equipWeapons[i] ?? 0) === 0) {
|
|
|
return i;
|
|
|
}
|
|
|
}
|
|
|
return -1;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 获取level等级的level_para配置的level_para参数
|
|
|
* @param id 武器id
|
|
|
* @param level 武器等级
|
|
|
* @returns
|
|
|
*/
|
|
|
public getLevelParaByWeaponLevel(level: number, _weaponId: number) {
|
|
|
const lv = Math.max(1, level);
|
|
|
return 1 + (lv - 1) * 0.1;
|
|
|
}
|
|
|
|
|
|
/** 根据局外武器配置列表获取已解锁技能(Weapon1.id) */
|
|
|
public getAllUnlockedSkillByWeaponIds(weaponIds: ITableHero[]) {
|
|
|
const weapon1Ids = weaponIds.map((w) => w.id).filter((id) => id > 0);
|
|
|
return this.getAllUnlockedSkillByWeapon1Ids(weapon1Ids);
|
|
|
}
|
|
|
|
|
|
/** 根据 Weapon1 id 列表获取可进入三选1 池的技能 */
|
|
|
public getAllUnlockedSkillByWeapon1Ids(weapon1Ids: number[], levelByWeapon1?: Map<number, number>): ITableSkill[] {
|
|
|
const skillIdSet = createMembershipSet<number>();
|
|
|
for (const weapon1Id of weapon1Ids) {
|
|
|
const level = levelByWeapon1?.get(weapon1Id) ?? 1;
|
|
|
for (const id of this.getSkillIdsForWeapon1(weapon1Id, level)) {
|
|
|
membershipAdd(skillIdSet, id);
|
|
|
}
|
|
|
}
|
|
|
const result: ITableSkill[] = [];
|
|
|
for (const skillId of skillIdSet.keys()) {
|
|
|
const row = gg.data.table.getTableData<ITableSkill>(TableNames.Skill, skillId);
|
|
|
if (row) result.push(row);
|
|
|
}
|
|
|
return result;
|
|
|
}
|
|
|
|
|
|
/** 英雄 atk_id + act_skill + 当前装备武器 act_skill */
|
|
|
public getSkillIdsForWeapon1(weapon1Id: number, _level: number): number[] {
|
|
|
const config = gg.data.table.getTableData<ITableHero>(TableNames.Hero, weapon1Id);
|
|
|
if (!config) return [];
|
|
|
const skillIdArr: number[] = [];
|
|
|
for (const id of config.atk_id ?? []) {
|
|
|
if (id > 0) skillIdArr.push(id);
|
|
|
}
|
|
|
for (const id of config.act_skill ?? []) {
|
|
|
if (id > 0) skillIdArr.push(id);
|
|
|
}
|
|
|
try {
|
|
|
const wid = gg.equip?.getHeroDisplayWeaponId?.(weapon1Id) ?? 0;
|
|
|
const weaponId = wid > 0 ? wid : (Number(config.startweapon) || 0);
|
|
|
if (weaponId > 0) {
|
|
|
const w = gg.data.table.getTableData<ITableWeapon>(TableNames.Weapon, weaponId);
|
|
|
const sid = Number(w?.act_skill) || 0;
|
|
|
if (sid > 0) skillIdArr.push(sid);
|
|
|
}
|
|
|
} catch { /* equip 未就绪 */ }
|
|
|
return skillIdArr.filter((id, i, arr) => arr.indexOf(id) === i);
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 根据武器等级获取可用的所有技能(Weapon1.id)
|
|
|
* @param id Weapon1 id
|
|
|
* @param level 武器等级
|
|
|
*/
|
|
|
public getAllCanUseSkillByWeaponLevel(id: number, level: number) {
|
|
|
return this.getSkillIdsForWeapon1(id, level);
|
|
|
}
|
|
|
|
|
|
/** 升级所需碎片数(Weapon.synthesisnumber) */
|
|
|
public getWeaponFlagNumByLevel(weaponId: number) {
|
|
|
const conf = this.getWeaponData(weaponId);
|
|
|
if (!conf) return -1;
|
|
|
const weaponRow = this._getHeroWeaponRow(conf);
|
|
|
if (weaponRow && weaponRow.synthesisnumber > 0) {
|
|
|
return weaponRow.synthesisnumber;
|
|
|
}
|
|
|
return -1;
|
|
|
}
|
|
|
|
|
|
/**获取紫色及以上武器最大等级 */
|
|
|
public getMaxLevelPurpleWeapon() {
|
|
|
let list = this.getWeaponDataList().filter(x => x.quality >= 3);
|
|
|
let maxlevel = 1;
|
|
|
let haveArr = this.getHaveWeaponDataList();
|
|
|
for (let i = 0; i < list.length; i++) {
|
|
|
if (haveArr.find(x => x.id == list[i].id) == null) {
|
|
|
continue;
|
|
|
}
|
|
|
let level = this.getWeaponLevel(list[i].id);
|
|
|
if (level > maxlevel) {
|
|
|
maxlevel = level;
|
|
|
}
|
|
|
}
|
|
|
return maxlevel
|
|
|
}
|
|
|
|
|
|
/*获取已装备的武器id和等级*/
|
|
|
public getEquippedWeaponIdAndLevel() {
|
|
|
let arr = gg.data.project.getHaveWeaponDataList();
|
|
|
let result: number[][] = [];
|
|
|
for (let i = 0; i < arr.length; i++) {
|
|
|
let item = arr[i];
|
|
|
let weaponId = parseInt(item[0]);
|
|
|
let weaponLevel = parseInt(item[1]);
|
|
|
result.push([weaponId, weaponLevel]);
|
|
|
}
|
|
|
return result;
|
|
|
}
|
|
|
|
|
|
/**获取武器最大等级 */
|
|
|
public getMaxLevelWeapon() {
|
|
|
let list = this.getWeaponDataList()
|
|
|
let maxlevel = 1;
|
|
|
let haveArr = this.getHaveWeaponDataList();
|
|
|
for (let i = 0; i < list.length; i++) {
|
|
|
if (haveArr.find(x => x.id == list[i].id) == null) {
|
|
|
continue;
|
|
|
}
|
|
|
let level = this.getWeaponLevel(list[i].id);
|
|
|
if (level > maxlevel) {
|
|
|
maxlevel = level;
|
|
|
}
|
|
|
}
|
|
|
return maxlevel
|
|
|
}
|
|
|
|
|
|
//指定品质武器
|
|
|
public getMaxLevelWeaponByQuality(quality: number) {
|
|
|
let list = this.getWeaponDataList().filter(x => x.quality == quality);
|
|
|
let maxlevel = 1;
|
|
|
let haveArr = this.getHaveWeaponDataList();
|
|
|
for (let i = 0; i < list.length; i++) {
|
|
|
if (haveArr.find(x => x.id == list[i].id) == null) {
|
|
|
continue;
|
|
|
}
|
|
|
let level = this.getWeaponLevel(list[i].id);
|
|
|
if (level > maxlevel) {
|
|
|
maxlevel = level;
|
|
|
}
|
|
|
}
|
|
|
return maxlevel
|
|
|
}
|
|
|
|
|
|
//#endregion
|
|
|
|
|
|
///////////////////////----------------战斗相关-------------------///////////////////////
|
|
|
//#region 战斗
|
|
|
/**消耗体力 */
|
|
|
vigourCost: number = 3;
|
|
|
|
|
|
/** 从 Const 同步单场战斗体力消耗 */
|
|
|
syncBattleVigourCost(): void {
|
|
|
const cost = Math.floor(Number(gg.data.table.getConst(ConstNumType.BattleVigourCost)) || 0);
|
|
|
if (cost <= 0) {
|
|
|
console.log('[ConfigProject] Const 战斗体力消耗非法', ConstNumType.BattleVigourCost);
|
|
|
return;
|
|
|
}
|
|
|
this.vigourCost = cost;
|
|
|
}
|
|
|
|
|
|
/** 单场战斗体力消耗(Const id=6) */
|
|
|
getBattleVigourCost(): number {
|
|
|
const cost = Math.floor(Number(gg.data.table.getConst(ConstNumType.BattleVigourCost)) || 0);
|
|
|
if (cost <= 0) {
|
|
|
console.log('[ConfigProject] Const 战斗体力消耗非法', ConstNumType.BattleVigourCost);
|
|
|
}
|
|
|
return cost > 0 ? cost : this.vigourCost;
|
|
|
}
|
|
|
|
|
|
/** 获取战场 Level 配表 */
|
|
|
getLevelConfig(levelId: number) {
|
|
|
try {
|
|
|
return gg.data.table.getTableData<ITableLevel>(TableNames.Level, levelId);
|
|
|
} catch (error) {
|
|
|
return null;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/** 战场列表(Level 表全部有效 id) */
|
|
|
getBattleLevelList(): ITableLevel[] {
|
|
|
try {
|
|
|
return (gg.data.table.getTableList<ITableLevel>(TableNames.Level) ?? [])
|
|
|
.filter((x) => x.id > 0);
|
|
|
} catch (error) {
|
|
|
return [];
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/** 规范战场 id 到配表有效范围 */
|
|
|
clampBattleLevelId(levelId: number): number {
|
|
|
const list = this.getBattleLevelList();
|
|
|
if (list.length === 0) return 1;
|
|
|
const ids = list.map((x) => x.id);
|
|
|
const id = Math.max(1, Math.floor(Number(levelId)) || 1);
|
|
|
if (ids.includes(id)) return id;
|
|
|
return ids[0];
|
|
|
}
|
|
|
|
|
|
/** 读档后修正星数/段位字段 */
|
|
|
repairRankOnDocLoad(): void {
|
|
|
const doc = gg.data.doc as {
|
|
|
rankStars?: number;
|
|
|
rankId?: number;
|
|
|
rankLastSyncStars?: number;
|
|
|
rankLastSyncSoul?: number;
|
|
|
};
|
|
|
if (!(gg.data.doc.rankId > 0)) {
|
|
|
gg.data.doc.rankId = 1;
|
|
|
}
|
|
|
if (doc.rankStars == null || doc.rankStars < 1) {
|
|
|
const legacyId = doc.rankId > 0 ? Math.floor(doc.rankId) : 0;
|
|
|
const legacyIntra = doc.rankStars != null && doc.rankStars >= 0 ? Math.floor(doc.rankStars) : -1;
|
|
|
if (legacyId > 0 && legacyIntra >= 0) {
|
|
|
const row = gg.data.table.getTableData<ITableRank>(TableNames.Rank, legacyId);
|
|
|
const min = row?.level?.length >= 2 ? Math.floor(Number(row.level[0]) || 1) : 1;
|
|
|
doc.rankStars = min + legacyIntra;
|
|
|
} else if (legacyId > 0) {
|
|
|
const row = gg.data.table.getTableData<ITableRank>(TableNames.Rank, legacyId);
|
|
|
doc.rankStars = row?.level?.length >= 2 ? Math.floor(Number(row.level[0]) || 1) : 1;
|
|
|
} else {
|
|
|
doc.rankStars = 1;
|
|
|
}
|
|
|
}
|
|
|
doc.rankStars = Math.max(1, Math.floor(Number(doc.rankStars) || 1));
|
|
|
if (doc.rankLastSyncStars == null || doc.rankLastSyncStars < 0) {
|
|
|
const legacySync = doc.rankLastSyncSoul;
|
|
|
doc.rankLastSyncStars = legacySync != null && legacySync >= 0 ? legacySync : -1;
|
|
|
}
|
|
|
delete doc.rankLastSyncSoul;
|
|
|
DailyWinStats.ensureToday();
|
|
|
MatchWinStats.ensureToday();
|
|
|
}
|
|
|
|
|
|
/** 连续失败次数(按战场) */
|
|
|
getFailCount(levelId: number) {
|
|
|
const str = gg.data.getkeyKeyValue(OtherDataType.FailContinuousCount, `l${levelId}`);
|
|
|
if (str == null || str === '') return 0;
|
|
|
const n = parseInt(str, 10);
|
|
|
return Number.isFinite(n) ? n : 0;
|
|
|
}
|
|
|
|
|
|
/** 设置连续失败次数 */
|
|
|
setFailCount(levelId: number, failCount: number) {
|
|
|
gg.data.setkeyKeyValue(OtherDataType.FailContinuousCount, `l${levelId}`, failCount.toString());
|
|
|
}
|
|
|
|
|
|
/** 增加失败次数 */
|
|
|
addFailCount(levelId: number) {
|
|
|
this.setFailCount(levelId, this.getFailCount(levelId) + 1);
|
|
|
}
|
|
|
|
|
|
/** 战场胜负奖励串(`道具id|数量`,多项逗号分隔) */
|
|
|
getLevelRewardStr(levelId: number, win: boolean) {
|
|
|
const row = this.getLevelConfig(levelId);
|
|
|
if (!row) return '';
|
|
|
const raw = win ? row.reward : row.loserreward;
|
|
|
return LevelFieldResolver.pickString(raw);
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 解析 `道具id|数量`;多项为 `id|num,id|num`。
|
|
|
* @param raw 奖励串
|
|
|
*/
|
|
|
parseItemPipeNumList(raw: string): ItemData[] {
|
|
|
const list: ItemData[] = [];
|
|
|
if (!raw) return list;
|
|
|
const segs = raw.split(',');
|
|
|
for (let i = 0; i < segs.length; i++) {
|
|
|
const seg = segs[i].trim();
|
|
|
if (!seg) continue;
|
|
|
const kv = seg.split('|');
|
|
|
if (kv.length !== 2) {
|
|
|
console.log('[LevelReward] 格式须为 道具id|数量', seg);
|
|
|
continue;
|
|
|
}
|
|
|
const id = Number(kv[0]);
|
|
|
const num = Number(kv[1]);
|
|
|
if (!(id > 0) || !(num > 0) || !Number.isFinite(num)) {
|
|
|
console.log('[LevelReward] id/数量非法', seg);
|
|
|
continue;
|
|
|
}
|
|
|
if (!gg.data.table.getItemData(id)) {
|
|
|
console.log('[LevelReward] Item 表缺少 id=', id);
|
|
|
continue;
|
|
|
}
|
|
|
list.push(new ItemData(id, num));
|
|
|
}
|
|
|
return list;
|
|
|
}
|
|
|
|
|
|
/**验证体力是否足够 */
|
|
|
checkVigourEnough() {
|
|
|
return gg.data.getVigour() >= this.getBattleVigourCost();
|
|
|
}
|
|
|
|
|
|
/**根据战场id获取新机制数组 */
|
|
|
getNewPlayArr(_levelId: number) {
|
|
|
return [];
|
|
|
}
|
|
|
|
|
|
/**获取战场所有机制配置 */
|
|
|
getChapterAllMechanismConfig(_levelId: number) {
|
|
|
return [];
|
|
|
}
|
|
|
|
|
|
/**获取战场所有障碍物配置 */
|
|
|
getChapterAllObstacleConfig(_levelId: number) {
|
|
|
}
|
|
|
|
|
|
|
|
|
private _monsterList: ITableMonster[] = [];
|
|
|
private _monsterMap: Map<number, ITableMonster> = new Map();
|
|
|
|
|
|
/**获取所有怪物配置 */
|
|
|
getAllMonsterList1() {
|
|
|
if (this._monsterList.length == 0) {
|
|
|
this._monsterList = gg.data.table.getTableList<ITableMonster>(TableNames.Monster);
|
|
|
}
|
|
|
return this._monsterList;
|
|
|
}
|
|
|
/**根据怪物id获取怪物数据 */
|
|
|
getMonsterDataByID(monsterId: number) {
|
|
|
if (this._monsterMap.size == 0) {
|
|
|
let monsterList = this.getAllMonsterList1();
|
|
|
for (let i = 0; i < monsterList.length; i++) {
|
|
|
const element = monsterList[i];
|
|
|
this._monsterMap.set(element.id, element);
|
|
|
}
|
|
|
}
|
|
|
return this._monsterMap.get(monsterId);
|
|
|
}
|
|
|
|
|
|
/**解析出怪扣配置信息 */
|
|
|
spawnMonsterCall1(monsterStr: string) {
|
|
|
let idArr = []
|
|
|
if (monsterStr == '') {
|
|
|
return idArr
|
|
|
}
|
|
|
let t = monsterStr.split('|')
|
|
|
for (let i = 0; i < t.length; i++) {
|
|
|
if (t[i] == '') {
|
|
|
continue
|
|
|
}
|
|
|
let s = t[i].split(';')
|
|
|
for (let j = 0; j < s.length; j++) {
|
|
|
let id = s[j].split(',')[0]
|
|
|
if (idArr.indexOf(id) == -1) {
|
|
|
idArr.push(Number(id))
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
return idArr
|
|
|
}
|
|
|
|
|
|
spawnMonsterCall2(monsterStr) {
|
|
|
let idArr = []
|
|
|
if (monsterStr == '') {
|
|
|
return idArr
|
|
|
}
|
|
|
let t = monsterStr.split('|')
|
|
|
for (let i = 0; i < t.length; i++) {
|
|
|
let id = t[i].split(',')[2]
|
|
|
|
|
|
if (idArr.indexOf(id) == -1) {
|
|
|
idArr.push(Number(id))
|
|
|
}
|
|
|
}
|
|
|
return idArr;
|
|
|
}
|
|
|
|
|
|
/**根据难度值获取难度称呼 */
|
|
|
getDifficultyName(difficulty: number) {
|
|
|
switch (difficulty) {
|
|
|
case 1: return '简单';
|
|
|
case 2: return '普通';
|
|
|
case 3: return '困难';
|
|
|
case 4: return '地狱';
|
|
|
case 5: return '末世';
|
|
|
default: return '普通';
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/** Level_weight 表已移除,返回 null */
|
|
|
getChapterWeightConfig(_qualityGroupId: number, _dangerCoefficient: number) {
|
|
|
return null;
|
|
|
}
|
|
|
|
|
|
/** 词条 base_attr 类型 → 三选1 DPS 乘区类型 */
|
|
|
private static readonly _baseAttrToMultiplyType: Record<number, number> = {
|
|
|
1: SkillMultiplyType.Damage,
|
|
|
2: SkillMultiplyType.Fire,
|
|
|
3: SkillMultiplyType.CritRate,
|
|
|
4: SkillMultiplyType.CritDamage,
|
|
|
5: SkillMultiplyType.CD,
|
|
|
6: SkillMultiplyType.DamageMultiple1,
|
|
|
};
|
|
|
|
|
|
/** 英雄关联武器配表(专属优先,否则默认武器) */
|
|
|
private _getHeroWeaponRow(hero: ITableHero | null | undefined): ITableWeapon | null {
|
|
|
if (!hero) return null;
|
|
|
const wid = hero.personalweapon > 0 ? hero.personalweapon : hero.startweapon;
|
|
|
if (!(wid > 0)) return null;
|
|
|
return gg.data.table.getTableData<ITableWeapon>(TableNames.Weapon, wid);
|
|
|
}
|
|
|
|
|
|
/** 读取 Weapon1 配置(Skill.weapon / 组合 weapon1Id / 局外武器页) */
|
|
|
getWeapon1Data(weapon1Id: number): ITableHero | null {
|
|
|
return gg.data.table.getTableData<ITableHero>(TableNames.Hero, weapon1Id) ?? null;
|
|
|
}
|
|
|
|
|
|
/** 局外库存键列表(HaveWeapon / EquipWeapon 均为 Weapon1.id) */
|
|
|
getWeapon1Ids(): number[] {
|
|
|
return gg.data.table.getTableList<ITableHero>(TableNames.Hero)
|
|
|
.map((w) => w.id)
|
|
|
.filter((id) => Number.isFinite(id));
|
|
|
}
|
|
|
|
|
|
/** 武器展示用 Item(Weapon.itemid → Item.icon) */
|
|
|
getWeaponDisplayItem(weapon1Id: number): ITableItem | null {
|
|
|
const w1 = this.getWeapon1Data(weapon1Id);
|
|
|
const weaponRow = this._getHeroWeaponRow(w1);
|
|
|
if (!(weaponRow?.itemid > 0)) return null;
|
|
|
return gg.data.table.getItemData(weaponRow.itemid) ?? null;
|
|
|
}
|
|
|
|
|
|
/** 武器碎片道具 id(Weapon.itemid) */
|
|
|
getWeaponScrapId(weapon1Id: number): number {
|
|
|
const w1 = this.getWeapon1Data(weapon1Id);
|
|
|
const weaponRow = this._getHeroWeaponRow(w1);
|
|
|
return weaponRow?.itemid ?? 0;
|
|
|
}
|
|
|
|
|
|
/**根据一组技能计算指定类型乘区值总和(兼容旧 multiply_type + 新 base_attr) */
|
|
|
getSumValueBySkills(skillArr: ITableSkill[], type: number) {
|
|
|
let sum = 0;
|
|
|
for (let i = 0; i < skillArr.length; i++) {
|
|
|
const skill = skillArr[i] as ITableSkill & {
|
|
|
multiply_type1?: number;
|
|
|
multiply_num1?: number;
|
|
|
multiply_type2?: number;
|
|
|
multiply_num2?: number;
|
|
|
};
|
|
|
const t1 = skill.multiply_type1;
|
|
|
const v1 = skill.multiply_num1;
|
|
|
if (t1 === type) sum += v1 ?? 0;
|
|
|
const t2 = skill.multiply_type2;
|
|
|
const v2 = skill.multiply_num2;
|
|
|
if (t2 === type) sum += v2 ?? 0;
|
|
|
}
|
|
|
return sum;
|
|
|
}
|
|
|
|
|
|
/** 根据一组技能获取所有乘区类型(旧 base_attr 词条已删,仅兼容遗留 multiply_type 扩展字段) */
|
|
|
getAllMultiplyType(skillArr: ITableSkill[]) {
|
|
|
const typeArr: number[] = [];
|
|
|
for (let i = 0; i < skillArr.length; i++) {
|
|
|
const skill = skillArr[i] as ITableSkill & {
|
|
|
multiply_type1?: number;
|
|
|
multiply_type2?: number;
|
|
|
};
|
|
|
const t1 = skill.multiply_type1;
|
|
|
const t2 = skill.multiply_type2;
|
|
|
if (t1 > 0 && typeArr.indexOf(t1) === -1) typeArr.push(t1);
|
|
|
if (t2 > 0 && typeArr.indexOf(t2) === -1) typeArr.push(t2);
|
|
|
}
|
|
|
return typeArr;
|
|
|
}
|
|
|
|
|
|
/**获取玩家当前暴击率 */
|
|
|
getPlayerCritRate(weaponId: number) {
|
|
|
return gg.attrInterface.getAttrCritValue(weaponId);
|
|
|
}
|
|
|
/**获取玩家当前暴击伤害 */
|
|
|
getPlayerCritDamage(weaponId: number) {
|
|
|
return gg.attrInterface.getAttrCritDamageAddValue(weaponId);
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 场上已激活塔的理论秒伤总和(旧 BattleCore.dps / getAllDps 同源)。
|
|
|
* 按武器聚合合成等级之和,不随实际命中波动。
|
|
|
*/
|
|
|
getBattlefieldDps(skills: readonly ITableSkill[], compositions: readonly ComposedUnit[]): number {
|
|
|
const stackByWeapon = new Map<number, number>();
|
|
|
for (const comp of compositions) {
|
|
|
if (comp.weapon1Id <= 0 || comp.level <= 0) continue;
|
|
|
stackByWeapon.set(
|
|
|
comp.weapon1Id,
|
|
|
(stackByWeapon.get(comp.weapon1Id) ?? 0) + comp.level,
|
|
|
);
|
|
|
}
|
|
|
return this.getAllDps(skills, mapKeysToArray(stackByWeapon), stackByWeapon);
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 指定武器列表的理论秒伤总和;stackSumOverride 为各 weapon1Id 的合成等级之和。
|
|
|
*/
|
|
|
getAllDps(
|
|
|
skills: readonly ITableSkill[],
|
|
|
weapon1Ids: readonly number[],
|
|
|
stackSumOverride?: ReadonlyMap<number, number> | null,
|
|
|
): number {
|
|
|
let total = 0;
|
|
|
for (const weaponId of weapon1Ids) {
|
|
|
const stackSum = stackSumOverride?.get(weaponId) ?? 0;
|
|
|
if (stackSum > 0) {
|
|
|
total += this.getTowerTheoreticalDps(skills, weaponId, stackSum);
|
|
|
}
|
|
|
}
|
|
|
return Math.floor(total);
|
|
|
}
|
|
|
|
|
|
/** 单武器理论秒伤:与局内 deriveTowerSetupStats 同源(无角色攻击/冷却系数) */
|
|
|
getTowerTheoreticalDps(
|
|
|
skills: readonly ITableSkill[],
|
|
|
weapon1Id: number,
|
|
|
composeLevel: number,
|
|
|
): number {
|
|
|
const w1 = this.getWeapon1Data(weapon1Id);
|
|
|
if (!w1 || composeLevel <= 0) return 0;
|
|
|
|
|
|
// 旧词条 Skill.weapon 已删:DPS 乘区用传入 skills(局外带入/调试),无则按 1
|
|
|
const dpsMult = this.calculateDPSWeight(skills as ITableSkill[], weapon1Id);
|
|
|
const setup = deriveTowerSetupStats(w1, composeLevel);
|
|
|
if (!(setup.attackSpeed > 0)) return 0;
|
|
|
return dpsMult * setup.attack * setup.attackSpeed;
|
|
|
}
|
|
|
|
|
|
/** 计算一组词条对指定武器的理论 DPS 乘区(暴击期望 × 其他乘区) */
|
|
|
calculateDPSWeight(arr: readonly ITableSkill[], weaponId: number): number {
|
|
|
const typeArr = this.getAllMultiplyType([...arr]);
|
|
|
const multDataMap = new Map<number, number>();
|
|
|
for (const type of typeArr) {
|
|
|
multDataMap.set(type, this.getSumValueBySkills([...arr], type));
|
|
|
}
|
|
|
|
|
|
const critRate = multDataMap.get(SkillMultiplyType.CritRate) ?? 0;
|
|
|
const critDamage = multDataMap.get(SkillMultiplyType.CritDamage) ?? 0;
|
|
|
let otherTypeProduct = 1;
|
|
|
multDataMap.forEach((value, key) => {
|
|
|
if (key !== SkillMultiplyType.CritRate && key !== SkillMultiplyType.CritDamage) {
|
|
|
otherTypeProduct *= (value + 1);
|
|
|
}
|
|
|
});
|
|
|
return this._calculateCritDpsMultiplier(critRate, critDamage, otherTypeProduct, weaponId);
|
|
|
}
|
|
|
|
|
|
private _calculateCritDpsMultiplier(
|
|
|
critRate: number,
|
|
|
critDamage: number,
|
|
|
otherType: number,
|
|
|
weaponId: number,
|
|
|
): number {
|
|
|
const weaponData = this.getWeapon1Data(weaponId);
|
|
|
if (!weaponData) return otherType;
|
|
|
|
|
|
const baseCritRate = weaponData.critical_chance / 10000;
|
|
|
const playerCritRate = this.getPlayerCritRate(weaponId) / 10000;
|
|
|
const allCritRate = baseCritRate + playerCritRate + critRate;
|
|
|
|
|
|
const baseCritDamage = weaponData.critical_dam / 10000;
|
|
|
const playerCritDamage = this.getPlayerCritDamage(weaponId) / 10000;
|
|
|
const allCritDamage = baseCritDamage + playerCritDamage + critDamage;
|
|
|
|
|
|
const critRateExpect = Math.max(1, allCritRate * allCritDamage + (1 - allCritRate));
|
|
|
return critRateExpect * otherType;
|
|
|
}
|
|
|
|
|
|
/**根据地图id和格子索引获取格子是否被修复 */
|
|
|
getGridIsRepair(mapId: number, gridIndex: number) {
|
|
|
let isRepair = gg.data.getkeyKeyValue(OtherDataType.GridIsRepair, mapId + '&' + gridIndex);
|
|
|
if (isRepair == '') isRepair = "0";
|
|
|
return Number(isRepair);
|
|
|
}
|
|
|
/**根据地图id和格子索引设置格子是否被修复 */
|
|
|
setGridIsRepair(mapId: number, gridIndex: number, isRepair: number) {
|
|
|
gg.data.setkeyKeyValue(OtherDataType.GridIsRepair, mapId + '&' + gridIndex, isRepair);
|
|
|
}
|
|
|
|
|
|
//#endregion
|
|
|
|
|
|
|
|
|
//#region 新玩法
|
|
|
chapterNewPlayInfo = new Map<ChapterNewPlayTypeEnum, ChapterPlayData>()
|
|
|
newPlayIdArr: number[] = []
|
|
|
/** 本局进入战斗时缓存的「首次出现」机制 id(按钮/弹窗列表用) */
|
|
|
firstTimePlayIdsThisBattle: number[] = []
|
|
|
/** 生成机制触发时刻序列(与 Const 注释、buildParatroopsProperty 一致) */
|
|
|
private generateTimeSequence(
|
|
|
startTime: number,
|
|
|
countMin: number,
|
|
|
countMax: number,
|
|
|
intervalMin: number,
|
|
|
intervalMax: number
|
|
|
): number[] {
|
|
|
const eventCount = MTools.getRandomValue(countMin, countMax);
|
|
|
const timeArr: number[] = [];
|
|
|
let curTime = startTime;
|
|
|
|
|
|
for (let j = 0; j < eventCount; j++) {
|
|
|
if (j > 0) {
|
|
|
curTime += MTools.getRandomValue(intervalMin, intervalMax);
|
|
|
}
|
|
|
timeArr.push(curTime);
|
|
|
}
|
|
|
return timeArr;
|
|
|
}
|
|
|
private generateNumSequence(
|
|
|
countMin: number,
|
|
|
countMax: number,
|
|
|
intervalMin: number,
|
|
|
intervalMax: number
|
|
|
): number[] {
|
|
|
const randomLimit = MTools.getRandomValue(countMin, countMax); // 封装随机整数
|
|
|
const timeArr: number[] = [];
|
|
|
for (let j = 0; j < randomLimit; j++) {
|
|
|
timeArr.push(MTools.getRandomValue(intervalMin, intervalMax));
|
|
|
}
|
|
|
return timeArr;
|
|
|
}
|
|
|
|
|
|
/** 解析 `min,max`;只配单值时 min=max,避免 getRandomValue(n, undefined)→NaN */
|
|
|
private parseMinMaxPair(segment: string, fallback = 0): [number, number] {
|
|
|
const parts = (segment ?? '').split(',').map(Number).filter(n => Number.isFinite(n));
|
|
|
if (parts.length === 0) return [fallback, fallback];
|
|
|
const lo = parts[0];
|
|
|
const hi = parts.length > 1 ? parts[1] : lo;
|
|
|
return [lo, Math.max(lo, hi)];
|
|
|
}
|
|
|
|
|
|
/** par1 三元组:monsterId,numMin,numMax,... */
|
|
|
private parseParatroopsMonsterOptions(par1: string, isProp: boolean): IParatroopsMonsterOption[] {
|
|
|
const segment = (par1 ?? '').split('|')[isProp ? 1 : 0] ?? '';
|
|
|
const parts = segment.split(',').map(Number);
|
|
|
const options: IParatroopsMonsterOption[] = [];
|
|
|
for (let i = 0; i + 2 < parts.length; i += 3) {
|
|
|
const opt = new IParatroopsMonsterOption();
|
|
|
opt.monsterId = parts[i];
|
|
|
opt.numMin = parts[i + 1];
|
|
|
opt.numMax = parts[i + 2];
|
|
|
if (opt.monsterId > 0) options.push(opt);
|
|
|
}
|
|
|
return options;
|
|
|
}
|
|
|
|
|
|
private buildParatroopsProperty(
|
|
|
startTime: number,
|
|
|
limits: number[],
|
|
|
intervals: number[],
|
|
|
monsterOptions: IParatroopsMonsterOption[],
|
|
|
): IParatroopsProperty {
|
|
|
const info = new IParatroopsProperty();
|
|
|
info.monsterOptions = monsterOptions.map(o => ({ ...o }));
|
|
|
const limLo = Number.isFinite(limits[0]) ? limits[0] : 1;
|
|
|
const limHi = Number.isFinite(limits[1]) ? Math.max(limLo, limits[1]) : limLo;
|
|
|
const cdLo = Number.isFinite(intervals[0]) ? intervals[0] : 0;
|
|
|
const cdHi = Number.isFinite(intervals[1]) ? Math.max(cdLo, intervals[1]) : cdLo;
|
|
|
const eventCount = MTools.getRandomValue(limLo, limHi);
|
|
|
info.timeArr = [];
|
|
|
info.numArr = [];
|
|
|
// startTime = 首次空投(COMBAT 时间轴);之后叠加 cd。数量在触发时按每组各自 roll
|
|
|
let curTime = startTime;
|
|
|
for (let j = 0; j < eventCount; j++) {
|
|
|
if (j > 0) {
|
|
|
curTime += MTools.getRandomValue(cdLo, cdHi);
|
|
|
}
|
|
|
info.timeArr.push(curTime);
|
|
|
info.numArr.push(0); // 废弃字段,仅占位对齐 timeArr
|
|
|
}
|
|
|
return info;
|
|
|
}
|
|
|
|
|
|
private buildCharmProperty(
|
|
|
startTime: number,
|
|
|
limits: number[],
|
|
|
intervals: number[],
|
|
|
numRange: number[],
|
|
|
durationRange: number[],
|
|
|
): ICharmProperty {
|
|
|
const info = new ICharmProperty();
|
|
|
const eventCount = MTools.getRandomValue(limits[0], limits[1]);
|
|
|
info.timeArr = [];
|
|
|
info.numArr = [];
|
|
|
info.durationArr = [];
|
|
|
let curTime = startTime;
|
|
|
for (let j = 0; j < eventCount; j++) {
|
|
|
if (j > 0) {
|
|
|
curTime += MTools.getRandomValue(intervals[0], intervals[1]);
|
|
|
}
|
|
|
info.timeArr.push(curTime);
|
|
|
info.numArr.push(MTools.getRandomValue(numRange[0], numRange[1]));
|
|
|
info.durationArr.push(MTools.getRandomValue(durationRange[0], durationRange[1]));
|
|
|
}
|
|
|
return info;
|
|
|
}
|
|
|
|
|
|
private readonly playConfigHandlers: Partial<Record<ChapterNewPlayTypeEnum, PlayConfigDescriptor>> = {
|
|
|
[ChapterNewPlayTypeEnum.ThunderRemoveObstacle]: {
|
|
|
constKey: ChapterNewPlayTypeEnum.ThunderRemoveObstacle,
|
|
|
needsFilter: true,
|
|
|
paramMapping: {
|
|
|
startTime: (row, isProp) => parseInt(row.starttime.split('|')[isProp ? 1 : 0]),
|
|
|
limits: (row, isProp) => row.timeslimit.split('|')[isProp ? 1 : 0].split(',').map(Number),
|
|
|
intervals: (row, isProp) => row.cd.split('|')[isProp ? 1 : 0].split(',').map(Number),
|
|
|
},
|
|
|
parser: (p) => {
|
|
|
// p.limits = [min, max], p.intervals = [min, max]
|
|
|
return this.generateTimeSequence(p.startTime, p.limits[0], p.limits[1], p.intervals[0], p.intervals[1]);
|
|
|
}
|
|
|
},
|
|
|
|
|
|
|
|
|
// --- 2. 加速门 (AccelerateGate) ---
|
|
|
[ChapterNewPlayTypeEnum.AccelerateGate]: {
|
|
|
constKey: ChapterNewPlayTypeEnum.AccelerateGate,
|
|
|
paramMapping: {
|
|
|
startTime: (row, isProp) => parseInt(row.starttime.split('|')[isProp ? 1 : 0]),
|
|
|
limits: (row, isProp) => row.timeslimit.split('|')[isProp ? 1 : 0].split(',').map(Number),
|
|
|
intervals: (row, isProp) => row.cd.split('|')[isProp ? 1 : 0].split(',').map(Number),
|
|
|
// par1: "门存在时间,加速倍速,buff 时间"
|
|
|
duration: (row, isProp) => parseInt(row.duration.split('|')[isProp ? 1 : 0]),
|
|
|
extraParams: (row, isProp) => row.par1.split('|')[isProp ? 1 : 0].split(',').map(Number),
|
|
|
},
|
|
|
parser: (p) => {
|
|
|
const info = new IAccelerateGateProperty();
|
|
|
info.gateExistTime = p.duration;
|
|
|
info.accelerateSpeed = p.extraParams[0];
|
|
|
info.buffTime = p.extraParams[1];
|
|
|
info.timeArr = this.generateTimeSequence(p.startTime, p.limits[0], p.limits[1], p.intervals[0], p.intervals[1]);
|
|
|
return info;
|
|
|
}
|
|
|
},
|
|
|
|
|
|
// --- 3. 藤蔓草地 (VinesGrass) ---
|
|
|
[ChapterNewPlayTypeEnum.VinesGrass]: {
|
|
|
constKey: ChapterNewPlayTypeEnum.VinesGrass,
|
|
|
paramMapping: {
|
|
|
startTime: (row, isProp) => parseInt(row.starttime.split('|')[isProp ? 1 : 0]),
|
|
|
limits: (row, isProp) => row.timeslimit.split('|')[isProp ? 1 : 0].split(',').map(Number),
|
|
|
intervals: (row, isProp) => row.cd.split('|')[isProp ? 1 : 0].split(',').map(Number),
|
|
|
},
|
|
|
parser: (p) => {
|
|
|
return this.generateTimeSequence(p.startTime, p.limits[0], p.limits[1], p.intervals[0], p.intervals[1]);
|
|
|
}
|
|
|
},
|
|
|
|
|
|
// --- 4. 飞禽空投 (Paratroops) ---
|
|
|
[ChapterNewPlayTypeEnum.Paratroops]: {
|
|
|
constKey: ChapterNewPlayTypeEnum.Paratroops,
|
|
|
needsFilter: true,
|
|
|
paramMapping: {
|
|
|
startTime: (row, isProp) => parseInt(row.starttime.split('|')[isProp ? 1 : 0], 10) || 0,
|
|
|
limits: (row, isProp) => this.parseMinMaxPair(row.timeslimit.split('|')[isProp ? 1 : 0], 1),
|
|
|
intervals: (row, isProp) => this.parseMinMaxPair(row.cd.split('|')[isProp ? 1 : 0], 0),
|
|
|
monsterOptions: (row, isProp) => this.parseParatroopsMonsterOptions(row.par1, isProp),
|
|
|
},
|
|
|
parser: (p) => this.buildParatroopsProperty(
|
|
|
p.startTime,
|
|
|
p.limits,
|
|
|
p.intervals,
|
|
|
p.monsterOptions,
|
|
|
),
|
|
|
},
|
|
|
|
|
|
// --- 5. 血月 (BloodMoon) ---
|
|
|
[ChapterNewPlayTypeEnum.BloodMoon]: {
|
|
|
constKey: ChapterNewPlayTypeEnum.BloodMoon,
|
|
|
paramMapping: {
|
|
|
startTime: (row, isProp) => parseInt(row.starttime.split('|')[isProp ? 1 : 0]),
|
|
|
intervalTime: (row, isProp) => parseInt(row.cd.split('|')[isProp ? 1 : 0]), // 假设间隔在 cd 列
|
|
|
durationRange: (row, isProp) => row.duration.split('|')[isProp ? 1 : 0].split(',').map(Number), // 持续时间范围
|
|
|
accelerateSpeed: (row, isProp) => parseFloat(row.par1.split('|')[isProp ? 1 : 0]),
|
|
|
},
|
|
|
parser: (p) => {
|
|
|
const info = new IBloodMoonProperty();
|
|
|
info.startTime = p.startTime;
|
|
|
info.intervalTime = p.intervalTime;
|
|
|
info.durationRange = Array.isArray(p.durationRange)
|
|
|
? p.durationRange.slice()
|
|
|
: [5, 10];
|
|
|
info.accelerateSpeed = p.accelerateSpeed;
|
|
|
info.openTimeArr = [];
|
|
|
info.closeTimeArr = [];
|
|
|
|
|
|
// 旧 Schedule 仍读数组;新机制用 start/interval/duration 动态调度(duration 从施法完成起算)
|
|
|
let openTime = p.startTime;
|
|
|
for (let j = 0; j < 100; j++) {
|
|
|
const randTime = MTools.getRandomValue(p.durationRange[0], p.durationRange[1]);
|
|
|
info.openTimeArr.push(openTime);
|
|
|
const closeTime = openTime + randTime;
|
|
|
info.closeTimeArr.push(closeTime);
|
|
|
openTime = closeTime + p.intervalTime;
|
|
|
}
|
|
|
return info;
|
|
|
}
|
|
|
},
|
|
|
|
|
|
// --- 6. 魔法眼 (MagicEye) ---
|
|
|
[ChapterNewPlayTypeEnum.MagicEye]: {
|
|
|
constKey: ChapterNewPlayTypeEnum.MagicEye,
|
|
|
paramMapping: {
|
|
|
startTime: (row, isProp) => parseInt(row.starttime.split('|')[isProp ? 1 : 0]),
|
|
|
intervalTime: (row, isProp) => parseInt(row.cd.split('|')[isProp ? 1 : 0]),
|
|
|
existTime: (row, isProp) => parseInt(row.duration.split('|')[isProp ? 1 : 0].split(',')[0]), // 假设取第一个
|
|
|
effectRange: (row, isProp) => row.par1.split('|')[isProp ? 1 : 0].split(',').map(Number),
|
|
|
// shieldCoeff: (row, isProp) => parseFloat(row.par2.split('|')[isProp ? 1 : 0]),
|
|
|
},
|
|
|
parser: (p) => {
|
|
|
const info = new IMagicEyeProperty();
|
|
|
info.startTime = p.startTime;
|
|
|
info.intervalTime = p.intervalTime;
|
|
|
info.existTime = p.existTime;
|
|
|
info.effectTimeLowerLimit = p.effectRange[0];
|
|
|
info.effectTimeUpperLimit = p.effectRange[1];
|
|
|
info.shieldHpCoefficient = p.effectRange[2];
|
|
|
return info;
|
|
|
}
|
|
|
},
|
|
|
// --- 7. 女巫 (Witch) ---
|
|
|
[ChapterNewPlayTypeEnum.Witch]: {
|
|
|
constKey: ChapterNewPlayTypeEnum.Witch,
|
|
|
paramMapping: {
|
|
|
startTime: (row, isProp) => parseInt(row.starttime.split('|')[isProp ? 1 : 0]),
|
|
|
intervalTime: (row, isProp) => parseInt(row.cd.split('|')[isProp ? 1 : 0]),
|
|
|
existTime: (row, isProp) => parseInt(row.duration.split('|')[isProp ? 1 : 0].split(',')[0]),
|
|
|
effectRange: (row, isProp) => row.par1.split('|')[isProp ? 1 : 0].split(',').map(Number),
|
|
|
// maxMonsterNum: (row, isProp) => parseInt(row.par2.split('|')[isProp ? 1 : 0].split(',')[0]),
|
|
|
// addHpCoeff: (row, isProp) => parseFloat(row.par2.split('|')[isProp ? 1 : 0].split(',')[1] || "0"),
|
|
|
},
|
|
|
parser: (p) => {
|
|
|
const info = new IWitchProperty();
|
|
|
info.startTime = p.startTime;
|
|
|
info.intervalTime = p.intervalTime;
|
|
|
info.existTime = p.existTime;
|
|
|
// par1: 回血系数,生效间隔下限,生效间隔上限,最大回血怪物数
|
|
|
info.addHpCoefficient = p.effectRange[0];
|
|
|
info.effectTimeLowerLimit = p.effectRange[1];
|
|
|
info.effectTimeUpperLimit = p.effectRange[2];
|
|
|
info.maxAddHpMonsterNum = p.effectRange[3];
|
|
|
return info;
|
|
|
}
|
|
|
},
|
|
|
|
|
|
// --- 8. 血包加血 (BloodBagAddHp) ---
|
|
|
[ChapterNewPlayTypeEnum.BloodBagAddHp]: {
|
|
|
constKey: ChapterNewPlayTypeEnum.BloodBagAddHp,
|
|
|
paramMapping: {
|
|
|
startTime: (row, isProp) => parseInt(row.starttime.split('|')[isProp ? 1 : 0]),
|
|
|
limits: (row, isProp) => row.timeslimit.split('|')[isProp ? 1 : 0].split(',').map(Number),
|
|
|
intervals: (row, isProp) => row.cd.split('|')[isProp ? 1 : 0].split(',').map(Number),
|
|
|
gateExistTime: (row, isProp) => parseInt(row.duration.split('|')[isProp ? 1 : 0]),
|
|
|
addHpCoeff: (row, isProp) => parseFloat(row.par1.split('|')[isProp ? 1 : 0]),
|
|
|
},
|
|
|
parser: (p) => {
|
|
|
const info = new IBloodBagAddHpProperty();
|
|
|
info.gateExistTime = p.gateExistTime;
|
|
|
info.addHpCoefficient = p.addHpCoeff;
|
|
|
info.timeArr = this.generateTimeSequence(p.startTime, p.limits[0], p.limits[1], p.intervals[0], p.intervals[1]);
|
|
|
return info;
|
|
|
}
|
|
|
},
|
|
|
|
|
|
// --- 9. Boss 空投 (BossAirdrop) ---
|
|
|
[ChapterNewPlayTypeEnum.BossAirdrop]: {
|
|
|
constKey: ChapterNewPlayTypeEnum.BossAirdrop,
|
|
|
needsFilter: true,
|
|
|
paramMapping: {
|
|
|
startTime: (row, isProp) => parseInt(row.starttime.split('|')[isProp ? 1 : 0]),
|
|
|
limits: (row, isProp) => row.timeslimit.split('|')[isProp ? 1 : 0].split(',').map(Number),
|
|
|
intervals: (row, isProp) => row.cd.split('|')[isProp ? 1 : 0].split(',').map(Number),
|
|
|
monsterId: (row, isProp) => parseInt(row.par1.split('|')[isProp ? 1 : 0]),
|
|
|
numRange: (row, isProp) => {
|
|
|
const val = row.par2 ? row.par2.split('|')[isProp ? 1 : 0] : "1,1";
|
|
|
return val.split(',').map(Number);
|
|
|
}
|
|
|
},
|
|
|
parser: (p) => {
|
|
|
const opt = new IParatroopsMonsterOption();
|
|
|
opt.monsterId = p.monsterId;
|
|
|
opt.numMin = p.numRange[0] ?? 1;
|
|
|
opt.numMax = p.numRange[1] ?? opt.numMin;
|
|
|
return this.buildParatroopsProperty(
|
|
|
p.startTime,
|
|
|
p.limits,
|
|
|
p.intervals,
|
|
|
[opt],
|
|
|
);
|
|
|
}
|
|
|
},
|
|
|
|
|
|
// --- 10. 魅惑(狐狸精) ---
|
|
|
[ChapterNewPlayTypeEnum.Charm]: {
|
|
|
constKey: ChapterNewPlayTypeEnum.Charm,
|
|
|
paramMapping: {
|
|
|
startTime: (row, isProp) => parseInt(row.starttime.split('|')[isProp ? 1 : 0]),
|
|
|
limits: (row, isProp) => row.timeslimit.split('|')[isProp ? 1 : 0].split(',').map(Number),
|
|
|
intervals: (row, isProp) => row.cd.split('|')[isProp ? 1 : 0].split(',').map(Number),
|
|
|
numRange: (row, isProp) => row.num.split('|')[isProp ? 1 : 0].split(',').map(Number),
|
|
|
// par1 魅惑时长(秒);只配单值如 "3|3" 时 min=max
|
|
|
durationRange: (row, isProp) => this.parseMinMaxPair(
|
|
|
row.par1.split('|')[isProp ? 1 : 0] ?? '',
|
|
|
2,
|
|
|
),
|
|
|
},
|
|
|
parser: (p) => this.buildCharmProperty(
|
|
|
p.startTime,
|
|
|
p.limits,
|
|
|
p.intervals,
|
|
|
p.numRange,
|
|
|
p.durationRange,
|
|
|
),
|
|
|
},
|
|
|
|
|
|
//新机制寒冬
|
|
|
[ChapterNewPlayTypeEnum.ColdFire]: {
|
|
|
constKey: ChapterNewPlayTypeEnum.ColdFire,
|
|
|
needsFilter: true,
|
|
|
paramMapping: {
|
|
|
|
|
|
timeInterval: (row, isProp) => parseInt(row.cd.split('|')[isProp ? 1 : 0]),
|
|
|
subTemperatureArr: (row, isProp) => row.par1.split('|')[isProp ? 1 : 0].split(',').map(Number),
|
|
|
subHpByTemperatureArr: (row, isProp) => row.par2.split('|')[isProp ? 1 : 0].split('&')[0].split(',').map(Number),
|
|
|
subHpArr: (row, isProp) => row.par2.split('|')[isProp ? 1 : 0].split('&')[1].split(',').map(Number),
|
|
|
},
|
|
|
parser: (p) => {
|
|
|
const info = new IColdFireProperty(); // 复用伞兵属性类?如果是新类请替换
|
|
|
info.startTemperature = 0;
|
|
|
info.timeInterval = p.timeInterval;
|
|
|
info.subTemperatureArr = p.subTemperatureArr;
|
|
|
info.subHpByTemperatureArr = p.subHpByTemperatureArr;
|
|
|
info.subHpArr = p.subHpArr;
|
|
|
return info;
|
|
|
}
|
|
|
},
|
|
|
|
|
|
//新机制大雾
|
|
|
[ChapterNewPlayTypeEnum.FogAndWind]: {
|
|
|
constKey: ChapterNewPlayTypeEnum.FogAndWind,
|
|
|
needsFilter: true,
|
|
|
paramMapping: {
|
|
|
intervals: (row, isProp) => parseInt(row.cd.split('|')[isProp ? 1 : 0]),
|
|
|
limitNum: (row, isProp) => parseInt(row.timeslimit.split('|')[isProp ? 1 : 0]),
|
|
|
fogDownDistance: (row, isProp) => parseInt(row.par1.split('|')[isProp ? 1 : 0]),
|
|
|
},
|
|
|
parser: (p) => {
|
|
|
const info = new IFogAndWindProperty(); // 复用伞兵属性类?如果是新类请替换
|
|
|
info.timeInterval = p.intervals;
|
|
|
info.limitNum = p.limitNum;
|
|
|
info.fogDownDistance = p.fogDownDistance;
|
|
|
return info;
|
|
|
}
|
|
|
},
|
|
|
|
|
|
//新机制黑夜
|
|
|
[ChapterNewPlayTypeEnum.NightAndLight]: {
|
|
|
constKey: ChapterNewPlayTypeEnum.NightAndLight,
|
|
|
needsFilter: true,
|
|
|
paramMapping: {
|
|
|
|
|
|
intervals: (row, isProp) => parseInt(row.cd.split('|')[isProp ? 1 : 0]),
|
|
|
existTime: (row, isProp) => parseInt(row.duration.split('|')[isProp ? 1 : 0]),
|
|
|
|
|
|
},
|
|
|
parser: (p) => {
|
|
|
const info = new INightAndLightProperty(); // 复用伞兵属性类?如果是新类请替换
|
|
|
info.timeInterval = p.intervals;
|
|
|
info.durationTime = p.existTime;
|
|
|
return info;
|
|
|
}
|
|
|
},
|
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
/** @param battleHandle 优先用当前对局句柄;避免仅依赖 CurentBattle 且地图未 bind 时 newPlayIdArr 未解析 */
|
|
|
initChapterNewPlayInfo(battleHandle?: IBattleRuntimeHandle | null) {
|
|
|
this.chapterNewPlayInfo.clear();
|
|
|
const battle = battleHandle ?? gg.game.CurentBattle;
|
|
|
if (!battle) {
|
|
|
this.newPlayIdArr = [];
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
// 以 ChapterId 直读配表为主;CurentConfigChapter 可能尚未赋值或不是当前闯关章
|
|
|
this.newPlayIdArr = this._collectNewPlayIdsFromBattle(battle);
|
|
|
|
|
|
// 4. 【核心优化】遍历并自动解析配置
|
|
|
for (const playType of this.newPlayIdArr) {
|
|
|
const descriptor = this.playConfigHandlers[playType];
|
|
|
|
|
|
if (!descriptor) {
|
|
|
console.log(`⚠️ 未知的玩法类型 ID: ${playType},请在 playConfigHandlers 中注册`);
|
|
|
continue;
|
|
|
}
|
|
|
|
|
|
// 获取完整的配置行对象 (fullStr 现在是对象)
|
|
|
const rowData = null;
|
|
|
|
|
|
if (!rowData) {
|
|
|
console.error(`❌ 找不到配置 ID: ${descriptor.constKey}`);
|
|
|
continue;
|
|
|
}
|
|
|
|
|
|
// 【自动化】根据 paramMapping 提取并清洗所有参数
|
|
|
const extractedParams: Record<string, any> = {};
|
|
|
|
|
|
// 获取所有 key,然后遍历
|
|
|
Object.keys(descriptor.paramMapping).forEach((paramName) => {
|
|
|
const extractor = descriptor.paramMapping[paramName];
|
|
|
extractedParams[paramName] = extractor(rowData, false);
|
|
|
});
|
|
|
|
|
|
const resultData = descriptor.parser(extractedParams);
|
|
|
this.chapterNewPlayInfo.set(playType, resultData);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
getChapterNewPlayInfo(type: ChapterNewPlayTypeEnum.Paratroops): IParatroopsProperty | undefined;
|
|
|
getChapterNewPlayInfo(type: ChapterNewPlayTypeEnum.AccelerateGate): IAccelerateGateProperty | undefined;
|
|
|
getChapterNewPlayInfo(type: ChapterNewPlayTypeEnum.BloodBagAddHp): IBloodBagAddHpProperty | undefined;
|
|
|
getChapterNewPlayInfo(type: ChapterNewPlayTypeEnum.BloodMoon): IBloodMoonProperty | undefined;
|
|
|
getChapterNewPlayInfo(type: ChapterNewPlayTypeEnum.MagicEye): IMagicEyeProperty | undefined;
|
|
|
getChapterNewPlayInfo(type: ChapterNewPlayTypeEnum.Witch): IWitchProperty | undefined;
|
|
|
getChapterNewPlayInfo(type: ChapterNewPlayTypeEnum.BloodBagAddHp): IBloodBagAddHpProperty | undefined;
|
|
|
getChapterNewPlayInfo(type: ChapterNewPlayTypeEnum.BossAirdrop): IParatroopsProperty | undefined;
|
|
|
getChapterNewPlayInfo(type: ChapterNewPlayTypeEnum.Charm): ICharmProperty | undefined;
|
|
|
// 3. 新机制寒冬
|
|
|
getChapterNewPlayInfo(type: ChapterNewPlayTypeEnum.ColdFire): IColdFireProperty | undefined;
|
|
|
getChapterNewPlayInfo(type: ChapterNewPlayTypeEnum.ThunderRemoveObstacle): number[] | undefined;
|
|
|
getChapterNewPlayInfo(type: ChapterNewPlayTypeEnum.VinesGrass): number[] | undefined;
|
|
|
// 4. 新机制大雾/吹风机
|
|
|
getChapterNewPlayInfo(type: ChapterNewPlayTypeEnum.FogAndWind): IFogAndWindProperty | undefined;
|
|
|
// 5. 新机制黑夜/灯塔
|
|
|
getChapterNewPlayInfo(type: ChapterNewPlayTypeEnum.NightAndLight): INightAndLightProperty | undefined;
|
|
|
|
|
|
// 2. 通用签名 (兜底,返回联合类型)
|
|
|
getChapterNewPlayInfo(type: ChapterNewPlayTypeEnum): ChapterPlayData | undefined;
|
|
|
|
|
|
getChapterNewPlayInfo(type: ChapterNewPlayTypeEnum) {
|
|
|
return this.chapterNewPlayInfo.get(type)
|
|
|
}
|
|
|
|
|
|
/** 合并本局 Level new_play(新机制表已移除) */
|
|
|
private _collectNewPlayIdsFromBattle(_battle: IBattleRuntimeHandle): number[] {
|
|
|
return [];
|
|
|
}
|
|
|
|
|
|
/** 是否属于新机制说明弹窗展示的玩法(排除摆放类道具机制 0~10) */
|
|
|
isMechanismPopupPlayMode(id: number): boolean {
|
|
|
return id >= ChapterNewPlayTypeEnum.ThunderRemoveObstacle;
|
|
|
}
|
|
|
|
|
|
/** 该机制是否在以往战斗中已出现过 */
|
|
|
hasSeenPlayMode(id: number): boolean {
|
|
|
return this.getSeenPlayModeIds().includes(id);
|
|
|
}
|
|
|
|
|
|
getSeenPlayModeIds(): number[] {
|
|
|
return gg.data.getArrayDataInt(OtherDataType.SeenPlayModeIds);
|
|
|
}
|
|
|
|
|
|
markPlayModesSeen(ids: readonly number[]): void {
|
|
|
if (!ids.length) return;
|
|
|
const seen = membershipFromArray(this.getSeenPlayModeIds());
|
|
|
for (const id of ids) membershipAdd(seen, id);
|
|
|
gg.data.setArrayData(OtherDataType.SeenPlayModeIds, membershipToArray(seen));
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 本局 new_play 解析完成后调用:缓存首次机制并写入持久化。
|
|
|
* 须在 initChapterNewPlayInfo 之后调用一次。
|
|
|
*/
|
|
|
prepareBattleFirstTimePlayModes(): void {
|
|
|
this.firstTimePlayIdsThisBattle = [];
|
|
|
const firstTime: number[] = [];
|
|
|
for (let i = 0; i < this.newPlayIdArr.length; i++) {
|
|
|
const id = Number(this.newPlayIdArr[i]);
|
|
|
if (!this.isMechanismPopupPlayMode(id)) continue;
|
|
|
if (this.hasSeenPlayMode(id)) continue;
|
|
|
firstTime.push(id);
|
|
|
}
|
|
|
this.firstTimePlayIdsThisBattle = firstTime;
|
|
|
this.markPlayModesSeen(firstTime);
|
|
|
}
|
|
|
|
|
|
hasFirstTimePlayModeThisBattle(): boolean {
|
|
|
return this.firstTimePlayIdsThisBattle.length > 0;
|
|
|
}
|
|
|
|
|
|
/** 本局是否配置了机制说明弹窗玩法(不含摆放类道具 0~10) */
|
|
|
hasBattleMechanismPopupPlayModes(): boolean {
|
|
|
return this.getBattleMechanismPopupPlayModeIds().length > 0;
|
|
|
}
|
|
|
|
|
|
getBattleMechanismPopupPlayModeIds(): number[] {
|
|
|
const ids: number[] = [];
|
|
|
for (let i = 0; i < this.newPlayIdArr.length; i++) {
|
|
|
const id = Number(this.newPlayIdArr[i]);
|
|
|
if (this.isMechanismPopupPlayMode(id)) ids.push(id);
|
|
|
}
|
|
|
return ids;
|
|
|
}
|
|
|
|
|
|
/** 自动弹窗:仅本局首次出现的机制 */
|
|
|
haveNewPlayInfo(): ChapterNewPlayTypeEnum[] {
|
|
|
return this.firstTimePlayIdsThisBattle.map(id => id as ChapterNewPlayTypeEnum);
|
|
|
}
|
|
|
|
|
|
/** 手动弹窗:本局全部生效的机制 */
|
|
|
getAllBattlePlayModePopupIds(): ChapterNewPlayTypeEnum[] {
|
|
|
return this.getBattleMechanismPopupPlayModeIds().map(id => id as ChapterNewPlayTypeEnum);
|
|
|
}
|
|
|
|
|
|
/**是否有移除障碍物道具机制 */
|
|
|
isHaveRemoveObsProp() {
|
|
|
return this.newPlayIdArr.indexOf(ChapterNewPlayTypeEnum.RemoveObstacleProp) >= 0
|
|
|
}
|
|
|
/**是否有炸弹道具 */
|
|
|
isHaveBombProp() {
|
|
|
return this.newPlayIdArr.indexOf(ChapterNewPlayTypeEnum.PlaceBombProp) >= 0
|
|
|
}
|
|
|
|
|
|
//#endregion
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 道具绑定数据项
|
|
|
*/
|
|
|
export class ItemData {
|
|
|
constructor(id, num = 1, data: ITableItem = null) {
|
|
|
this.id = id;
|
|
|
this.num = Math.floor(num);
|
|
|
this.staticData = data;
|
|
|
}
|
|
|
/**道具id */
|
|
|
id: number;
|
|
|
/**数量 */
|
|
|
num: number;
|
|
|
//是否是碎片ID转获取武器
|
|
|
isFragment: boolean = false;
|
|
|
/**静态配表数据 */
|
|
|
staticData: ITableItem;
|
|
|
/** 是否为成品武器(id 为 Weapon.id,走 HaveEquip) */
|
|
|
isEquip = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
export const ColorDefance = {
|
|
|
/**全局字体统一默认颜色 */
|
|
|
defaultColor: color(138, 94, 68, 255),
|
|
|
defaultBlue: color(45, 123, 161, 255),
|
|
|
defaultYellow: color(171, 109, 31, 255),
|
|
|
warnColor: color(192, 87, 23, 255),
|
|
|
defaultGreen: color(25, 103, 23, 255),
|
|
|
defaultRed: color(146, 32, 29, 255),
|
|
|
defaultOutLineGreen: color(8, 126, 8, 255),
|
|
|
|
|
|
qualityColor: [
|
|
|
color().fromHEX("#8A5E44"),
|
|
|
color().fromHEX("#E5DCC2"),
|
|
|
color().fromHEX("#1a85f0ff"),
|
|
|
color().fromHEX("#fa12eeff"),
|
|
|
color().fromHEX("#ffef0aff"),
|
|
|
color().fromHEX("#FDE77A"),
|
|
|
]
|
|
|
}
|
|
|
|
|
|
/**商店类型 */
|
|
|
export enum ShopType {
|
|
|
/**人才抽取 */
|
|
|
talent = 1,
|
|
|
/**废品回收 */
|
|
|
waste = 2,
|
|
|
/**商店 */
|
|
|
shop = 3,
|
|
|
/**拍卖 */
|
|
|
auction = 4,
|
|
|
}
|
|
|
|
|
|
/**道具池类型 */
|
|
|
export enum PoolType {
|
|
|
/**普通 */
|
|
|
common = 1,
|
|
|
/**高级 */
|
|
|
high = 2,
|
|
|
/**人才碎片任务基础池子 */
|
|
|
talentTaskBase = 3,
|
|
|
/**人才碎片任务高级池子 */
|
|
|
talentTaskHigh = 4,
|
|
|
/**人才碎片章节基础池子 */
|
|
|
talentChapterBase = 5,
|
|
|
/**人才碎片章节高级池子 */
|
|
|
talentChapterHigh = 6,
|
|
|
/**废品站任务基础池子 */
|
|
|
WasteTaskBase = 7,
|
|
|
/**废品站任务高级池子 */
|
|
|
WasteTaskHigh = 8,
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|