import { readdir, readFile } from 'fs/promises'; import { parseFloorInfo, parseTowerInfo } from './info'; import { IAutoLabelConfig, IConvertedMapInfo, ITowerInfo } from './types'; import { join } from 'path'; import { Presets, SingleBar } from 'cli-progress'; import { convertTowerMap, runTowerCode } from './tower'; import { MapTileConverter } from './converter'; export interface ILabelResult { /** 塔信息列表 */ readonly tower: ITowerInfo; /** 转换后的楼层列表 */ readonly maps: IConvertedMapInfo[]; } function addIssuePrefix(maxLength: number, path: string, content: string) { return `${path}: ${' '.repeat(maxLength - path.length)}${content}`; } /** * 自动标注塔地图 * @param towerInfo 所有塔的信息路径,文件包括颜色、标签等 * @param pathList 塔文件路径列表 * @param config 自动标记配置 */ export async function autoLabelTowers( towerInfo: string, pathList: string[], config: IAutoLabelConfig ) { const labelResult: ILabelResult[] = []; // 统计被不同规则过滤掉的楼层 let ignoredFloorsSize = 0; let ignoredMaxEmptyArea = 0; let ignoredMaxResourceArea = 0; let ignoredFloorsEnemy = 0; let ignoredFloorsWall = 0; let ignoredFloorsResource = 0; let ignoredFloorsDoor = 0; let ignoredFloorsEntry = 0; let ignoredFloorsCustom = 0; let ignoredFloorsIdle = 0; let ignoredFloorsIdleDoor = 0; let ignoredFloorsIdleEnemy = 0; let ignoredFloorsUseless = 0; let ignoredFloorsContinuous = 0; let ignoredFloorsContinuousDoor = 0; let ignoredFloorsContinuousEnemy = 0; const towers = await parseTowerInfo(towerInfo); const paths: string[] = []; await Promise.all( pathList.map(async path => { const dir = await readdir(path); paths.push(...dir.map(v => join(path, v))); }) ); const issues: [string, string][] = []; const progress = new SingleBar({}, Presets.shades_classic); progress.start(paths.length, 0); let i = 0; for (const path of paths) { progress.update(++i); let project: string; let floors: string; try { project = await readFile( join(path, 'project', 'project.min.js'), 'utf-8' ); floors = await readFile( join(path, 'project', 'floors.min.js'), 'utf-8' ); } catch { issues.push([path, '读取塔信息失败']); continue; } const result = runTowerCode(project, floors); if (result.issue.length > 0) { issues.push(...result.issue.map<[string, string]>(v => [path, v])); continue; } const converter = new MapTileConverter(result, config); const info = towers.get(result.data.firstData.name); if (!info) continue; const customPass = config.customTowerFilter?.(info) ?? true; if (!customPass) continue; const convertedMaps: IConvertedMapInfo[] = []; // 处理每个塔的每个楼层 for (const [name, floor] of Object.entries(result.main.floors)) { const width = floor.map[0].length; const height = floor.map.length; // 尺寸不匹配 const sizePass = config.allowedSize.some( ([w, h]) => w === width && h === height ); if (!sizePass) { ignoredFloorsSize++; continue; } // 转换楼层 const converted = convertTowerMap(result, floor, config, converter); const otherLayers = []; if (floor.bgmap) { otherLayers.push(floor.bgmap); } if (floor.bg2map) { otherLayers.push(floor.bg2map); } if (floor.fgmap) { otherLayers.push(floor.fgmap); } if (floor.fg2map) { otherLayers.push(floor.fg2map); } const floorInfo = parseFloorInfo( info, floor.map, converted.map, otherLayers, config, converter, name ); const floorData: IConvertedMapInfo = { data: converted, tower: info, mapId: name, info: floorInfo }; // 配置过滤楼层 if (floorInfo.maxEmptyArea > config.maxEmptyArea) { ignoredMaxEmptyArea++; continue; } if (floorInfo.maxResourceArea > config.maxResourceArea) { ignoredMaxResourceArea++; continue; } if ( floorInfo.enemyDensity < config.minEnemyRatio || floorInfo.enemyDensity > config.maxEnemyRatio ) { ignoredFloorsEnemy++; continue; } if ( floorInfo.wallDensity < config.minWallRatio || floorInfo.wallDensity > config.maxWallRatio ) { ignoredFloorsWall++; continue; } if ( floorInfo.resourceDensity < config.minResourceRatio || floorInfo.resourceDensity > config.maxResourceRatio ) { ignoredFloorsResource++; continue; } if ( floorInfo.doorDensity < config.minDoorRatio || floorInfo.doorDensity > config.maxDoorRatio ) { ignoredFloorsDoor++; continue; } if ( floorInfo.entryCount < config.minEntryCount || floorInfo.entryCount > config.maxEntryCount ) { ignoredFloorsEntry++; continue; } const filteredByLargeDoorCluster = !config.allowLargeDoorCluster && floorInfo.hasLargeDoorCluster; const filteredByLargeEnemyCluster = !config.allowLargeEnemyCluster && floorInfo.hasLargeEnemyCluster; if (filteredByLargeDoorCluster) { ignoredFloorsContinuousDoor++; } if (filteredByLargeEnemyCluster) { ignoredFloorsContinuousEnemy++; } if (filteredByLargeDoorCluster || filteredByLargeEnemyCluster) { ignoredFloorsContinuous++; continue; } const filteredByIdleDoorBranch = !config.allowIdleBranch && floorInfo.idleDoorBranchCount > 0; const filteredByRepeatedGuardDoorBranch = !config.allowRepeatedGuardIdleBranch && floorInfo.repeatedGuardDoorBranchCount > 0; const filteredByIdleEnemyBranch = !config.allowIdleBranch && floorInfo.idleEnemyBranchCount > 0; const filteredByRepeatedGuardEnemyBranch = !config.allowRepeatedGuardIdleBranch && floorInfo.repeatedGuardEnemyBranchCount > 0; if (filteredByIdleDoorBranch || filteredByRepeatedGuardDoorBranch) { ignoredFloorsIdleDoor++; } if ( filteredByIdleEnemyBranch || filteredByRepeatedGuardEnemyBranch ) { ignoredFloorsIdleEnemy++; } if ( (!config.allowIdleBranch && floorInfo.hasIdleBranch) || (!config.allowRepeatedGuardIdleBranch && floorInfo.hasRepeatedGuardIdleBranch) ) { ignoredFloorsIdle++; continue; } if (!config.allowUselessBranch && floorInfo.hasUselessBranch) { ignoredFloorsUseless++; continue; } // 自定义过滤楼层 const customPass = config.customFloorFilter?.(floorData) ?? true; if (!customPass) { ignoredFloorsCustom++; continue; } // 楼层过滤通过 convertedMaps.push(floorData); } labelResult.push({ tower: info, maps: convertedMaps }); } progress.stop(); if (!config.ignoreIssues) { const maxLength = Math.max(...issues.map(v => v[0].length)); issues.forEach(v => { console.log(addIssuePrefix(maxLength, v[0], v[1])); }); } const totalFilted = ignoredFloorsSize + ignoredFloorsEnemy + ignoredFloorsWall + ignoredFloorsResource + ignoredFloorsDoor + ignoredFloorsEntry + ignoredFloorsContinuous + ignoredFloorsIdle + ignoredFloorsUseless + ignoredFloorsCustom; console.log( `已处理 ${labelResult.length} 个塔,共 ${labelResult.reduce( (prev, curr) => prev + curr.maps.length, 0 )} 层,过滤掉 ${totalFilted} 层:` ); console.log(`尺寸过滤:${ignoredFloorsSize} 层`); console.log(`空地过滤:${ignoredMaxEmptyArea} 层`); console.log(`资源区域过滤:${ignoredMaxResourceArea} 层`); console.log(`怪物过滤:${ignoredFloorsEnemy} 层`); console.log(`墙壁过滤:${ignoredFloorsWall} 层`); console.log(`资源过滤:${ignoredFloorsResource} 层`); console.log(`门过滤:${ignoredFloorsDoor} 层`); console.log(`入口过滤:${ignoredFloorsEntry} 层`); console.log(`连续门过滤:${ignoredFloorsContinuousDoor} 层`); console.log(`连续怪过滤:${ignoredFloorsContinuousEnemy} 层`); console.log(`闲置门过滤:${ignoredFloorsIdleDoor} 层`); console.log(`闲置怪过滤:${ignoredFloorsIdleEnemy} 层`); console.log(`闲置节点过滤:${ignoredFloorsIdle} 层`); console.log(`无用节点过滤:${ignoredFloorsUseless} 层`); console.log(`自定义过滤:${ignoredFloorsCustom} 层`); return labelResult; }