@cs2dak/maps 0.2.1 → 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.
Files changed (54) hide show
  1. package/LICENSE +7 -0
  2. package/README.md +33 -2
  3. package/callout-grid/de_ancient.json +1 -0
  4. package/callout-grid/de_anubis.json +1 -0
  5. package/callout-grid/de_dust2.json +1 -0
  6. package/callout-grid/de_inferno.json +1 -0
  7. package/callout-grid/de_mirage.json +1 -0
  8. package/callout-grid/de_nuke.json +1 -0
  9. package/callout-grid/de_overpass.json +1 -0
  10. package/map-nav/de_ancient.nav.json +1 -0
  11. package/map-nav/de_anubis.nav.json +1 -0
  12. package/map-nav/de_dust2.nav.json +1 -0
  13. package/map-nav/de_inferno.nav.json +1 -0
  14. package/map-nav/de_mirage.nav.json +1 -0
  15. package/map-nav/de_nuke.nav.json +1 -0
  16. package/map-nav/de_overpass.nav.json +1 -0
  17. package/map-routes/de_ancient.json +231 -0
  18. package/map-routes/de_anubis.json +204 -0
  19. package/map-routes/de_dust2.json +153 -0
  20. package/map-routes/de_inferno.json +207 -0
  21. package/map-routes/de_mirage.json +156 -0
  22. package/map-routes/de_nuke.json +63 -0
  23. package/map-routes/de_overpass.json +87 -0
  24. package/package.json +9 -3
  25. package/src/callout-grid-browser.test.ts +36 -0
  26. package/src/callout-grid-browser.ts +31 -0
  27. package/src/callout-grid-node.test.ts +27 -0
  28. package/src/callout-grid-node.ts +28 -0
  29. package/src/callout-grid.test.ts +73 -0
  30. package/src/callout-grid.ts +168 -0
  31. package/src/callout-names.ts +238 -0
  32. package/src/default-positions.test.ts +109 -0
  33. package/src/default-positions.ts +266 -0
  34. package/src/geometry-assets.test.ts +22 -0
  35. package/src/geometry-assets.ts +57 -0
  36. package/src/index.ts +41 -0
  37. package/src/lineups.test.ts +95 -0
  38. package/src/lineups.ts +336 -0
  39. package/src/nav.test.ts +68 -0
  40. package/src/nav.ts +228 -0
  41. package/src/position-candidates.test.ts +62 -0
  42. package/src/position-candidates.ts +183 -0
  43. package/src/radar-grid.ts +66 -0
  44. package/src/route-assets.test.ts +80 -0
  45. package/src/route-assets.ts +23 -0
  46. package/src/routes.test.ts +46 -0
  47. package/src/routes.ts +93 -0
  48. package/src/site-entry-chokes.ts +234 -0
  49. package/src/site-entry-lexicon.ts +131 -0
  50. package/src/tri-assets.ts +72 -0
  51. package/src/visibility.test.ts +36 -0
  52. package/src/visibility.ts +237 -0
  53. package/src/zone-assets.ts +23 -0
  54. package/src/zones.ts +7 -7
