diff --git a/README.md b/README.md index f19147e..2df92d8 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,11 @@ Controls: WASD/arrows drive, Shift drift, Space boost, R reset (or retry after C - **Cruise** — free roam with traffic. - **Crash Mode** — 60 seconds, rack up damage dollars ramming traffic and parked cars. +- **Crash Junctions** — pick a "CRASH JCT" entry in the level list: launch into scripted + cross-traffic convoys at a real intersection, then Aftertouch-steer your wreck (arrows) + and detonate the Crashbreaker (space). Damage tally + bronze/silver/gold. Events live in + `levels//junctions.json`; generate more from real intersections with + `tools/osm_junction.py "Name|Street A|Street B"`. - **Race** — 3 laps vs 3 rubber-banded rivals on the level's `RacePath`. The boost loop is Burnout 3's: earn boost by drifting, near-missing traffic, and smashing diff --git a/levels/albert_parklands/junctions.json b/levels/albert_parklands/junctions.json new file mode 100644 index 0000000..4e88bdf --- /dev/null +++ b/levels/albert_parklands/junctions.json @@ -0,0 +1,426 @@ +[ + { + "name": "Alice x George", + "spawn": { + "x": -143.83199552181551, + "z": 142.01018819980536, + "dx": 0.7668348571083106, + "dz": -0.6418444530598335 + }, + "streams": [ + { + "pts": [ + [ + -71.62300756706763, + 85.0918821008208 + ], + [ + -72.10279819575109, + 85.49061753866586 + ], + [ + -82.55345192912239, + 94.80588029839768 + ], + [ + -104.44928325520195, + 113.36472803055571 + ], + [ + -114.55295338424757, + 122.93815788447793 + ], + [ + -118.63551709634598, + 126.33214688818488 + ], + [ + -124.17972594270637, + 130.60060399325408 + ], + [ + -132.4126578466066, + 136.84137198262476 + ], + [ + -135.42619497476394, + 139.13115862719584 + ], + [ + -137.0173915026139, + 140.32992242527928 + ], + [ + -142.22691046279974, + 144.18129435873087 + ] + ], + "speed": 12.0, + "interval": 2.2 + }, + { + "pts": [ + [ + -145.4370805808313, + 139.83908204087984 + ], + [ + -140.2367304705485, + 135.99450357474848 + ], + [ + -138.6871885076824, + 134.82698377267795 + ], + [ + -135.67603860636424, + 132.53900681717874 + ], + [ + -127.45466133363814, + 126.30702760692736 + ], + [ + -121.99640893818653, + 122.1055163120108 + ], + [ + -118.19749682812089, + 118.9535245154313 + ], + [ + -108.01476176313997, + 109.30919316947609 + ], + [ + -86.07852578965569, + 90.71517690194068 + ], + [ + -75.68968831102165, + 81.45400766143295 + ], + [ + -75.08305996358739, + 80.94603629889595 + ] + ], + "speed": 11.0, + "interval": 2.6 + }, + { + "pts": [ + [ + 29.76081815300818, + -3.9437773782503602 + ], + [ + 0.8522815554834703, + 21.169938953797764 + ], + [ + -51.1224413806449, + 67.0201498038004 + ] + ], + "speed": 12.0, + "interval": 2.2 + }, + { + "pts": [ + [ + -54.694766613638436, + 62.97064459578822 + ], + [ + -2.7090703425578857, + 17.11077984650507 + ], + [ + 26.219388577689813, + -8.020329421713114 + ] + ], + "speed": 11.0, + "interval": 2.6 + }, + { + "pts": [ + [ + -75.361941989609, + 84.82292365250544 + ], + [ + -80.93953886827424, + 78.61163770120406 + ], + [ + -82.56599989409156, + 76.7348960854059 + ], + [ + -115.40558143561937, + 38.74155522137386 + ] + ], + "speed": 12.0, + "interval": 2.2 + }, + { + "pts": [ + [ + -111.32018174165945, + 35.21033597859207 + ], + [ + -78.48079808538708, + 73.20344791473178 + ], + [ + -76.9072614072839, + 75.01987789888024 + ], + [ + -71.34412554104603, + 81.2149947472113 + ] + ], + "speed": 11.0, + "interval": 2.6 + } + ], + "targets": [ + 15000, + 35000, + 60000 + ] + }, + { + "name": "Margaret x Edward", + "spawn": { + "x": 88.92833122889411, + "z": -205.50177900009658, + "dx": 0.7566215209215891, + "dz": -0.6538530982401947 + }, + "streams": [ + { + "pts": [ + [ + 87.16391537755105, + -207.5455100742669 + ], + [ + 176.66505381848225, + -284.81459374842393 + ], + [ + 179.4786506494242, + -287.246171304069 + ], + [ + 182.2405613884401, + -289.6339705318028 + ], + [ + 185.85530062949178, + -292.8318579572001 + ] + ], + "speed": 12.0, + "interval": 2.2 + }, + { + "pts": [ + [ + 189.43331887674478, + -288.78738204278 + ], + [ + 185.7986806034979, + -285.57197746787017 + ], + [ + 183.00997804674455, + -283.16086509609204 + ], + [ + 180.19394928641634, + -280.72718665144987 + ], + [ + 90.69274708023717, + -203.45804792592625 + ] + ], + "speed": 11.0, + "interval": 2.6 + }, + { + "pts": [ + [ + 185.8338152967176, + -292.8126451679154 + ], + [ + 193.65593395098287, + -299.88251164434115 + ], + [ + 211.37786867540518, + -315.2307257048046 + ], + [ + 216.02310568686988, + -319.25459820267537 + ], + [ + 237.6429961947087, + -338.58229333857753 + ], + [ + 239.66099718191472, + -341.2883463335457 + ] + ], + "speed": 12.0, + "interval": 2.2 + }, + { + "pts": [ + [ + 243.95347049022743, + -338.01196526635846 + ], + [ + 241.32949950132615, + -334.63644826175323 + ], + [ + 219.61114951437548, + -315.21901379742513 + ], + [ + 214.91318335480702, + -311.1488694949895 + ], + [ + 197.21785955435436, + -295.8238559560563 + ], + [ + 189.45480420951895, + -288.8065948320647 + ] + ], + "speed": 11.0, + "interval": 2.6 + }, + { + "pts": [ + [ + 116.8650308963045, + -376.11651856442666 + ], + [ + 122.97019010007931, + -369.0715631207818 + ], + [ + 179.81860554472172, + -304.1656834379468 + ], + [ + 181.35983927566346, + -302.4069925620872 + ], + [ + 184.2133034529848, + -299.1448137854387 + ], + [ + 189.71555797124375, + -292.5416508365839 + ], + [ + 194.9234531376747, + -286.3366793395667 + ], + [ + 234.05675616721896, + -241.1315253602194 + ], + [ + 244.51403825990147, + -229.32469253293408 + ], + [ + 255.05386474930117, + -217.31435380098776 + ], + [ + 256.9119332285974, + -215.07700198364842 + ] + ], + "speed": 12.0, + "interval": 2.2 + }, + { + "pts": [ + [ + 252.7568367319326, + -211.628064016186 + ], + [ + 250.9803399157384, + -213.769442598948 + ], + [ + 240.4633914001549, + -225.75366186691826 + ], + [ + 229.982416914263, + -237.5875502401286 + ], + [ + 190.83429465784906, + -282.80981346079136 + ], + [ + 185.5730615349928, + -289.07758916339617 + ], + [ + 180.09260788812207, + -295.6548470145767 + ], + [ + 177.29669340310514, + -298.8501898379666 + ], + [ + 175.7564478621494, + -300.60775216234316 + ], + [ + 118.90617949034443, + -365.5157484793863 + ], + [ + 112.7841869787125, + -372.58003543563154 + ] + ], + "speed": 11.0, + "interval": 2.6 + } + ], + "targets": [ + 15000, + 35000, + 60000 + ] + } +] \ No newline at end of file diff --git a/levels/albert_parklands/level.glb b/levels/albert_parklands/level.glb index f7db7b3..b66ddee 100644 Binary files a/levels/albert_parklands/level.glb and b/levels/albert_parklands/level.glb differ diff --git a/levels/kangaroo_point/junctions.json b/levels/kangaroo_point/junctions.json new file mode 100644 index 0000000..7e9c35b --- /dev/null +++ b/levels/kangaroo_point/junctions.json @@ -0,0 +1,134 @@ +[ + { + "name": "Cliffs Corner", + "spawn": { + "x": 49.98333461330541, + "z": -68.64433920000972, + "dx": 0.4680499904857942, + "dz": -0.8837019895905226 + }, + "streams": [ + { + "pts": [ + [ + 47.4831595229508, + -69.66371377177926 + ], + [ + 63.94149741985269, + -110.02953643297833 + ], + [ + 78.3750416498091, + -148.8340213803462 + ] + ], + "speed": 12.0, + "interval": 2.2 + }, + { + "pts": [ + [ + 83.43625143590378, + -146.95142862004474 + ], + [ + 68.97210013309922, + -108.06662116698321 + ], + [ + 52.48350970366002, + -67.62496462824019 + ] + ], + "speed": 11.0, + "interval": 2.6 + }, + { + "pts": [ + [ + 114.66387685267605, + -57.53027727981913 + ], + [ + 109.86666098023801, + -74.50891216449172 + ], + [ + 107.12471250932794, + -155.39569742995997 + ], + [ + 106.88904877360271, + -160.41551590552262 + ], + [ + 106.3952482221942, + -170.89775425802756 + ] + ], + "speed": 12.0, + "interval": 2.2 + }, + { + "pts": [ + [ + 111.78926635550437, + -171.15185734246612 + ], + [ + 112.28308355725834, + -160.66926529510476 + ], + [ + 112.52147634417389, + -155.58262017041363 + ], + [ + 115.2509302403802, + -74.92079143598734 + ], + [ + 119.8558354049959, + -59.01471872056136 + ] + ], + "speed": 11.0, + "interval": 2.6 + }, + { + "pts": [ + [ + 105.14797091687734, + -213.92504025483834 + ], + [ + 104.12084845022, + -326.6884054546313 + ] + ], + "speed": 12.0, + "interval": 2.2 + }, + { + "pts": [ + [ + 109.5206244517033, + -326.7375901453881 + ], + [ + 110.54774691836064, + -213.9742249455951 + ] + ], + "speed": 11.0, + "interval": 2.6 + } + ], + "targets": [ + 15000, + 35000, + 60000 + ] + } +] \ No newline at end of file diff --git a/levels/kangaroo_point/level.glb b/levels/kangaroo_point/level.glb index 6bbde4b..c5e8e3f 100644 Binary files a/levels/kangaroo_point/level.glb and b/levels/kangaroo_point/level.glb differ diff --git a/levels/queen_st_mall/junctions.json b/levels/queen_st_mall/junctions.json new file mode 100644 index 0000000..41af2ce --- /dev/null +++ b/levels/queen_st_mall/junctions.json @@ -0,0 +1,538 @@ +[ + { + "name": "Albert x Adelaide", + "spawn": { + "x": -141.55578237343948, + "z": -187.08015060009473, + "dx": 0.735035843445057, + "dz": 0.6780282507764803 + }, + "streams": [ + { + "pts": [ + [ + -63.049013950748005, + -111.03404120330288 + ], + [ + -67.67311197746825, + -116.88556835863362 + ], + [ + -69.33640432781439, + -119.01651536179543 + ], + [ + -72.74965354025018, + -122.55658461736515 + ], + [ + -98.24408864239251, + -142.51997682349207 + ], + [ + -106.03262882232805, + -149.46281622691504 + ], + [ + -127.33652985317063, + -167.58066870505155 + ], + [ + -136.23707672722756, + -177.3382472125045 + ], + [ + -138.0495681108421, + -179.37835778679877 + ], + [ + -139.33017336122177, + -180.76880262140824 + ], + [ + -143.5234010906565, + -185.23123832968892 + ] + ], + "speed": 12.0, + "interval": 2.2 + }, + { + "pts": [ + [ + -139.58816365622246, + -188.92906287050053 + ], + [ + -135.38602332573416, + -184.45711937808235 + ], + [ + -134.03937221770562, + -182.99475501313762 + ], + [ + -132.23691688556315, + -180.9657423870203 + ], + [ + -123.67509820126826, + -171.5497892951127 + ], + [ + -102.50831375382124, + -153.55417337313145 + ], + [ + -94.84937670776534, + -146.71949237661244 + ], + [ + -69.3394816295263, + -126.74355578269574 + ], + [ + -65.32529933336386, + -122.63190423762802 + ], + [ + -63.432906391639634, + -120.22931724123647 + ], + [ + -58.81222264887106, + -114.38211519656227 + ] + ], + "speed": 11.0, + "interval": 2.6 + }, + { + "pts": [ + [ + 48.262595664052256, + 14.981139500351844 + ], + [ + 44.3597027328176, + 10.810878499923838 + ], + [ + 12.513337259306525, + -23.281770877978737 + ], + [ + -24.437975734562166, + -67.23871181646255 + ] + ], + "speed": 12.0, + "interval": 2.2 + }, + { + "pts": [ + [ + -20.30439756562987, + -70.71341058345321 + ], + [ + 16.56426693178581, + -26.85248072178107 + ], + [ + 48.305406828335755, + 7.124224299892156 + ], + [ + 52.205267737000426, + 11.291242899470646 + ] + ], + "speed": 11.0, + "interval": 2.6 + }, + { + "pts": [ + [ + -86.70325250389301, + -94.08771386994844 + ], + [ + -77.60701968874399, + -101.92094169090129 + ], + [ + -72.06522503228508, + -106.94786296530025 + ], + [ + -68.79209061830386, + -109.50838129624599 + ], + [ + -62.692661313561906, + -114.75385541597699 + ], + [ + -56.45081914916183, + -120.13849830634709 + ], + [ + -53.14418135326541, + -122.9809049049611 + ], + [ + -38.75486932340002, + -135.5677577748611 + ], + [ + -8.138880164463979, + -163.05430391717795 + ], + [ + 7.529561567214451, + -176.87932594543773 + ], + [ + 21.92572970063743, + -189.79048578560977 + ], + [ + 25.62426293364586, + -193.04071169887433 + ], + [ + 26.615516895233576, + -193.87905456229723 + ], + [ + 27.606646263868104, + -194.72792646553222 + ], + [ + 32.11664235322457, + -198.8840883667601 + ], + [ + 57.105103286489076, + -221.92761298838238 + ] + ], + "speed": 12.0, + "interval": 2.2 + }, + { + "pts": [ + [ + 60.765864254843116, + -217.9578738114873 + ], + [ + 35.77719298273772, + -194.91415523271732 + ], + [ + 31.239945374075152, + -190.73303753393867 + ], + [ + 30.11741737461444, + -189.76849583719522 + ], + [ + 29.172798227938216, + -188.97034350113577 + ], + [ + 25.522915182059183, + -185.76304781436195 + ], + [ + 11.118032425570394, + -172.8441212543805 + ], + [ + -4.543064055336218, + -159.02564328250827 + ], + [ + -35.16387720923361, + -131.5347966250365 + ], + [ + -49.59536988091847, + -118.91077749494814 + ], + [ + -52.92601130212681, + -116.04756569366813 + ], + [ + -59.168575286057155, + -110.66230098388816 + ], + [ + -65.33626025215173, + -105.35901550369637 + ], + [ + -68.5423745958537, + -102.85524463438601 + ], + [ + -74.0429581637501, + -97.86416150877923 + ], + [ + -83.17948939242227, + -89.9958813300908 + ] + ], + "speed": 11.0, + "interval": 2.6 + } + ], + "targets": [ + 15000, + 35000, + 60000 + ] + }, + { + "name": "Edward x Elizabeth", + "spawn": { + "x": 329.71079592281694, + "z": -5.595044399780328, + "dx": -0.6532357656212038, + "dz": -0.7571545644802516 + }, + "streams": [ + { + "pts": [ + [ + 258.4805413787254, + -92.28866542876725 + ], + [ + 262.2737122591504, + -88.11954201221562 + ], + [ + 266.8273793037693, + -83.1100839184909 + ], + [ + 270.87333374760516, + -78.65784777138674 + ], + [ + 298.1107780903454, + -47.27921270488467 + ], + [ + 323.17112576090676, + -16.89985163934848 + ], + [ + 325.81187978583756, + -13.69226732374769 + ], + [ + 331.6793038120916, + -7.443009954076085 + ] + ], + "speed": 12.0, + "interval": 2.2 + }, + { + "pts": [ + [ + 327.7422880335423, + -3.7470788454845705 + ], + [ + 321.7969353555861, + -10.08114267594156 + ], + [ + 319.0053923982548, + -13.463768760358409 + ], + [ + 293.9898388277115, + -43.789533694763236 + ], + [ + 266.80554434016864, + -75.10635662867679 + ], + [ + 262.8314530609969, + -79.47792568097934 + ], + [ + 258.27861498952456, + -84.48647198726033 + ], + [ + 254.4863239075441, + -88.6546281707152 + ] + ], + "speed": 11.0, + "interval": 2.6 + }, + { + "pts": [ + [ + 142.50264789077926, + 5.258364995815184 + ], + [ + 148.94725068367612, + -0.3323178390103432 + ], + [ + 154.9526328669413, + -5.595836199336438 + ], + [ + 184.81445478686962, + -31.59521315098408 + ], + [ + 200.9901724381037, + -45.56696118289086 + ], + [ + 206.29822476676847, + -50.115127103271014 + ], + [ + 208.01277161716473, + -51.59585061763732 + ], + [ + 243.56749157860247, + -82.55471207644285 + ], + [ + 246.1313684904348, + -84.84463727526342 + ], + [ + 254.68470539632878, + -92.48524564052305 + ] + ], + "speed": 12.0, + "interval": 2.2 + }, + { + "pts": [ + [ + 258.28215988994066, + -88.4580479589594 + ], + [ + 249.72869976275985, + -80.81732952477397 + ], + [ + 247.11707918748664, + -78.48526152300741 + ], + [ + 211.55809324014456, + -47.52268298180012 + ], + [ + 209.81572383420533, + -46.017908496633005 + ], + [ + 204.51598169911696, + -41.47689161686228 + ], + [ + 188.3547169687786, + -27.51764724920578 + ], + [ + 158.5007797295594, + -1.5251294005982539 + ], + [ + 152.49584713714452, + 3.737997039525666 + ], + [ + 146.0412138327473, + 9.337403004260793 + ] + ], + "speed": 11.0, + "interval": 2.6 + }, + { + "pts": [ + [ + 299.8317240905086, + -131.8999654994903 + ], + [ + 302.3544939757109, + -134.11003589468572 + ], + [ + 328.3524909104502, + -156.3039359135965 + ], + [ + 335.8998393149425, + -162.7566931458765 + ], + [ + 376.4710012181815, + -198.14867992704066 + ] + ], + "speed": 12.0, + "interval": 2.2 + }, + { + "pts": [ + [ + 380.02079919024186, + -194.079412872944 + ], + [ + 339.4433211293332, + -158.68192485417183 + ], + [ + 331.85925322106283, + -152.19752408611663 + ], + [ + 305.86522961245754, + -130.0070205048928 + ], + [ + 303.390286276, + -127.8383604999869 + ] + ], + "speed": 11.0, + "interval": 2.6 + } + ], + "targets": [ + 15000, + 35000, + 60000 + ] + } +] \ No newline at end of file diff --git a/levels/queen_st_mall/level.glb b/levels/queen_st_mall/level.glb index 5be711d..107422c 100644 Binary files a/levels/queen_st_mall/level.glb and b/levels/queen_st_mall/level.glb differ diff --git a/levels/southbank/junctions.json b/levels/southbank/junctions.json new file mode 100644 index 0000000..3bbeb59 --- /dev/null +++ b/levels/southbank/junctions.json @@ -0,0 +1,346 @@ +[ + { + "name": "Melbourne x Merivale", + "spawn": { + "x": -590.6178507503562, + "z": -217.37742659981197, + "dx": 0.8530074497663933, + "dz": -0.5218987360044418 + }, + "streams": [ + { + "pts": [ + [ + -592.80332396911, + -218.96289397888663 + ], + [ + -586.6662380952372, + -227.08658029182723 + ], + [ + -522.2006803362468, + -263.3161280710875 + ], + [ + -513.2984743659174, + -268.5646201058227 + ], + [ + -505.76645618454586, + -272.8477143314658 + ], + [ + -497.90899041561073, + -277.2629244179048 + ] + ], + "speed": 12.0, + "interval": 2.2 + }, + { + "pts": [ + [ + -495.26368453007734, + -272.55523318206934 + ], + [ + -503.1094123444767, + -268.14663806831595 + ], + [ + -510.58925218160783, + -263.8934194939837 + ], + [ + -519.5432562746829, + -258.6152667290545 + ], + [ + -583.8043810107387, + -222.50730370835848 + ], + [ + -588.4323775316024, + -215.7919592207373 + ] + ], + "speed": 11.0, + "interval": 2.6 + }, + { + "pts": [ + [ + -497.7097209821479, + -277.3642792991366 + ], + [ + -489.2653413897242, + -281.21700470668776 + ], + [ + -470.16395422466655, + -292.9741741951012 + ], + [ + -453.3973200149641, + -303.3696498434199 + ], + [ + -438.7171383481014, + -313.0237718470059 + ], + [ + -428.17568968279613, + -319.6952218182481 + ] + ], + "speed": 12.0, + "interval": 2.2 + }, + { + "pts": [ + [ + -425.28794348242786, + -315.13222698127333 + ], + [ + -435.78284848324455, + -308.49056735250105 + ], + [ + -450.4943046193153, + -298.8163541564887 + ], + [ + -467.3265789435252, + -288.3796882049204 + ], + [ + -486.6041919466704, + -276.5182512930966 + ], + [ + -495.46295396354014, + -272.4538783008375 + ] + ], + "speed": 11.0, + "interval": 2.6 + }, + { + "pts": [ + [ + -511.28940466251, + -256.3197151971008 + ], + [ + -539.5118789529853, + -239.934377879393 + ], + [ + -576.453313765919, + -219.8022382435751 + ], + [ + -581.3296752702165, + -217.31316962414144 + ], + [ + -589.8527843522658, + -214.78808818884423 + ] + ], + "speed": 12.0, + "interval": 2.2 + }, + { + "pts": [ + [ + -591.3829171484466, + -219.9667650107797 + ], + [ + -583.2152104060733, + -222.37328397575936 + ], + [ + -579.0251997651409, + -224.55043815585836 + ], + [ + -542.1517485157323, + -244.64511972030104 + ], + [ + -514.0007454638794, + -260.98968640245414 + ] + ], + "speed": 11.0, + "interval": 2.6 + } + ], + "targets": [ + 15000, + 35000, + 60000 + ] + }, + { + "name": "Grey x Glenelg", + "spawn": { + "x": -132.09447432867904, + "z": -92.67206939992113, + "dx": 0.5885516813877528, + "dz": 0.8084595959821672 + }, + "streams": [ + { + "pts": [ + [ + -66.8941396578206, + 18.021454924938546 + ], + [ + -68.93721227192415, + 14.729250272207434 + ], + [ + -74.03417645108553, + 5.788135121643277 + ], + [ + -78.66584427851572, + -1.692678191469608 + ], + [ + -82.8231789453831, + -8.34834069992463 + ], + [ + -134.38713557935665, + -91.24598932165014 + ] + ], + "speed": 12.0, + "interval": 2.2 + }, + { + "pts": [ + [ + -129.80181307800143, + -94.09814947819211 + ], + [ + -78.23825562104584, + -11.201142499900957 + ], + [ + -74.07981104388904, + -4.543695408341438 + ], + [ + -69.38832494823436, + 3.0356700785490762 + ], + [ + -64.27327518677029, + 12.007542928328153 + ], + [ + -62.30660151137585, + 15.172859875635417 + ] + ], + "speed": 11.0, + "interval": 2.6 + }, + { + "pts": [ + [ + -62.42866919418658, + -1.5633764655737972 + ], + [ + -56.22534149367183, + 7.176315693408943 + ], + [ + -31.591830618167315, + 46.425585467880346 + ], + [ + 5.553177653998906, + 108.18727469018124 + ] + ], + "speed": 12.0, + "interval": 2.2 + }, + { + "pts": [ + [ + 0.9256242241840709, + 110.9703933104001 + ], + [ + -36.19855979655569, + 49.24303933209625 + ], + [ + -60.768388814750516, + 10.095343106510734 + ], + [ + -66.8313293113839, + 1.5633764655737972 + ] + ], + "speed": 11.0, + "interval": 2.6 + }, + { + "pts": [ + [ + -146.4043520315188, + -133.05664071966743 + ], + [ + -108.51077823239306, + -74.87735033479748 + ], + [ + -83.23180011197921, + -33.360028872910426 + ] + ], + "speed": 12.0, + "interval": 2.2 + }, + { + "pts": [ + [ + -87.84412394798038, + -30.551743126791347 + ], + [ + -113.07214707658274, + -71.98703646477304 + ], + [ + -150.92919892918795, + -130.10947927979782 + ] + ], + "speed": 11.0, + "interval": 2.6 + } + ], + "targets": [ + 15000, + 35000, + 60000 + ] + } +] \ No newline at end of file diff --git a/levels/southbank/level.glb b/levels/southbank/level.glb index df0095b..c038ee8 100644 Binary files a/levels/southbank/level.glb and b/levels/southbank/level.glb differ diff --git a/levels/story_bridge/junctions.json b/levels/story_bridge/junctions.json new file mode 100644 index 0000000..c7bea8e --- /dev/null +++ b/levels/story_bridge/junctions.json @@ -0,0 +1,198 @@ +[ + { + "name": "Bridge Approach", + "spawn": { + "x": 8.830464544568363, + "z": 869.4323045999147, + "dx": 0.07444215026982055, + "dz": -0.9972253337451898 + }, + "streams": [ + { + "pts": [ + [ + 6.138933818706997, + 869.2186170903494 + ], + [ + 7.71576357448385, + 849.3684911078449 + ], + [ + 10.638374835867648, + 826.3950177708408 + ], + [ + 13.290793665081416, + 807.4161707785927 + ], + [ + 16.464164221809273, + 786.8204240256596 + ] + ], + "speed": 12.0, + "interval": 2.2 + }, + { + "pts": [ + [ + 21.80118214360166, + 787.6427619747212 + ], + [ + 18.633212388594718, + 808.2026580211177 + ], + [ + 15.991303480558578, + 827.1064634292346 + ], + [ + 13.086203238107553, + 849.932740892406 + ], + [ + 11.521995270429729, + 869.64599210948 + ] + ], + "speed": 11.0, + "interval": 2.6 + }, + { + "pts": [ + [ + 18.99248078399424, + 762.39669386784 + ], + [ + 25.743285889885765, + 718.3587501785464 + ], + [ + 28.769387854660618, + 692.1866319902829 + ], + [ + 30.0890967826306, + 666.9831668179552 + ], + [ + 31.42169559470248, + 651.6049662957291 + ], + [ + 31.81270185839052, + 638.4108005958807 + ], + [ + 32.07909068323288, + 624.4813765582306 + ] + ], + "speed": 12.0, + "interval": 2.2 + }, + { + "pts": [ + [ + 37.478103203918025, + 624.5846422421965 + ], + [ + 37.211108262175465, + 638.5419810043787 + ], + [ + 36.81191635813593, + 651.9298049040424 + ], + [ + 35.47759634851261, + 667.3354059817195 + ], + [ + 34.150141407857646, + 692.6421440096167 + ], + [ + 31.09171746204034, + 719.1032498212462 + ], + [ + 24.33013387059421, + 763.2148989320567 + ] + ], + "speed": 11.0, + "interval": 2.6 + }, + { + "pts": [ + [ + 46.61372413580953, + 629.091352780875 + ], + [ + 46.0791068558773, + 644.9124964101077 + ], + [ + 45.148393336775854, + 661.3968152993656 + ], + [ + 43.42646160590967, + 688.423534995915 + ], + [ + 41.167633319989406, + 712.1819019166054 + ], + [ + 39.13812925040989, + 728.9192605875077 + ] + ], + "speed": 12.0, + "interval": 2.2 + }, + { + "pts": [ + [ + 33.77740686081252, + 728.2691410126066 + ], + [ + 35.797668406451436, + 711.6131516837517 + ], + [ + 38.04296969005352, + 688.0016170039884 + ], + [ + 39.75839999553985, + 661.068224700852 + ], + [ + 40.68465217733579, + 644.6678359900116 + ], + [ + 41.21680242495871, + 628.9090452194655 + ] + ], + "speed": 11.0, + "interval": 2.6 + } + ], + "targets": [ + 15000, + 35000, + 60000 + ] + } +] \ No newline at end of file diff --git a/levels/story_bridge/level.glb b/levels/story_bridge/level.glb index 9ad59de..5f2b91b 100644 Binary files a/levels/story_bridge/level.glb and b/levels/story_bridge/level.glb differ diff --git a/levels/woolies_carpark/junctions.json b/levels/woolies_carpark/junctions.json new file mode 100644 index 0000000..0292ea6 --- /dev/null +++ b/levels/woolies_carpark/junctions.json @@ -0,0 +1,13 @@ +[ + { + "name": "Trolley Run", + "spawn": {"x": 33.0, "z": 52.0, "dx": 0.0, "dz": -1.0}, + "streams": [ + {"pts": [[-70.0, 15.0], [70.0, 15.0]], "speed": 11.0, "interval": 2.0}, + {"pts": [[70.0, 12.0], [-70.0, 12.0]], "speed": 12.0, "interval": 2.4}, + {"pts": [[-70.0, -21.0], [70.0, -21.0]], "speed": 10.0, "interval": 2.2}, + {"pts": [[70.0, -24.0], [-70.0, -24.0]], "speed": 12.0, "interval": 2.8} + ], + "targets": [8000, 20000, 40000] + } +] diff --git a/shot_bridge.png.import b/shot_bridge.png.import new file mode 100644 index 0000000..45ce9ee --- /dev/null +++ b/shot_bridge.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bqaxeko1tl5w7" +path="res://.godot/imported/shot_bridge.png-2373c808c2e6eab2488c5d984769ec6f.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://shot_bridge.png" +dest_files=["res://.godot/imported/shot_bridge.png-2373c808c2e6eab2488c5d984769ec6f.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/shot_jct.png.import b/shot_jct.png.import new file mode 100644 index 0000000..636621a --- /dev/null +++ b/shot_jct.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dkm1p5n1xyg7t" +path="res://.godot/imported/shot_jct.png-d1005de9ee52815e0d48b6f3b6f72efa.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://shot_jct.png" +dest_files=["res://.godot/imported/shot_jct.png-d1005de9ee52815e0d48b6f3b6f72efa.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/shot_jct_woolies.png.import b/shot_jct_woolies.png.import new file mode 100644 index 0000000..175cc80 --- /dev/null +++ b/shot_jct_woolies.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://b4qj6hifty7co" +path="res://.godot/imported/shot_jct_woolies.png-8055f5e30848dcfa5c2e67aa3a1cbdb1.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://shot_jct_woolies.png" +dest_files=["res://.godot/imported/shot_jct_woolies.png-8055f5e30848dcfa5c2e67aa3a1cbdb1.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/shot_southbank.png.import b/shot_southbank.png.import new file mode 100644 index 0000000..d79ccc4 --- /dev/null +++ b/shot_southbank.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dq74to3ccwpby" +path="res://.godot/imported/shot_southbank.png-c868fc85f6d129b498f68423d5fba80c.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://shot_southbank.png" +dest_files=["res://.godot/imported/shot_southbank.png-c868fc85f6d129b498f68423d5fba80c.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/src/car.gd b/src/car.gd index b938446..66b28ef 100644 --- a/src/car.gd +++ b/src/car.gd @@ -29,6 +29,7 @@ var steer_in := 0.0 var drift_in := false var boost_in := false var power_scale := 1.0 # rubber-band hook, set by game.gd +var hold_wreck := false # junction mode: stay wrecked (aftertouch phase) var steer := 0.0 var boost := 0.0 @@ -140,6 +141,8 @@ func _impact(jolt: Vector3) -> void: get_tree().create_timer(2.2, true, false, true).timeout.connect(_recover) func _recover() -> void: + if hold_wreck: + return # junction mode: the wreck is the gameplay now wrecked = false if is_player: boost = boost_max # Burnout rule: recovery refills the meter diff --git a/src/game.gd b/src/game.gd index 7ae844d..767e719 100644 --- a/src/game.gd +++ b/src/game.gd @@ -9,11 +9,17 @@ extends Node3D static var car_path := "" static var level_path := "" static var mode := "cruise" +static var junction: Dictionary = {} const LAPS := 3 const CRASH_TIME := 60.0 const RIVAL_COUNT := 3 const TRAFFIC_COUNT := {"cruise": 6, "crash": 10, "race": 4} +const JCT_RUN_TIME := 30.0 # crash into something before this or the run ends +const JCT_SETTLE := 9.0 # aftertouch/crashbreaker window after the crash + +var jstate := "" # junction: "run" -> "settle" -> "done" +var breaker_used := false var car: Car var cam: Camera3D @@ -57,7 +63,12 @@ func _ready() -> void: car.stats = load(car_path) # place BEFORE add_child: teleporting a continuous_cd body after it enters # the physics world sweeps the jump as motion and hits everything en route - if mode == "race" and race_path: + if mode == "junction": + var sp: Dictionary = junction["spawn"] + var jf := Vector3(sp["dx"], 0, sp["dz"]).normalized() + car.transform = Transform3D(Basis.looking_at(jf, Vector3.UP), Vector3(sp["x"], 0.7, sp["z"])) + car.hold_wreck = true + elif mode == "race" and race_path: car.transform = _grid_xform(0.0) else: var spawn := level.find_child("Spawn", true, false) @@ -92,7 +103,31 @@ func _ready() -> void: rivals.append(r) car.spawn_xform = car.global_transform - if race_path: + if mode == "junction": + jstate = "run" + time_left = JCT_RUN_TIME + var midx := 0 + for stream in junction["streams"]: + var path := Path3D.new() + var curve := Curve3D.new() + for p in stream["pts"]: + curve.add_point(Vector3(p[0], 0, p[1])) + path.curve = curve + add_child(path) + var L := curve.get_baked_length() + var headway: float = stream["speed"] * stream["interval"] + var n: int = clampi(int(L / headway), 1, 9) + for i in n: + var t := TrafficCar.new() + t.path = path + t.offset = fposmod(i * headway, L) + t.speed = stream["speed"] + t.model_idx = midx + midx += 1 + add_child(t) + t.smashed.connect(_traffic_smashed) + traffic.append(t) + elif race_path: var n: int = TRAFFIC_COUNT.get(mode, 4) for i in n: var t := TrafficCar.new() @@ -169,6 +204,17 @@ func _physics_process(delta: float) -> void: _msg("NEAR MISS", 32) if mode == "race" and race_path and not over: _race_tick() + if mode == "junction" and jstate == "settle": + if Input.is_action_just_pressed("boost") and not breaker_used: + _crashbreaker() + if car.wrecked: # aftertouch: nudge the tumbling wreck, camera-relative + var right := cam.global_basis.x + right.y = 0 + var fwd := -cam.global_basis.z + fwd.y = 0 + var push := right.normalized() * Input.get_axis("steer_left", "steer_right") \ + + fwd.normalized() * Input.get_axis("brake", "throttle") + car.apply_central_force(push * car.mass * 9.0) func _race_tick() -> void: var L := race_path.curve.get_baked_length() @@ -206,8 +252,13 @@ func _nearest_off(pos: Vector3, seed_off: float, L: float) -> float: return best func _player_crash(dv: float) -> void: - if mode == "crash" and not over: + if mode in ["crash", "junction"] and not over: score += minf(dv, 30.0) * 120.0 + if mode == "junction" and jstate == "run" and dv > 5.0: + jstate = "settle" + time_left = JCT_SETTLE + _msg("CRASH!\narrows = aftertouch space = crashbreaker", 36) + return if dv > 6.0: _msg("SMASH", 40) @@ -225,11 +276,67 @@ func _rival_wrecked(r: Rival) -> void: _msg("TAKEDOWN!", 56) func _traffic_smashed(impulse: float) -> void: - if mode == "crash" and not over: + if mode in ["crash", "junction"] and not over: score += impulse * 400.0 _msg("$%d" % int(impulse * 400.0), 40) car.earn_boost(0.2) +func _crashbreaker() -> void: + breaker_used = true + score += 2000.0 + var origin := car.global_position + var q := PhysicsShapeQueryParameters3D.new() + var sphere := SphereShape3D.new() + sphere.radius = 14.0 + q.shape = sphere + q.transform = Transform3D(Basis.IDENTITY, origin) + var hits := get_world_3d().direct_space_state.intersect_shape(q, 64) + for h in hits: + var b: Object = h.collider + if b == car or not b is RigidBody3D: + continue + var dir: Vector3 = (b as RigidBody3D).global_position - origin + var dist := dir.length() + dir = (dir / maxf(dist, 0.5) + Vector3.UP * 0.6).normalized() + var strength := 16.0 / (1.0 + dist * 0.25) + if b is TrafficCar: + (b as TrafficCar).blast(dir * strength) + score += 500.0 + else: + (b as RigidBody3D).apply_central_impulse(dir * strength * (b as RigidBody3D).mass) + car.apply_central_impulse(Vector3.UP * car.mass * 7.0) + _msg("CRASHBREAKER!", 52) + var boom := MeshInstance3D.new() + var sm := SphereMesh.new() + sm.radius = 1.0 + sm.height = 2.0 + var mat := StandardMaterial3D.new() + mat.albedo_color = Color(1.0, 0.5, 0.1, 0.7) + mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA + mat.emission_enabled = true + mat.emission = Color(1.0, 0.4, 0.05) + sm.material = mat + boom.mesh = sm + add_child(boom) + boom.global_position = origin + var tw := create_tween() + tw.tween_property(boom, "scale", Vector3.ONE * 14.0, 0.45) + tw.parallel().tween_property(boom, "transparency", 1.0, 0.45) + tw.tween_callback(boom.queue_free) + +func _jct_tally() -> void: + jstate = "done" + over = true + var t: Array = junction.get("targets", [15000, 35000, 60000]) + var medal := "NO MEDAL, MATE" + if score >= float(t[2]): + medal = "GOLD!" + elif score >= float(t[1]): + medal = "SILVER" + elif score >= float(t[0]): + medal = "BRONZE" + _msg("DAMAGE: $%d\n%s\nR to retry, Esc for menu" % [int(score), medal], 48, true) + func _msg(text: String, size: int, sticky := false) -> void: msg_label.text = text msg_label.add_theme_font_size_override("font_size", size) @@ -252,7 +359,14 @@ func _process(delta: float) -> void: if time_left <= 0.0: over = true _msg("DAMAGE: $%d\nR to retry, Esc for menu" % int(score), 48, true) - if over and mode == "crash" and Input.is_action_just_pressed("reset"): + if mode == "junction" and not over: + time_left -= delta + info_label.text = "$%d %d s" % [int(score), ceili(time_left)] + if time_left <= 0.0: + if jstate == "run": + _msg("Didn't even crash. Cooked.", 40) + _jct_tally() + if over and mode in ["crash", "junction"] and Input.is_action_just_pressed("reset"): get_tree().reload_current_scene() return var target := car.global_position + car.global_basis * Vector3(0, 2.4, 6.0) diff --git a/src/main.gd b/src/main.gd index 62306bb..cd17192 100644 --- a/src/main.gd +++ b/src/main.gd @@ -9,6 +9,7 @@ const MODES := [["cruise", "Cruise"], ["crash", "Crash Mode"], ["race", "Race"]] var car_paths: Array = [] var level_paths: Array = [] +var junction_data: Array = [] func _ready() -> void: var cars := Registry.cars() @@ -20,6 +21,11 @@ func _ready() -> void: for id in levels: level_list.add_item(String(id).capitalize()) level_paths.append(levels[id]) + junction_data.append({}) + for j in Registry.junctions(): + level_list.add_item("CRASH JCT: %s (%s)" % [j.name, String(j.level_id).capitalize()]) + level_paths.append(j.level_path) + junction_data.append(j.data) for m in MODES: mode_list.add_item(m[1]) if not car_paths.is_empty(): @@ -33,6 +39,9 @@ func _start() -> void: if car_list.get_selected_items().is_empty() or level_list.get_selected_items().is_empty(): return Game.car_path = car_paths[car_list.get_selected_items()[0]] - Game.level_path = level_paths[level_list.get_selected_items()[0]] - Game.mode = MODES[mode_list.get_selected_items()[0]][0] + var li: int = level_list.get_selected_items()[0] + Game.level_path = level_paths[li] + Game.junction = junction_data[li] + # picking a CRASH JCT entry forces junction mode, whatever the mode column says + Game.mode = "junction" if not Game.junction.is_empty() else MODES[mode_list.get_selected_items()[0]][0] get_tree().change_scene_to_file("res://src/game.tscn") diff --git a/src/registry.gd b/src/registry.gd index d9a510c..2a965cd 100644 --- a/src/registry.gd +++ b/src/registry.gd @@ -19,3 +19,17 @@ static func levels() -> Dictionary: out[dir] = p break return out + +static func junctions() -> Array: + ## crash-junction events: [{level_id, level_path, name, data}] + var out: Array = [] + var lvls := levels() + for id in lvls: + var jp := "res://levels/%s/junctions.json" % id + if not FileAccess.file_exists(jp): + continue + var parsed = JSON.parse_string(FileAccess.get_file_as_string(jp)) + if parsed is Array: + for ev in parsed: + out.append({"level_id": id, "level_path": lvls[id], "name": ev.get("name", "?"), "data": ev}) + return out diff --git a/src/traffic.gd b/src/traffic.gd index 5bbdf53..3003b09 100644 --- a/src/traffic.gd +++ b/src/traffic.gd @@ -14,14 +14,18 @@ var live := true static var _model_pool: Array[String] = [] static var _pool_built := false -static func pick_model() -> String: +var model_idx := -1 # >= 0: deterministic pick (junction convoys are learnable puzzles) + +static func pick_model(idx := -1) -> String: if not _pool_built: _pool_built = true for p in Registry.cars().values(): var s: CarStats = load(p) if s.model_path != "" and ResourceLoader.exists(s.model_path): _model_pool.append(s.model_path) - return _model_pool.pick_random() if not _model_pool.is_empty() else "" + if _model_pool.is_empty(): + return "" + return _model_pool[idx % _model_pool.size()] if idx >= 0 else _model_pool.pick_random() func _init() -> void: mass = 900.0 @@ -35,19 +39,22 @@ func _init() -> void: box.size = Vector3(1.7, 1.2, 4.0) cs.shape = box add_child(cs) - var mp := pick_model() + body_entered.connect(_hit) + +func _ready() -> void: + # visuals here, not _init, so model_idx set by the spawner is respected + var mp := pick_model(model_idx) if mp != "": add_child((load(mp) as PackedScene).instantiate()) else: var mi := MeshInstance3D.new() var bm := BoxMesh.new() - bm.size = box.size + bm.size = Vector3(1.7, 1.2, 4.0) var mat := StandardMaterial3D.new() mat.albedo_color = Color.from_hsv(randf(), 0.4, 0.8) bm.material = mat mi.mesh = bm add_child(mi) - body_entered.connect(_hit) func _physics_process(delta: float) -> void: if not live or path == null: @@ -58,6 +65,14 @@ func _physics_process(delta: float) -> void: var ahead := path.to_global(path.curve.sample_baked(fmod(offset + 2.0, L))) + Vector3.UP * 0.75 look_at_from_position(pos, ahead, Vector3.UP) +func blast(impulse: Vector3) -> void: + ## Crashbreaker shockwave: go dynamic and fly, no contact needed + if live: + live = false + freeze = false + smashed.emit(clampf(impulse.length(), 3.0, 30.0)) + apply_central_impulse(impulse * mass) + func _hit(body: Node) -> void: if not live or not body is RigidBody3D: return diff --git a/tests/screenshot.gd b/tests/screenshot.gd index 96e83e2..f602b12 100644 --- a/tests/screenshot.gd +++ b/tests/screenshot.gd @@ -17,6 +17,11 @@ func _run() -> void: var levels := Registry.levels() Game.level_path = levels.get(opts.get("level", ""), levels.values()[0]) Game.mode = opts.get("mode", "cruise") + Game.junction = {} + if opts.has("jct"): # jct= within the chosen level's junction events + var jcts := Registry.junctions().filter(func(j): return j.level_path == Game.level_path) + Game.junction = jcts[int(opts["jct"])].data + Game.mode = "junction" var game: Game = (load("res://src/game.tscn") as PackedScene).instantiate() root.add_child(game) await process_frame diff --git a/tests/smoke.gd b/tests/smoke.gd index 4c19abe..3ef2fc4 100644 --- a/tests/smoke.gd +++ b/tests/smoke.gd @@ -59,6 +59,32 @@ func _run() -> void: var crash_score := game.score game.free() + # junction: convoys spawn, driving straight in causes the crash -> settle -> + # crashbreaker -> tally + var jcts := Registry.junctions().filter(func(j): return j.level_id == "woolies_carpark") + assert(not jcts.is_empty(), "no woolies junction event") + Game.junction = jcts[0].data + game = await _boot("junction") + assert(game.traffic.size() >= 8, "junction convoys missing (%d)" % game.traffic.size()) + Input.action_press("throttle") + for i in 950: + await physics_frame + if game.jstate == "settle" and not game.breaker_used: + Input.action_release("throttle") + Input.action_press("boost") + elif game.breaker_used: + Input.action_release("boost") + if game.over: + break + Input.action_release("throttle") + Input.action_release("boost") + assert(game.jstate != "run", "never crashed into the convoy") + assert(game.breaker_used, "crashbreaker never fired") + assert(game.over and game.score > 0.0, "junction never tallied (score %d)" % int(game.score)) + var jct_score := game.score + Game.junction = {} + game.free() + # race: rivals spawn and make progress along the path game = await _boot("race") assert(game.rivals.size() == Game.RIVAL_COUNT, "rivals missing") @@ -72,8 +98,8 @@ func _run() -> void: assert(moved, "no rival is driving") game.free() - print("smoke ok: %d cars, %d levels | cruise %.1f m | crash $%d | race AI driving" % - [cars.size(), levels.size(), dist, int(crash_score)]) + print("smoke ok: %d cars, %d levels | cruise %.1f m | crash $%d | junction $%d | race AI driving" % + [cars.size(), levels.size(), dist, int(crash_score), int(jct_score)]) quit(0) func _boot(mode: String) -> Game: diff --git a/tools/build_all_levels.sh b/tools/build_all_levels.sh new file mode 100755 index 0000000..01f9a19 --- /dev/null +++ b/tools/build_all_levels.sh @@ -0,0 +1,19 @@ +#!/bin/bash +# Rebuild every OSM level. Area JSONs are cached in $OSM_CACHE (defaults to the +# session scratchpad); refetch via the README curl + osm_xml_to_json.py if absent. +set -e +cd "$(dirname "$0")/.." +B=/Applications/Blender.app/Contents/MacOS/Blender +C="${OSM_CACHE:-/private/tmp/claude-501/-Users-johnking-Documents-BurnoutInfinityShitbox/09d758a4-f01e-48b8-b6a7-542f1069a0ff/scratchpad}" +while IFS='|' read -r id json lat lon sway loop bridge; do + ARGS=$(python3 tools/osm_level_args.py "$C/$json" "$sway" "$loop") || { echo "ARGS FAILED $id"; continue; } + SPAWN=$(echo "$ARGS" | head -1) + RP=$(echo "$ARGS" | tail -1) + "$B" -b -P tools/build_level_osm.py -- "$C/$json" "$id" "$lat" "$lon" "$SPAWN" "$RP" "$bridge" 2>&1 | grep -E '^(level built|bridge truss)' +done <<'EOF' +queen_st_mall|brisbane_cbd.json|-27.4699|153.0251|Queen Street Mall|Adelaide Street,Edward Street,Elizabeth Street,George Street|- +southbank|southbank.json|-27.4775|153.0215|Little Stanley Street|trace:Grey Street|- +albert_parklands|albert_parklands.json|-27.4740|153.0280|Albert Street|Alice Street,George Street,Margaret Street,Edward Street|- +story_bridge|story_bridge.json|-27.4635|153.0355|Bradfield Highway|trace:Bradfield Highway|Bradfield Highway +kangaroo_point|kangaroo_point.json|-27.4780|153.0345|River Terrace|trace:River Terrace|- +EOF diff --git a/tools/build_level_osm.py b/tools/build_level_osm.py index cb904a0..fefc426 100644 --- a/tools/build_level_osm.py +++ b/tools/build_level_osm.py @@ -71,6 +71,7 @@ M_LEAF = flat_material("leaf", (0.18, 0.42, 0.16, 1)) M_TRUNK = flat_material("trunk", (0.28, 0.20, 0.12, 1)) def uv_project(bm, scale): + bm.normal_update() # fresh faces have zero normals -> wrong projection axis uv = bm.loops.layers.uv.new("UVMap") for f in bm.faces: n = f.normal diff --git a/tools/osm_junction.py b/tools/osm_junction.py new file mode 100644 index 0000000..e26aa8a --- /dev/null +++ b/tools/osm_junction.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python3 +"""Generate crash-junction events from real OSM intersections. + +Usage: osm_junction.py "Name|Street A|Street B" ... + +For each spec: junction = shared node of the two streets. Every same-named way +passing within RADIUS of it becomes a convoy stream (clipped, plus a reversed +copy lane-offset the other way). Spawn point sits ~120m up Street A aimed at +the junction. Coordinates are level-local Godot metres (x east, z south). +""" +import json +import math +import sys + +RADIUS = 170.0 +LANE = 2.7 +TARGETS = [15000, 35000, 60000] + +src, out = sys.argv[1], sys.argv[2] +LAT0, LON0 = float(sys.argv[3]), float(sys.argv[4]) +M_LAT = 110574.0 +M_LON = 111320.0 * math.cos(math.radians(LAT0)) + +def xz(lat, lon): + return ((lon - LON0) * M_LON, -(lat - LAT0) * M_LAT) + +data = json.load(open(src)) +ways = {} +for e in data["elements"]: + t = e.get("tags", {}) + if e["type"] == "way" and t.get("highway") and t.get("name") and e.get("geometry"): + ways.setdefault(t["name"], []).append(e) + +def offset(pts, d): + outp = [] + for i, p in enumerate(pts): + a = pts[max(i - 1, 0)] + b = pts[min(i + 1, len(pts) - 1)] + dx, dz = b[0] - a[0], b[1] - a[1] + L = math.hypot(dx, dz) or 1.0 + outp.append([p[0] + dz / L * d, p[1] - dx / L * d]) + return outp + +events = [] +for spec in sys.argv[5:]: + name, a, b = spec.split("|")[:3] + na = {n for w in ways.get(a, []) for n in w["nodes"]} + nb = {n for w in ways.get(b, []) for n in w["nodes"]} + shared = na & nb + if not shared: + print("SKIP %s: %s never meets %s" % (name, a, b)) + continue + jn = sorted(shared)[0] + jpt = None + for w in ways[a]: + if jn in w["nodes"]: + g = w["geometry"][w["nodes"].index(jn)] + jpt = xz(g["lat"], g["lon"]) + streams = [] + spawn = None + for street in (a, b): + for w in ways[street]: + pts = [xz(g["lat"], g["lon"]) for g in w["geometry"]] + near = [p for p in pts if math.hypot(p[0] - jpt[0], p[1] - jpt[1]) < RADIUS] + if len(near) < 2: + continue + length = sum(math.hypot(near[i + 1][0] - near[i][0], near[i + 1][1] - near[i][1]) + for i in range(len(near) - 1)) + if length < 60: + continue + streams.append({"pts": offset(near, LANE), "speed": 12.0, "interval": 2.2}) + streams.append({"pts": offset(list(reversed(near)), LANE), "speed": 11.0, "interval": 2.6}) + if street == a and spawn is None: + best = max(near, key=lambda p: math.hypot(p[0] - jpt[0], p[1] - jpt[1])) + dx, dz = jpt[0] - best[0], jpt[1] - best[1] + L = math.hypot(dx, dz) or 1.0 + spawn = {"x": best[0], "z": best[1], "dx": dx / L, "dz": dz / L} + if not streams or spawn is None: + print("SKIP %s: no usable streams" % name) + continue + events.append({"name": name, "spawn": spawn, "streams": streams[:6], "targets": TARGETS}) + print("OK %s: %d streams, spawn %.0fm out" % (name, min(len(streams), 6), + math.hypot(spawn["x"] - jpt[0], spawn["z"] - jpt[1]))) + +json.dump(events, open(out, "w"), indent=1) +print("wrote %s (%d events)" % (out, len(events)))