From 81485a282df24ef902269c9954ad901efd701f65 Mon Sep 17 00:00:00 2001 From: Luke Policinski Date: Wed, 19 Aug 2026 16:48:24 -0400 Subject: [PATCH 01/49] feature: nades library and practice ui --- components/match/MatchMapAnalysis.vue | 90 +- components/match/MatchNadeUtility.vue | 706 +++++++++++ components/match/MatchTabs.vue | 17 +- components/match/ReplayViewer.vue | 70 +- components/nades/NadeCalibrationGate.vue | 71 ++ components/nades/NadeCollectionPicker.vue | 285 +++++ components/nades/NadeConfidenceNote.vue | 121 ++ components/nades/NadeDifficultyChip.vue | 65 + components/nades/NadeFilters.vue | 273 +++++ components/nades/NadeForkDialog.vue | 140 +++ components/nades/NadeImportPanel.vue | 498 ++++++++ components/nades/NadeLineupCard.vue | 209 ++++ components/nades/NadeLineupPickerDialog.vue | 207 ++++ components/nades/NadeLineupViewer3D.vue | 351 ++++++ components/nades/NadeMissPatternPanel.vue | 222 ++++ .../nades/NadePlaybookCoveragePanel.vue | 519 ++++++++ components/nades/NadePlaybookEditor.vue | 827 +++++++++++++ components/nades/NadePracticePlanPanel.vue | 316 +++++ components/nades/NadeProgressPanel.vue | 132 ++ components/nades/NadeRadarBoard.vue | 497 ++++++++ components/nades/NadeRepairDialog.vue | 423 +++++++ components/nades/NadeSaveLineupDialog.vue | 342 ++++++ components/nades/NadeSightlinePanel.vue | 569 +++++++++ components/nades/NadeSolvePanel.vue | 469 +++++++ components/nades/NadeTypeChips.vue | 54 + components/nades/StartPracticeDialog.vue | 810 ++++++++++++ components/team/TeamNadeUtility.vue | 311 +++++ composables/useNadeSolverCalibration.ts | 100 ++ composables/useRadarProjection.ts | 178 +++ composables/useSettingsNav.ts | 5 + graphql/nadesGraphql.ts | 1084 +++++++++++++++++ i18n/locales/en.json | 781 +++++++++++- layouts/components/LeftNav.vue | 38 + pages/nades/[map].vue | 458 +++++++ pages/nades/block/[map].vue | 500 ++++++++ pages/nades/drift/index.vue | 779 ++++++++++++ pages/nades/index.vue | 195 +++ pages/nades/lineup/[id].vue | 564 +++++++++ pages/nades/meta/[map].vue | 462 +++++++ pages/nades/new/[map].vue | 726 +++++++++++ pages/nades/playbooks/[map].vue | 334 +++++ pages/settings/application/nades.vue | 333 +++++ pages/teams/[id].vue | 2 + types/nade.ts | 1067 ++++++++++++++++ utilities/nadeDisplay.ts | 991 +++++++++++++++ 45 files changed, 17047 insertions(+), 144 deletions(-) create mode 100644 components/match/MatchNadeUtility.vue create mode 100644 components/nades/NadeCalibrationGate.vue create mode 100644 components/nades/NadeCollectionPicker.vue create mode 100644 components/nades/NadeConfidenceNote.vue create mode 100644 components/nades/NadeDifficultyChip.vue create mode 100644 components/nades/NadeFilters.vue create mode 100644 components/nades/NadeForkDialog.vue create mode 100644 components/nades/NadeImportPanel.vue create mode 100644 components/nades/NadeLineupCard.vue create mode 100644 components/nades/NadeLineupPickerDialog.vue create mode 100644 components/nades/NadeLineupViewer3D.vue create mode 100644 components/nades/NadeMissPatternPanel.vue create mode 100644 components/nades/NadePlaybookCoveragePanel.vue create mode 100644 components/nades/NadePlaybookEditor.vue create mode 100644 components/nades/NadePracticePlanPanel.vue create mode 100644 components/nades/NadeProgressPanel.vue create mode 100644 components/nades/NadeRadarBoard.vue create mode 100644 components/nades/NadeRepairDialog.vue create mode 100644 components/nades/NadeSaveLineupDialog.vue create mode 100644 components/nades/NadeSightlinePanel.vue create mode 100644 components/nades/NadeSolvePanel.vue create mode 100644 components/nades/NadeTypeChips.vue create mode 100644 components/nades/StartPracticeDialog.vue create mode 100644 components/team/TeamNadeUtility.vue create mode 100644 composables/useNadeSolverCalibration.ts create mode 100644 composables/useRadarProjection.ts create mode 100644 graphql/nadesGraphql.ts create mode 100644 pages/nades/[map].vue create mode 100644 pages/nades/block/[map].vue create mode 100644 pages/nades/drift/index.vue create mode 100644 pages/nades/index.vue create mode 100644 pages/nades/lineup/[id].vue create mode 100644 pages/nades/meta/[map].vue create mode 100644 pages/nades/new/[map].vue create mode 100644 pages/nades/playbooks/[map].vue create mode 100644 pages/settings/application/nades.vue create mode 100644 types/nade.ts create mode 100644 utilities/nadeDisplay.ts diff --git a/components/match/MatchMapAnalysis.vue b/components/match/MatchMapAnalysis.vue index 4a25d4c5a..54389ec97 100644 --- a/components/match/MatchMapAnalysis.vue +++ b/components/match/MatchMapAnalysis.vue @@ -29,16 +29,10 @@ import { import { matchHeatmapQuery } from "~/graphql/matchHeatmapGraphql"; import { matchMovementMapQuery } from "~/graphql/matchMovementPathsGraphql"; import RoundSelector from "~/components/match/RoundSelector.vue"; - -type MapSplit = { - bounds: { top: number; bottom: number }; - offset: { x: number; y: number }; -}; -type RadarMeta = { - resolution: number; - offset: { x: number; y: number }; - splits?: MapSplit[]; -}; +import { + RADAR_CANVAS, + useRadarProjection, +} from "~/composables/useRadarProjection"; type DotCategory = "kills" | "deaths" | "utility" | "util_damage"; @@ -86,8 +80,7 @@ const { t } = useI18n(); const side = useMatchSide(); const { client } = useApolloClient(); -const CANVAS = 1024; -const RADAR_PX = 1024; +const CANVAS = RADAR_CANVAS; const KILL_COLOR = "#fbbf24"; const DEATH_COLOR = "rgb(239, 68, 68)"; @@ -117,7 +110,6 @@ const WINDOW_OPTIONS = [10, 20, 30, 45]; const mode = ref<"heatmap" | "movement">("heatmap"); const renderStyle = ref<"heat" | "dots">("dots"); -const calibrations = ref | null>(null); const radarFailed = ref(false); const selectedMapId = ref(null); @@ -237,30 +229,13 @@ function open3dPlayback() { } } -const normalizedMap = computed(() => - (activeMatchMap.value?.map?.name || "") - .trim() - .toLowerCase() - .replace(/_night$/, ""), -); - -const calibration = computed(() => { - if (!calibrations.value || !normalizedMap.value) { - return null; - } - return calibrations.value[normalizedMap.value] ?? null; -}); - -const radarSrc = computed(() => { - if (!calibration.value || !normalizedMap.value || radarFailed.value) { - return null; - } - return `/radars/${normalizedMap.value}.png`; -}); - -const has2dRadar = computed(() => - calibrations.value === null ? true : !!calibration.value, -); +const { + normalizedMap, + calibration, + radarSrc, + hasCalibration: has2dRadar, + projectCalibrated, +} = useRadarProjection(() => activeMatchMap.value?.map?.name, { radarFailed }); const has3dMesh = ref(true); watch( @@ -281,19 +256,6 @@ watch( { immediate: true }, ); -onMounted(async () => { - try { - const res = await fetch("/radars/metadata.json"); - if (res.ok) { - const data = await res.json(); - const { _comment, ...rest } = data; - calibrations.value = rest as Record; - } - } catch { - /* */ - } -}); - async function loadHeatmap() { const matchId = props.match?.id; if (!matchId || matchId === loadedHeatmapMatchId.value) { @@ -403,33 +365,7 @@ function parseCoords( return { x: parts[0], y: parts[1], z: parts[2] ?? 0 }; } -function applySplit(z: number, splits: MapSplit[] | undefined) { - if (!splits) { - return { dx: 0, dy: 0 }; - } - for (const s of splits) { - if (z > s.bounds.bottom && z < s.bounds.top) { - return { dx: s.offset.x, dy: s.offset.y }; - } - } - return { dx: 0, dy: 0 }; -} - -function projectRaw(p: { x: number; y: number; z?: number }) { - if (!calibration.value) { - return null; - } - const { resolution, offset, splits } = calibration.value; - const split = applySplit(p.z ?? 0, splits); - const gameX = p.x + offset.x; - const gameY = p.y + offset.y; - const pxX = gameX / resolution + (split.dx / 100) * RADAR_PX; - const pxYFromBottom = gameY / resolution + (split.dy / 100) * RADAR_PX; - return { - x: pxX * (CANVAS / RADAR_PX), - y: CANVAS - pxYFromBottom * (CANVAS / RADAR_PX), - }; -} +const projectRaw = projectCalibrated; const lineupBySteamId = computed(() => { const out = new Map(); diff --git a/components/match/MatchNadeUtility.vue b/components/match/MatchNadeUtility.vue new file mode 100644 index 000000000..3f703e2c2 --- /dev/null +++ b/components/match/MatchNadeUtility.vue @@ -0,0 +1,706 @@ + + + diff --git a/components/match/MatchTabs.vue b/components/match/MatchTabs.vue index 70e83a3e4..7695b8ee4 100644 --- a/components/match/MatchTabs.vue +++ b/components/match/MatchTabs.vue @@ -14,6 +14,7 @@ import MatchMapAnalysis from "~/components/match/MatchMapAnalysis.vue"; import MatchEconomyTimeline from "~/components/match/MatchEconomyTimeline.vue"; import HeadToHead from "~/components/match/HeadToHead.vue"; import MatchRoles from "~/components/match/MatchRoles.vue"; +import MatchNadeUtility from "~/components/match/MatchNadeUtility.vue"; import MatchSideFilter from "~/components/match/MatchSideFilter.vue"; import TableColumnPicker from "~/components/common/TableColumnPicker.vue"; import TeamUtilitySummary from "~/components/match/TeamUtilitySummary.vue"; @@ -208,6 +209,9 @@ provide("commander", commander); {{ $t("match.tabs.map_analysis") }} + + {{ $t("match.tabs.nades") }} + {{ $t("match.tabs.settings") }} @@ -250,6 +254,9 @@ provide("commander", commander); {{ $t("match.tabs.map_analysis") }} + + {{ $t("match.tabs.nades") }} + @@ -552,6 +559,11 @@ provide("commander", commander); + +
+ +
+
| null>(null); const radarFailed = ref(false); // 2D top-down board vs lightweight 3D perspective (radar plane + Z-lifted // entities, no map geometry). Toggling preserves all playback state. const viewMode = ref<"2d" | "3d">(props.initialView ?? "2d"); -onMounted(async () => { - try { - const res = await fetch("/radars/metadata.json"); - if (res.ok) { - const data = await res.json(); - const { _comment, ...rest } = data; - calibrations.value = rest as Record; - } - } catch { - /* metadata absent — fall back to auto-fit */ - } -}); - const normalizedMap = computed(() => (props.mapName || "") .trim() @@ -308,16 +288,10 @@ const normalizedMap = computed(() => .replace(/_night$/, ""), ); -const calibration = computed(() => { - if (!calibrations.value || !normalizedMap.value) return null; - return calibrations.value[normalizedMap.value] ?? null; -}); - -const radarSrc = computed(() => { - if (!calibration.value || !normalizedMap.value || radarFailed.value) - return null; - return `/radars/${normalizedMap.value}.png`; -}); +const { calibration, radarSrc, projectCalibrated } = useRadarProjection( + normalizedMap, + { radarFailed }, +); // Lightweight collision mesh (awpy .tri) for 3D-lite, served from the CDN // (config.public.mapMeshCdn, build-tag pinned + Brotli'd). The 3D renderer @@ -1926,32 +1900,14 @@ const bounds = computed(() => { }; }); -const CANVAS = 1024; -const RADAR_PX = 1024; - -function applySplit(z: number, splits: MapSplit[] | undefined) { - if (!splits) return { dx: 0, dy: 0 }; - for (const s of splits) { - if (z > s.bounds.bottom && z < s.bounds.top) { - return { dx: s.offset.x, dy: s.offset.y }; - } - } - return { dx: 0, dy: 0 }; -} +const CANVAS = RADAR_CANVAS; function projectRaw(p: { x: number; y: number; z?: number }) { - if (calibration.value) { - const { resolution, offset, splits } = calibration.value; - const split = applySplit(p.z ?? 0, splits); - const gameX = p.x + offset.x; - const gameY = p.y + offset.y; - const pxX = gameX / resolution + (split.dx / 100) * RADAR_PX; - const pxYFromBottom = gameY / resolution + (split.dy / 100) * RADAR_PX; - return { - x: pxX * (CANVAS / RADAR_PX), - y: CANVAS - pxYFromBottom * (CANVAS / RADAR_PX), - }; + const projected = projectCalibrated(p); + if (projected) { + return projected; } + // No calibration for this map: auto-fit to the bbox of everything sampled. const b = bounds.value; const w = b.maxX - b.minX || 1; const h = b.maxY - b.minY || 1; diff --git a/components/nades/NadeCalibrationGate.vue b/components/nades/NadeCalibrationGate.vue new file mode 100644 index 000000000..ca492d48d --- /dev/null +++ b/components/nades/NadeCalibrationGate.vue @@ -0,0 +1,71 @@ + + + diff --git a/components/nades/NadeCollectionPicker.vue b/components/nades/NadeCollectionPicker.vue new file mode 100644 index 000000000..8b8741427 --- /dev/null +++ b/components/nades/NadeCollectionPicker.vue @@ -0,0 +1,285 @@ + + + diff --git a/components/nades/NadeConfidenceNote.vue b/components/nades/NadeConfidenceNote.vue new file mode 100644 index 000000000..435d67449 --- /dev/null +++ b/components/nades/NadeConfidenceNote.vue @@ -0,0 +1,121 @@ + + + diff --git a/components/nades/NadeDifficultyChip.vue b/components/nades/NadeDifficultyChip.vue new file mode 100644 index 000000000..8957533d2 --- /dev/null +++ b/components/nades/NadeDifficultyChip.vue @@ -0,0 +1,65 @@ + + + diff --git a/components/nades/NadeFilters.vue b/components/nades/NadeFilters.vue new file mode 100644 index 000000000..69d0de0d0 --- /dev/null +++ b/components/nades/NadeFilters.vue @@ -0,0 +1,273 @@ + + + diff --git a/components/nades/NadeForkDialog.vue b/components/nades/NadeForkDialog.vue new file mode 100644 index 000000000..aa1ed12b5 --- /dev/null +++ b/components/nades/NadeForkDialog.vue @@ -0,0 +1,140 @@ + + + diff --git a/components/nades/NadeImportPanel.vue b/components/nades/NadeImportPanel.vue new file mode 100644 index 000000000..571c8da03 --- /dev/null +++ b/components/nades/NadeImportPanel.vue @@ -0,0 +1,498 @@ + + +