import { color } from "cc"; import { Singleton } from "../../mx/tools/Singleton"; import { LevelFieldResolver } from "../manager/LevelFieldResolver"; import { SkillMultiplyType, TableNames } from "./ConfigTableData"; import { heroNormalSkillId, isBasicHero } from '../battle/config/BattleHeroTable'; import { resolveAtkLevelCoef } from '../battle/combat/TowerStatResolver'; import { parseEntryBaseAttr } from '../battle/skill/SkillEntryParse'; 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; // 具体的业务解析逻辑 parser: (params: any) => ChapterPlayData; } /** * 项目动态数据 * 大部分通用的动态数据在gg.data中已经实现了 * 根据项目自定义数据获取实现方式在这个脚本里写,方便统一管理和调用 */ export class ConfigProjectData extends Singleton { ///////////////////////----------------道具相关-------------------/////////////////////// //#region 道具 /**将id相同的道具合并 */ mergeSameItem(items: ItemData[]) { let itemMap = new Map(); 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 = new Map(); private _weaponDataList: ITableHero[] = []; private _weaponIds: number[] = []; /** 获取所有局外武器数据(Weapon1) */ getWeaponDataList(): ITableHero[] { if (this._weaponDataList.length <= 0) { this._weaponDataList = gg.data.table.getTableList(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): ITableSkill[] { const skillIdSet = createMembershipSet(); 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(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(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(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(TableNames.Level, levelId); } catch (error) { return null; } } /** 战场列表(Level 表全部有效 id) */ getBattleLevelList(): ITableLevel[] { try { return (gg.data.table.getTableList(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(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(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 = new Map(); /**获取所有怪物配置 */ getAllMonsterList1() { if (this._monsterList.length == 0) { this._monsterList = gg.data.table.getTableList(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 = { 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(TableNames.Weapon, wid); } /** 读取 Weapon1 配置(Skill.weapon / 组合 weapon1Id / 局外武器页) */ getWeapon1Data(weapon1Id: number): ITableHero | null { return gg.data.table.getTableData(TableNames.Hero, weapon1Id) ?? null; } /** 局外库存键列表(HaveWeapon / EquipWeapon 均为 Weapon1.id) */ getWeapon1Ids(): number[] { return gg.data.table.getTableList(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; for (const { type: attrType, value } of parseEntryBaseAttr(skill.base_attr)) { if (ConfigProjectData._baseAttrToMultiplyType[attrType] === type) { sum += value / 10000; } } } return sum; } /**根据一组技能获取所有乘区类型(兼容旧 multiply_type + 新 base_attr) */ 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); for (const { type: attrType } of parseEntryBaseAttr(skill.base_attr)) { const multType = ConfigProjectData._baseAttrToMultiplyType[attrType]; if (multType > 0 && typeArr.indexOf(multType) === -1) { typeArr.push(multType); } } } 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(); 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 | 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; const weaponSkills = skills.filter(s => s.weapon === weapon1Id); const dpsMult = this.calculateDPSWeight(weaponSkills, weapon1Id); const levelLimit = w1.level_limit > 0 ? w1.level_limit : composeLevel; const effectiveLevel = Math.max(1, Math.min(composeLevel, levelLimit)); const weaponRow = this._getHeroWeaponRow(w1); const exclusiveMul = weaponRow ? gg.equip.getEquipExclusiveMul(w1.id, weaponRow) : 1; const weaponAtk = (weaponRow?.atk ?? 0) * exclusiveMul; const allAttack = w1.atk * resolveAtkLevelCoef(effectiveLevel) + weaponAtk; const skillId = heroNormalSkillId(w1, effectiveLevel); const skillRow = skillId > 0 ? gg.data.table.getTableData(TableNames.Skill, skillId) : null; const baseCd = skillRow?.cd ?? 1; if (baseCd <= 0) return 0; return dpsMult * allAttack / baseCd; } /** 计算一组词条对指定武器的理论 DPS 乘区(暴击期望 × 其他乘区) */ calculateDPSWeight(arr: readonly ITableSkill[], weaponId: number): number { const typeArr = this.getAllMultiplyType([...arr]); const multDataMap = new Map(); 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() 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> = { [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 = {}; // 获取所有 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, }