mirror of
https://github.com/unanmed/ginka-generator.git
synced 2026-05-16 06:11:11 +08:00
refactor: 新的筛选算法
This commit is contained in:
parent
c64a783d5e
commit
ef79dd4d31
@ -254,14 +254,16 @@ const labelConfig: IAutoLabelConfig = {
|
||||
entry: 10
|
||||
},
|
||||
allowedSize: [[13, 13]],
|
||||
allowUselessBranch: true,
|
||||
maxWallDensityStd: 0.23,
|
||||
allowUselessBranch: false,
|
||||
maxWallDensityStd: 1,
|
||||
maxEmptyArea: 8,
|
||||
maxResourceArea: 8,
|
||||
minEnemyRatio: 0.02,
|
||||
maxEnemyRatio: 0.3,
|
||||
minWallRatio: 0.2,
|
||||
maxWallRatio: 0.6,
|
||||
minResourceRatio: 0.02,
|
||||
maxResourceRatio: 0.3,
|
||||
minResourceRatio: 0.05,
|
||||
maxResourceRatio: 0.25,
|
||||
minDoorRatio: 0,
|
||||
maxDoorRatio: 0.12,
|
||||
minFishCount: 0,
|
||||
@ -269,15 +271,15 @@ const labelConfig: IAutoLabelConfig = {
|
||||
minEntryCount: 1,
|
||||
maxEntryCount: 4,
|
||||
guassainRadius: 0,
|
||||
heatmapKernel: 0,
|
||||
heatmapKernel: 1,
|
||||
ignoreIssues: true,
|
||||
customTowerFilter: info => {
|
||||
// if (info.name !== 'Apeiria') {
|
||||
// return false;
|
||||
// }
|
||||
if (info.color !== TowerColor.Blue && info.color !== TowerColor.Green) {
|
||||
return false;
|
||||
}
|
||||
// if (info.color !== TowerColor.Blue && info.color !== TowerColor.Green) {
|
||||
// return false;
|
||||
// }
|
||||
if (info.people < 1000) {
|
||||
return false;
|
||||
}
|
||||
@ -290,26 +292,26 @@ const labelConfig: IAutoLabelConfig = {
|
||||
if (info.name.startsWith('24') && info.name.length > 2) {
|
||||
return false;
|
||||
}
|
||||
if (ignoredTower.includes(info.name)) {
|
||||
return false;
|
||||
}
|
||||
// if (ignoredTower.includes(info.name)) {
|
||||
// return false;
|
||||
// }
|
||||
return true;
|
||||
},
|
||||
customFloorFilter: floor => {
|
||||
if (floor.info.topo.graphs.length > 1) {
|
||||
if (floor.info.topo.graph.areas.size > 1) {
|
||||
return false;
|
||||
}
|
||||
if (floor.data.hasCannotInOut) {
|
||||
return false;
|
||||
}
|
||||
if (floor.info.topo.unreachable.size > 0) {
|
||||
if (floor.info.topo.graph.unreachableArea.size > 0) {
|
||||
return false;
|
||||
}
|
||||
if (ignoredFloor[floor.tower.name]?.includes(floor.mapId)) {
|
||||
return false;
|
||||
}
|
||||
if (floor.tower.name === 'Apeiria') {
|
||||
return Math.random() < 0.2;
|
||||
return Math.random() < 0.1;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@ 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 {
|
||||
/** 塔信息列表 */
|
||||
@ -31,6 +32,8 @@ export async function autoLabelTowers(
|
||||
|
||||
// 统计被不同规则过滤掉的楼层
|
||||
let ignoredFloorsSize = 0;
|
||||
let ignoredMaxEmptyArea = 0;
|
||||
let ignoredMaxResourceArea = 0;
|
||||
let ignoredFloorsEnemy = 0;
|
||||
let ignoredFloorsWall = 0;
|
||||
let ignoredFloorsResource = 0;
|
||||
@ -76,6 +79,8 @@ export async function autoLabelTowers(
|
||||
continue;
|
||||
}
|
||||
|
||||
const converter = new MapTileConverter(result, config);
|
||||
|
||||
const info = towers.get(result.data.firstData.name);
|
||||
if (!info) continue;
|
||||
const customPass = config.customTowerFilter?.(info) ?? true;
|
||||
@ -94,8 +99,29 @@ export async function autoLabelTowers(
|
||||
continue;
|
||||
}
|
||||
// 转换楼层
|
||||
const converted = convertTowerMap(result, floor, config);
|
||||
const floorInfo = parseFloorInfo(info, converted.map, config);
|
||||
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,
|
||||
@ -103,6 +129,14 @@ export async function autoLabelTowers(
|
||||
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
|
||||
@ -131,13 +165,6 @@ export async function autoLabelTowers(
|
||||
ignoredFloorsDoor++;
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
floorInfo.fishCount < config.minFishCount ||
|
||||
floorInfo.fishCount > config.maxFishCount
|
||||
) {
|
||||
ignoredFloorsFish++;
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
floorInfo.entryCount < config.minEntryCount ||
|
||||
floorInfo.entryCount > config.maxEntryCount
|
||||
@ -191,6 +218,8 @@ export async function autoLabelTowers(
|
||||
)} 层,过滤掉 ${totalFilted} 层:`
|
||||
);
|
||||
console.log(`尺寸过滤:${ignoredFloorsSize} 层`);
|
||||
console.log(`空地过滤:${ignoredMaxEmptyArea} 层`);
|
||||
console.log(`资源区域过滤:${ignoredMaxResourceArea} 层`);
|
||||
console.log(`怪物过滤:${ignoredFloorsEnemy} 层`);
|
||||
console.log(`墙壁过滤:${ignoredFloorsWall} 层`);
|
||||
console.log(`资源过滤:${ignoredFloorsResource} 层`);
|
||||
|
||||
343
data/src/auto/converter.ts
Normal file
343
data/src/auto/converter.ts
Normal file
@ -0,0 +1,343 @@
|
||||
import { sum } from 'lodash-es';
|
||||
import { IAutoLabelConfig, ICodeRunResult } from './types';
|
||||
import { CannotInOut, IMapTileConverter, ResourceType } from './types';
|
||||
|
||||
export class MapTileConverter implements IMapTileConverter {
|
||||
private readonly tower: ICodeRunResult;
|
||||
private readonly config: IAutoLabelConfig;
|
||||
private readonly noPassMap = new Map<number, boolean>();
|
||||
private readonly cannotInMap = new Map<number, number>();
|
||||
private readonly cannotOutMap = new Map<number, number>();
|
||||
private readonly labelMap = new Map<number, number>();
|
||||
private readonly resourceMap = new Map<number, Map<ResourceType, number>>();
|
||||
|
||||
private readonly emptyTiles = new Set<number>([0]);
|
||||
private readonly doorTiles = new Set<number>();
|
||||
private readonly enemyTiles = new Set<number>();
|
||||
private readonly resourceTiles = new Set<number>();
|
||||
private readonly keyTiles = new Set<number>();
|
||||
private readonly itemTiles = new Set<number>();
|
||||
|
||||
constructor(tower: ICodeRunResult, config: IAutoLabelConfig) {
|
||||
this.tower = tower;
|
||||
this.config = config;
|
||||
|
||||
// 基于 maps 字典预计算各 tile 的分类与属性,避免重复解析。
|
||||
const tileMap = tower.map ?? {};
|
||||
for (const key of Object.keys(tileMap)) {
|
||||
const tile = Number(key);
|
||||
if (!Number.isFinite(tile)) continue;
|
||||
this.precomputeTile(tile);
|
||||
}
|
||||
}
|
||||
|
||||
private static parseDirectionFlag(d: string | undefined): number {
|
||||
if (!d) return 0;
|
||||
switch (d) {
|
||||
case 'left':
|
||||
return CannotInOut.Left;
|
||||
case 'right':
|
||||
return CannotInOut.Right;
|
||||
case 'up':
|
||||
case 'top':
|
||||
return CannotInOut.Top;
|
||||
case 'down':
|
||||
case 'bottom':
|
||||
return CannotInOut.Bottom;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private parseFlags(arr?: string[]): number {
|
||||
if (!arr || arr.length === 0) return 0;
|
||||
let result = 0;
|
||||
for (const d of arr) {
|
||||
result |= MapTileConverter.parseDirectionFlag(d);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private normalizeResource(values: Partial<Record<ResourceType, number>>) {
|
||||
const out = new Map<ResourceType, number>();
|
||||
for (const [k, v] of Object.entries(values)) {
|
||||
const n = Number(v);
|
||||
if (!Number.isFinite(n) || n <= 0) continue;
|
||||
out.set(Number(k) as ResourceType, n);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private evalItemEffectResource(
|
||||
itemEffect?: string
|
||||
): Map<ResourceType, number> {
|
||||
if (!itemEffect) return new Map();
|
||||
|
||||
const heroStatus = {
|
||||
hp: 0,
|
||||
atk: 0,
|
||||
def: 0,
|
||||
mdef: 0
|
||||
};
|
||||
const thisMap = { ratio: 1 };
|
||||
const values = this.tower.data?.values;
|
||||
if (!values) return new Map();
|
||||
|
||||
const core = {
|
||||
values: new Proxy(values, {
|
||||
set() {
|
||||
return true;
|
||||
}
|
||||
}),
|
||||
status: {
|
||||
hero: new Proxy(heroStatus, {
|
||||
set(target, p: string, newValue) {
|
||||
if (typeof newValue !== 'number') return true;
|
||||
if (
|
||||
p !== 'hp' &&
|
||||
p !== 'atk' &&
|
||||
p !== 'def' &&
|
||||
p !== 'mdef'
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
target[p] = newValue;
|
||||
return true;
|
||||
}
|
||||
}),
|
||||
thisMap: new Proxy(thisMap, {
|
||||
set() {
|
||||
return true;
|
||||
}
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
const log = console.log;
|
||||
console.log = () => {};
|
||||
try {
|
||||
eval(itemEffect);
|
||||
} catch (e) {
|
||||
console.log = log;
|
||||
return new Map();
|
||||
}
|
||||
console.log = log;
|
||||
|
||||
return this.normalizeResource({
|
||||
[ResourceType.Hp]: heroStatus.hp,
|
||||
[ResourceType.Atk]: heroStatus.atk,
|
||||
[ResourceType.Def]: heroStatus.def,
|
||||
[ResourceType.Mdef]: heroStatus.mdef
|
||||
});
|
||||
}
|
||||
|
||||
private precomputeTile(tile: number): void {
|
||||
if (this.labelMap.has(tile)) return;
|
||||
|
||||
const labels = this.config.classes;
|
||||
const block = this.tower.map?.[tile];
|
||||
|
||||
if (this.emptyTiles.has(tile)) {
|
||||
this.noPassMap.set(tile, false);
|
||||
this.cannotInMap.set(tile, 0);
|
||||
this.cannotOutMap.set(tile, 0);
|
||||
this.labelMap.set(tile, labels.empty);
|
||||
this.resourceMap.set(tile, new Map());
|
||||
return;
|
||||
}
|
||||
|
||||
if (!block || tile === 17) {
|
||||
// 未知 tile 默认按墙处理
|
||||
this.noPassMap.set(tile, true);
|
||||
this.cannotInMap.set(tile, 0b1111);
|
||||
this.cannotOutMap.set(tile, 0b1111);
|
||||
this.labelMap.set(tile, labels.wall);
|
||||
this.resourceMap.set(tile, new Map());
|
||||
return;
|
||||
}
|
||||
|
||||
const cannotIn = this.parseFlags(block.cannotIn);
|
||||
const cannotOut = this.parseFlags(block.cannotOut);
|
||||
this.cannotInMap.set(tile, cannotIn);
|
||||
this.cannotOutMap.set(tile, cannotOut);
|
||||
|
||||
const blockId = block.id;
|
||||
const cls = block.cls;
|
||||
|
||||
// 1. 钥匙资源识别(doorInfo.keys)
|
||||
let isKey = false;
|
||||
let keyCount = 0;
|
||||
if (block.doorInfo && block.doorInfo.keys) {
|
||||
const keys = block.doorInfo.keys;
|
||||
keyCount = sum(Object.values(keys));
|
||||
if (keyCount > 0) {
|
||||
isKey = true;
|
||||
this.keyTiles.add(tile);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 道具资源识别(tools 且不是钥匙)
|
||||
let isItem = false;
|
||||
const item = this.tower.item?.[blockId];
|
||||
if (item?.cls === 'tools') {
|
||||
// 不是钥匙的 tools
|
||||
if (!isKey) {
|
||||
isItem = true;
|
||||
this.itemTiles.add(tile);
|
||||
}
|
||||
}
|
||||
|
||||
const isDoor =
|
||||
block.doorInfo ||
|
||||
blockId.toLowerCase().endsWith('door') ||
|
||||
blockId === 'specialDoor';
|
||||
const isEnemy = cls === 'enemys' || cls === 'enemy48';
|
||||
|
||||
let isResource = false;
|
||||
let resources = new Map<ResourceType, number>();
|
||||
if (isKey) {
|
||||
resources.set(ResourceType.Key, keyCount > 0 ? keyCount : 1);
|
||||
isResource = true;
|
||||
} else if (isItem) {
|
||||
resources.set(ResourceType.Item, 1);
|
||||
isResource = true;
|
||||
} else if (cls === 'items') {
|
||||
if (item?.cls === 'items') {
|
||||
resources = this.evalItemEffectResource(item?.itemEffect);
|
||||
isResource = resources.size > 0;
|
||||
} else if (item?.cls === 'equip') {
|
||||
if (item?.equip?.value) {
|
||||
resources = this.normalizeResource({
|
||||
[ResourceType.Hp]: item.equip.value.hp,
|
||||
[ResourceType.Atk]: item.equip.value.atk,
|
||||
[ResourceType.Def]: item.equip.value.def,
|
||||
[ResourceType.Mdef]: item.equip.value.mdef
|
||||
});
|
||||
isResource = resources.size > 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const isEmpty =
|
||||
!isDoor && !isEnemy && !isResource && block.canPass === true;
|
||||
|
||||
if (isDoor) {
|
||||
this.doorTiles.add(tile);
|
||||
this.noPassMap.set(tile, false);
|
||||
const label = labels.commonDoors[0];
|
||||
this.labelMap.set(tile, label);
|
||||
} else if (isEnemy) {
|
||||
this.enemyTiles.add(tile);
|
||||
this.noPassMap.set(tile, false);
|
||||
this.labelMap.set(tile, labels.enemies[0]);
|
||||
} else if (isResource) {
|
||||
this.resourceTiles.add(tile);
|
||||
this.noPassMap.set(tile, false);
|
||||
this.resourceMap.set(tile, resources);
|
||||
|
||||
let label = labels.items[0] ?? labels.empty;
|
||||
const hp = resources.get(ResourceType.Hp) ?? 0;
|
||||
const atk = resources.get(ResourceType.Atk) ?? 0;
|
||||
const def = resources.get(ResourceType.Def) ?? 0;
|
||||
const mdef = resources.get(ResourceType.Mdef) ?? 0;
|
||||
const key = resources.get(ResourceType.Key) ?? 0;
|
||||
const item = resources.get(ResourceType.Item) ?? 0;
|
||||
const max = Math.max(hp, atk, def, mdef, key, item);
|
||||
if (max > 0) {
|
||||
if (max === hp) {
|
||||
label = labels.potions[0] ?? label;
|
||||
} else if (max === atk) {
|
||||
label = labels.redGems[0] ?? label;
|
||||
} else if (max === def) {
|
||||
label = labels.blueGems[0] ?? label;
|
||||
} else if (max === mdef) {
|
||||
label = labels.greenGems[0] ?? label;
|
||||
} else if (max === key) {
|
||||
label = labels.keys[0] ?? label;
|
||||
} else if (max === item) {
|
||||
label = labels.items[0] ?? label;
|
||||
}
|
||||
}
|
||||
this.labelMap.set(tile, label);
|
||||
} else if (isEmpty) {
|
||||
this.noPassMap.set(tile, false);
|
||||
this.labelMap.set(tile, labels.empty);
|
||||
} else if (block.canPass) {
|
||||
this.noPassMap.set(tile, false);
|
||||
this.labelMap.set(tile, labels.empty);
|
||||
} else {
|
||||
this.noPassMap.set(tile, true);
|
||||
this.labelMap.set(tile, labels.empty);
|
||||
}
|
||||
|
||||
if (!this.resourceMap.has(tile)) {
|
||||
this.resourceMap.set(tile, new Map());
|
||||
}
|
||||
}
|
||||
|
||||
getLabeledTile(tile: number): number {
|
||||
this.precomputeTile(tile);
|
||||
return this.labelMap.get(tile) ?? this.config.classes.wall;
|
||||
}
|
||||
|
||||
isEmpty(tile: number): boolean {
|
||||
this.precomputeTile(tile);
|
||||
return this.labelMap.get(tile) === this.config.classes.empty;
|
||||
}
|
||||
|
||||
isEntry(tile: number, x: number, y: number, floorId: string): boolean {
|
||||
const loc = `${x},${y}`;
|
||||
if (this.tower.main.floors[floorId].changeFloor[loc]) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
isDoor(tile: number): boolean {
|
||||
this.precomputeTile(tile);
|
||||
return this.doorTiles.has(tile);
|
||||
}
|
||||
|
||||
isEnemy(tile: number): boolean {
|
||||
this.precomputeTile(tile);
|
||||
return this.enemyTiles.has(tile);
|
||||
}
|
||||
|
||||
isResource(tile: number): boolean {
|
||||
this.precomputeTile(tile);
|
||||
return (
|
||||
this.resourceTiles.has(tile) ||
|
||||
this.keyTiles.has(tile) ||
|
||||
this.itemTiles.has(tile)
|
||||
);
|
||||
}
|
||||
|
||||
getNoPass(tile: number, x: number, y: number): boolean {
|
||||
void x;
|
||||
void y;
|
||||
this.precomputeTile(tile);
|
||||
return this.noPassMap.get(tile) ?? true;
|
||||
}
|
||||
|
||||
getCannotIn(tile: number, x: number, y: number): number {
|
||||
void x;
|
||||
void y;
|
||||
this.precomputeTile(tile);
|
||||
return this.cannotInMap.get(tile) ?? 0;
|
||||
}
|
||||
|
||||
getCannotOut(tile: number, x: number, y: number): number {
|
||||
void x;
|
||||
void y;
|
||||
this.precomputeTile(tile);
|
||||
return this.cannotOutMap.get(tile) ?? 0;
|
||||
}
|
||||
|
||||
getResource(tile: number, x: number, y: number): Map<ResourceType, number> {
|
||||
void x;
|
||||
void y;
|
||||
this.precomputeTile(tile);
|
||||
return new Map(this.resourceMap.get(tile) ?? []);
|
||||
}
|
||||
}
|
||||
@ -8,7 +8,6 @@ export function generateHeatmap(
|
||||
tokens: Set<number>,
|
||||
kernel: number = 5
|
||||
): number[][] {
|
||||
if (kernel === 0) return map.map(v => v.slice());
|
||||
if (kernel % 2 !== 1) {
|
||||
throw new Error(`Kernal size must be odd.`);
|
||||
}
|
||||
|
||||
@ -1,8 +1,13 @@
|
||||
import { readFile } from 'fs/promises';
|
||||
import { IAutoLabelConfig, IFloorInfo, ITowerInfo, TowerColor } from './types';
|
||||
import { buildTopologicalGraph } from '../topology/graph';
|
||||
import {
|
||||
commonDoorTiles,
|
||||
GraphNodeType,
|
||||
IAutoLabelConfig,
|
||||
IFloorInfo,
|
||||
IMapTileConverter,
|
||||
ITowerInfo,
|
||||
TowerColor
|
||||
} from './types';
|
||||
import {
|
||||
doorTiles,
|
||||
enemyTiles,
|
||||
entryTiles,
|
||||
@ -15,8 +20,8 @@ import {
|
||||
specialDoorTiles,
|
||||
wallTiles
|
||||
} from '../shared';
|
||||
import { NodeType } from '../topology/interface';
|
||||
import { gaussainHeatmap, generateHeatmap } from './heatmap';
|
||||
import { MapTopology } from './topo';
|
||||
|
||||
interface IRawTowerInfo {
|
||||
/** 作者 id */
|
||||
@ -155,62 +160,71 @@ export function computeWallDensityStd(
|
||||
*/
|
||||
export function parseFloorInfo(
|
||||
tower: ITowerInfo,
|
||||
originMap: number[][],
|
||||
map: number[][],
|
||||
config: IAutoLabelConfig
|
||||
otherLayers: number[][][],
|
||||
config: IAutoLabelConfig,
|
||||
converter: IMapTileConverter,
|
||||
floorId: string
|
||||
): IFloorInfo {
|
||||
const topo = buildTopologicalGraph(map);
|
||||
const topo = new MapTopology(
|
||||
floorId,
|
||||
originMap,
|
||||
map,
|
||||
otherLayers,
|
||||
converter,
|
||||
config.classes
|
||||
);
|
||||
const flattened = map.flat();
|
||||
const area = flattened.length;
|
||||
|
||||
let hasUselessBranch = false;
|
||||
|
||||
// 统计咸鱼门数量
|
||||
let fishCount = 0;
|
||||
topo.graphs.forEach(graph => {
|
||||
// 其实就是判断纯血瓶钥匙的资源节点的邻居是不是全都是门,是的话就判定为咸鱼门
|
||||
// 这么做虽然会有一定的误差,但是也大差不差了
|
||||
// 两个门对一个也判定为一个咸鱼门
|
||||
graph.areaMap.forEach(v => {
|
||||
const res = [...v.resources.entries()];
|
||||
const onlyPotion = res.every(([tile, value]) => {
|
||||
if (!potionTiles.has(tile) && !keyTiles.has(tile)) {
|
||||
return value <= 0;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
if (!onlyPotion) {
|
||||
// 包含血瓶钥匙之外的不考虑
|
||||
return;
|
||||
}
|
||||
|
||||
let branchCount = 0;
|
||||
let noneBranchCount = 0;
|
||||
|
||||
v.neighbor.forEach(value => {
|
||||
const node = graph.graph.get(value);
|
||||
if (!node) {
|
||||
noneBranchCount++;
|
||||
return;
|
||||
}
|
||||
|
||||
if (node.type === NodeType.Branch) {
|
||||
if (!commonDoorTiles.has(node.tile)) {
|
||||
branchCount++;
|
||||
// 统计拓扑图信息
|
||||
let maxEmptyArea = 0;
|
||||
let maxResourceArea = 0;
|
||||
topo.graph.areas.forEach(area => {
|
||||
area.nodes.forEach(v => {
|
||||
if (v.type === GraphNodeType.Empty) {
|
||||
let branchConnection = 0;
|
||||
v.neighbors.forEach(v => {
|
||||
// 对节点的每个邻居遍历,如果邻居是分支节点,且直接相连的分支节点数小于 2,
|
||||
// 说明这个连接可能会导致无用节点
|
||||
// 至于为什么要多一次额外的邻居节点判断:
|
||||
// |---|---|---|---|---|
|
||||
// | W | W | D | W | W |
|
||||
// |---|---|---|---|---|
|
||||
// | W | | E | | W |
|
||||
// |---|---|---|---|---|
|
||||
// | W | W | D | W | W |
|
||||
// |---|---|---|---|---|
|
||||
if (v.type === GraphNodeType.Branch) {
|
||||
let directBranch = 0;
|
||||
for (const n of v.neighbors) {
|
||||
if (n.type === GraphNodeType.Branch) {
|
||||
directBranch++;
|
||||
}
|
||||
}
|
||||
if (directBranch < 2) {
|
||||
branchConnection++;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
noneBranchCount++;
|
||||
}
|
||||
});
|
||||
if (noneBranchCount >= 0 && branchCount === 0) {
|
||||
fishCount++;
|
||||
}
|
||||
});
|
||||
|
||||
graph.graph.forEach(v => {
|
||||
if (v.type === NodeType.Branch) {
|
||||
if (v.neighbor.size === 1) {
|
||||
});
|
||||
// 如果连接的分支数与邻居数相同,且小于等于 0,说明是门或怪物后面连接了一整片空地,是无用分支
|
||||
// 如果连接的分支数与邻居数不相同,说明可能连接了资源节点、入口节点等,这些显然不应该算入无用分支
|
||||
if (
|
||||
branchConnection <= 1 &&
|
||||
v.neighbors.size === branchConnection
|
||||
) {
|
||||
hasUselessBranch = true;
|
||||
}
|
||||
if (v.tiles.size > maxEmptyArea) {
|
||||
maxEmptyArea = v.tiles.size;
|
||||
}
|
||||
} else if (v.type === GraphNodeType.Resource) {
|
||||
if (v.tiles.size > maxResourceArea) {
|
||||
maxResourceArea = v.tiles.size;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@ -219,6 +233,9 @@ export function parseFloorInfo(
|
||||
tower,
|
||||
topo,
|
||||
map,
|
||||
maxEmptyArea,
|
||||
maxResourceArea,
|
||||
hasUselessBranch,
|
||||
globalDensity: count(flattened, nonEmptyTiles) / area,
|
||||
wallDensity: count(flattened, wallTiles) / area,
|
||||
doorDensity: count(flattened, doorTiles) / area,
|
||||
@ -230,8 +247,6 @@ export function parseFloorInfo(
|
||||
itemDensity: count(flattened, itemTiles) / area,
|
||||
entryCount: count(flattened, entryTiles),
|
||||
specialDoorCount: count(flattened, specialDoorTiles),
|
||||
fishCount,
|
||||
hasUselessBranch,
|
||||
wallDensityStd: computeWallDensityStd(map, wallTiles, 5),
|
||||
wallHeatmap: gaussainHeatmap(
|
||||
generateHeatmap(map, wallTiles, config.heatmapKernel),
|
||||
|
||||
397
data/src/auto/topo.ts
Normal file
397
data/src/auto/topo.ts
Normal file
@ -0,0 +1,397 @@
|
||||
import {
|
||||
CannotInOut,
|
||||
GraphNodeType,
|
||||
BranchType,
|
||||
ResourceType,
|
||||
type IMapTopology,
|
||||
type IMapGraph,
|
||||
type IMapGraphArea,
|
||||
type MapGraphNode,
|
||||
type IEntryMapGraphNode,
|
||||
type IMapTileConverter,
|
||||
IMapBlockConfig
|
||||
} from './types';
|
||||
|
||||
/** [dx, dy, 离开方向标记, 进入方向标记] */
|
||||
const dirs: [number, number, CannotInOut, CannotInOut][] = [
|
||||
[-1, 0, CannotInOut.Left, CannotInOut.Right],
|
||||
[1, 0, CannotInOut.Right, CannotInOut.Left],
|
||||
[0, -1, CannotInOut.Top, CannotInOut.Bottom],
|
||||
[0, 1, CannotInOut.Bottom, CannotInOut.Top]
|
||||
];
|
||||
|
||||
const ALL_BLOCKED =
|
||||
CannotInOut.Left | CannotInOut.Top | CannotInOut.Right | CannotInOut.Bottom;
|
||||
|
||||
export class MapTopology implements IMapTopology {
|
||||
readonly originMap: number[][];
|
||||
readonly otherLayersMap: number[][][];
|
||||
readonly convertedMap: number[][];
|
||||
readonly noPass: boolean[][];
|
||||
readonly cannotIn: number[][];
|
||||
readonly cannotOut: number[][];
|
||||
readonly graph: IMapGraph;
|
||||
|
||||
constructor(
|
||||
readonly floorId: string,
|
||||
map: number[][],
|
||||
convertedMap: number[][],
|
||||
otherLayers: number[][][],
|
||||
converter: IMapTileConverter,
|
||||
readonly config: IMapBlockConfig
|
||||
) {
|
||||
this.originMap = map;
|
||||
this.otherLayersMap = otherLayers;
|
||||
this.convertedMap = convertedMap;
|
||||
|
||||
const height = map.length;
|
||||
const width = height > 0 ? map[0].length : 0;
|
||||
|
||||
this.noPass = map.map((row, y) =>
|
||||
row.map((tile, x) => converter.getNoPass(tile, x, y))
|
||||
);
|
||||
|
||||
this.cannotIn = map.map((row, y) =>
|
||||
row.map((tile, x) => {
|
||||
if (this.noPass[y][x]) return ALL_BLOCKED;
|
||||
let flags = converter.getCannotIn(tile, x, y);
|
||||
for (const layer of otherLayers) {
|
||||
flags |= converter.getCannotIn(layer[y]?.[x] ?? 0, x, y);
|
||||
}
|
||||
return flags;
|
||||
})
|
||||
);
|
||||
|
||||
this.cannotOut = map.map((row, y) =>
|
||||
row.map((tile, x) => {
|
||||
if (this.noPass[y][x]) return ALL_BLOCKED;
|
||||
let flags = converter.getCannotOut(tile, x, y);
|
||||
for (const layer of otherLayers) {
|
||||
flags |= converter.getCannotOut(layer[y]?.[x] ?? 0, x, y);
|
||||
}
|
||||
return flags;
|
||||
})
|
||||
);
|
||||
|
||||
this.graph = this.buildGraph(width, height, converter);
|
||||
}
|
||||
|
||||
private buildGraph(
|
||||
width: number,
|
||||
height: number,
|
||||
converter: IMapTileConverter
|
||||
): IMapGraph {
|
||||
const size = width * height;
|
||||
|
||||
// 1. 使用 converter 对每个图块进行分类
|
||||
const tileType = new Array<GraphNodeType | null>(size).fill(null);
|
||||
for (let y = 0; y < height; y++) {
|
||||
for (let x = 0; x < width; x++) {
|
||||
const tile = this.convertedMap[y][x];
|
||||
const origin = this.originMap[y][x];
|
||||
const idx = y * width + x;
|
||||
if (tile === this.config.wall) {
|
||||
tileType[idx] = GraphNodeType.Wall;
|
||||
} else if (
|
||||
tile === this.config.entry ||
|
||||
converter.isEntry(origin, x, y, this.floorId)
|
||||
) {
|
||||
tileType[idx] = GraphNodeType.Entry;
|
||||
} else if (
|
||||
this.config.enemies.includes(tile) ||
|
||||
converter.isEnemy(origin)
|
||||
) {
|
||||
tileType[idx] = GraphNodeType.Branch;
|
||||
} else if (
|
||||
this.config.commonDoors.includes(tile) ||
|
||||
this.config.specialDoors.includes(tile) ||
|
||||
converter.isDoor(origin)
|
||||
) {
|
||||
tileType[idx] = GraphNodeType.Branch;
|
||||
} else if (
|
||||
this.config.potions.includes(tile) ||
|
||||
this.config.redGems.includes(tile) ||
|
||||
this.config.blueGems.includes(tile) ||
|
||||
this.config.greenGems.includes(tile) ||
|
||||
this.config.items.includes(tile) ||
|
||||
this.config.keys.includes(tile) ||
|
||||
converter.isResource(origin)
|
||||
) {
|
||||
tileType[idx] = GraphNodeType.Resource;
|
||||
} else if (
|
||||
tile === this.config.empty ||
|
||||
converter.isEmpty(origin)
|
||||
) {
|
||||
tileType[idx] = GraphNodeType.Empty;
|
||||
} else {
|
||||
tileType[idx] = GraphNodeType.Wall;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 通过 BFS 将图块分组为节点
|
||||
// 空白和资源节点:相邻同类型的图块合并为一个节点
|
||||
// 分支和入口节点:每个图块独立为一个节点
|
||||
const nodeMap = new Map<number, MapGraphNode>();
|
||||
const visited = new Set<number>();
|
||||
let nodeIndex = 0;
|
||||
|
||||
for (let y = 0; y < height; y++) {
|
||||
for (let x = 0; x < width; x++) {
|
||||
const idx = y * width + x;
|
||||
if (visited.has(idx)) continue;
|
||||
visited.add(idx);
|
||||
|
||||
const type = tileType[idx]!;
|
||||
if (type === GraphNodeType.Wall) continue;
|
||||
const tiles = new Set<number>([idx]);
|
||||
const neighbors = new Set<MapGraphNode>();
|
||||
|
||||
// 分支和入口节点不合并,每个图块独立为一个节点
|
||||
if (type === GraphNodeType.Entry) {
|
||||
nodeMap.set(idx, {
|
||||
type: GraphNodeType.Entry,
|
||||
index: nodeIndex++,
|
||||
tiles,
|
||||
neighbors
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (type === GraphNodeType.Branch) {
|
||||
const tile = this.originMap[y][x];
|
||||
nodeMap.set(idx, {
|
||||
type: GraphNodeType.Branch,
|
||||
index: nodeIndex++,
|
||||
tiles,
|
||||
neighbors,
|
||||
branch: converter.isDoor(tile)
|
||||
? BranchType.Door
|
||||
: BranchType.Enemy
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
// 空白和资源节点:BFS 合并相邻同类型图块
|
||||
const queue: number[] = [idx];
|
||||
while (queue.length > 0) {
|
||||
const ci = queue.shift()!;
|
||||
const cx = ci % width;
|
||||
const cy = (ci - cx) / width;
|
||||
|
||||
for (const [dx, dy, outFlag, inFlag] of dirs) {
|
||||
const nx = cx + dx;
|
||||
const ny = cy + dy;
|
||||
if (nx < 0 || nx >= width || ny < 0 || ny >= height)
|
||||
continue;
|
||||
const ni = ny * width + nx;
|
||||
if (visited.has(ni) || tileType[ni] !== type) continue;
|
||||
|
||||
// 任一方向可通行即可合并
|
||||
const canGo =
|
||||
!(this.cannotOut[cy][cx] & outFlag) &&
|
||||
!(this.cannotIn[ny][nx] & inFlag);
|
||||
const canCome =
|
||||
!(this.cannotOut[ny][nx] & inFlag) &&
|
||||
!(this.cannotIn[cy][cx] & outFlag);
|
||||
if (false) continue;
|
||||
// if (!canGo && !canCome) continue;
|
||||
|
||||
visited.add(ni);
|
||||
tiles.add(ni);
|
||||
queue.push(ni);
|
||||
}
|
||||
}
|
||||
|
||||
let node: MapGraphNode;
|
||||
if (type === GraphNodeType.Empty) {
|
||||
node = {
|
||||
type: GraphNodeType.Empty,
|
||||
index: nodeIndex++,
|
||||
tiles,
|
||||
neighbors
|
||||
};
|
||||
} else {
|
||||
const resources = new Map<ResourceType, number>();
|
||||
for (const t of tiles) {
|
||||
const tx = t % width;
|
||||
const ty = (t - tx) / width;
|
||||
const res = converter.getResource(
|
||||
this.originMap[ty][tx],
|
||||
tx,
|
||||
ty
|
||||
);
|
||||
for (const [k, v] of res) {
|
||||
resources.set(k, (resources.get(k) ?? 0) + v);
|
||||
}
|
||||
}
|
||||
node = {
|
||||
type: GraphNodeType.Resource,
|
||||
index: nodeIndex++,
|
||||
tiles,
|
||||
neighbors,
|
||||
resources
|
||||
};
|
||||
}
|
||||
|
||||
for (const t of tiles) {
|
||||
nodeMap.set(t, node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 构建节点间的邻接关系
|
||||
for (let y = 0; y < height; y++) {
|
||||
for (let x = 0; x < width; x++) {
|
||||
const node = nodeMap.get(y * width + x);
|
||||
if (!node) continue;
|
||||
|
||||
for (const [dx, dy, outFlag, inFlag] of dirs) {
|
||||
const nx = x + dx;
|
||||
const ny = y + dy;
|
||||
if (nx < 0 || nx >= width || ny < 0 || ny >= height)
|
||||
continue;
|
||||
|
||||
const neighbor = nodeMap.get(ny * width + nx);
|
||||
if (!neighbor || neighbor === node) continue;
|
||||
|
||||
// 至少一个方向可通行则建立邻接关系
|
||||
const canGo =
|
||||
!(this.cannotOut[y][x] & outFlag) &&
|
||||
!(this.cannotIn[ny][nx] & inFlag);
|
||||
const canCome =
|
||||
!(this.cannotOut[ny][nx] & inFlag) &&
|
||||
!(this.cannotIn[y][x] & outFlag);
|
||||
// if (canGo || canCome) {
|
||||
if (true) {
|
||||
node.neighbors.add(neighbor);
|
||||
neighbor.neighbors.add(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 通过 BFS 连通分量构建区域
|
||||
const areas = new Set<IMapGraphArea>();
|
||||
const unreachableArea = new Set<IMapGraphArea>();
|
||||
const entries = new Set<IEntryMapGraphNode>();
|
||||
const visitedNodes = new Set<MapGraphNode>();
|
||||
|
||||
for (const node of new Set(nodeMap.values())) {
|
||||
if (visitedNodes.has(node)) continue;
|
||||
visitedNodes.add(node);
|
||||
|
||||
const areaNodes = new Set<MapGraphNode>();
|
||||
const areaEntries: IEntryMapGraphNode[] = [];
|
||||
const queue: MapGraphNode[] = [node];
|
||||
|
||||
while (queue.length > 0) {
|
||||
const current = queue.shift()!;
|
||||
if (areaNodes.has(current)) continue;
|
||||
areaNodes.add(current);
|
||||
visitedNodes.add(current);
|
||||
|
||||
if (current.type === GraphNodeType.Entry) {
|
||||
areaEntries.push(current);
|
||||
}
|
||||
|
||||
for (const nb of current.neighbors) {
|
||||
if (!visitedNodes.has(nb)) {
|
||||
visitedNodes.add(nb);
|
||||
queue.push(nb);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const area: IMapGraphArea = { nodes: areaNodes };
|
||||
areas.add(area);
|
||||
|
||||
if (areaEntries.length > 0) {
|
||||
for (const e of areaEntries) {
|
||||
entries.add(e);
|
||||
}
|
||||
} else {
|
||||
unreachableArea.add(area);
|
||||
}
|
||||
}
|
||||
|
||||
// console.log(areas.size);
|
||||
|
||||
return { unreachableArea, areas, entries, nodeMap };
|
||||
}
|
||||
|
||||
private resolveIgnored(
|
||||
ignoredNode?: (MapGraphNode | number)[]
|
||||
): Set<MapGraphNode> {
|
||||
const ignored = new Set<MapGraphNode>();
|
||||
if (!ignoredNode) return ignored;
|
||||
for (const item of ignoredNode) {
|
||||
if (typeof item === 'number') {
|
||||
const node = this.graph.nodeMap.get(item);
|
||||
if (node) ignored.add(node);
|
||||
} else {
|
||||
ignored.add(item);
|
||||
}
|
||||
}
|
||||
return ignored;
|
||||
}
|
||||
|
||||
connectedToAnyEntry(
|
||||
pos: number,
|
||||
ignoredNode?: (MapGraphNode | number)[]
|
||||
): boolean {
|
||||
const startNode = this.graph.nodeMap.get(pos);
|
||||
if (!startNode) return false;
|
||||
if (startNode.type === GraphNodeType.Entry) return true;
|
||||
|
||||
const ignored = this.resolveIgnored(ignoredNode);
|
||||
if (ignored.has(startNode)) return false;
|
||||
|
||||
const visited = new Set<MapGraphNode>([startNode]);
|
||||
const queue: MapGraphNode[] = [startNode];
|
||||
|
||||
while (queue.length > 0) {
|
||||
const current = queue.shift()!;
|
||||
for (const nb of current.neighbors) {
|
||||
if (visited.has(nb) || ignored.has(nb)) continue;
|
||||
if (nb.type === GraphNodeType.Entry) return true;
|
||||
visited.add(nb);
|
||||
queue.push(nb);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
connectedToSpecificEntry(
|
||||
pos: number,
|
||||
entry: number | IEntryMapGraphNode,
|
||||
ignoredNode?: (MapGraphNode | number)[]
|
||||
): boolean {
|
||||
const startNode = this.graph.nodeMap.get(pos);
|
||||
if (!startNode) return false;
|
||||
|
||||
const targetNode =
|
||||
typeof entry === 'number' ? this.graph.nodeMap.get(entry) : entry;
|
||||
if (!targetNode) return false;
|
||||
if (startNode === targetNode) return true;
|
||||
|
||||
const ignored = this.resolveIgnored(ignoredNode);
|
||||
if (ignored.has(startNode)) return false;
|
||||
|
||||
const visited = new Set<MapGraphNode>([startNode]);
|
||||
const queue: MapGraphNode[] = [startNode];
|
||||
|
||||
while (queue.length > 0) {
|
||||
const current = queue.shift()!;
|
||||
for (const nb of current.neighbors) {
|
||||
if (visited.has(nb) || ignored.has(nb)) continue;
|
||||
if (nb === targetNode) return true;
|
||||
visited.add(nb);
|
||||
queue.push(nb);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@ -1,3 +1,4 @@
|
||||
import { IMapTileConverter, ResourceType } from './types';
|
||||
import {
|
||||
IAutoLabelConfig,
|
||||
ICodeRunResult,
|
||||
@ -26,6 +27,8 @@ export function runTowerCode(project: string, floors: string): ICodeRunResult {
|
||||
result.item = items_296f5d02_12fd_4166_a7c1_b5e830c9ee3a;
|
||||
`;
|
||||
const main = result.main!;
|
||||
const log = console.log;
|
||||
console.log = () => {};
|
||||
try {
|
||||
eval(projectCode);
|
||||
eval(floors);
|
||||
@ -35,17 +38,15 @@ export function runTowerCode(project: string, floors: string): ICodeRunResult {
|
||||
} catch {
|
||||
result.issue?.push(`代码运行错误`);
|
||||
}
|
||||
console.log = log;
|
||||
return result as ICodeRunResult;
|
||||
}
|
||||
|
||||
function edge(x: number, y: number, width: number, height: number) {
|
||||
return x === 0 || y === 0 || x === width - 1 || y === height - 1;
|
||||
}
|
||||
|
||||
export function convertTowerMap(
|
||||
result: ICodeRunResult,
|
||||
floor: INeededFloorData,
|
||||
config: IAutoLabelConfig
|
||||
config: IAutoLabelConfig,
|
||||
converter: IMapTileConverter
|
||||
): IConvertedMap {
|
||||
const width = floor.map[0].length;
|
||||
const height = floor.map.length;
|
||||
@ -73,48 +74,6 @@ export function convertTowerMap(
|
||||
mdef: 0
|
||||
};
|
||||
|
||||
const thisMap = {
|
||||
ratio: 1
|
||||
};
|
||||
|
||||
// 给后面的 eval 用的
|
||||
const core = {
|
||||
values: new Proxy(result.data.values, {
|
||||
set() {
|
||||
// 防止被修改
|
||||
return true;
|
||||
}
|
||||
}),
|
||||
status: {
|
||||
hero: new Proxy(heroStatus, {
|
||||
set(target, p: string, newValue) {
|
||||
if (typeof newValue !== 'number') return true;
|
||||
if (
|
||||
p !== 'hp' &&
|
||||
p !== 'atk' &&
|
||||
p !== 'def' &&
|
||||
p !== 'mdef'
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
target[p] = newValue;
|
||||
return true;
|
||||
}
|
||||
}),
|
||||
thisMap: new Proxy(thisMap, {
|
||||
set() {
|
||||
// 防止被修改
|
||||
return true;
|
||||
}
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
core.status.hero.hp = 0;
|
||||
core.status.hero.atk = 0;
|
||||
core.status.hero.def = 0;
|
||||
core.status.hero.mdef = 0;
|
||||
|
||||
const tiles = config.classes;
|
||||
|
||||
for (let nx = 0; nx < width; nx++) {
|
||||
@ -188,17 +147,11 @@ export function convertTowerMap(
|
||||
continue;
|
||||
}
|
||||
// 执行道具效果
|
||||
if (item.cls === 'items' && item.itemEffect) {
|
||||
try {
|
||||
eval(item.itemEffect);
|
||||
} catch {
|
||||
// 执行失败就清空一下防止被误识别为宝石血瓶
|
||||
heroStatus.hp = 0;
|
||||
heroStatus.atk = 0;
|
||||
heroStatus.def = 0;
|
||||
heroStatus.mdef = 0;
|
||||
}
|
||||
}
|
||||
const effect = converter.getResource(num, nx, ny);
|
||||
heroStatus.hp = effect.get(ResourceType.Hp) ?? 0;
|
||||
heroStatus.atk = effect.get(ResourceType.Atk) ?? 0;
|
||||
heroStatus.def = effect.get(ResourceType.Def) ?? 0;
|
||||
heroStatus.mdef = effect.get(ResourceType.Mdef) ?? 0;
|
||||
const arr: [number, number, number, number] = [
|
||||
heroStatus.hp,
|
||||
heroStatus.atk,
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { GinkaTopologicalGraphs } from 'src/topology/interface';
|
||||
import { GinkaTopologicalGraphs } from '../topology/interface';
|
||||
|
||||
export const enum TowerColor {
|
||||
White,
|
||||
@ -62,7 +62,11 @@ export interface IFloorInfo {
|
||||
/** 楼层所属的塔信息 */
|
||||
readonly tower: ITowerInfo;
|
||||
/** 楼层拓扑图 */
|
||||
readonly topo: GinkaTopologicalGraphs;
|
||||
readonly topo: IMapTopology;
|
||||
/** 最大空地面积 */
|
||||
readonly maxEmptyArea: number;
|
||||
/** 最大资源区域面积 */
|
||||
readonly maxResourceArea: number;
|
||||
/** 地图矩阵 */
|
||||
readonly map: number[][];
|
||||
/** 地图整体密度,非空白图块/地图面积 */
|
||||
@ -87,9 +91,7 @@ export interface IFloorInfo {
|
||||
readonly entryCount: number;
|
||||
/** 机关门数量 */
|
||||
readonly specialDoorCount: number;
|
||||
/** 咸鱼门数量,多层咸鱼门算一个 */
|
||||
readonly fishCount: number;
|
||||
/** 是否包含只连接了一个节点的分支节点。这种节点相当于门或怪物后面什么都不加,多数是无用的。 */
|
||||
/** 是否包含只连接了一个节点的空白节点。这种节点相当于门或怪物后面什么都不加,多数是无用的。 */
|
||||
readonly hasUselessBranch: boolean;
|
||||
/** 墙壁密度标准差 */
|
||||
readonly wallDensityStd: number;
|
||||
@ -183,6 +185,10 @@ export interface IAutoLabelConfig {
|
||||
|
||||
/** 最大墙壁密度标准差,用于描述一个地图墙壁分布是否均匀的,较大的时候可能是特殊地图,不符合要求 */
|
||||
readonly maxWallDensityStd: number;
|
||||
/** 最大空地区域面积,超过这个的地图会忽略 */
|
||||
readonly maxEmptyArea: number;
|
||||
/** 最大资源区域面积,超过这个的地图会忽略 */
|
||||
readonly maxResourceArea: number;
|
||||
/** 热力图统计算子 */
|
||||
readonly heatmapKernel: number;
|
||||
/** 热力图高斯模糊的标准差 */
|
||||
@ -260,15 +266,195 @@ export interface INeededFloorData {
|
||||
readonly fgmap?: number[][];
|
||||
readonly fg2map?: number[][];
|
||||
readonly changeFloor: Record<string, unknown>;
|
||||
readonly events?: Record<string, any[] | { readonly noPass: boolean }>;
|
||||
readonly cannotMove?: ('left' | 'right' | 'up' | 'down')[];
|
||||
readonly cannotMoveIn?: ('left' | 'right' | 'up' | 'down')[];
|
||||
}
|
||||
|
||||
export interface ICodeRunResult {
|
||||
issue: string[];
|
||||
data: INeededCoreData;
|
||||
enemy: Record<string, INeededEnemyData>;
|
||||
/** Tile 数字到其内容的映射 */
|
||||
map: Record<number, INeededMapData>;
|
||||
item: Record<string, INeededItemData>;
|
||||
main: {
|
||||
floors: Record<string, INeededFloorData>;
|
||||
};
|
||||
}
|
||||
|
||||
export const enum CannotInOut {
|
||||
/** 左侧不可入 / 不可出 */
|
||||
Left = 0b0001,
|
||||
/** 上侧不可入 / 不可出 */
|
||||
Top = 0b0010,
|
||||
/** 右侧不可入 / 不可出 */
|
||||
Right = 0b0100,
|
||||
/** 下侧不可入 / 不可出 */
|
||||
Bottom = 0b1000
|
||||
}
|
||||
|
||||
export const enum GraphNodeType {
|
||||
Empty,
|
||||
/** 资源节点,由资源组成 */
|
||||
Resource,
|
||||
/** 分支节点,由门或怪物组成 */
|
||||
Branch,
|
||||
/** 入口节点 */
|
||||
Entry,
|
||||
/** 墙 */
|
||||
Wall
|
||||
}
|
||||
|
||||
export const enum ResourceType {
|
||||
Hp,
|
||||
Atk,
|
||||
Def,
|
||||
Mdef,
|
||||
Item,
|
||||
Key
|
||||
}
|
||||
|
||||
export const enum BranchType {
|
||||
Door,
|
||||
Enemy
|
||||
}
|
||||
|
||||
export interface IMapGraphNodeBase {
|
||||
/** 节点类型 */
|
||||
readonly type: GraphNodeType;
|
||||
/** 当前节点在拓扑图中的索引 */
|
||||
readonly index: number;
|
||||
/** 此节点包含的所有地图坐标 */
|
||||
readonly tiles: Set<number>;
|
||||
/** 当前节点的邻居节点 */
|
||||
readonly neighbors: Set<MapGraphNode>;
|
||||
}
|
||||
|
||||
export interface IEmptyMapGraphNode extends IMapGraphNodeBase {
|
||||
readonly type: GraphNodeType.Empty;
|
||||
}
|
||||
|
||||
export interface IResourceMapGraphNode extends IMapGraphNodeBase {
|
||||
readonly type: GraphNodeType.Resource;
|
||||
/** 节点包含的资源数量 */
|
||||
readonly resources: Map<ResourceType, number>;
|
||||
}
|
||||
|
||||
export interface IBranchMapGraphNode extends IMapGraphNodeBase {
|
||||
readonly type: GraphNodeType.Branch;
|
||||
/** 分支节点类型 */
|
||||
readonly branch: BranchType;
|
||||
}
|
||||
|
||||
export interface IEntryMapGraphNode extends IMapGraphNodeBase {
|
||||
readonly type: GraphNodeType.Entry;
|
||||
}
|
||||
|
||||
export type MapGraphNode =
|
||||
| IEmptyMapGraphNode
|
||||
| IResourceMapGraphNode
|
||||
| IBranchMapGraphNode
|
||||
| IEntryMapGraphNode;
|
||||
|
||||
export interface IMapGraphArea {
|
||||
/** 当前区域包含的所有节点 */
|
||||
readonly nodes: Set<MapGraphNode>;
|
||||
}
|
||||
|
||||
export interface IMapGraph {
|
||||
/** 不可到达的区域 */
|
||||
readonly unreachableArea: Set<IMapGraphArea>;
|
||||
/** 当前拓扑图包含的区域信息 */
|
||||
readonly areas: Set<IMapGraphArea>;
|
||||
/** 当前拓扑图的所有入口 */
|
||||
readonly entries: Set<IEntryMapGraphNode>;
|
||||
/** 坐标至其所在节点的映射,可以根据坐标获取其对应的节点 */
|
||||
readonly nodeMap: Map<number, MapGraphNode>;
|
||||
}
|
||||
|
||||
export interface IMapTopology {
|
||||
/** 原始地图 */
|
||||
readonly originMap: number[][];
|
||||
/** 事件层除外的层的地图 */
|
||||
readonly otherLayersMap: number[][][];
|
||||
/** 经过转换的地图 */
|
||||
readonly convertedMap: number[][];
|
||||
/** 不可通行标记 */
|
||||
readonly noPass: boolean[][];
|
||||
/** 不可入标记 */
|
||||
readonly cannotIn: number[][];
|
||||
/** 不可出标记 */
|
||||
readonly cannotOut: number[][];
|
||||
/** 地图的拓扑图 */
|
||||
readonly graph: IMapGraph;
|
||||
|
||||
/**
|
||||
* 判断一个点是否连接至任意一个入口节点,数字表示 y * width + x
|
||||
* @param pos 需要判断的点
|
||||
* @param ignoredNode 路径中不允许通过的节点
|
||||
*/
|
||||
connectedToAnyEntry(
|
||||
pos: number,
|
||||
ignoredNode?: (MapGraphNode | number)[]
|
||||
): boolean;
|
||||
|
||||
/**
|
||||
* 判断一个点是否连接至指定的入口节点,数字表示 y * width + x
|
||||
* @param pos 需要判断的点
|
||||
* @param entry 指定入口
|
||||
* @param ignoredNode 路径中不允许通过的节点
|
||||
*/
|
||||
connectedToSpecificEntry(
|
||||
pos: number,
|
||||
entry: number | IEntryMapGraphNode,
|
||||
ignoredNode?: (MapGraphNode | number)[]
|
||||
): boolean;
|
||||
}
|
||||
|
||||
export interface IMapTileConverter {
|
||||
/**
|
||||
* 根据地图原始图块,获取对应的标签图块
|
||||
* @param tile 地图原始图块
|
||||
*/
|
||||
getLabeledTile(tile: number): number;
|
||||
|
||||
isEmpty(tile: number): boolean;
|
||||
|
||||
isEntry(tile: number, x: number, y: number, floorId: string): boolean;
|
||||
|
||||
isDoor(tile: number): boolean;
|
||||
|
||||
isEnemy(tile: number): boolean;
|
||||
|
||||
isResource(tile: number): boolean;
|
||||
|
||||
/**
|
||||
* 获取指定原始图块在指定位置的通行信息
|
||||
* @param tile 地图原始图块
|
||||
* @param x 图块所在位置
|
||||
* @param y 图块所在位置
|
||||
*/
|
||||
getNoPass(tile: number, x: number, y: number): boolean;
|
||||
|
||||
/**
|
||||
* 获取指定原始图块在指定位置的不可入信息
|
||||
* @param tile 地图原始图块
|
||||
* @param x 图块所在位置
|
||||
* @param y 图块所在位置
|
||||
*/
|
||||
getCannotIn(tile: number, x: number, y: number): number;
|
||||
|
||||
/**
|
||||
* 获取指定原始图块在指定位置的不可出信息
|
||||
* @param tile 地图原始图块
|
||||
* @param x 图块所在位置
|
||||
* @param y 图块所在位置
|
||||
*/
|
||||
getCannotOut(tile: number, x: number, y: number): number;
|
||||
|
||||
/**
|
||||
* 获取指定图块所包含的资源
|
||||
*/
|
||||
getResource(tile: number, x: number, y: number): Map<ResourceType, number>;
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user