Where MvM cash vanishes on mvm_mannhattan: TF2 currency-pack physics and auto-collect rules in a 3D viewer
  • JavaScript 68.9%
  • Python 17.5%
  • CSS 7.5%
  • HTML 5.7%
  • Shell 0.4%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
zclaude fb5edd6007 browser_check: test the no-nav demo, wait for chrome before cleanup
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 00:24:23 -04:00
extract Ship the map's nav mesh: no-nav rule applied, nav-based findings and analysis 2026-10-27 00:22:35 -04:00
scripts Ship the map's nav mesh: no-nav rule applied, nav-based findings and analysis 2026-10-27 00:22:35 -04:00
tests browser_check: test the no-nav demo, wait for chrome before cleanup 2026-10-27 00:24:23 -04:00
web Ship the map's nav mesh: no-nav rule applied, nav-based findings and analysis 2026-10-27 00:22:35 -04:00
.gitignore Mannhattan cash box: BSP extractor, TF2 cash physics port, nav reader, three.js viewer 2026-10-26 21:20:15 -04:00
README.md Ship the map's nav mesh: no-nav rule applied, nav-based findings and analysis 2026-10-27 00:22:35 -04:00

Mannhattan Cash Box

Where MvM cash vanishes on mvm_mannhattan, simulated with TF2's own rules. Live at https://apps.wizardhat.info/mvm-cashbox/

Click the map to kill a robot (or giant, or tank). Each currency pack is moved the way the server moves it and judged the way the server judges it when it stops.

What the simulation follows

From the Source SDK 2013 TF2 server code:

Rule Source
Pack is a 20-unit box (SetCollisionBounds(±10)), lifetime 30 s entity_currencypack.cpp Spawn, tf_powerup.h
Robot drop: from WorldSpaceCenter, RandomVector(-1,1) with z=1, normalised, ×250 tf_player.cpp DropCurrencyPack
Tank drop: z = RandomFloat(5,20), ×250×RandomFloat(1,4) tf_base_boss.cpp
MOVETYPE_FLYGRAVITY + MOVECOLLIDE_FLY_BOUNCE; bounce = reflect × surface elasticity (≤0.9); floor if normal.z > 0.7; stop under 30 u/s tf_powerup.cpp, physics_main_shared.cpp
Collides with MASK_SOLID plus player clips item_world.cpp PhysicsSolidMaskForEntity
Stuck with zero velocity off the ground → treated as landed item_world.cpp FallThink
At rest: no nav area (5 above … 120 below the centre) → paid; inside an enabled trigger_hurt's AABB → paid; inside any func_respawnroom's AABB, either team → paid entity_currencypack.cpp ComeToRest, nav_mesh.cpp GetNavArea

Box traces against brushes use every brush side including bevels, as the engine does. Displacements are traced as triangles; ones compiled with "No hull collision" (ddispinfo_t.minTess high bit set, flag 0x4) are skipped, since box traces pass through them.

What it does not model

  • Static props. Their collision shapes are in TF2's VPKs, not the map. Positions are shown.
  • Per-surface elasticity. Comes from each material's $surfaceprop, also in the VPKs. A single value (default 0.25, adjustable) stands in.

Nav mesh

The BSP has no nav inside it. scripts/build.sh ships maps/mvm_mannhattan.nav (or a second argument) next to the map data, and the page loads it. Its header records the BSP size it was built for; the extractor checks it against the map. A different .nav can be dropped onto the page.

node tests/nav_analysis.mjs runs the whole map with the nav and reports, for every no-nav payout, why GetNavArea missed (off the mesh / dip under it / stranded high above) and what the pack rests on. With the shipped nav, for robots killed anywhere they walk: 17% of packs (19% for giants) are paid by the no-nav rule, mostly in the strip the nav leaves along walls, and 55% of those rest on displacements.

Build

scripts/build.sh maps/mvm_mannhattan.bsp       # writes web/data/ (map + nav) and dist/
node tests/sim_test.mjs                          # physics against the real map
CHROME=<chrome-headless-shell> node tests/browser_check.mjs dist <shots dir>   # end to end

extract/bsp_extract.py reads VBSP v20 with the standard library only.