@cs2dak/maps 0.2.0 → 1.0.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 +7 -0
- package/README.md +33 -2
- package/callout-grid/de_ancient.json +1 -0
- package/callout-grid/de_anubis.json +1 -0
- package/callout-grid/de_dust2.json +1 -0
- package/callout-grid/de_inferno.json +1 -0
- package/callout-grid/de_mirage.json +1 -0
- package/callout-grid/de_nuke.json +1 -0
- package/callout-grid/de_overpass.json +1 -0
- package/map-nav/de_ancient.nav.json +1 -0
- package/map-nav/de_anubis.nav.json +1 -0
- package/map-nav/de_dust2.nav.json +1 -0
- package/map-nav/de_inferno.nav.json +1 -0
- package/map-nav/de_mirage.nav.json +1 -0
- package/map-nav/de_nuke.nav.json +1 -0
- package/map-nav/de_overpass.nav.json +1 -0
- package/map-routes/de_ancient.json +231 -0
- package/map-routes/de_anubis.json +204 -0
- package/map-routes/de_dust2.json +153 -0
- package/map-routes/de_inferno.json +207 -0
- package/map-routes/de_mirage.json +156 -0
- package/map-routes/de_nuke.json +63 -0
- package/map-routes/de_overpass.json +87 -0
- package/package.json +9 -3
- package/src/callout-grid-browser.test.ts +36 -0
- package/src/callout-grid-browser.ts +31 -0
- package/src/callout-grid-node.test.ts +27 -0
- package/src/callout-grid-node.ts +28 -0
- package/src/callout-grid.test.ts +73 -0
- package/src/callout-grid.ts +168 -0
- package/src/callout-names.ts +238 -0
- package/src/default-positions.test.ts +109 -0
- package/src/default-positions.ts +266 -0
- package/src/geometry-assets.test.ts +22 -0
- package/src/geometry-assets.ts +57 -0
- package/src/index.ts +41 -0
- package/src/lineups.test.ts +95 -0
- package/src/lineups.ts +336 -0
- package/src/nav.test.ts +68 -0
- package/src/nav.ts +228 -0
- package/src/position-candidates.test.ts +62 -0
- package/src/position-candidates.ts +183 -0
- package/src/radar-grid.ts +66 -0
- package/src/route-assets.test.ts +80 -0
- package/src/route-assets.ts +23 -0
- package/src/routes.test.ts +46 -0
- package/src/routes.ts +93 -0
- package/src/site-entry-chokes.ts +234 -0
- package/src/site-entry-lexicon.ts +131 -0
- package/src/tri-assets.ts +72 -0
- package/src/visibility.test.ts +36 -0
- package/src/visibility.ts +237 -0
- package/src/zone-assets.ts +23 -0
- package/src/zones.ts +7 -7
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import type { Vec3 } from "./nav.js";
|
|
2
|
+
|
|
3
|
+
export type { Vec3 } from "./nav.js";
|
|
4
|
+
|
|
5
|
+
export interface Triangle {
|
|
6
|
+
a: Vec3;
|
|
7
|
+
b: Vec3;
|
|
8
|
+
c: Vec3;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface Bounds {
|
|
12
|
+
min: Vec3;
|
|
13
|
+
max: Vec3;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface TriangleBvh {
|
|
17
|
+
bounds: Bounds;
|
|
18
|
+
left?: TriangleBvh;
|
|
19
|
+
right?: TriangleBvh;
|
|
20
|
+
triangles?: Triangle[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const TRIANGLE_BYTES = 9 * Float32Array.BYTES_PER_ELEMENT;
|
|
24
|
+
const LEAF_SIZE = 16;
|
|
25
|
+
const EPSILON = 1e-7;
|
|
26
|
+
|
|
27
|
+
export function parseAwpyTri(input: ArrayBuffer | Uint8Array): Triangle[] {
|
|
28
|
+
const bytes = input instanceof Uint8Array ? input : new Uint8Array(input);
|
|
29
|
+
if (bytes.byteLength % TRIANGLE_BYTES !== 0) {
|
|
30
|
+
throw new Error(`Invalid awpy triangle stream: ${bytes.byteLength} bytes`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
34
|
+
const triangles: Triangle[] = [];
|
|
35
|
+
for (let offset = 0; offset < bytes.byteLength; offset += TRIANGLE_BYTES) {
|
|
36
|
+
const point = (base: number): Vec3 => ({
|
|
37
|
+
x: view.getFloat32(offset + base, true),
|
|
38
|
+
y: view.getFloat32(offset + base + 4, true),
|
|
39
|
+
z: view.getFloat32(offset + base + 8, true),
|
|
40
|
+
});
|
|
41
|
+
triangles.push({ a: point(0), b: point(12), c: point(24) });
|
|
42
|
+
}
|
|
43
|
+
return triangles;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** 三角形 + 预计算质心;建树时避免在 sort 比较器里反复算质心并分配 Vec3。 */
|
|
47
|
+
interface CentroidTriangle {
|
|
48
|
+
tri: Triangle;
|
|
49
|
+
c: Vec3;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function buildTriangleBvh(triangles: Triangle[]): TriangleBvh {
|
|
53
|
+
if (triangles.length === 0) throw new Error("Cannot build a BVH without triangles");
|
|
54
|
+
// 质心只算一次(旧实现在每层 sort 比较器里重算并 new Vec3,百万三角形下产生
|
|
55
|
+
// 数千万次冗余计算与分配)。建树拓扑、分裂点与叶子内容完全不变。
|
|
56
|
+
const items = triangles.map((tri) => ({ tri, c: triangleCentroid(tri) }));
|
|
57
|
+
return buildNode(items);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function staticLineOfSight(root: TriangleBvh, start: Vec3, end: Vec3): boolean {
|
|
61
|
+
return !segmentHitsNode(root, start, end);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function buildNode(items: CentroidTriangle[]): TriangleBvh {
|
|
65
|
+
const bounds = boundsForCentroidTriangles(items);
|
|
66
|
+
if (items.length <= LEAF_SIZE) return { bounds, triangles: items.map((item) => item.tri) };
|
|
67
|
+
|
|
68
|
+
const axis = largestAxis(bounds);
|
|
69
|
+
// 只需把中位数选到 middle 处(左半 ≤、右半 ≥),无需整层全排序:
|
|
70
|
+
// quickselect 让建树从 O(n log²n) 降到 O(n log n)。LOS 与树结构无关
|
|
71
|
+
// (每个节点按实际三角形重算 bounds,叶子全量求交),任何合法划分结果都等价。
|
|
72
|
+
const middle = items.length >> 1;
|
|
73
|
+
nthElement(items, middle, axis);
|
|
74
|
+
return {
|
|
75
|
+
bounds,
|
|
76
|
+
left: buildNode(items.slice(0, middle)),
|
|
77
|
+
right: buildNode(items.slice(middle)),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Hoare 划分的 quickselect:原地重排,使 arr[k] 落在按 axis 排序后的位置。 */
|
|
82
|
+
function nthElement(arr: CentroidTriangle[], k: number, axis: keyof Vec3): void {
|
|
83
|
+
let lo = 0;
|
|
84
|
+
let hi = arr.length - 1;
|
|
85
|
+
while (lo < hi) {
|
|
86
|
+
const pivot = arr[(lo + hi) >> 1].c[axis];
|
|
87
|
+
let i = lo;
|
|
88
|
+
let j = hi;
|
|
89
|
+
while (i <= j) {
|
|
90
|
+
while (arr[i].c[axis] < pivot) i++;
|
|
91
|
+
while (arr[j].c[axis] > pivot) j--;
|
|
92
|
+
if (i <= j) {
|
|
93
|
+
const tmp = arr[i];
|
|
94
|
+
arr[i] = arr[j];
|
|
95
|
+
arr[j] = tmp;
|
|
96
|
+
i++;
|
|
97
|
+
j--;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (k <= j) hi = j;
|
|
101
|
+
else if (k >= i) lo = i;
|
|
102
|
+
else break;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function boundsForCentroidTriangles(items: CentroidTriangle[]): Bounds {
|
|
107
|
+
const bounds: Bounds = {
|
|
108
|
+
min: { x: Number.POSITIVE_INFINITY, y: Number.POSITIVE_INFINITY, z: Number.POSITIVE_INFINITY },
|
|
109
|
+
max: { x: Number.NEGATIVE_INFINITY, y: Number.NEGATIVE_INFINITY, z: Number.NEGATIVE_INFINITY },
|
|
110
|
+
};
|
|
111
|
+
for (const { tri } of items) {
|
|
112
|
+
for (const point of [tri.a, tri.b, tri.c]) {
|
|
113
|
+
bounds.min.x = Math.min(bounds.min.x, point.x);
|
|
114
|
+
bounds.min.y = Math.min(bounds.min.y, point.y);
|
|
115
|
+
bounds.min.z = Math.min(bounds.min.z, point.z);
|
|
116
|
+
bounds.max.x = Math.max(bounds.max.x, point.x);
|
|
117
|
+
bounds.max.y = Math.max(bounds.max.y, point.y);
|
|
118
|
+
bounds.max.z = Math.max(bounds.max.z, point.z);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return bounds;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function segmentHitsNode(node: TriangleBvh, start: Vec3, end: Vec3): boolean {
|
|
125
|
+
if (!segmentHitsBounds(start, end, node.bounds)) return false;
|
|
126
|
+
if (node.triangles) {
|
|
127
|
+
for (const triangle of node.triangles) {
|
|
128
|
+
if (segmentHitsTriangle(start, end, triangle)) return true;
|
|
129
|
+
}
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
return (
|
|
133
|
+
(node.left !== undefined && segmentHitsNode(node.left, start, end)) ||
|
|
134
|
+
(node.right !== undefined && segmentHitsNode(node.right, start, end))
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function segmentHitsBounds(start: Vec3, end: Vec3, bounds: Bounds): boolean {
|
|
139
|
+
let minT = 0;
|
|
140
|
+
let maxT = 1;
|
|
141
|
+
const sx = start.x, sy = start.y, sz = start.z;
|
|
142
|
+
const dx = end.x - sx, dy = end.y - sy, dz = end.z - sz;
|
|
143
|
+
if (Math.abs(dx) < EPSILON) {
|
|
144
|
+
if (sx < bounds.min.x || sx > bounds.max.x) return false;
|
|
145
|
+
} else {
|
|
146
|
+
const inv = 1 / dx;
|
|
147
|
+
let first = (bounds.min.x - sx) * inv;
|
|
148
|
+
let second = (bounds.max.x - sx) * inv;
|
|
149
|
+
if (first > second) {
|
|
150
|
+
const tmp = first;
|
|
151
|
+
first = second;
|
|
152
|
+
second = tmp;
|
|
153
|
+
}
|
|
154
|
+
minT = Math.max(minT, first);
|
|
155
|
+
maxT = Math.min(maxT, second);
|
|
156
|
+
if (minT > maxT) return false;
|
|
157
|
+
}
|
|
158
|
+
if (Math.abs(dy) < EPSILON) {
|
|
159
|
+
if (sy < bounds.min.y || sy > bounds.max.y) return false;
|
|
160
|
+
} else {
|
|
161
|
+
const inv = 1 / dy;
|
|
162
|
+
let first = (bounds.min.y - sy) * inv;
|
|
163
|
+
let second = (bounds.max.y - sy) * inv;
|
|
164
|
+
if (first > second) {
|
|
165
|
+
const tmp = first;
|
|
166
|
+
first = second;
|
|
167
|
+
second = tmp;
|
|
168
|
+
}
|
|
169
|
+
minT = Math.max(minT, first);
|
|
170
|
+
maxT = Math.min(maxT, second);
|
|
171
|
+
if (minT > maxT) return false;
|
|
172
|
+
}
|
|
173
|
+
if (Math.abs(dz) < EPSILON) {
|
|
174
|
+
if (sz < bounds.min.z || sz > bounds.max.z) return false;
|
|
175
|
+
} else {
|
|
176
|
+
const inv = 1 / dz;
|
|
177
|
+
let first = (bounds.min.z - sz) * inv;
|
|
178
|
+
let second = (bounds.max.z - sz) * inv;
|
|
179
|
+
if (first > second) {
|
|
180
|
+
const tmp = first;
|
|
181
|
+
first = second;
|
|
182
|
+
second = tmp;
|
|
183
|
+
}
|
|
184
|
+
minT = Math.max(minT, first);
|
|
185
|
+
maxT = Math.min(maxT, second);
|
|
186
|
+
if (minT > maxT) return false;
|
|
187
|
+
}
|
|
188
|
+
return true;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function segmentHitsTriangle(start: Vec3, end: Vec3, triangle: Triangle): boolean {
|
|
192
|
+
const dirX = end.x - start.x, dirY = end.y - start.y, dirZ = end.z - start.z;
|
|
193
|
+
const edge1X = triangle.b.x - triangle.a.x;
|
|
194
|
+
const edge1Y = triangle.b.y - triangle.a.y;
|
|
195
|
+
const edge1Z = triangle.b.z - triangle.a.z;
|
|
196
|
+
const edge2X = triangle.c.x - triangle.a.x;
|
|
197
|
+
const edge2Y = triangle.c.y - triangle.a.y;
|
|
198
|
+
const edge2Z = triangle.c.z - triangle.a.z;
|
|
199
|
+
const hX = dirY * edge2Z - dirZ * edge2Y;
|
|
200
|
+
const hY = dirZ * edge2X - dirX * edge2Z;
|
|
201
|
+
const hZ = dirX * edge2Y - dirY * edge2X;
|
|
202
|
+
const determinant = edge1X * hX + edge1Y * hY + edge1Z * hZ;
|
|
203
|
+
if (Math.abs(determinant) < EPSILON) return false;
|
|
204
|
+
|
|
205
|
+
const inverse = 1 / determinant;
|
|
206
|
+
const sX = start.x - triangle.a.x;
|
|
207
|
+
const sY = start.y - triangle.a.y;
|
|
208
|
+
const sZ = start.z - triangle.a.z;
|
|
209
|
+
const u = inverse * (sX * hX + sY * hY + sZ * hZ);
|
|
210
|
+
if (u < 0 || u > 1) return false;
|
|
211
|
+
|
|
212
|
+
const qX = sY * edge1Z - sZ * edge1Y;
|
|
213
|
+
const qY = sZ * edge1X - sX * edge1Z;
|
|
214
|
+
const qZ = sX * edge1Y - sY * edge1X;
|
|
215
|
+
const v = inverse * (dirX * qX + dirY * qY + dirZ * qZ);
|
|
216
|
+
if (v < 0 || u + v > 1) return false;
|
|
217
|
+
|
|
218
|
+
const t = inverse * (edge2X * qX + edge2Y * qY + edge2Z * qZ);
|
|
219
|
+
return t > EPSILON && t < 1 - EPSILON;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function largestAxis(bounds: Bounds): keyof Vec3 {
|
|
223
|
+
const extents = {
|
|
224
|
+
x: bounds.max.x - bounds.min.x,
|
|
225
|
+
y: bounds.max.y - bounds.min.y,
|
|
226
|
+
z: bounds.max.z - bounds.min.z,
|
|
227
|
+
};
|
|
228
|
+
return extents.x >= extents.y && extents.x >= extents.z ? "x" : extents.y >= extents.z ? "y" : "z";
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function triangleCentroid(triangle: Triangle): Vec3 {
|
|
232
|
+
return {
|
|
233
|
+
x: (triangle.a.x + triangle.b.x + triangle.c.x) / 3,
|
|
234
|
+
y: (triangle.a.y + triangle.b.y + triangle.c.y) / 3,
|
|
235
|
+
z: (triangle.a.z + triangle.b.z + triangle.c.z) / 3,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import ancientZones from "../map-zones/de_ancient.json" with { type: "json" };
|
|
2
|
+
import dust2Zones from "../map-zones/de_dust2.json" with { type: "json" };
|
|
3
|
+
import infernoZones from "../map-zones/de_inferno.json" with { type: "json" };
|
|
4
|
+
import mirageZones from "../map-zones/de_mirage.json" with { type: "json" };
|
|
5
|
+
// legacy zone 资产:不是当前控图/战术语义真相源;控图优先用 callout grid。
|
|
6
|
+
// 其余 3 图不再主动补 zone,除非旧 shadow spatial 路径明确需要。
|
|
7
|
+
// import anubisZones from "../map-zones/de_anubis.json" with { type: "json" };
|
|
8
|
+
// import nukeZones from "../map-zones/de_nuke.json" with { type: "json" };
|
|
9
|
+
// import overpassZones from "../map-zones/de_overpass.json" with { type: "json" };
|
|
10
|
+
import type { MapZones } from "./zones.js";
|
|
11
|
+
import type { ActiveDutyMap } from "./zones.js";
|
|
12
|
+
|
|
13
|
+
/** legacy 手标 zone 多边形。未标定的图 getMapZones 返回 null。 */
|
|
14
|
+
export const MAP_ZONE_ASSETS: Partial<Record<ActiveDutyMap, MapZones>> = {
|
|
15
|
+
de_ancient: ancientZones as unknown as MapZones,
|
|
16
|
+
de_dust2: dust2Zones as unknown as MapZones,
|
|
17
|
+
de_inferno: infernoZones as unknown as MapZones,
|
|
18
|
+
de_mirage: mirageZones as unknown as MapZones,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export function getMapZones(mapName: string): MapZones | null {
|
|
22
|
+
return (MAP_ZONE_ASSETS as Record<string, MapZones | undefined>)[mapName] ?? null;
|
|
23
|
+
}
|
package/src/zones.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* map-zones —
|
|
2
|
+
* map-zones — legacy 手标多边形辅助层
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* 当前控图/战术语义真相源不是这里,而是 replay place 生成的 callout grid。
|
|
5
|
+
* 本文件只保留给旧的 shadow spatial/utility 路径做几何辅助,新增控图判定不要用 zone 当语义来源。
|
|
6
6
|
*
|
|
7
|
-
* 坐标系:多边形顶点用**世界坐标 XY**(与
|
|
7
|
+
* 坐标系:多边形顶点用**世界坐标 XY**(与 v3 replay 的 position 同系),
|
|
8
8
|
* 分辨率无关、无需 radar 标定。多层地图(nuke / vertigo)用可选 [zMin, zMax] 区分上下层。
|
|
9
9
|
*
|
|
10
|
-
* 边界:本文件只做「点 →
|
|
11
|
-
*
|
|
10
|
+
* 边界:本文件只做「点 → 手标多边形」的几何归属,不算任何指标,不依赖 demo 数据。
|
|
11
|
+
* 这些多边形不是当前地图理解的真相源。
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
/** 区域角色:决定该区域在分析里的语义分类(不限定地图)。 */
|
|
@@ -78,7 +78,7 @@ export function zoneAt(zones: MapZones, x: number, y: number, z?: number): MapZo
|
|
|
78
78
|
return null;
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
/** 当前 CS2 现役图池(train
|
|
81
|
+
/** 当前 CS2 现役图池(train 已移除)。保留给 legacy zone 资产枚举。 */
|
|
82
82
|
export const ACTIVE_DUTY_MAPS = [
|
|
83
83
|
"de_ancient",
|
|
84
84
|
"de_anubis",
|