Instruction file imported from sevsunday/vt-stats (
.cursor/rules/project-overview.mdc). Copyright stays with the author.
VT Stats — Project Context
Architecture
Static-site dashboard for Battlezone: Combat Commander match statistics. Seven-part system:
-
Python pipeline (
scripts/process_stats.py) reads raw.binpb.gzprotobuf session files fromdata/sessions/<username>/, aggregates all statistics, and writes pre-computed JSON todata/processed/. -
Browser dashboard (
index.html,js/theme.js,js/vtstats-fx.js,js/charts.js,js/charts-radar.js,js/positioning-charts.js,js/timeline-player.js,js/positioning-player.js,js/storyline.js,js/match-elo.js,js/all-matches-aggregator.js,js/app.js) loads the tiny pre-computed JSON files and renders interactive UI with tabbed navigation and glassmorphic theme layer. A client-side global filter layer derives player/team-scoped views from the loaded JSON without re-fetching. Statistical aggregation is the pipeline's responsibility with one documented exception: the All Matches view is built client-side byjs/all-matches-aggregator.jsfromdata/processed/match_contributions.jsonso it can scope to the picker-filtered subset of matches. The aggregator is pure summation — every per-match number it consumes was pre-computed by the pipeline. The default landing is the most-recent match — any visit toindex.htmlwithout an explicit?match=/?tab=/?filter=URL intent opens straight into the newest match (manifest[0], sorted newest-first inrunInitialBoot()). The All Matches aggregate stays reachable via?match=all, the match picker modal, and the per-match banner's "All Matches" button. (Historical note: the aggregate was briefly the universal default landing; that was reverted to de-emphasize the corpus-wide VTSR-T/career view.) The legacy first-visit landing-preferences modal was removed; the orphanedvt-landing-preflocalStorage key is silently nuked on every boot so returning users get no stale-pref bleed-through. Tabs under#all-matches-view: Overview · Weapons & Rivalries · Commanders · Meta. Within the All Matches → Overview tab the card order is Career Leaderboard → Career Highlights → VTSR-T Leaderboard → Career Radar (VTSR-T deliberately pushed below the fold to reduce rating pressure). The Players topnav link is restored onindex.html(desktop + mobile), sitting immediately left of ELO (canonical topnav order on every shell: Models · Maps · Players · ELO · Tools); inline player-name cross-links (vtPlayerLinkHtml) also resolve to the player profile pages. -
Raw Data Browser (
raw.html,js/raw-browser.js,css/raw-browser.css) is a standalone, isolated page for inspecting per-match data at every layer: raw binpb metadata + download, faithful client-side decode of the protobuf (vendored protobufjs-light + nativeDecompressionStream), and the processed JSON. Rendered as a virtualized JSON tree with domain-aware resolvers (Steam64 → nickname, ODF → prettified weapon/unit name via match-globalodf_map). Loaded on demand per match from the main dashboard's "View Raw Data" button on the match-info banner, or from thedocs.htmlnavbar. -
ODF Browser (
odf/index.html,js/odf-browser.js,css/odf-browser.css) is a standalone reference page for inspecting BZ2's Object Definition File (ODF) database: per-category sidebar (Vehicle / Weapon / Pilot / Building / Ordnance / Powerup) + 3-column property detail view + side-by-side compare mode + inline.wavaudio preview +?odf=/?compare=URL routing. Readsdata/odf.min.jsonanddata/audio/*.wav. Linked from the topnav of every other page, and the dashboard's kill-feed weapon ODF chips deep-link into it via?odf=<basename>in a new tab. The seed-era VSR Build Tree feature is intentionally deferred and not present in this v1. -
Player Profile Pages (
player/index.html,js/player.js,css/player.css,scripts/generate_player_pages.py,scripts/player_template.html) — pre-generated/player/<slug>/profile stubs (one per player withmatches_played >= 5) carrying per-player Open Graph meta for rich Discord/Slack/Twitter unfurls; the sameplayer/index.htmldoubles as a rich Directory landing (card grid with search/filter chips + sort + compare-mode toggle), the runtime single-player fallback (?p=<steam64>/?slug=<slug>) for un-stubbed players, and the Compare view (?compare=<csv-of-slugs>, up to 4 players: overlaid 8-axis radar, overlaid VTSR-T time-series withBy date↔By matches playedtoggle, transposed stat grid with best/worst row highlighting, common-matches table, inline "Add player" picker). Single profile has six tabs: Overview · Rating (interactive VTSR-T time-series + virtualized match log) · Axes (per-axis time-series mini-cards) · Highlights · Rivals (incl. conditional Most-Commanded-Against panel) · Loadout. Linked from the topnav of every other page; cross-linked fromjs/app.jsin six spots (per-match leaderboard, VTSR-T leaderboard, career table, kill feed, highlights, commander head-to-head) via thevtPlayerLinkHtml(name, steam64)helper which resolves Steam64 →player/<slug>/from a cacheddata/processed/player_slugs.json(sticky slug map — once allocated, a slug never changes for a given Steam64). Production host:https://vtstats.bz(perCNAME). The page system uses Chart.js with the vendoredchartjs-plugin-zoomfor the rating time-series; inline SVG sparklines avoid Chart.js overhead for the 20+ directory cards. -
Map Browser (
map/index.html,js/maps.js,css/maps.css,scripts/generate_map_pages.py,scripts/map_template.html) is a standalone catalog page for the VSR map library, paralleling the Player Profile Pages architecture. Pre-generated/map/<mapfile>/index.htmlstubs (one perdata/map-registry.jsonentry; ~143 once Phase 1 of the universe-extension lands) carry per-map Open Graph meta with each map's actual top-down screenshot for Discord/Slack/Twitter unfurls. The samemap/index.htmldoubles as a rich Directory landing (responsive thumb grid with search + pools/size/tags chips + played-status radio + author dropdown + sort dropdown) and a runtime single-map fallback (?file=<slug>) for the rare uncovered map. Single-map view (v1, deliberately lean): hero strip lifting Map Info Modal markup (image · title · author · description · chip row of pools/loose/size/b2b/tags/mod/team-name svars + match-summary stats), Top Commanders card (top 10 bymatches_commandedfromdata/processed/map_stats.json, names link to/player/<slug>/), Recent matches table (10 most recent with click-through toindex.html?match=<id>), and a six-card greyed-outComing soonplaceholder section advertising future depth (team wins donut · faction balance · best players · best commanders · map records · player count histogram). Pages are corpus-wide and NOT picker-filter aware (mirrors VTSR-T leaderboard). Slug = lowercasedmap_filestem frombuild_map_registry.map_key(); no allocator needed.MAP_TEMPLATE_VERSION = 2(bumped 1→2 in the Lobby Tools rollout to thread the newToolstopnav link through every pre-generated map stub). Registry universe extended to all vsrmaplist entries viabuild_map_registry.discover_all_maps()with a politeINTER_MAP_REQUEST_DELAY_SEC = 2.0between non-cached fetches on first run. Cross-links: Maps topnav link sibling of Players on every standalone page; Map Info Modal title becomestarget="_blank"link to/map/<slug>/; modal footer gainsView full map pagebutton; match-info banner gainsinfo-map-link; All Matches → Meta tab maps stacked-bar entries open per-map page in a new tab on click. -
Lobby Tools Page (
tools/index.html,css/tools.css,js/tools/main.js+js/tools/player-resolver.js+js/tools/live-session.js+js/tools/wheel.js+js/tools/coinflip.js+js/tools/map-roll.js+js/tools/team-balonce.js+js/tools/toast-manager.js, with the factored renderer atjs/live-session-card.jsand local registry enrich atjs/live-map-enrich.js) — purely-client-side lobby-management surface. Six section cards in a viewport-fit 2-col CSS grid above 1280px (single-col stacked below): Live Session (mirrors the bz2api session card; force-refresh + lock-lobby + ignore-live toggles; known-host-allowlist + VSR-mod filtered; multi-survivor session picker pre-selects largest), Active Roster (Mode toggle in card header: Auto syncs with live data, Manual is hand-curated withAdd playersearch +Custom entryfallback; provisional/unknown/custom anchored at VTSR 1500), Player Wheel (canvas wheel with theme-reactive alternating--kb-primary/--kb-secondaryslices viaMutationObserveron<html>; spin physics 4–6s ease-out cubic viarequestAnimationFrame;prefers-reduced-motion800ms snap; method pillsWheel/Plinko/Sniper— onlyWheelenabled; extravagant result modal with Steam + VTstats deep-link icons + Remove-from-wheel + Spin again), Coinflip (horizontal-shuffle animation with team labels from live session'ssvar1/svar2when present; mode pillsSingle/Best 3 of 5— only Single enabled), Random Map (three-reel slot machine: Popular fromvsrmaplist.jsonTags=popular/ Played frommatches.json/ Unplayed frommap-registry.jsonminus Played; pool-count pills 7+/6+/All filter all three reels; staggered reel deceleration 3s/4s/5s; reveal cards link to per-mapmap/<file>/), and Team Balonce (intentional misspell community in-joke; commander configurator with 3-scenario banner driven by 0/1/2 commanders set; candidacy scorevtsr_z + 1.5 * cmdr_experience_ztie-broken bymatches_as_commanderDESC; exhaustive2^M-subset thug-pool partition that handles odd lobbies — 4v3/5v4/3v2 — naturally with slot cap ≤5/team; drag-to-swap with live ΔVTSR; per-row commander-history badgesStrong cmdr/Cmdr-curious/Rare cmdr; the Played Meter imbalance gauge: horizontal gradient track with chevron at50% + 50% * min(|d|/1000, 1) * sign(d), four color bands (green/yellow/orange/red at 100/300/600 ΔVTSR), explicit disadvantaged-team badge on column header when|d| >= 100). Page state is purely in-memory (refresh wipes everything);beforeunloadguard fires when the dirty flag is set (manual roster non-empty, wheel spun/removed, coin flipped, map rolled, balonce computed/swapped). Three orthogonal global toggles with strict cross-toggle contract: Mode (Auto/Manual; Auto→Manual snapshots liveRoster, Manual→Auto confirm-discards manual entries), Lock lobby (freezes the surfaced roster snapshot while polling continues silently — force-cleared by ignoreLive ON and by Manual mode), Ignore live data (panic kill-switch — stops all polling, forces Manual mode + disables Auto radio until toggled OFF; toggling OFF does NOT auto-restore Auto).Reset allbutton wipes every component + restores defaults. Player join/leave Bootstrap toasts (top-right stack, max 5, 4s autohide) suppressed whenlobbyLocked || ignoreLive || mode === 'manual'AND on first-load baseline / first-poll afterignoreLiveOFF / first-poll after sessionId change / first-poll afterlobbyLockedOFF. Identity resolution chain:player_slugs.json→elo_current.ratings[].name→steamid_to_name.txt→ lobby nickname (resolver shares Steam profile URL + slug-deep-linked VTstats URL on every row). Topnav scope ("Reading C"): the formerly-elaborate LIVE pill widget inindex.html's topnav (dropdown picker + Join-via-Steam button + GameWatch chip + click-through modal) was deleted from every shell; the dashboard-onlyjs/active-game-indicator.jspolls every 60s (never faster; 60s floor while the tab is hidden, 120s error cap) and flipsdata-vt-tools-live="1"on the dashboard[data-vt-tools-link]when a known-host VSR lobby is live, driving a subtle pulse animation on theToolslink (iconbi-controller). Game Watch / Tools live-session poll at 60s idle / 15s with an of-interest lobby (visible tab only; 60s hidden floor). The indicator is not loaded on/gwor other standalone shells; pre-gen templates drop it atPLAYER_TEMPLATE_VERSION11→12 /MAP_TEMPLATE_VERSION6→7. CSS class naming:.vt-active-game-modal-*retained verbatim by the factored renderer for stability (semantic rename to.vt-live-session-card-*deferred). TheToolstopnav link is a sibling alongside Dashboard / Docs / Raw data / ODF / Models / Players / Maps on every page (Models inserted after ODF). Convention: NEVER add "Coming soon" chips, labels, or tooltip teasers to the disabled pills (Plinko / Sniper on the wheel; Best 3 of 5 on coinflip) — disabled state alone signals future-only. Full design + cross-cutting state contract in.cursor/plans/tools-page-lobby-utilities_0b86d232.plan.md. -
Models Browser (
models/index.html,js/models.js,js/models-viewer.js,css/models.css) — standalone three.js viewer for the BZCC 3D model corpus (~700 units / buildings / projectiles), promoted from the_object-render/POC. Directory grid of committed static thumbnails (search + faction / category chips + sort + Prefer-HQ) routing to a single-object viewer (?model=<stem>): full 360 OrbitControls orbit, dual performance-PNG / native-HQ-DDS textures runtime-assigned by material name, a fixed world-space sun key light with cast shadows (toggle + intensity / azimuth / elevation panel persisted inlocalStoragevt.obj.light.*), a Free spin mode (camera-locked, fling-to-spin with momentum + friction), and an HQ multi-angle Capture. three.js r170 vendored atvendor/three/(+ addonsOrbitControls/GLTFLoader/DDSLoader/BufferGeometryUtils/SimplexNoise+ the postprocessing setEffectComposer/TAARenderPass/SSAARenderPass/GTAOPass/UnrealBloomPass/SMAAPass/OutputPass+ supporting shaders). Lighting mood presets (Noon / Sunset / Overcast segmented control atop the Light pane; exportedLIGHT_PRESETStable injs/models-viewer.js— sun/hemi/ambient tints + intensity scales + shadow opacity + suggested elevation + per-bg backdrop tint; multiplies the Intensity slider, persistsvt.obj.light.preset, live with sun off, Capture forces Noon). Ultra rendering — single max-quality toggle (Scene pane, persistedvt.obj.ultra, one-shot migration from legacyvt.obj.ultra.aoAO checkbox):TAA -> GTAO -> Bloom -> Output(sRGB) -> SMAAcomposer chain (TAA = 32-sample idle-time temporal supersampling gated by_isSceneStill(); GTAO screen-space-radius ground-truth AO + Poisson denoise; bloom luminance-gated to emissives + ship lights, gate raised in light-bg mode so the backdrop never glows) plus enable-scoped upgrades reverted on disable: 4096 shadow map + DPR uncapped tomin(devicePixelRatio, 3). Quality-only by design — never touches tone mapping orscene.environment(an earlier AgX + RoomEnvironment-IBL variant muddled the hand-tuned light rig and was dropped; RoomEnvironment un-vendored — do NOT reintroduce look-changing passes under this toggle). Wireframe bypasses the stack; HQ Capture suspends it;ULTRA_*constants injs/models-viewer.jsare the knobs. Readsdata/models/(geometry GLB +textures/{perf,hq,teamcolor,emissive,normal,specular}+textures/mods/<pack_id>/{perf,hq,teamcolor,emissive,normal,specular}+ thumbnails +index.json) emitted by thescripts/object-render/conversion pipeline. Corpus-wide, picker-unaware, NOT in the pipeline cache key. Mobile: horizontally-scrollable control bar, bottom-sheet light panel,touch-action: nonecanvas. TopnavModelslink (bi-box) sits immediately after ODF on every shell. Interactive moveable parts (articulation):scripts/object-render/convert_msh.pyemits a node-hierarchy GLB for EVERY rigid multi-node model (not only the ~126 with baked clips —build_animated_glb()handles the no-clips case), preserving named moveable parts (turret_y/turret_x/recoil*nodes + thetread/fvtreadmaterial).js/models-viewer.jsbinds turret joints from the ODFturretName*declarations ONLY (anim v5 — undeclared convention-namedturret_y/turret_xnodes stay fixed, true to the engine: the ISDF Tank / Missile Scout guns are hull-fixed in game; recoil keeps the declared+convention union;js/models.jspasses empty hints instead of null forparts: nullmodels so the viewer's legacy name-convention fallback can't resurrect engine-fixed joints) andjs/models.jsadds a Parts panel: turret yaw/pitch sliders + a point-to-aim mode where the turret tracks the cursor over the model (yaw from the cursor's point on the horizontal plane through the pivot, pitch from vertical cursor position; mutually exclusive with auto-rotate / free-spin), a Fire button that pulsesrecoil*nodes back INWARD along the barrel axis (RECOIL_AXIS_SIGN = -1) and springs them home, and a Drive slider that scrolls the tread material UV (and plays the bodyforward/reversebank clip when present). Articulation is applied AFTERmixer.update()each frame and reset by Reset all / on load / before HQ Capture. New per-modelpartsblock onindex.json({turret, pitch, recoil, treads, bankClips}, null when none) drives the directory Articulated badge. Gated byANIM_FORMAT_VERSION 2 → 3; regen with--no-renderso onlygeometry/*.glbis rewritten (thumbnails/shots reused). Magnitudes (recoil distance, tread scroll rate, pitch limits) are tuned viewer-side constants (RECOIL_*/TREAD_SCROLL_RATE/ART_PITCH_MIN/MAX/ART_AIM_SENS/RECOIL_AXIS_SIGNinjs/models-viewer.js) since the source carries no authored values. Team colors (BZCC_cmask):.materialfiles declareteamColor = <stem>_c.dds(BC3: alpha = colorizable region, RGB = shading detail). The pipeline (parse_material+resolve_teammaskinconvert_msh.py) extracts these to a single perf-resolution settextures/teamcolor/<diffuse_stem>.png(no HQ variant), keyed by the diffuse/material stem so a material name maps straight to its mask; eachindex.jsonentry carries ateamColorTextureslist (~200 of ~700 models have masks).js/models-viewer.jscomposites in a GPU shader (per-materialonBeforeCompile:mix(diffuse, uTeamColor * maskLuminance * TEAM_GAIN, coverage * uTeamMix)before lighting) viahasTeamColor()/setTeamColor(hex)/clearTeamColor();js/models.jsadds a Colors panel (preset team-palette swatches + freeform<input type=color>+ Original/off) shown only for masked models + a directory Team color badge. Default OFF per open (original baked diffuse); wireframe suppresses the tint; recolor is a live uniform update.TEAM_GAINis the sole tuning knob (engine formula unpublished). Gated byTEXTURE_FORMAT_VERSION(now3) + indexschema_version(now14): a bump forces a TEXTURE-only re-emit (otherwise-fresh models reprocess to emit masks, but the GLB write is guarded so geometry GLBs don't churn). Settings panes (Light / Anim / Parts / Colors / Textures / Scene) stack in the left-edge viewer dock; the stage's top-right corner belongs to the Loadout overlay. Emissive glow maps (_e, corpus-wide):resolve_emissive()emitstextures/emissive/<diffuse_stem>.png(≤512px RGB;.material-declared name or_esuffix-convention fallback via_aux_name_candidates()); per-modelemissiveTextureslist; viewer_applyEmissive()setsmaterial.emissiveMap+ whiteemissive(glow independent of the sun); the wireframe override stashes + nullsemissiveMap(its unlit-white-lines look IS a solid white emissive). Mod texture sets:MOD_TEXTURE_PACKSregistry mirrors three workshop re-texture packs (1554202061Scion Stock-Enhanced /1581901346ISDF Stock-Enhanced /3365986032ISDF Redux — pure DDS overlays keyed by stock stems) as switchable sets: per covered stem emits 512px perf PNG + verbatim 2048 HQ DDS + the pack's own_c/_eundertextures/mods/<pack_id>/; missing pack dirs soft-skip. Per-modeltextureSetsblock ([{id, textures, teamColorTextures, emissiveTextures}], ≥1 diffuse hit; ~145 models) + top-leveltexture_packs({id: {label, url}}). ViewersetTextureSet(idOrNull)re-runs texture assignment with per-stem stock fallback (manifest-driven, caches keyed${set}:${quality}:${name}; team-color hue/mix persist; set's own mask wins, stock fills). UI: Textures dock pane (Stock + per-set rows withN of Mcoverage + Steam Workshop credit link-out — always credit),chip-skinsdirectory badge, partial coverage always offered, Reset all → Stock. Normal maps (_n, corpus-wide, SHIPPED): stock_n.ddssources are legacy-fourcc BC5-SNORM (NOT BC1/BC3 —dds_decode.pygrew a BC5 branch: two signed BC4 channels, Z reconstructed assqrt(1−x²−y²), standard[0,1]PNG encoding);resolve_normal()emitstextures/normal/<diffuse_stem>.pngat ≤1024px (NORMAL_MAX_DIM— detail survives downscaling poorly; ~401 stems ≈ 379 MB accepted); per-modelnormalTextureslist; viewer_applyNormals()bindsmaterial.normalMap(NoColorSpace,normalScale.set(1, -1)viaNORMAL_FLIP_G— DirectX→OpenGL green flip, viewer-side so it's reversible without re-emit); GLBs carry no TANGENT attribute so three.js's derivative-tangent fallback applies; no HQ DDS tier (vendored DDSLoader has no BC5/RGTC support); always-on when present, no UI. Specular→roughness maps (_s, corpus-wide, SHIPPED; texture format v5 — DUAL sets):resolve_specular()decodes once and emits BOTH conversions viaroughness = clamp((1−G)^SPEC_ROUGHNESS_K · (1 − 0.3·log10(specularPower)/2), 0.05, 1)(k=0.6;specularPowerfrom.material[solid]):textures/specular/from RGB luminance (stylized glossier look — the viewer DEFAULT, deliberate aesthetic choice) +textures/specular_true/from the alpha gloss channel (BZCC convention: spec RGB = tint, A = gloss;_spec_gloss_band()falls back to luminance for BC1 / flat-255-alpha) — game-accurate, behind the Light pane True lighting checkbox (per-open, default OFF, Reset all → OFF); mod packs mirror both dirs via_emit_mod_spec()(same stems → manifest shape unchanged); viewer_loadSpecular()routes by_trueLightingwith variant-keyed_specCache(hasSpecular()/setTrueLighting()/getTrueLighting()); spec-conversion changes require purgingtextures/specular*/+mods/*/specular*/before the regen (emits skip existing files) and themain()mod mkdir loop must list every subdir; shared stems are emitted with the race-winning model'sspecularPowerbias under--jobs N— after any spec regen,git checkout --unexpectedly-modified shared stems (v5 flipped 13, e.g. ivtank00 went visibly duller) to keep the committed bias;resolve_specular()emits ≤512px grayscaletextures/specular/<diffuse_stem>.png; per-modelspecularTextureslist; viewer_applySpecular()bindsmaterial.roughnessMapwithroughness = 1.0while mapped (map multiplies; baked 0.65 stashed inuserData.vtBaseRoughnessand restored when unmapped); metalness untouched. Both map kinds are mod-set-aware (textures/mods/<pack_id>/{normal,specular}/, per-setnormalTextures/specularTextureslists, set's own map wins + stock fills), join the wireframe stash (normalMap/normalScale/roughnessMap/roughnessnulled in the white override, restored on exit) and the dispose path (_normCache/_specCache). Tunables:NORMAL_FLIP_G(js/models-viewer.js),SPEC_ROUGHNESS_K/SPEC_ROUGHNESS_MIN/NORMAL_MAX_DIM/SPECULAR_MAX_DIM(convert_msh.py). Historical spec (incl. the BC5S format correction):models/model_render_improvements.txt. Hover altitude (index schema 15): hover/morphdriveblocks carrysetAltitude(linear meters, per the vendored ODF guide; scout 1.0 / APC 1.5 / bomber 6 / FE artillery 75; engine default 1.0);_frame()floats the hull bottom at that height (<= 0 grounded) and the lifted center drives camera target/home/Capture framing;HOVER_LIFT_SCALEtunable; drive locomotion preserves the altitude. Flight mode (index schema 16):drive.flightAltitudefromAPCClass/BomberClass/SavClass(APC 75 / Bomber 85 / SAV 150 m; 3 models: ivapc00/ivbomb00/fvsav00) powers aFlytoolbar toggle — eased climb atFLIGHT_EASE, displayed altitude scaled byFLIGHT_ALT_SCALE = 2/9(authored ceilings read too high on the bare grid), with camera/orbit-target/_center riding the same delta (view follows; sun shadow frustum re-tracked); Reset all grounds instantly (_snapFlightGrounded()). Collision radius (index schema 18):_extract_odf_collision()inconvert_msh.pybakes a sparse per-ODFcollisionRadiiByOdfmap ({odf: meters}, explicitGameObjectClass.collisionRadiusoverrides only — ~500 models; the drive-block pattern, injected on the fresh path AND_cached_entry()so a--no-renderrerun rewritesindex.jsonwith zero GLB/texture churn). The enginecollisionRadiusis a single scalar -> a SPHERE, so the viewer draws a 3D wireframe sphere (three great circles + a faint translucent shell) centered on the model body PLUS a flat y=0 ground footprint circle (COLLISION_*constants injs/models-viewer.js) sized in world meters viasetCollisionData()/setCollisionRadiusForOdf()/setCollisionVisible(); the overlay FOLLOWS the Loadout-variant<select>(re-sizes on change) and falls back to the engine default (boundingSphere * 0.75from the shippedradius) for any ODF without an explicit value. ACollision radiuscheckbox in the Scene pane (localStoragevt.obj.collision, default OFF) toggles it; suspended during HQ Capture + hidden in drive mode; Reset all → OFF. Weapon loadouts + ship lights (index schema 14):_extract_odf_loadout()+_extract_odf_lights()inconvert_msh.pybake two ODF-derived manifest blocks at enumeration time (drive-block pattern — injected on the fresh path AND_cached_entry(), so a--no-renderrerun rewritesindex.jsonwith zero GLB/texture churn): per-modelloadouts([{odf, unit, slots: [{hard, type, weaponOdf, weaponName, assault}]}], one per candidate ODF with ≥1weaponHard1..5slot,virtual_class_*skipped;unit= source ODF's unitName so foreign-unit variants sharing the mesh get labeled in the dropdown (e.g. ivapc00 →ivsnowplow — Snowplow APC) and a lone foreign variant keeps the select visible; slottype= hardpoint-name prefix vs the 8WEAPON_HARD_CATEGORIEScodes GUN/CANN/MORT/ROCK/SPEC/SHIE/HAND/PACK, weaponwpnCategoryfallback;weaponNamevia_lookup_weapon()followinginheritanceChain; ~90 models) +defaultLoadoutOdf(VSR-preferred: shortest*_vsr.odf, else a stock/same-unitName candidate; null when only foreign-unit variants are armed → viewer defaults to aNone — <unit> has no hardpointsoption with foreign variants opt-in) +lights([{hard, lightOdf, kind, color, range, attenuation, coneInner/Outer, onlyWhenPiloted}]fromlightHard1..8/lightName1..8resolved against Effect-category LightClass ODFs — authored color/range/cones; ~141 models). Viewer (js/models-viewer.js):_applyShipLights()builds a realTHREE.PointLight/SpotLightper def PARENTED to itshp_light_*node (tracks articulation; forward = node-local -Z,SHIPLIGHT_FORWARD_SIGN = -1measured, +SHIPLIGHT_AIM_DOWN~8° down-tilt; color components > 1 normalized into intensity; additive bulb sprite per source; missing nodes silently skipped); spots also render a soft beam of 3 nested additive cone shells (node-parented;SHIPLIGHT_BEAM_SHELLSdiffuse the edge) + a scene-rooted WIDE-oval ground-pool decal aligned to the beam heading (SHIPLIGHT_POOL_SPREAD/_ASPECT) tracked per frame by_updateShipLights()(the floor is a ShadowMaterial — invisible to real lights); default ON per open;hasShipLights()/setShipLights()/getShipLightsOn();Ship lightscheckbox row in the Light pane (exempt from the sun-off dim); Reset all → ON; intensity tunablesSHIPLIGHT_*. Loadout overlay (#loadout-panel, top-right of the stage): collapsible glass card (collapsed on mobile) with ODF-variant<select>(defaultdefaultLoadoutOdf) + slot rows grouped by (type, weapon, assault) with×Nmultiplicity (same-type hardpoints fire together in-game), weapon links toodf/index.html?odf=<weaponOdf>+ mono ODF-stem chip, em-dash empty slots, row-hover hardpoint highlighting viahighlightHardpoints(names|null)(HP_MARKER_*).SLOT_LABELSinjs/models.js; semantics per the vendoreddocs/reference/odf-properties-guide.md(GenBlackDragon's ODF Documentation, BZCC 2.0.185 — grep it, never machine-parse it). WASD Drive Mode (Tier 1 + Tier 2): every driveable model gets aDrive · WASDtoggle in the Parts pane — REAL locomotion on the_spinpivot at ODF-accurate speeds while the archetype's gait animates in place. Controls are archetype-aware: hover/morph use the game-true scheme — W/S throttle, A/D strafe (ODFvelocStrafe, own smoothed ramp), ArrowLeft/Right turn, ArrowUp/Down hull aim pitch (no turret on these craft; the whole ship tilts to aim, clamped atDRIVE_AIM_PITCH_MAX_DEG, slewed atDRIVE_AIM_PITCH_RATE, chase cam follows); walker/tracked/pilot keep W/S throttle + A/D steer with arrows force-enabled as turret aim. Per-modeldriveblock onindex.json({archetype, velocForward, velocReverse, velocStrafe, omegaTurn, omegaSpin, alphaSteer, animSteer, setAltitude}, null when non-driveable; ~94 models;setAltitudehover/morph only) baked by_extract_odf_drive()inconvert_msh.py— archetype from movement-class priorityWalkerClass > MorphTankClass > TrackedVehicleClass > HoverCraftClass > PersonClass(walker/morph/tracked/hover/pilot; pilots read the*Run-stance speeds), omega values > 10 treated as deg/s and normalized to rad/s at bake,alphaSteer= angular acceleration rad/s² passed through unnormalized. Indexschema_version 8 → 9 → 10 → 11 → 12(additive; regen with--no-render, zero GLB/texture churn). Turn response is engine-true: the yaw rate ramps toward the commandedomegaTurn/omegaSpinat the ODF'salphaSteer(floored byDRIVE_ALPHA_MINso the walker's authored 0.1 rad/s² stays keyboard-playable) and coasts back to zero on release — never an instant snap to full omega; a persisted Turn response slider in the Parts drive section (25–100%, default 45%,localStoragevt.obj.drive.turnSens, viewersetDriveTurnSens()/DRIVE_TURN_SENS_DEFAULT) scales the commanded rate since a held key is always full deflection (in-game the mouse is analog). Gait mapping (engine-true): every bank clip in the corpus (forward/neutral/reverse±2) is a 3-keyframe (0.067s) authored LATERAL ARC, not an animation — frame 0 = full left-bank extreme, middle frame = the symmetric stance, last frame = full right-bank extreme (left/right parts mirror, verified onfvsent_skel+ivscout00). The viewer pins all three actions paused at the MIDDLE frame (rest in drive mode = the normal-viewer pose) and (a) drives their weights from a smoothed throttle (DRIVE_ACCELramp;wFwd = max(v,0),wRev = max(−v,0),wNeutral = 1−|v|, always summing to 1 — idle craft hold the neutral STANCE, not the bind pose; craft with identical stances like the ISDF Tank's tail fins correctly read as static) while (b) scrubbing their shared time across the arc from a smoothed lateral value (DRIVE_LAT_ACCELramp): strafe at full strength + the SMOOTHED yaw-rate fraction atDRIVE_TURN_ARC_GAIN, signed by motion — side-slip OPPOSITE the yaw while moving forward, WITH the yaw at standstill/reverse (matches in-game observation); arc handedness flippable viaDRIVE_ARC_SIGN. Locomotion + tread scroll ride the same throttle (the vehicle accelerates/brakes; the manual Drive slider is a direct throttle into the same blend, arc centered). Steer lean (DRIVE_TURN_ROLL/DRIVE_LEAN_LERPprocedural bank-into-turn) applies ONLY to craft whose ODF declaresanimSteer(drive.animSteerflag — effectively the ISDF Scout class; the steer pose is baked into zero GLBs) — all other craft keep a flat hull through turns, true to the game. Morph adds a Deploy toggle playingdeployonce (reversed on undeploy) then re-targeting the blend at theforward2/neutral2/reverse2bank; tracked = tread UV scroll; walker =run/walk+turn+idleand pilot =walk+standas genuine looped cyclic gaits (1.0–6.6s) crossfaded overDRIVE_GAIT_FADE; missing clips degrade gracefully (locomotion still runs). Infinite floor: locomotion is unbounded — a large, denser drive grid (max(DRIVE_GRID_MIN, radius * DRIVE_GRID_FACTOR)atDRIVE_GRID_DIVSline density) re-centers under the vehicle in whole-grid-cell increments per frame (lines never slide), the invisible shadow plane + sun shadow frustum follow continuously, and a drive-only fog band (recomputed per frame from the camera-to-vehicle distance) hazes out the floor's far edge like a horizon without ever fogging the model. Drive scenery: world-fixed low-poly pyramid landmarks (oneInstancedMesh, ≤2 perDRIVE_SCENERY_TILEtile atDRIVE_SCENERY_DENSITY, deterministichash2(tileX, tileZ, salt)placement so the "world" is stable across revisits) populate a 7×7 tile neighborhood rebuilt only on tile crossings — true parallax movement reference (they emerge from the fog, slide past, recede); home spot kept clear (DRIVE_SCENERY_CLEAR_RADII); built on drive entry, disposed on exit. Chase cam: third-person behind+above, facing the firing component's aim direction (hull yaw + turret yaw when present), shortest-arc smoothed; wheel zooms the follow distance (OrbitControls disabled while driving); right-drag free look orbits the camera around the vehicle (yaw + clamped spherical pitch,CHASE_ORBIT_YAW_SENS/CHASE_ORBIT_PITCH_SENS/CHASE_ORBIT_PITCH_MIN/MAX) with the offset PERSISTING until theReset viewbutton (shown next to the Drive toggle only while driving, viewerresetChaseView()— also clears wheel zoom) or drive exit; the canvascontextmenuis suppressed while driving so right-drag never pops the browser menu; vertical aim follow — when the model can pitch (turret_x / walker head, else the hover/morph hull aim pitch) the camera trails aim pitch too: aiming up drops the camera (CHASE_PITCH_DROP) and raises the look target (CHASE_PITCH_LOOK), aiming down mirrors, floor-clamped atCHASE_MIN_HEIGHT_RADII. Arrow-key turret slew is force-enabled during drive so users drive AND aim simultaneously — EXCEPT on the hover/morph scheme, where the arrows belong to hull turn/aim pitch;Escexits; drive mode is mutually exclusive with auto-rotate / free-spin / aim-at-cursor and force-exited by Reset all / model swap / HQ Capture /resetArticulation(). Hidden on touch-only devices ((hover: hover) and (pointer: fine)gate). Viewer API:setDriveProfile/getDriveCaps/setDriveMode/setDriveInput(fwd, turn, strafe, pitch)/setDriveDeployed/setDriveTurnSensonObjectViewer; tunablesDRIVE_SPEED_SCALE/DRIVE_FALLBACK/DRIVE_ALPHA_MIN/DRIVE_LAT_ACCEL/DRIVE_TURN_ARC_GAIN/DRIVE_ARC_SIGN/DRIVE_AIM_PITCH_*/DRIVE_GRID_*/DRIVE_FOG_*/DRIVE_SCENERY_*/CHASE_*injs/models-viewer.js. -
ELO Page (
elo/index.html,js/elo.js,js/vtsr-explainers.js,css/elo.css) — the project's dedicated VTSR-T surface and the canonical home of the full VTSR-T leaderboard (the 13-column sortable table + per-row expandable detail panels MOVED here from the dashboard's All Matches view; the dashboard's#section-vtsrcard is now a teaser: top 5 non-provisional players + muted provisional chips that never occupy a top-5 slot + a "View full leaderboard" link rendered byrenderVtsrTeaser()injs/app.js). Six nav-pills with?tab=URL routing (vtsr-tdefault ·vtsr-c·how·axes·fairness·accuracy), lazy per-tab rendering, deep-link boot. The default VTSR-T pane writes NO?tab=at all; the pre-splitleaderboardslug is a permanent inbound alias (TAB_SLUGSmaps both it andvtsr-tto#elo-tab-leaderboard, andactivateTabFromUrl()normalizes a legacy alias out of the URL on boot). The VTSR-C Elo pill (#elo-tab-vtsr-c) is table-only (mirrors the VTSR-T pane): the VTSR-C commander-ladder card (#section-vtsr-c, lazy-rendered byrenderCommanderLadder()from a 404-safeelo_commander_current.jsonfetch: experimental banner, Rank · Commander · VTSR-C · Record W-L-D · Win % · Games · Peak · Last Δ, provisional badges below 5 rated games, own#elo-vtsr-c-emptyempty state); the ladder's explainer lives on the How tab and the card's "How It's Calculated" button switches to that pill. How it works renders the annotated ΔR = K(P−E) "chalk-talk" stage plus the 13.1 α-blend stage (two-dials story: the wins LADDER is real and running, only the mixer α is 0), the "The commander ladder (VTSR-C)" section (commanderLadderHtml()injs/vtsr-explainers.js— mirror-blend story + static KaTeX handicap formula + real Oldboy worked example, closing with a link to the VTSR-C Elo pill), tier ladder, and the Domakus worked example; The 8 axes renders an annotated mixing-board (weight bar whose segments are the stage anchors, solo-reveal mode) + weights table + a "What about VTSR-C?" addendum (axes power VTSR-T only; VTSR-C's five economy axes are recorded-not-scored until ≥25 telemetry duels; link to the axis-outcome study); Commanders & fairness is a card grid of the fairness rules (commander axis-shift, campod/partial omission gates, pilot-victim exclusion, low-tier at-base lift, the commander ladder + stacked-thugs handicap cards, loss aversion + floor); Does it work? renders honest accuracy stats from the committeddata/processed/validation_summary.json(headline stat cards with layman captions, winner-provability funnel, rating-gap breakout, VTSR-C duel-prediction card + λ-ablation table and the axis-vs-outcome sign-agreement bar list — both absent-safe fromlatest_detail.vtsr_c/latest_detail.axis_outcome, Chart.js metric-history sparkline; friendly empty state on 404). All explainer content + the generalized annotated-stage engine live in the sharedjs/vtsr-explainers.js(window.VTSRExplain:initIn(root)+deltaRBlockHtml/blendBlockHtml/axesBoardHtml/methodologyModalbuilders; stage CSS =.vt-eq-*/.vt-axesboard-*incss/vtstats-theme.css), loaded by BOTHindex.html(the dashboard's slimmed methodology modal, whose footer now links toelo/?tab=how) andelo/index.html. KaTeX terms are\htmlId-tagged (trust:true, strict:false— author-controlled input); connector endpoints are measured from the live DOM each draw with a ResizeObserver redraw; below 620px the free-form layer collapses to the stacked.vt-eq-list(sr-only on desktop). Explainer copy is held true to the shipped constants — the R^W wins ladder is real and running (only the mixer α sits at 0, justified by the failed pre-registered promote rule incritique/decisions/phase-5-wins-blend.mdplus the team-result-vs-individual-rating argument; never "stubbed at 1500" / "winners can't be proven"), and VTSR-C's economy axes are "recorded, not scored" at α_c = 1 (never "no axes yet"). Worked-example digits are real corpus values with source comments — re-read those history entries after any re-rate. Data:elo_current.json+elo_history.json, the commander pair (elo_commander_current.json+elo_commander_history.json),player_slugs.json,validation_summary.json, andmatch_contributions.json→VTAggregate.build(..., {minMatchesThreshold: 0})for the corpus-wide career join (Ship / K/D / Acc columns + detail panels). Corpus-wide, picker-unaware, NOT in the pipeline cache key. TopnavELOlink (bi-trophy) sits between Players and Tools on every shell + both pre-gen templates (Players reordered next to ELO atPLAYER_TEMPLATE_VERSION10 → 11,MAP_TEMPLATE_VERSION5 → 6).
Key File Locations
- Proto schema (source of truth, v4 — current):
scripts/statsgate.proto. v4 is purely additive over v3 (no v3 freeze needed — the current descriptor decodes the whole corpus):ResourceState team1_resources/team2_resourceson everyUpdateTick(per-tick team scrap bank: current/max scrap, derivedScrapStatus, pool + upgrade census) and theBuildEvent build_event = 9oneof arm (QUEUE/CANCEL/BUILD × FACTORY/CONSTRUCTOR/ARMORY + teamnum + build_odf; recycler builds arrive as FACTORY — pipeline reverse-map disambiguates; armory odfs inconsistently.odf-suffixed on the wire — normalized). v4 detection is presence-based and runs BEFORE theheader.playerscheck inload_session()/fetchAndDecodeBinpb()(any build_event or resource-bearing UpdateTick → v4; a degenerate empty-roster v4 file must not fall through to the frozen-v2 re-parse). Downstream: per-matcheconomy+buildsblocks (match-global, always-unfiltered;has_resource_data/has_build_dataflags default absent-=-false), the per-match Economy tab, highlights 12 → 16 (the_tycoon/ Elon Musk = Σ positive full-rate bank deltas,loose_collector/ Loose Collector = income_loose,war_machine/ Conveyor Belt = combat-ship value fielded,first_upgrade= first live upgraded pool), thecommander_economycontribution slice → career econ rollup (Commanders-tab Economy Leaders card, player-page commander econ block,career_tycoon/career_loose_collector/career_war_machinecards), raw-browserbuildEventarm + reconcile rows, and the first-batch audit runbookscripts/audit_build_events.mjs.PIPELINE_VERSION 29 → 30 → 31 → 33 → 34 → 35 → 36 → 37,match.schema_version 16 → 17 → 18 → 19 → 20 → 21. v21 (build-feed economic context, purely additive, display-only): everybuilds.feed[]row carriesscrap_at_event/max_scrap_at_event/pool_count_at_event— the team's bank, storage cap and pool count read at that row's own tick — driving the Build Order Log's four-segment badge (cost | bank | cap | pools). Stamped through the same "latest wire value" join that already writesscrap_status_at_*(econ_state[side]gainedcur_bank/cur_cap/cur_poolsbesidecur_status; do NOT reuse the income classifier'sprev_*keys). A queue row reports the PRE-purchase bank (the engine debits on the following tick), v24 a matched BUILD copies that QUEUE-time triad (orphan BUILD keeps completion-time);nullwithout resource data, where the badge degrades to the bare cost. Verified against the wire by_investigation/probe_feed_context.py; rating-inert for free (golden_builds_inert.pyblanks the whole feed). Shipped alongside a Build Order Log UI pass: taller scroll area, a commander-scope group that renders one commander's log full width, and a debounced find bar over the displayed unit name / ODF stem / producer lane. v20 (build ledger: charge-at-START + bulk cancel, display-only): the ledger assumed scrap is deducted at QUEUE, but the engine charges each unit when it starts building (isolated queues coincide — that is why the original 92-96% audit could not separate them). Gate_investigation/golden_builds_inert.pyperturbs every affected field and requires byte-identical VTSR-T + VTSR-C, sparingscrap_spent_units/ships_builtsince VTSR-C's econ composite reads those and this change does not touch them. Each producer (recycler / factory / armory, one-per-team — a faction factory upgrade REPLACES its predecessor) holds ONE order at a time, stackable N-deep with the same ODF, and a bulk cancel clears the ENTIRE queue while only the in-production unit was ever charged: a burst now books ONE unit, enqueues ONE refund expectation, frees the rest and resets the lane queue. Verified on the wire (burst size == tracked depth on every clean case; a 3-Harvester stack cancel returned ~10 scrap not 30; strictly sequential production at exactly 200 then 71 ticks apart; only 205 of 784 pod QUEUE ticks show the charge at queue). Two-level bookkeeping — the per-(side, stem)FIFO still matches events to orders while a new per-(side, lane)mirror tracks production; do not re-key the FIFO to lane (blind head-popping mis-attributes once tracking drifts). Collector gap: bulk-cancel emission caps at 10 CANCEL events (every depth>burst case has burst == exactly 10, all pure Service Pod queues — pods alone exceed the engine's 10-slot queue, reaching depth 39, while no non-pod queue passes 7), soorders_trimmedderives from tracked depth NEVER burst size and the shortfall iscancel_events_truncated.outflow_pending_at_end_costbecomes the in-progress head unit per lane (186 → 20); newoutflow_clamped_costnames scrap destroyed when a dying pool dropsmax_scrapbelow the bank (20 / 54). The outflow ledger is now a strict partition (six components +scrap_outflow_unaccounted==scrap_outflow_gross, gated); residual -552 → +24 and -72 → -64, the remainder being the deliberately-untouched inferred-mode constructor lane. Newbuilds.teams{n}fieldsorders_backed_out/orders_trimmed/cancels_unmatched/cancel_events_truncated/orders_open_at_end;units_cancelledis now a raw-event-count reconcile field only. Card:Cancelled orderssplit intoOrders backed out(real sunk cost) +Queue trimmed(free). v19 (income classifier + human-only thug losses, display-only): three measured corrections gated by_investigation/golden_income_thug_inert.py, which PERTURBS every income /thug_supplyvalue (2090 fields) and requires byte-identical VTSR-T + VTSR-C — value-insensitivity, which a value correction needs (the v18 gate only strips fields). (1) refund-match floorECON_REFUND_MIN_MATCH = ECON_LOOSE_SIZE(5): Service Pod'scost // 2= 1 was fabricating 74 of team 1's 114 "refund" scrap off plain +1 regen ticks. (2) cap-clamp + withheld-overflow rules: a 5-scrap loose pickup landing with under 5 headroom is credited only as far as it fits and the engine HOLDS the rest until there is room (measured one tick / 0.05 s after a purchase; release tracks headroom, not planting a scav — 61 of 62 pool plants moved the bank by 0 or +1). Both halves used to land inincome_unclassified(127 of its 151 scrap); now the at-cap delta books toincome_loose(+1 pickup) withpending_overflowcarrying the remainder for a later sub-cap 2/3/4 draw-down (no piece-count bump), leavingincome_unclassifieda genuine anomaly detector near zero. The four income fields are a strict partition ofscrap_income(gated every match). (3)thug_ships_lost/thug_ship_value_lostnow count human-piloted losses only — the one non-additive change, so values moved: the old kill-feed scan gated only onvictim_teamlanding in a thug slot and swept in AI craft owned by that slot (victim Steam64 0, rendered as literal"Team 1"/"Team 2"), 659 rows corpus-wide = 46% of team 1's figure on the first v4 match (54 → 29; corpus 3088 → 2921). Now derived fromleaderboard[].per_ship_combat[].deathsfiltered tocombat_ship_odfs, human-only by construction. Addsthug_supply.{n}.thugs[](per-thugname/steam64/slot/ships_lost/ship_value_lost/pilot_sec/at_base_pilot_sec/reship_median_sec/reship_spells, pipeline-sorted heaviest-first) rendered as indented sub-rows withvtPlayerLinkHtmllinks underThug ships lost/Thug time on foot; team totals equal Σ over the array exactly whilereship_median_secstays a pooled median. Onlyelo_history.jsonlacks acomputed_atstamp, so it is the hash that proves zero rating drift after a reprocess. v18 (Economy card v2 + thug supply, purely additive, rating-inert): new ALWAYS-present top-levelthug_supplyblock (per-team thug ships/value lost + on-foot time and pooled median re-ship time from positioning — corpus-wide, NOT v4-gated); neweconomy.teamsfieldspeak_max_scrap/green_sec/yellow_sec/red_sec/upgrades_built; new positioning metricspilot_sec/reship_median_sec/reship_spells(spell-based: maximal on-foot runs, excluding censored tails, the index-0 spawn run, and hops underRESHIP_MIN_SPELL_SEC = 10). The Economy card was reworked around peaks/cumulatives (finals dropped), every row gained a plain-language tooltip, the derived-red rounding bug was fixed (readred_share/red_secdirectly), andmedian_rebuild_latency_secwas RETIRED from the card (still emitted; career surfaces unchanged) because a post-loss build QUEUE is unattributable to the dead thug and invisible when the commander re-ships from stock. Gate:_investigation/golden_v18_no_drift.pystrips every v18 field and requires byte-identical VTSR-T + VTSR-C. v3 (retained): per-tick-accumulatedrepeated PlayerInfo playersroster (captures late joiners; empty slots can emit garbage Steam64s — validity-gated by_valid_steam64()in the pipeline's_build_identity_maps()shim, which takes the UpdateTick union as the participant set — header-only 0-tick ghosts are dropped — then places active players evidence-first at the majority-vote slot from the engine's per-eventteamattributions becausePlayerInfo.teamnumis only the first-seen slot and lobby shuffles corrupt it silently) andOutcome game_outcome(host-attested end-of-game dialog result: team win / draw / cancelled), combined with kill-feed inference viaresolve_match_outcome()'s evidence-first trust ladder (a clean_win inference beats a contradicting attestation →disputedflag). - Frozen proto schemas:
scripts/statsgate_v2.proto+statsgate_v2_pb2.py+vendor/protobufjs/statsgate_v2.proto.json(v2, 2026-04..08 era — identity maps readable) andscripts/statsgate_v1.proto+statsgate_v1_pb2.py+vendor/protobufjs/statsgate_v1.proto.json(v1, pre-Nomad). Used only by the triple-descriptor fallback path inscripts/process_stats.py::load_sessionandjs/raw-browser.js::fetchAndDecodeBinpbso the historical corpus still decodes byte-identically — v2-vs-v3 is aheader.playerspresence check (no wire conflicts in either direction), v1 is parse-failure (JS) / races+oneof fingerprint (Python). The pipeline normalizes all three schemas to a single internal shape (DamageEventadapter + identity shim); downstream aggregators are schema-agnostic. Detected schema is stamped onmatch.proto_schema_version(debugging telemetry only — UI never branches on it). - 3D Replay v26 (
PIPELINE_VERSION46,match.schema_version26): upstreamBuildEvent.build_position = 6(BUILD-only Vec3) synced intoscripts/statsgate.protoand emitted asbuilds.feed[].position;trail.target[](0/1 T-lock) +trail.speed[](authoredPlayerState.speed); top-levelstructures[](starting recyclers + constructor BUILD xyz;UnitDestroyed-only deaths; turret-classdeath_reason: "untracked"whileTURRET_DEATHS_RELIABLEis False — never HP-infer deaths). HUD: scrap meters from census, unified event feed (pods off by default), v20 now-building, beat toasts, desktop Elo Δ strip / compact roster sheet. Rating-inert (_investigation/golden_replay_v26_inert.py). Engine/HUD live under_map-analysis/render/. - Pipeline:
scripts/process_stats.py - Radar renderer:
js/charts-radar.js(Player Performance Radar — 8-axis spiderweb with single / compare / team / career modes; Survivability is a p95-clipped composite of damage-trade ratio and K/D, career mode adds Bayesian shrinkage toward the league mean; reusesactiveChartsandglassTooltipConfigfromjs/charts.js) - ODF weapon DB:
data/odf.min.json - Raw data:
data/sessions/<username>/*.binpb.gz(gzip-compressed protobuf, organized by submitter) - Processed output:
data/processed/*.json - Theme layer:
css/vtstats-theme.css(glass surfaces, typography, animations, migrated inline styles) - Effects engine:
js/vtstats-fx.js(animated counters, entrance stagger, Chart.js shadow plugin) - Replay player:
js/timeline-player.js(Replay tab engine: animated timeline playback, transport controls, companion stats) - Positioning renderers:
js/positioning-charts.js(Positioning tab: Movement Leaderboard, combined + per-player heatmaps with shared viewport/intensity + legend, distance timeline, ring histogram) - Positioning player:
js/positioning-player.js(Positioning tab animated trail playback with transport controls, sub-second trail interpolation, teleport-segment gaps; exposeswindow.VTPositionPlayer) - Storyline tab (match.schema_version 22, PIPELINE_VERSION 39):
compute_storyline()+restamp_storyline_outcome()+build_extractor_odfs()inscripts/process_stats.pyemit the per-matchstorylineblock (bucketed lanes + wire-enum scrap-status band segments + curated structured-args beats + typed facts + archetype) on every match carrying BOTH v4 halves;js/storyline.js(window.VTStoryline, loaded aftercharts.js) renders it as the Storyline tab (#tab-storyline, pill immediately left of 3D Replay, Economy-style hide+bounce pre-v4): auto-narrative fromSTORY_COPYclause templates (every sentence fact-backed; archetypesdivergence/comeback/stomp/attrition_grind/even/unclear), team-attributed verdict cards, four drag-zoom-synced Chart.js lanes (momentum + bands / map control + intruders / war machine / intensity; gold weight-5 beat flags; crosshair synced with the beats rail), and the key-moments rail (chevron-expandable payloads; row click →window.vtOpenReplayAtTick(tick)reboots the 3D-replay iframe at that second). Match-global, always-unfiltered; rating-inert (gates:_investigation/golden_storyline_inert.pystrip+perturb,_investigation/check_story_templates.mjstemplate coverage); adjudication outcome changes restampfacts.winner/facts.archetype/the result beat on cached matches;facts.base_defensereserved null plug for future structure-location telemetry;.vt-story-*styles incss/vtstats-theme.css. Full reference:docs/DATA_DICTIONARY.md§5 +.cursor/rules/data-schema.mdc"Storyline block" +DEVELOPER_GUIDE.md"Storyline Tab". - Per-match Elo tab:
js/match-elo.js(window.VTMatchElo) joinselo_history.json(already fetched on dashboard boot) to the current match and renders#tab-elo— lobby Δ list (default-selects the biggest gainer), selected-player P-vs-E verdict + 8-axis bars, P-vs-E scatter, and a compact VTSR-C two-row strip whenelo_commander_history.jsonhas a duel for this match (self-hides on the historical undetermined corpus; new determined matches should show it). Always-visible pill (empty-state when the match was excluded from rating). Match-global, always-unfiltered (highlights passthrough); the per-match player filter only preselects a row. Helped/hurt copy excludes the v2.10 luxury axes (LUXURY_AXES). Deep link?tab=elo. No pipeline / schema bump. - Match Highlights:
compute_highlights()inscripts/process_stats.py(per-match award catalog — fixed slate of 12 always-on cards: The Bully, Grim Reaper, Bullet Sponge, Hustler, Sharpshooter, Gunner, Puppeteer, Frenemies, Roadrunner, Pod Goblin, Chris Kyle, Locksmith, plus v4 Elon Musk / Loose Collector / Conveyor Belt / First Upgrade) +renderHighlights()(mode='match'|'career') +HIGHLIGHT_COPY/CAREER_HIGHLIGHT_COPYinjs/app.js+.vt-highlights-card/.vt-highlight-tileblocks incss/vtstats-theme.css. Section markup at#section-highlightsinindex.html, immediately below#section-factioninside#tab-overview. Match-global, always-unfiltered (renderer readscurrentData.highlights, never the filtereddataview). New top-level field added atmatch.schema_version = 2; pre-v2 matches simply have no block (UI hides the section). Cards self-omit when their data gates fail (positioning / pickups / T-key / snipe / resource gates). The Career Highlights sibling lives at#section-career-highlightson the All Matches → Overview tab; built client-side bybuildCareerHighlightsInline()injs/all-matches-aggregator.js(24 cards: 12career_*siblings + 12 cross-match-only originals likethe_champion/the_veteran/the_workhorse/the_carry/the_anchor/isdf_loyalist/hadean_loyalist/scion_loyalist/the_diplomat/map_master/streak_king/the_polymath). - VTSR / VTSR-T (VT Stats Rating — Thug) — pipeline-side Thug ELO (
scripts/elo.py) built on an eight-axis thug composite (v2.3) with a per-match commander role adjustment (v2.4). v2.4 makes the unified VTSR-T leaderboard role-fair: each commander match-row gets a per-axis additive shift in post-clip space (afterclip(z, -2, +2) / 2) then re-clipped to[-1, +1]. Asymmetric design: 4 audit-derived priors (mobility,thug_kill_rate,net_damage_share,thug_efficiency) ride a shrunk rolling baseline (COMMANDER_BASELINE_SHRINKAGE = 30) so live data takes over the seed as the corpus grows; 2 hand-tuned priors LOCKED at seed (target_lock_pctcushion at -0.10,pve_sharereward boost at -0.05) — no shrinkage so design intent doesn't drift; 2 axes role-blind (thug_accuracy,snipe_bonus— effect sizes at/below noise floor). New constants inscripts/elo.py:COMMANDER_AXIS_PRIOR,COMMANDER_BASELINE_SHRINKAGE,COMMANDER_BASELINE_LOCKED_AXES. Newcommander_shrunk_baseline(axis, sum, count)helper;compute_performance_index()takes optionalcommander_baseline_snapshotarg; commander rows carry an optionalaxis_contributions_metablock ({axis: {z_pre_shift, shift, z_post_shift}}) on each delta inelo_history.jsonfor forensics — flataxis_contributionsshape unchanged so existing JS rendering works as-is. New top-level fields onelo_current.json:commander_axis_prior,commander_baseline_shrinkage,commander_baseline_locked_axes,commander_baseline_observed(withlocked: boolper axis). New per-rating fields:matches_as_commander,matches_as_thug. Architectural rename (v2.3, retained): Combat ELO → Thug ELO (combat_eloJSON field →thug_elo;COMBAT_WEIGHTSPython dict →THUG_WEIGHTS; math notation R^C → R^T). Outputs:data/processed/elo_current.json(schema_version: 5) anddata/processed/elo_history.json. Per-axis attribution (v2.3): each delta carriesaxis_contributionsand each rating carriesaxis_means(powers the VTSR-T leaderboard's expandable detail panel). Renderer:renderVtsrLeaderboard()injs/app.jspowers#section-vtsr— 14-column primary row (Tier · Player · Primary Ship · VTSR-T · PvP K/D · PvE K/D · Acc · PvP Acc · Last · Peak · Matches · Trend) with chevron-toggled per-row detail panel (combat split + career-axis bar grid + last-match axis breakdown + ship loadout top 5 + peak context). Loadout / per-ship combat blocks are organized per ODF with pretty ship names ("Tank", "Scout", "Assault Tank", "Pilot") viaprettify_odf()— no class taxonomy. Corpus-wide, NOT picker-filter aware. Schema bumps:ELO_SCHEMA_VERSION 4 → 5,PIPELINE_VERSION 14 → 15. Pre-v2.4peak_vtsrvalues are no longer comparable — corpus re-rated. v2.5 (current) — row-level exclusion gates: two new boolean flags on each per-matchleaderboard[]row set byscripts/process_stats.py(is_campodwhen > 25% of match wall-clock spent in a camera-pod ship fromCAMPOD_ODFS={evcamr_vsr, fvcamr_vsr, ivcamr_vsr, camerapod_vsr}.odf;is_low_activitywhen the event-stream presence window covers < 75% of match duration — catches late joiner
Truncated - read the full file at https://github.com/sevsunday/vt-stats/blob/9d2f183da02ecad379e88187edfab8be6275ae27/.cursor/rules/project-overview.mdc.