@cs2dak/maps 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +7 -0
- package/package.json +15 -0
- package/src/index.ts +52 -0
- package/src/zones.test.ts +51 -0
- package/src/zones.ts +91 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Starfie1d
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# @cs2dak/maps
|
|
2
|
+
|
|
3
|
+
English | 简体中文
|
|
4
|
+
|
|
5
|
+
Map calibration and world-to-radar transforms. Initial constants are aligned with the RivalHub/AWPy-style radar formula and can be expanded per map as fixtures grow.
|
|
6
|
+
|
|
7
|
+
地图标定与世界坐标到 radar 像素坐标转换。第一版只放常用竞技地图的基础常量,后续根据 fixtures 和实际 demo 校准。
|
package/package.json
ADDED
package/src/index.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export * from "./zones.js";
|
|
2
|
+
|
|
3
|
+
export interface MapCalibration {
|
|
4
|
+
mapName: string;
|
|
5
|
+
posX: number;
|
|
6
|
+
posY: number;
|
|
7
|
+
scale: number;
|
|
8
|
+
radarSize: number;
|
|
9
|
+
lowerLevelMaxUnits?: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface RadarPoint {
|
|
13
|
+
x: number;
|
|
14
|
+
y: number;
|
|
15
|
+
outOfBounds: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const MAP_CALIBRATIONS: Record<string, MapCalibration> = {
|
|
19
|
+
de_mirage: { mapName: "de_mirage", posX: -3230, posY: 1713, scale: 5, radarSize: 1024 },
|
|
20
|
+
de_dust2: { mapName: "de_dust2", posX: -2476, posY: 3239, scale: 4.4, radarSize: 1024 },
|
|
21
|
+
de_inferno: { mapName: "de_inferno", posX: -2087, posY: 3870, scale: 4.9, radarSize: 1024 },
|
|
22
|
+
de_anubis: { mapName: "de_anubis", posX: -2796, posY: 3328, scale: 5.22, radarSize: 1024 },
|
|
23
|
+
de_nuke: { mapName: "de_nuke", posX: -3453, posY: 2887, scale: 7, radarSize: 1024, lowerLevelMaxUnits: -495 },
|
|
24
|
+
de_ancient: { mapName: "de_ancient", posX: -2953, posY: 2164, scale: 5, radarSize: 1024 },
|
|
25
|
+
de_vertigo: { mapName: "de_vertigo", posX: -3168, posY: 1762, scale: 4, radarSize: 1024, lowerLevelMaxUnits: 11700 },
|
|
26
|
+
de_train: { mapName: "de_train", posX: -2308, posY: 2078, scale: 4.082077, radarSize: 1024 },
|
|
27
|
+
de_overpass: { mapName: "de_overpass", posX: -4831, posY: 1781, scale: 5.2, radarSize: 1024 },
|
|
28
|
+
de_cache: { mapName: "de_cache", posX: -2000, posY: 3250, scale: 5.5, radarSize: 1024 }
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export function getMapCalibration(mapName: string): MapCalibration | null {
|
|
32
|
+
return MAP_CALIBRATIONS[mapName] ?? null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function worldToRadar(
|
|
36
|
+
point: { x: number; y: number },
|
|
37
|
+
calibration: MapCalibration
|
|
38
|
+
): RadarPoint {
|
|
39
|
+
const x = (point.x - calibration.posX) / calibration.scale;
|
|
40
|
+
const y = (calibration.posY - point.y) / calibration.scale;
|
|
41
|
+
const pad = 96;
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
x,
|
|
45
|
+
y,
|
|
46
|
+
outOfBounds:
|
|
47
|
+
x < -pad ||
|
|
48
|
+
x > calibration.radarSize + pad ||
|
|
49
|
+
y < -pad ||
|
|
50
|
+
y > calibration.radarSize + pad
|
|
51
|
+
};
|
|
52
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { pointInPolygon, zoneAt, type MapZones } from "./zones.js";
|
|
3
|
+
|
|
4
|
+
// 合成地图:两个不相邻方块 + 一个上层方块(测 z 过滤)
|
|
5
|
+
const zones: MapZones = {
|
|
6
|
+
mapName: "de_test",
|
|
7
|
+
version: "test-0",
|
|
8
|
+
zones: [
|
|
9
|
+
{ id: "a_site", name: "A", role: "site", bombsite: "a", polygon: [[0, 0], [100, 0], [100, 100], [0, 100]] },
|
|
10
|
+
{ id: "b_site", name: "B", role: "site", bombsite: "b", polygon: [[200, 0], [300, 0], [300, 100], [200, 100]] },
|
|
11
|
+
{ id: "upper", name: "Upper", role: "other", polygon: [[0, 0], [100, 0], [100, 100], [0, 100]], zMin: 500, zMax: 700 },
|
|
12
|
+
],
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
describe("pointInPolygon", () => {
|
|
16
|
+
it("detects inside / outside", () => {
|
|
17
|
+
const square: Array<[number, number]> = [[0, 0], [10, 0], [10, 10], [0, 10]];
|
|
18
|
+
expect(pointInPolygon(5, 5, square)).toBe(true);
|
|
19
|
+
expect(pointInPolygon(15, 5, square)).toBe(false);
|
|
20
|
+
expect(pointInPolygon(-1, 5, square)).toBe(false);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("auto-closes the polygon (last→first edge)", () => {
|
|
24
|
+
const tri: Array<[number, number]> = [[0, 0], [10, 0], [5, 10]];
|
|
25
|
+
expect(pointInPolygon(5, 3, tri)).toBe(true);
|
|
26
|
+
expect(pointInPolygon(0, 9, tri)).toBe(false);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe("zoneAt", () => {
|
|
31
|
+
it("resolves a point to its zone", () => {
|
|
32
|
+
expect(zoneAt(zones, 50, 50)?.id).toBe("a_site");
|
|
33
|
+
expect(zoneAt(zones, 250, 50)?.id).toBe("b_site");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("returns null when no zone contains the point", () => {
|
|
37
|
+
expect(zoneAt(zones, 150, 50)).toBeNull();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("respects z-range on multi-level overlap (a_site has no z, returned first)", () => {
|
|
41
|
+
// 同一 XY,z=600 仍落 a_site,因为 a_site 无 z 约束且排在前(优先级)
|
|
42
|
+
expect(zoneAt(zones, 50, 50, 600)?.id).toBe("a_site");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("uses z to pick an upper-only zone when lower zone is z-bounded", () => {
|
|
46
|
+
const upperOnly: MapZones = { ...zones, zones: [zones.zones[2], zones.zones[0]] };
|
|
47
|
+
// a_site 无 z 约束,永远命中;把 upper 放前面且在其 z 段内才命中 upper
|
|
48
|
+
expect(zoneAt(upperOnly, 50, 50, 600)?.id).toBe("upper");
|
|
49
|
+
expect(zoneAt(upperOnly, 50, 50, 100)?.id).toBe("a_site"); // 不在 upper z 段 → 落 a_site
|
|
50
|
+
});
|
|
51
|
+
});
|
package/src/zones.ts
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* map-zones — 地图区域语义层(P4 空间分析的基础)
|
|
3
|
+
*
|
|
4
|
+
* 定位:把世界坐标的一个点映射到一个**有名字、有角色**的区域(A 点 / mid / banana …),
|
|
5
|
+
* 供 Area(区域占有)、Utility Block(道具封锁)等空间指标使用。
|
|
6
|
+
*
|
|
7
|
+
* 坐标系:多边形顶点用**世界坐标 XY**(与 positions-1s / replay 的 position 同系),
|
|
8
|
+
* 分辨率无关、无需 radar 标定。多层地图(nuke / vertigo)用可选 [zMin, zMax] 区分上下层。
|
|
9
|
+
*
|
|
10
|
+
* 边界:本文件只做「点 → 区域」的几何归属,不算任何指标,不依赖 demo 数据。
|
|
11
|
+
* 真实多边形坐标由人工在 `map-zones/<map>.json` 标定(这是唯一需要人工的一步)。
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** 区域角色:决定该区域在分析里的语义分类(不限定地图)。 */
|
|
15
|
+
export type ZoneRole =
|
|
16
|
+
| "site" // 包点(A/B)
|
|
17
|
+
| "connector" // 连接通道(mid-to-B、connector…)
|
|
18
|
+
| "mid" // 中路
|
|
19
|
+
| "lane" // 主路(banana / long / apps…)
|
|
20
|
+
| "spawn" // 出生区
|
|
21
|
+
| "approach" // 进攻方接近区
|
|
22
|
+
| "backsite" // 包点后区 / 残余空间
|
|
23
|
+
| "other";
|
|
24
|
+
|
|
25
|
+
export interface MapZone {
|
|
26
|
+
/** 稳定唯一 id(如 "a_site" / "banana" / "mid")。 */
|
|
27
|
+
id: string;
|
|
28
|
+
/** 人类可读名(如 "A 点" / "Banana")。 */
|
|
29
|
+
name: string;
|
|
30
|
+
role: ZoneRole;
|
|
31
|
+
/** 该区域归属的包点(site/connector 常关联),无则 null。 */
|
|
32
|
+
bombsite?: "a" | "b" | null;
|
|
33
|
+
/** 世界坐标 XY 多边形顶点,首尾不必闭合(自动闭合)。至少 3 点。 */
|
|
34
|
+
polygon: Array<[number, number]>;
|
|
35
|
+
/** 多层地图的高度范围(含),单层地图省略。 */
|
|
36
|
+
zMin?: number;
|
|
37
|
+
zMax?: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface MapZones {
|
|
41
|
+
/** 如 "de_mirage"。 */
|
|
42
|
+
mapName: string;
|
|
43
|
+
/** 标定版本,便于演进。 */
|
|
44
|
+
version: string;
|
|
45
|
+
zones: MapZone[];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 射线法判断点是否在多边形内(世界坐标 XY)。多边形自动闭合。
|
|
50
|
+
* 边界视为命中(<=),避免相邻区域漏判。
|
|
51
|
+
*/
|
|
52
|
+
export function pointInPolygon(x: number, y: number, polygon: Array<[number, number]>): boolean {
|
|
53
|
+
let inside = false;
|
|
54
|
+
const n = polygon.length;
|
|
55
|
+
for (let i = 0, j = n - 1; i < n; j = i++) {
|
|
56
|
+
const [xi, yi] = polygon[i];
|
|
57
|
+
const [xj, yj] = polygon[j];
|
|
58
|
+
const intersect = yi > y !== yj > y && x < ((xj - xi) * (y - yi)) / (yj - yi) + xi;
|
|
59
|
+
if (intersect) inside = !inside;
|
|
60
|
+
}
|
|
61
|
+
return inside;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* 把一个世界坐标点归属到一个区域。
|
|
66
|
+
* - 多层地图:先按 [zMin,zMax] 过滤再做多边形判定。
|
|
67
|
+
* - 重叠区域:返回**第一个**命中的(调用方应保证 zones 顺序 = 优先级,窄区域在前)。
|
|
68
|
+
* - 无命中:返回 null。
|
|
69
|
+
*/
|
|
70
|
+
export function zoneAt(zones: MapZones, x: number, y: number, z?: number): MapZone | null {
|
|
71
|
+
for (const zone of zones.zones) {
|
|
72
|
+
if (z !== undefined && (zone.zMin !== undefined || zone.zMax !== undefined)) {
|
|
73
|
+
if (zone.zMin !== undefined && z < zone.zMin) continue;
|
|
74
|
+
if (zone.zMax !== undefined && z > zone.zMax) continue;
|
|
75
|
+
}
|
|
76
|
+
if (pointInPolygon(x, y, zone.polygon)) return zone;
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** 当前 CS2 现役图池(train 已移除)。zone 标定与 P4 分析的优先地图。 */
|
|
82
|
+
export const ACTIVE_DUTY_MAPS = [
|
|
83
|
+
"de_ancient",
|
|
84
|
+
"de_anubis",
|
|
85
|
+
"de_dust2",
|
|
86
|
+
"de_inferno",
|
|
87
|
+
"de_mirage",
|
|
88
|
+
"de_nuke",
|
|
89
|
+
"de_overpass",
|
|
90
|
+
] as const;
|
|
91
|
+
export type ActiveDutyMap = (typeof ACTIVE_DUTY_MAPS)[number];
|