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/game/ConfigProjectData.ts

1563 lines
62 KiB

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