@@ -0,0 +1,73 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { calloutAt, calloutNear, type CalloutGrid } from "./callout-grid.js";
3
+
4
+ function grid(cells: CalloutGrid["cells"], vocabulary = ["A", "B", "Mid"]): CalloutGrid {
5
+ return {
6
+ mapName: "de_test",
7
+ gridSize: 10,
8
+ origin: [-100, -100, -100],
9
+ maxCoord: [100, 100, 100],
10
+ dims: [20, 20, 20],
11
+ vocabulary,
12
+ confidence: 0.51,
13
+ minSamples: 3,
14
+ cells,
15
+ };
16
+ }
17
+
18
+ describe("callout-grid", () => {
19
+ it("floors negative world coordinates to the same grid convention as generation", () => {
20
+ const g = grid({ "-10,20,0": [0, 0.9, 12] });
21
+ expect(calloutAt(g, { x: -1, y: 29.9, z: 0 })?.callout).toBe("A");
22
+ });
23
+
24
+ it("returns null for empty cells", () => {
25
+ expect(calloutAt(grid({}), { x: 0, y: 0, z: 0 })).toBeNull();
26
+ });
27
+
28
+ it("guards vocabulary index overflow", () => {
29
+ expect(calloutAt(grid({ "0,0,0": [9, 0.9, 12] }), { x: 0, y: 0, z: 0 })).toBeNull();
30
+ });
31
+
32
+ it("returns exact hits before nearby candidates", () => {
33
+ const g = grid({
34
+ "0,0,0": [0, 0.6, 5],
35
+ "10,0,0": [1, 0.99, 50],
36
+ });
37
+ expect(calloutNear(g, { x: 1, y: 1, z: 1 })).toMatchObject({
38
+ callout: "A",
39
+ source: "exact",
40
+ distance: 0,
41
+ offset: [0, 0, 0],
42
+ });
43
+ });
44
+
45
+ it("finds nearby cells for sparse effect positions", () => {
46
+ const g = grid({
47
+ "10,0,0": [1, 0.7, 20],
48
+ "0,20,0": [2, 0.95, 100],
49
+ });
50
+ expect(calloutNear(g, { x: 1, y: 1, z: 1 }, { horizontalRadius: 25, verticalRadius: 10 })).toMatchObject({
51
+ callout: "B",
52
+ source: "nearby",
53
+ offset: [10, 0, 0],
54
+ });
55
+ });
56
+
57
+ it("prefers same z or lower z before upper z for equal-distance candidates", () => {
58
+ const g = grid({
59
+ "0,0,-10": [0, 0.7, 10],
60
+ "0,0,10": [1, 0.99, 100],
61
+ });
62
+ expect(calloutNear(g, { x: 0, y: 0, z: 0 }, { horizontalRadius: 0, verticalRadius: 10 })?.callout).toBe("A");
63
+ });
64
+
65
+ it("does not bridge Nuke-style stacked floors outside vertical radius", () => {
66
+ const g = grid({
67
+ "0,0,0": [0, 0.8, 20],
68
+ "0,0,120": [1, 0.9, 30],
69
+ });
70
+ expect(calloutNear(g, { x: 0, y: 0, z: 90 }, { horizontalRadius: 0, verticalRadius: 20 })).toBeNull();
71
+ expect(calloutNear(g, { x: 0, y: 0, z: 90 }, { horizontalRadius: 0, verticalRadius: 40 })?.callout).toBe("B");
72
+ });
73
+ });
@@ -0,0 +1,168 @@
1
+ /**
2
+ * callout-grid — 3D 多数表决查找表。
3
+ *
4
+ * 来源:110 场 demo(pro 51 + NJU 59)replay 8Hz 流的 (x,y,z) + place 字段,
5
+ * 按 10-unit 3D 格点多数表决归纳得到。
6
+ * 生成:scripts/build-callout-grid.py
7
+ *
8
+ * 核心是纯函数 calloutAt()/calloutNear(),无平台依赖。
9
+ */
10
+ import type { Vec3 } from "./nav.js";
11
+
12
+ // ── 类型 ──
13
+
14
+ export interface CalloutGridMeta {
15
+ mapName: string;
16
+ gridSize: number;
17
+ origin: [number, number, number];
18
+ maxCoord: [number, number, number];
19
+ dims: [number, number, number];
20
+ vocabulary: string[];
21
+ confidence: number;
22
+ minSamples: number;
23
+ }
24
+
25
+ export type CalloutGridCell = [
26
+ vocabularyIndex: number,
27
+ confidence: number,
28
+ samples: number,
29
+ ];
30
+
31
+ export interface CalloutGrid extends CalloutGridMeta {
32
+ /**
33
+ * "gx,gy,gz" → [vocabularyIndex, confidence, sampleCount]
34
+ * vocabularyIndex -> vocabulary[] 查 callout 名
35
+ * confidence -> [0, 1] 多数占比
36
+ * sampleCount -> 该格总采样帧数
37
+ */
38
+ cells: Record<string, CalloutGridCell>;
39
+ }
40
+
41
+ export interface CalloutAtResult {
42
+ callout: string;
43
+ confidence: number;
44
+ samples: number;
45
+ source: "exact";
46
+ distance: 0;
47
+ offset: [number, number, number];
48
+ }
49
+
50
+ export interface CalloutNearResult {
51
+ callout: string;
52
+ confidence: number;
53
+ samples: number;
54
+ source: "exact" | "nearby";
55
+ distance: number;
56
+ offset: [number, number, number];
57
+ }
58
+
59
+ export interface CalloutNearOptions {
60
+ horizontalRadius?: number;
61
+ verticalRadius?: number;
62
+ }
63
+
64
+ // ── 核心查询(纯函数,浏览器/Node 通用) ──
65
+
66
+ function gridCoord(value: number, gridSize: number): number {
67
+ return Math.floor(value / gridSize) * gridSize;
68
+ }
69
+
70
+ function cellAt(grid: CalloutGrid, gx: number, gy: number, gz: number): CalloutNearResult | null {
71
+ const cell = grid.cells[`${gx},${gy},${gz}`];
72
+ if (!cell) return null;
73
+ const callout = grid.vocabulary[cell[0]];
74
+ if (!callout) return null;
75
+ return {
76
+ callout,
77
+ confidence: cell[1],
78
+ samples: cell[2],
79
+ source: "exact",
80
+ distance: 0,
81
+ offset: [0, 0, 0],
82
+ };
83
+ }
84
+
85
+ /**
86
+ * 查询一个世界坐标对应的 callout 名。
87
+ * 返回 null 表示该格无数据(不可达区域 / 置信度不足 / 稀疏不足)。
88
+ *
89
+ * @param grid 通过 loadCalloutGrid() 或 fetch() 获取的网格对象
90
+ * @param point 世界坐标(v3 replay position 同系)
91
+ */
92
+ export function calloutAt(
93
+ grid: CalloutGrid,
94
+ point: Vec3,
95
+ ): CalloutAtResult | null {
96
+ const result = cellAt(
97
+ grid,
98
+ gridCoord(point.x, grid.gridSize),
99
+ gridCoord(point.y, grid.gridSize),
100
+ gridCoord(point.z, grid.gridSize),
101
+ );
102
+ return result && result.source === "exact"
103
+ ? { ...result, source: "exact", distance: 0, offset: [0, 0, 0] }
104
+ : null;
105
+ }
106
+
107
+ /**
108
+ * 查询世界坐标附近的 callout。
109
+ *
110
+ * 道具落点常在空中、墙边或不可站立格,精确格可能没有玩家采样。此函数先做精确
111
+ * 命中;失败后在半径内查邻近格,优先同层/下方近格,再按距离、置信度、样本数排序。
112
+ */
113
+ export function calloutNear(
114
+ grid: CalloutGrid,
115
+ point: Vec3,
116
+ options: CalloutNearOptions = {},
117
+ ): CalloutNearResult | null {
118
+ const gx = gridCoord(point.x, grid.gridSize);
119
+ const gy = gridCoord(point.y, grid.gridSize);
120
+ const gz = gridCoord(point.z, grid.gridSize);
121
+ const exact = cellAt(grid, gx, gy, gz);
122
+ if (exact) return exact;
123
+
124
+ const horizontalRadius = options.horizontalRadius ?? 20;
125
+ const verticalRadius = options.verticalRadius ?? 40;
126
+ const hSteps = Math.max(0, Math.ceil(horizontalRadius / grid.gridSize));
127
+ const zSteps = Math.max(0, Math.ceil(verticalRadius / grid.gridSize));
128
+ const candidates: CalloutNearResult[] = [];
129
+
130
+ for (let dx = -hSteps; dx <= hSteps; dx += 1) {
131
+ for (let dy = -hSteps; dy <= hSteps; dy += 1) {
132
+ for (let dz = -zSteps; dz <= zSteps; dz += 1) {
133
+ if (dx === 0 && dy === 0 && dz === 0) continue;
134
+ const ox = dx * grid.gridSize;
135
+ const oy = dy * grid.gridSize;
136
+ const oz = dz * grid.gridSize;
137
+ const horizontalDistance = Math.hypot(ox, oy);
138
+ if (horizontalDistance > horizontalRadius) continue;
139
+ if (Math.abs(oz) > verticalRadius) continue;
140
+ const cell = cellAt(grid, gx + ox, gy + oy, gz + oz);
141
+ if (!cell) continue;
142
+ candidates.push({
143
+ ...cell,
144
+ source: "nearby",
145
+ distance: Math.hypot(ox, oy, oz),
146
+ offset: [ox, oy, oz],
147
+ });
148
+ }
149
+ }
150
+ }
151
+
152
+ candidates.sort((a, b) => {
153
+ const zPrefA = zPreference(a.offset[2]);
154
+ const zPrefB = zPreference(b.offset[2]);
155
+ return zPrefA - zPrefB ||
156
+ a.distance - b.distance ||
157
+ b.confidence - a.confidence ||
158
+ b.samples - a.samples ||
159
+ a.callout.localeCompare(b.callout);
160
+ });
161
+ return candidates[0] ?? null;
162
+ }
163
+
164
+ function zPreference(offsetZ: number): number {
165
+ if (offsetZ === 0) return 0;
166
+ if (offsetZ < 0) return 1;
167
+ return 2;
168
+ }
@@ -0,0 +1,238 @@
1
+ export type TacticalRegion = "a" | "b" | "mid";
2
+
3
+ /** callout 条目:中文名 + 可选战术倾向。 */
4
+ export interface CalloutNameEntry {
5
+ cn: string;
6
+ /** 战术倾向:首元素为该位置在阵容中的主要方向。取 tendency[0] 参与区域人数统计。 */
7
+ tendency?: TacticalRegion[];
8
+ }
9
+
10
+ /** 读取一个已知 callout 的静态定义;未知地图或 callout 返回 null。 */
11
+ export function getCalloutDefinition(mapName: string, id: string): CalloutNameEntry | null {
12
+ return CALLOUT_DICT[mapName]?.[id] ?? null;
13
+ }
14
+
15
+ /** 读取有序战术方向;已知但未标注方向时返回空数组,未知 callout 返回 null。 */
16
+ export function getCalloutTendencies(mapName: string, id: string): readonly TacticalRegion[] | null {
17
+ const entry = getCalloutDefinition(mapName, id);
18
+ return entry ? entry.tendency ?? [] : null;
19
+ }
20
+
21
+ /** 读取 callout 的主要方向。 */
22
+ export function getPrimaryCalloutRegion(mapName: string, id: string): TacticalRegion | null {
23
+ return getCalloutTendencies(mapName, id)?.[0] ?? null;
24
+ }
25
+
26
+ /** 判断 callout 是否可连接到指定方向(包含次要方向)。 */
27
+ export function calloutBelongsToRegion(mapName: string, id: string, region: TacticalRegion): boolean {
28
+ return getCalloutTendencies(mapName, id)?.includes(region) ?? false;
29
+ }
30
+
31
+ /** 取 callout 的中文名。 */
32
+ export function calloutCn(mapName: string, id: string): string {
33
+ return CALLOUT_DICT[mapName]?.[id]?.cn ?? "";
34
+ }
35
+
36
+ /**
37
+ * CS2 官方 callout 名 → 中文名 + 战术倾向映射表。
38
+ *
39
+ * 来源:79 场 demo(pro 24 + NJU 55)的 `lastPlaceName` 词表 + 社区约定。
40
+ * 定位:供 Area v1 / 动线 / 展示层消费,不含坐标与路由逻辑。
41
+ * 待核验:部分 callout 可能有更精确的社区叫法,本表为初稿。
42
+ */
43
+ export const CALLOUT_DICT: Record<string, Record<string, CalloutNameEntry>> = {
44
+ de_ancient: {
45
+ BombsiteA: { cn: "A包", tendency: ["a"] },
46
+ BombsiteB: { cn: "B包", tendency: ["b"] },
47
+ Middle: { cn: "中路", tendency: ["mid"] },
48
+ Outside: { cn: "匪口", tendency: ["a", "mid"] },
49
+ SideEntrance: { cn: "黑屋", tendency: ["b"] },
50
+ Alley: { cn: "底线", tendency: ["b"] },
51
+ TSideUpper: { cn: "跳台", tendency: ["mid", "b"] },
52
+ TSideLower: { cn: "B小", tendency: ["b"] },
53
+ CTSpawn: { cn: "警家", tendency: ["a"] },
54
+ SideHall: { cn: "甜甜圈", tendency: ["a"] },
55
+ MainHall: { cn: "A厅", tendency: ["a"] },
56
+ Ruins: { cn: "B外", tendency: ["b"] },
57
+ House: { cn: "VIP", tendency: ["mid"] },
58
+ Ramp: { cn: "B坡", tendency: ["b"] },
59
+ TSpawn: { cn: "匪家" },
60
+ Tunnel: { cn: "隧道", tendency: ["b"] },
61
+ Water: { cn: "水路", tendency: ["b"] },
62
+ TopofMid: { cn: "中远", tendency: ["mid"] },
63
+ },
64
+
65
+ de_anubis: {
66
+ BombsiteB: { cn: "B包", tendency: ["b"] },
67
+ Canal: { cn: "水下", tendency: ["a", "b"] },
68
+ Middle: { cn: "中路", tendency: ["mid"] },
69
+ Bridge: { cn: "中桥", tendency: ["mid"] },
70
+ OutsideLong: { cn: "B外", tendency: ["b"] },
71
+ BombsiteA: { cn: "A包", tendency: ["a"] },
72
+ Ruins: { cn: "B外", tendency: ["b"] },
73
+ Connector: { cn: "黑屋", tendency: ["b"] },
74
+ PalaceInterior: { cn: "B连", tendency: ["b"] },
75
+ Main: { cn: "A厅", tendency: ["a"] },
76
+ Walkway: { cn: "A连", tendency: ["a"] },
77
+ BackofB: { cn: "B包台上", tendency: ["b"] },
78
+ TSpawn: { cn: "匪家" },
79
+ Street: { cn: "街道", tendency: ["a"] },
80
+ Heaven: { cn: "天堂", tendency: ["a"] },
81
+ MidDoors: { cn: "中门", tendency: ["mid"] },
82
+ TSideUpper: { cn: "匪跳", tendency: ["a"] },
83
+ CTSideUpper: { cn: "警家" },
84
+ Alley: { cn: "警家", tendency: ["b"] },
85
+ Bricks: { cn: "B连阳光房", tendency: ["b"] },
86
+ Fountain: { cn: "喷泉", tendency: ["a"] },
87
+ TStairs: { cn: "匪梯", tendency: ["a", "b"] },
88
+ LowerTunnel: { cn: "警家隧道", tendency: ["b"] },
89
+ CTSpawn: { cn: "警家后" },
90
+ TunnelStairs: { cn: "隧道楼梯", tendency: ["a"] },
91
+ SnipersNest: { cn: "警家狙位", tendency: ["b"] },
92
+ Tunnel: { cn: "隧道", tendency: ["a"] },
93
+ Water: { cn: "警家水下", tendency: ["a", "b"] },
94
+ },
95
+
96
+ de_dust2: {
97
+ BombsiteB: { cn: "B包", tendency: ["b"] },
98
+ LongDoors: { cn: "A门", tendency: ["a"] },
99
+ LongA: { cn: "A大", tendency: ["a"] },
100
+ MidDoors: { cn: "中门", tendency: ["mid"] },
101
+ UpperTunnel: { cn: "B洞", tendency: ["b"] },
102
+ TopofMid: { cn: "中远匪口", tendency: ["mid"] },
103
+ OutsideLong: { cn: "A门外", tendency: ["a"] },
104
+ UnderA: { cn: "警家", tendency: ["a"] },
105
+ CTSpawn: { cn: "警家", tendency: ["mid", "a"] },
106
+ Catwalk: { cn: "A小", tendency: ["a", "mid"] },
107
+ ExtendedA: { cn: "A小过点", tendency: ["a"] },
108
+ ShortStairs: { cn: "A小楼梯", tendency: ["a"] },
109
+ Middle: { cn: "中路", tendency: ["mid"] },
110
+ BombsiteA: { cn: "A包", tendency: ["a"] },
111
+ TSpawn: { cn: "匪家" },
112
+ LowerTunnel: { cn: "B1", tendency: ["mid"] },
113
+ BDoors: { cn: "B门", tendency: ["b"] },
114
+ ARamp: { cn: "A斜坡", tendency: ["a"] },
115
+ OutsideTunnel: { cn: "B洞外", tendency: ["b"] },
116
+ Pit: { cn: "大坑", tendency: ["a"] },
117
+ TunnelStairs: { cn: "B洞楼梯", tendency: ["b"] },
118
+ Hole: { cn: "狗洞", tendency: ["b"] },
119
+ Side: { cn: "大坑平台", tendency: ["a"] },
120
+ TRamp: { cn: "后花", tendency: ["b"] },
121
+ },
122
+
123
+ de_inferno: {
124
+ Banana: { cn: "香蕉道", tendency: ["b"] },
125
+ BombsiteB: { cn: "B包", tendency: ["b"] },
126
+ BombsiteA: { cn: "A包", tendency: ["a"] },
127
+ Apartments: { cn: "二楼", tendency: ["a"] },
128
+ Middle: { cn: "中路", tendency: ["mid"] },
129
+ CTSpawn: { cn: "警家" },
130
+ TopofMid: { cn: "中路", tendency: ["mid"] },
131
+ Arch: { cn: "拱门", tendency: ["a"] },
132
+ Ruins: { cn: "警家教堂", tendency: ["b"] },
133
+ Pit: { cn: "大坑", tendency: ["a"] },
134
+ SecondMid: { cn: "侧道", tendency: ["mid", "a"] },
135
+ TRamp: { cn: "匪口", tendency: ["mid", "b"] },
136
+ TSpawn: { cn: "匪家" },
137
+ Balcony: { cn: "阳台", tendency: ["a"] },
138
+ LowerMid: { cn: "匪口", tendency: ["mid", "b"] },
139
+ Library: { cn: "书房", tendency: ["a"] },
140
+ BackAlley: { cn: "匪二楼", tendency: ["a"] },
141
+ Underpass: { cn: "下水道", tendency: ["mid"] },
142
+ Quad: { cn: "马棚", tendency: ["a"] },
143
+ Deck: { cn: "匪二阳台", },
144
+ Bridge: { cn: "匪桥", tendency: ["mid", "a"] },
145
+ Upstairs: { cn: "匪二楼", tendency: ["mid", "a"] },
146
+ Graveyard: { cn: "墓地", tendency: ["a"] },
147
+ Kitchen: { cn: "厨房", },
148
+ },
149
+
150
+ de_mirage: {
151
+ BombsiteA: { cn: "A包", tendency: ["a"] },
152
+ CTSpawn: { cn: "警家" },
153
+ BombsiteB: { cn: "B包", tendency: ["b"] },
154
+ PalaceInterior: { cn: "A二楼", tendency: ["a"] },
155
+ Middle: { cn: "中路", tendency: ["mid"] },
156
+ TopofMid: { cn: "中远/匪口", tendency: ["mid"] },
157
+ Catwalk: { cn: "B小", tendency: ["b"] },
158
+ Apartments: { cn: "B二楼", tendency: ["b"] },
159
+ Underpass: { cn: "下水道", tendency: ["mid", "b"] },
160
+ TSpawn: { cn: "匪家" },
161
+ PalaceAlley: { cn: "A1", tendency: ["a"] },
162
+ Connector: { cn: "拱门", tendency: ["a", "mid"] },
163
+ SideAlley: { cn: "匪口", tendency: ["mid"] },
164
+ BackAlley: { cn: "B二楼", tendency: ["b"] },
165
+ Jungle: { cn: "Jungle", tendency: ["a"] },
166
+ Shop: { cn: "超市", tendency: ["b"] },
167
+ TRamp: { cn: "A1", tendency: ["a"] },
168
+ SnipersNest: { cn: "VIP", tendency: ["mid"] },
169
+ Truck: { cn: "白车", tendency: ["b"] },
170
+ House: { cn: "匪二楼", tendency: ["b"] },
171
+ Stairs: { cn: "跳台", tendency: ["a"] },
172
+ Ladder: { cn: "黑屋", tendency: ["mid", "b"] },
173
+ Scaffolding: { cn: "A2上下", tendency: ["a"] },
174
+ },
175
+
176
+ de_nuke: {
177
+ Outside: { cn: "外场", tendency: ["mid"] },
178
+ BombsiteA: { cn: "A包", tendency: ["a"] },
179
+ Ramp: { cn: "铁板", tendency: ["b"] },
180
+ Lobby: { cn: "匪厅", tendency: ["a"] },
181
+ Tunnels: { cn: "K1地下", tendency: ["b"] },
182
+ BombsiteB: { cn: "B包", tendency: ["b"] },
183
+ TSpawn: { cn: "匪家" },
184
+ Mini: { cn: "正门", tendency: ["a", "mid"] },
185
+ Squeaky: { cn: "铁门房", tendency: ["a"] },
186
+ Control: { cn: "链接", tendency: ["b"] },
187
+ Garage: { cn: "大仓", tendency: ["mid"] },
188
+ Rafters: { cn: "三楼横梁", tendency: ["a"] },
189
+ Hut: { cn: "黄房", tendency: ["a"] },
190
+ Vending: { cn: "链接", tendency: ["b"] },
191
+ Heaven: { cn: "三楼", tendency: ["a"] },
192
+ Admin: { cn: "铁板三楼下", tendency: ["b"] },
193
+ CTSpawn: { cn: "警家" },
194
+ Secret: { cn: "K1", tendency: ["b", "mid"] },
195
+ Trophy: { cn: "奖杯房", tendency: ["b"] },
196
+ HutRoof: { cn: "黄房顶", tendency: ["a"] },
197
+ Hell: { cn: "三楼下", tendency: ["a", "b", "mid"] },
198
+ Roof: { cn: "屋顶", tendency: ["a"] },
199
+ Silo: { cn: "山上", tendency: ["mid"] },
200
+ Vents: { cn: "管道", tendency: ["b"] },
201
+ Catwalk: { cn: "外场三楼", tendency: ["mid"] },
202
+ Decon: { cn: "死门", tendency: ["b"] },
203
+ Crane: { cn: "A包柱上", tendency: ["a"] },
204
+ Observation: { cn: "控制室", tendency: ["b"] },
205
+ LockerRoom: { cn: "更衣室", tendency: ["mid"] },
206
+ },
207
+
208
+ de_overpass: {
209
+ LowerPark: { cn: "A小厕所", tendency: ["a"] },
210
+ Water: { cn: "工地", tendency: ["b"] },
211
+ BombsiteA: { cn: "A包", tendency: ["a"] },
212
+ BombsiteB: { cn: "B包", tendency: ["b"] },
213
+ UpperPark: { cn: "A大厕所", tendency: ["a"] },
214
+ Canal: { cn: "长管", tendency: ["b"] },
215
+ Fountain: { cn: "喷泉", tendency: ["a"] },
216
+ Tunnels: { cn: "下水道", tendency: ["mid"] },
217
+ Connector: { cn: "下水道", tendency: ["mid", "a"] },
218
+ Walkway: { cn: "ABC", tendency: ["b"] },
219
+ Construction: { cn: "B小", tendency: ["b"] },
220
+ TSpawn: { cn: "匪家" },
221
+ Alley: { cn: "匪家B外", tendency: ["b"] },
222
+ SnipersNest: { cn: "B二楼", tendency: ["b"] },
223
+ UnderA: { cn: "一层", tendency: ["b"] },
224
+ BackofA: { cn: "垃圾桶", tendency: ["a"] },
225
+ Pipe: { cn: "短管", tendency: ["b"] },
226
+ Lobby: { cn: "银行", tendency: ["a"] },
227
+ Playground: { cn: "游乐园", tendency: ["a"] },
228
+ Restroom: { cn: "厕所", tendency: ["a"] },
229
+ TStairs: { cn: "匪楼梯" },
230
+ Bridge: { cn: "桥", tendency: ["b"] },
231
+ Stairs: { cn: "楼梯", tendency: ["a"] },
232
+ StorageRoom: { cn: "银行", tendency: ["a"] },
233
+ SideAlley: { cn: "匪家保枪位" },
234
+ },
235
+ };
236
+
237
+ /** 所有已标定的地图名。 */
238
+ export const CALLOUT_MAPS = Object.keys(CALLOUT_DICT) as ReadonlyArray<string>;
@@ -0,0 +1,109 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ CALLOUT_MAPS,
4
+ CALLOUT_DICT,
5
+ calloutBelongsToRegion,
6
+ getCalloutDefinition,
7
+ getCalloutTendencies,
8
+ getPrimaryCalloutRegion,
9
+ } from "./callout-names.js";
10
+ import {
11
+ DEFAULT_POSITION_GROUPS,
12
+ positionGroupOf,
13
+ classifyTacticalLocation,
14
+ getDefaultPositionGroup,
15
+ } from "./default-positions.js";
16
+
17
+ describe("default-positions", () => {
18
+ it("mirage T 默认位含 5 个位置责任组", () => {
19
+ expect(Object.keys(DEFAULT_POSITION_GROUPS.de_mirage.t.groups)).toEqual([
20
+ "a_ramp",
21
+ "a_palace",
22
+ "top_mid",
23
+ "underpass",
24
+ "b_apps",
25
+ ]);
26
+ });
27
+
28
+ it("anchor 里引用的 callout 都是合法 callout(在 callout-names 中)", () => {
29
+ for (const [map, sides] of Object.entries(DEFAULT_POSITION_GROUPS)) {
30
+ const table = CALLOUT_DICT[map];
31
+ for (const side of ["t", "ct"] as const) {
32
+ for (const group of Object.values(sides[side].groups)) {
33
+ for (const callout of group.callouts) expect(table[callout]).toBeTruthy();
34
+ }
35
+ }
36
+ }
37
+ });
38
+
39
+ it("7 图都有 t/ct 默认位且每个 anchor 至少 1 callout", () => {
40
+ for (const map of CALLOUT_MAPS) {
41
+ const defaults = DEFAULT_POSITION_GROUPS[map];
42
+ expect(defaults, map).toBeTruthy();
43
+ for (const side of ["t", "ct"] as const) {
44
+ const groups = Object.values(defaults[side].groups);
45
+ expect(groups.length, `${map}.${side}`).toBeGreaterThan(0);
46
+ for (const group of groups) {
47
+ expect(group.callouts.length, `${map}.${side}.${group.name}`).toBeGreaterThan(0);
48
+ }
49
+ }
50
+ }
51
+ });
52
+
53
+ it("基础 callout API 保留多倾向顺序,未知 callout 不猜测", () => {
54
+ expect(getCalloutDefinition("de_mirage", "Connector")).toEqual({ cn: "拱门", tendency: ["a", "mid"] });
55
+ expect(getCalloutTendencies("de_mirage", "Connector")).toEqual(["a", "mid"]);
56
+ expect(getPrimaryCalloutRegion("de_mirage", "Connector")).toBe("a");
57
+ expect(calloutBelongsToRegion("de_mirage", "Connector", "mid")).toBe(true);
58
+ expect(getCalloutDefinition("de_mirage", "NoSuchPlace")).toBeNull();
59
+ expect(getCalloutTendencies("de_mirage", "NoSuchPlace")).toBeNull();
60
+ expect(getPrimaryCalloutRegion("de_mirage", "NoSuchPlace")).toBeNull();
61
+ });
62
+
63
+ it("默认位置组只保留人工确认的开局停挂点,不维护静态 contested/advanced 角色", () => {
64
+ expect(positionGroupOf("de_ancient", "t", "Outside")).toBe("t_outside");
65
+ expect(positionGroupOf("de_ancient", "t", "Tunnel")).toBeNull();
66
+ expect(positionGroupOf("de_ancient", "t", "Water")).toBeNull();
67
+
68
+ expect(positionGroupOf("de_anubis", "t", "Bridge")).toBe("mid_bridge");
69
+ expect(positionGroupOf("de_anubis", "t", "Middle")).toBeNull();
70
+
71
+ expect(positionGroupOf("de_dust2", "t", "LongA")).toBeNull();
72
+
73
+ expect(positionGroupOf("de_nuke", "t", "Ramp")).toBeNull();
74
+ expect(positionGroupOf("de_nuke", "t", "Trophy")).toBe("lobby");
75
+
76
+ expect(positionGroupOf("de_overpass", "t", "Water")).toBe("short_pipe");
77
+ });
78
+
79
+ it("统一位置分类只组合基础倾向与当前阵营默认位", () => {
80
+ expect(classifyTacticalLocation("de_mirage", "t", "PalaceAlley")).toEqual({
81
+ callout: "PalaceAlley",
82
+ tendencies: ["a"],
83
+ primaryRegion: "a",
84
+ positionGroupId: "a_ramp",
85
+ isDefaultPosition: true,
86
+ });
87
+ expect(classifyTacticalLocation("de_mirage", "ct", "PalaceAlley")).toEqual({
88
+ callout: "PalaceAlley",
89
+ tendencies: ["a"],
90
+ primaryRegion: "a",
91
+ positionGroupId: null,
92
+ isDefaultPosition: false,
93
+ });
94
+ expect(classifyTacticalLocation("de_mirage", "t", "NoSuchPlace")).toEqual({
95
+ callout: "NoSuchPlace",
96
+ tendencies: [],
97
+ primaryRegion: null,
98
+ positionGroupId: null,
99
+ isDefaultPosition: false,
100
+ });
101
+ expect(classifyTacticalLocation("de_mirage", "t", null).callout).toBeNull();
102
+ });
103
+
104
+ it("positionGroupOf:default 返回 positionGroupId,否则 null", () => {
105
+ expect(positionGroupOf("de_mirage", "t", "TRamp")).toBe("a_ramp");
106
+ expect(positionGroupOf("de_mirage", "t", "Catwalk")).toBeNull();
107
+ expect(getDefaultPositionGroup("de_mirage", "t", "TRamp")).toMatchObject({ id: "a_ramp", name: "A1" });
108
+ });
109
+ });