From 0c13c82eac92edc7b66044ce641957cf7c0acbd1 Mon Sep 17 00:00:00 2001 From: m3ultra Date: Tue, 28 Jul 2026 23:07:41 +1000 Subject: [PATCH] Pursuit, drag and derby modes; crash-damage paint; junction fixes Three new modes, all our own code: - Pursuit (Simpsons Hit & Run's meter): smashing in cruise fills heat, at full the police hunt you. Open 250 m for 6 s to escape, get pinned 4 s to be busted. Heat decays, so picking a fight is a choice not a timer. - Drag (NFSU2's): the gearbox IS the event. The builder finds each level's longest genuinely straight run of wide road (442-628 m across the five OSM levels) and drops DragStart/DragEnd, so no level needs hand-authored coords. - Derby: five opponents in the carpark, last one running. Reuses the pursuit AI, each hunting a different car so it's a brawl not a mob. Crash damage now shows: total control-point travel drives paint dulling, roughness and a scratch layer, so scuffs and crumple can never disagree. Junction generator fixes, all found by measurement rather than inspection: - streams[:6] truncated after street A's ways, so junctions where A yielded 3+ usable ways shipped with ZERO cross traffic. Interleaved before capping; every event now has 2-4 crossing streams. - RADIUS 170 -> 95 spread the convoy over a 340 m window, so you threaded the intersection through a 20 m gap. Margaret x Edward was unwinnable: 27 convoy cars alive, zero collisions, $0. - Launch points now walk the street to exactly SPAWN_DIST and pick the busiest shared node, and _clear_spawn() unblocks a buried start. Albert x Adelaide went from $0 (drove 1 m into a building) to $91k. - build_all_junctions.sh records the street pairs, which existed nowhere before. Fixes from an adversarial review (23 confirmed findings), the worst being: - Derby could only ever be LOST: wrecked opponents un-wrecked 2.2 s later via Car._recover, so "last one running" was unreachable and the kill bounty could be farmed to the boost cap off one revived car. They now hold their wreck. - Drag reported WON to a beaten player: the AI's lookahead wrapped on a 2-point path, U-turning before the line so its distance-to-finish grew again. - The scratch layer used BLEND_MODE_MIX, whose mask defaults to white -- it replaced the paint entirely rather than scuffing it. Now MUL. - Drag on a level with no strip left the car at the origin, half-buried, unable to end. Cop spawns used an unflattened basis. Stale "WANTED" HUD text. Test harness: smoke covers all seven modes, and failures now exit instead of hanging -- a failed assert in a headless SceneTree halts the script but leaves the tree spinning, which cost 53 minutes of wall clock to notice. Co-Authored-By: Claude Opus 5 --- README.md | 43 ++- assets/textures/scratches.jpg | Bin 0 -> 40973 bytes assets/textures/scratches.jpg.import | 40 ++ levels/albert_parklands/junctions.json | 408 ++++++++++---------- levels/albert_parklands/level.glb | Bin 14399408 -> 14399716 bytes levels/albert_parklands/level.glb.import | 2 +- levels/kangaroo_point/junctions.json | 152 ++++---- levels/kangaroo_point/level.glb | Bin 12419348 -> 12419652 bytes levels/kangaroo_point/level.glb.import | 2 +- levels/queen_st_mall/junctions.json | 468 ++++++++--------------- levels/queen_st_mall/level.glb | Bin 18775528 -> 18775836 bytes levels/queen_st_mall/level.glb.import | 2 +- levels/southbank/junctions.json | 332 ++++++++-------- levels/southbank/level.glb | Bin 18451840 -> 18452144 bytes levels/southbank/level.glb.import | 2 +- levels/story_bridge/junctions.json | 204 +++++----- levels/story_bridge/level.glb | Bin 20862596 -> 20862904 bytes levels/story_bridge/level.glb.import | 2 +- src/cop.gd | 50 +++ src/cop.gd.uid | 1 + src/cop.tscn | 8 + src/deform.gd | 69 +++- src/game.gd | 385 ++++++++++++++++++- src/main.gd | 3 +- tests/smoke.gd | 114 ++++-- tools/build_all_junctions.sh | 23 ++ tools/build_level_osm.py | 39 +- tools/gen_textures.sh | 1 + tools/osm_junction.py | 124 +++++- tools/tune_junctions.gd | 104 +++++ tools/tune_junctions.gd.uid | 1 + 31 files changed, 1679 insertions(+), 900 deletions(-) create mode 100644 assets/textures/scratches.jpg create mode 100644 assets/textures/scratches.jpg.import create mode 100644 src/cop.gd create mode 100644 src/cop.gd.uid create mode 100644 src/cop.tscn create mode 100755 tools/build_all_junctions.sh create mode 100644 tools/tune_junctions.gd create mode 100644 tools/tune_junctions.gd.uid diff --git a/README.md b/README.md index 847d71c..8c4444c 100644 --- a/README.md +++ b/README.md @@ -22,15 +22,46 @@ Controls: WASD/arrows drive, Shift drift, Space boost, R reset (or retry after C - **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"`. + `levels//junctions.json`, regenerated in one go by + [tools/build_all_junctions.sh](tools/build_all_junctions.sh) — which is also the only + record of *which two streets each event is*, so keep the spec list in it current. + + Medal targets come from [tools/tune_junctions.gd](tools/tune_junctions.gd), which plays + every event headless with varied launches and sets bronze/silver/gold at the + 35th/65th/88th percentile of what was actually achieved. They were previously one + uniform 15/35/60k guess across every event, which measurement showed was nonsense — + achievable damage ranges from a couple of thousand to nearly $100k depending on the + intersection. Percentiles rather than the max, so gold is a good run and not a freak one. + + ``` + Godot --headless --path . --script tools/tune_junctions.gd -- runs=14 + ``` + + Two traps worth knowing. It reloads the level for every run, so a full pass is slow — + budget tens of minutes. And **nothing else may touch Godot or the levels while it + runs**: a second Godot instance contends for the `.godot` import lock, and a level + rebuild mid-flight silently feeds it half-written levels, which reads as "this event + scores zero" rather than as an error. - **Race** — 3 laps vs 3 rubber-banded rivals on the level's `RacePath`. +- **Drag** — NFSU2's event, our code: a straight-line run down the level's longest + genuinely straight stretch of wide road (the builder finds it and drops + `DragStart`/`DragEnd` markers, so no level needs hand-authored coordinates). + The gearbox *is* the game — revs climb, **space shifts**, and shifting inside + the window pays while an early shift or sitting on the limiter bogs you. One + opponent alongside. +- **Derby** — Destruction Derby in the Woolies carpark: five opponents, last one + running wins. They use the pursuit AI, each hunting a different car so it's a + brawl rather than a mob, and leaving the ring for 8 s forfeits. +- **Pursuit** — not a menu mode; it happens *to* you in Cruise. Smashing things + fills a heat meter, and at full heat the police turn up and hunt you. Open + 250 m for 6 s to lose them, or get pinned for 4 s and you're busted. Heat + decays if you behave, so picking a fight is a choice rather than a timer. The boost loop is Burnout 3's: earn boost by drifting, near-missing traffic, and smashing things. Wrecking a rival near you = TAKEDOWN — refills the meter and grows it (up to 4x). Hard crashes shed body panels, crumple the bodywork and trigger Impact Time slow-mo. -## Crash deformation +## Crash deformation and damage [src/deform.gd](src/deform.gd) implements the control-hull model from the design PDF: a 3×3×5 lattice of control points encloses the car, an impact shoves the @@ -44,6 +75,12 @@ so they're precomputed once per car and each impact is a plain weighted sum — instead of folding through itself. Only the player and rivals carry a deformer; traffic and parked cars are plain bodies. +Paint damage rides the same numbers: total control-point travel drives the albedo +dulling toward dirty grey, the roughness climbing, and (past 35% damage) a scratch +detail layer switching on — so the scuffs and the crumple can never disagree. +Meshes *and* materials are deep-copied per car; share either and denting one +Kingswood dents every Kingswood in the level. + ## Cars A car is a folder: `cars//stats.tres` + optional `car.glb`. Models are built diff --git a/assets/textures/scratches.jpg b/assets/textures/scratches.jpg new file mode 100644 index 0000000000000000000000000000000000000000..161d25f99b948458fda3b177a3dc483308da5c76 GIT binary patch literal 40973 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 AF8}}l literal 0 HcmV?d00001 diff --git a/assets/textures/scratches.jpg.import b/assets/textures/scratches.jpg.import new file mode 100644 index 0000000..0f2b79c --- /dev/null +++ b/assets/textures/scratches.jpg.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cs6ren4juu8hi" +path="res://.godot/imported/scratches.jpg-cf4940209544b792b522de339ec4412b.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/textures/scratches.jpg" +dest_files=["res://.godot/imported/scratches.jpg-cf4940209544b792b522de339ec4412b.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/levels/albert_parklands/junctions.json b/levels/albert_parklands/junctions.json index 4e88bdf..3fbe57e 100644 --- a/levels/albert_parklands/junctions.json +++ b/levels/albert_parklands/junctions.json @@ -2,10 +2,10 @@ { "name": "Alice x George", "spawn": { - "x": -143.83199552181551, - "z": 142.01018819980536, - "dx": 0.7668348571083106, - "dz": -0.6418444530598335 + "x": -165.3192678228276, + "z": 159.9955423589618, + "dx": 0.7668331983120437, + "dz": -0.6418464348787193 }, "streams": [ { @@ -56,7 +56,29 @@ ] ], "speed": 12.0, - "interval": 2.2 + "interval": 1.5 + }, + { + "pts": [ + [ + -75.361941989609, + 84.82292365250544 + ], + [ + -80.93953886827424, + 78.61163770120406 + ], + [ + -82.56599989409156, + 76.7348960854059 + ], + [ + -115.40558143561937, + 38.74155522137386 + ] + ], + "speed": 12.0, + "interval": 1.5 }, { "pts": [ @@ -106,65 +128,7 @@ ] ], "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 + "interval": 1.8 }, { "pts": [ @@ -186,7 +150,147 @@ ] ], "speed": 11.0, - "interval": 2.6 + "interval": 1.8 + }, + { + "pts": [ + [ + -72.11668883510822, + 80.61865688274891 + ], + [ + -63.31551687082244, + 85.23196646473183 + ], + [ + -54.55178175870453, + 95.53822905343011 + ], + [ + -39.78320194928109, + 112.96784568335697 + ], + [ + -38.24953472930413, + 114.64142478239386 + ], + [ + -37.466790819459995, + 115.77089209241056 + ], + [ + -36.90754547102499, + 116.86830840435711 + ], + [ + -36.64154284861633, + 117.96012844030032 + ], + [ + -36.54962586770791, + 118.92829018795193 + ], + [ + -36.58380403095183, + 120.63163096695364 + ], + [ + -36.733516672801336, + 121.77324800253669 + ], + [ + -36.949600973076684, + 122.55506784741175 + ], + [ + -37.31118692587169, + 123.40422578212599 + ], + [ + -38.02064964168787, + 124.38554265247478 + ], + [ + -39.45793344555888, + 125.80100694689808 + ], + [ + -41.96015977939887, + 127.8781767661959 + ] + ], + "speed": 12.0, + "interval": 1.5 + }, + { + "pts": [ + [ + -45.40767874247836, + 123.72190283402932 + ], + [ + -43.03089623055088, + 121.75206425317394 + ], + [ + -41.9792929338678, + 120.7127857471952 + ], + [ + -41.99739811738572, + 120.72105141795794 + ], + [ + -42.06268798731611, + 120.81830615252781 + ], + [ + -42.041735423545425, + 120.78187839726885 + ], + [ + -41.974164266802894, + 120.30911503284231 + ], + [ + -41.94908321684239, + 119.00484301205215 + ], + [ + -41.975684667956976, + 118.80092035936316 + ], + [ + -41.94671891520851, + 118.80911519521383 + ], + [ + -42.0788310934576, + 118.57964350736775 + ], + [ + -42.38250615411705, + 118.1168452176333 + ], + [ + -43.89081206390128, + 116.47320431640294 + ], + [ + -58.67282832403665, + 99.02778134654227 + ], + [ + -66.81772305118318, + 89.3422647349603 + ], + [ + -74.5893786955468, + 85.41926151696784 + ] + ], + "speed": 11.0, + "interval": 1.8 } ], "targets": [ @@ -198,64 +302,12 @@ { "name": "Margaret x Edward", "spawn": { - "x": 88.92833122889411, - "z": -205.50177900009658, - "dx": 0.7566215209215891, - "dz": -0.6538530982401947 + "x": 276.7158840463856, + "z": -371.11587218595514, + "dx": -0.7427057931255263, + "dz": 0.6696178797327494 }, "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": [ [ @@ -284,7 +336,41 @@ ] ], "speed": 12.0, - "interval": 2.2 + "interval": 1.5 + }, + { + "pts": [ + [ + 179.81812157632743, + -304.16623584225226 + ], + [ + 181.35983927566346, + -302.4069925620872 + ], + [ + 184.2133034529848, + -299.1448137854387 + ], + [ + 189.71555797124375, + -292.5416508365839 + ], + [ + 194.9234531376747, + -286.3366793395667 + ], + [ + 234.05675616721896, + -241.1315253602194 + ], + [ + 244.50993125963066, + -229.32934036140196 + ] + ], + "speed": 12.0, + "interval": 1.5 }, { "pts": [ @@ -314,71 +400,13 @@ ] ], "speed": 11.0, - "interval": 2.6 + "interval": 1.8 }, { "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 + 240.4674984004257, + -225.74901403845038 ], [ 229.982416914263, @@ -401,20 +429,12 @@ -298.8501898379666 ], [ - 175.7564478621494, - -300.60775216234316 - ], - [ - 118.90617949034443, - -365.5157484793863 - ], - [ - 112.7841869787125, - -372.58003543563154 + 175.7569318305437, + -300.6071997580377 ] ], "speed": 11.0, - "interval": 2.6 + "interval": 1.8 } ], "targets": [ diff --git a/levels/albert_parklands/level.glb b/levels/albert_parklands/level.glb index dce32baf231cc1f0e251cb65c279e36bc74c29e9..305a5ab0ec9e6dcf72645636e2b23c50ca7878c4 100644 GIT binary patch delta 1151 zcma*i=U>bL0LSqYO3}b&p3y1uoZlXqQOO=~D5hcs& zG;~RnMOo1`tvIYYJW??>~wpyy{QxDPHUX{sivP1R6Sk)4vDOS&qXf+kh`EwlOs>zp)Ta#Sd= z`3h~mX7EFEv_MPvBLJ-sh#<5^8w4W+p$J1;v_pGX5snV%h)(E?F6asyx}iIIpeG_= zM=wMo3cb+>ebEp7F#rw>gcE}xK!gMt3RGy&VZcN*24e_@Vi<;F1Y$4}qY#TY#A7rP zFa~2W4&yNa6EO*sF$GhRh$KwIbj-j^%))FWV-8X<7pa&B7v^ID(vXgY$iO0GVlkE= z3)yfZ2TQRG9^}G{stGI?TT*nRgP>!3Z zz%AUy9aN$UcX1E*@c<9;2#@guPw@=T@dDL&i5k4ZYrH`%-r^nJ;{)pO5uflGU+@** Q@Et$!6Td2LzWS1Ye{W&RwEzGB delta 934 zcmWN=#Znaj002Nl1x3N`?k-S3#ZFA@?!dtA4(twWOzh-|irtE0cX0KL;~%&?vw!~O zhNS<^4T(rh8rU^BC@3gBGbboDI}i~WmZxP#d{|amM%=Z~;P4g=qobmYF*Z3NurVQ! z$6)iC&-{iMY5@xxW+4k3ZiGcFYB7sj!jhJFr7m;1D_rR+SG&fwCcDn{Zg8WU-0T*&y3OtGaHqT6?H>1<;y(9#z=Iz0 zut!YwsK-3+2~T>;G*5fRv!3(37rf{tFMGwSUNhZ**Uj*TH@#)1x4q+C?|I(`KJ<}~ ued1G}`P>)2G|N}MHrqG8^_}ni;733C*)M+eo8SH6Pk;G4Ga-E&CsQ$djqvo0mMT6aQIH=h`M>i4Rf&Xyq3Vi% zyQIi=gx4}{-L|En3xi3T5F+~LXSzo^x@`zs)1@OUJ@)&%&mA@587ZnX96lBe{&8=3p-7VLtMZj{+>fLM*~! zEWuJN!*Z;^O02?a6ru=guommE9viR`#qcA5P1uYr2x2R?p#-Jajxy{(Id)MI{op_T^)Y`QjpT` delta 808 zcmWN=Wm6LX007WA9p+|XcLy6PC}9`aje*@5*xeF}F%#_W4xGrWJ3n~mm;D|-``+FA zTiFn*DQ^fxN=l0ggMmOGz3g`&nW&A#!olqPL_zpZG7^Beai|$ajhX2%hdaV7vmNOubIdi*d`CORv5s@R6P)NICp*Qd7Fg&sr#r)$&T_VM zoa;R2yTFAmvdG0QajDBJcDXBD=_*&d#qna_RUOJDigH@@|q@BQFMKl#~OznJi=b$+wnq~HDF&-!@kSF-Lu D?GCyY diff --git a/levels/kangaroo_point/level.glb.import b/levels/kangaroo_point/level.glb.import index 789ebee..5647f63 100644 --- a/levels/kangaroo_point/level.glb.import +++ b/levels/kangaroo_point/level.glb.import @@ -3,7 +3,7 @@ importer="scene" importer_version=1 type="PackedScene" -uid="uid://cik0c7knt33ep" +uid="uid://dik381ld32een" path="res://.godot/imported/level.glb-30461b4810ef38297351fb89cbe9aa18.scn" [deps] diff --git a/levels/queen_st_mall/junctions.json b/levels/queen_st_mall/junctions.json index 41af2ce..af4d21f 100644 --- a/levels/queen_st_mall/junctions.json +++ b/levels/queen_st_mall/junctions.json @@ -2,155 +2,33 @@ { "name": "Albert x Adelaide", "spawn": { - "x": -141.55578237343948, - "z": -187.08015060009473, - "dx": 0.735035843445057, - "dz": 0.6780282507764803 + "x": 18.671705882497893, + "z": -22.919507778110138, + "dx": -0.6633868531476461, + "dz": -0.7482766086621067 }, "streams": [ { "pts": [ [ - -63.049013950748005, - -111.03404120330288 + -58.84614846630014, + -114.42417420924829 ], [ - -67.67311197746825, - -116.88556835863362 + -53.880474905008, + -108.39185473942744 ], [ - -69.33640432781439, - -119.01651536179543 + -51.24051503498836, + -105.52578654474546 ], [ - -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 + -20.352984240690695, + -70.76962163514554 ] ], "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 + "interval": 1.5 }, { "pts": [ @@ -191,66 +69,40 @@ -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 + 7.537426213222782, + -176.88629254786764 ] ], "speed": 12.0, - "interval": 2.2 + "interval": 1.5 }, { "pts": [ [ - 60.765864254843116, - -217.9578738114873 + -24.38938905950134, + -67.18250076477023 ], [ - 35.77719298273772, - -194.91415523271732 + -55.272013374374126, + -101.93315225504524 ], [ - 31.239945374075152, - -190.73303753393867 + -57.98533525129133, + -104.88327646076375 ], [ - 30.11741737461444, - -189.76849583719522 - ], + -63.01508813331892, + -110.99198219061685 + ] + ], + "speed": 11.0, + "interval": 1.8 + }, + { + "pts": [ [ - 29.172798227938216, - -188.97034350113577 - ], - [ - 25.522915182059183, - -185.76304781436195 - ], - [ - 11.118032425570394, - -172.8441212543805 + 11.110167779562063, + -172.83715465195058 ], [ -4.543064055336218, @@ -290,7 +142,75 @@ ] ], "speed": 11.0, - "interval": 2.6 + "interval": 1.8 + }, + { + "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.2548410984026, + -167.5083127878196 + ] + ], + "speed": 12.0, + "interval": 1.5 + }, + { + "pts": [ + [ + -123.7567869560363, + -171.62214521234466 + ], + [ + -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": 1.8 } ], "targets": [ @@ -302,105 +222,39 @@ { "name": "Edward x Elizabeth", "spawn": { - "x": 329.71079592281694, - "z": -5.595044399780328, - "dx": -0.6532357656212038, - "dz": -0.7571545644802516 + "x": 173.87950117253465, + "z": -177.50987452854054, + "dx": 0.688388502507091, + "dz": 0.7253421741606128 }, "streams": [ { "pts": [ [ - 258.4805413787254, - -92.28866542876725 + 228.7426016851313, + -123.80938841141814 ], [ - 262.2737122591504, - -88.11954201221562 + 250.48785179439338, + -101.12418691157934 ], [ - 266.8273793037693, - -83.1100839184909 + 253.19184475779528, + -98.13216139604938 ], [ - 270.87333374760516, - -78.65784777138674 - ], - [ - 298.1107780903454, - -47.27921270488467 - ], - [ - 323.17112576090676, - -16.89985163934848 - ], - [ - 325.81187978583756, - -13.69226732374769 - ], - [ - 331.6793038120916, - -7.443009954076085 + 258.48528328991137, + -92.28343983096748 ] ], "speed": 12.0, - "interval": 2.2 + "interval": 1.5 }, { "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 + 184.8196819904455, + -31.59973983603264 ], [ 200.9901724381037, @@ -428,7 +282,29 @@ ] ], "speed": 12.0, - "interval": 2.2 + "interval": 1.5 + }, + { + "pts": [ + [ + 254.4815819963581, + -88.65985376851496 + ], + [ + 249.1869798707451, + -94.50986140343801 + ], + [ + 246.5771986992584, + -97.40037268829819 + ], + [ + 224.8443191668579, + -120.07262598818004 + ] + ], + "speed": 11.0, + "interval": 1.8 }, { "pts": [ @@ -457,76 +333,60 @@ -41.47689161686228 ], [ - 188.3547169687786, - -27.51764724920578 - ], - [ - 158.5007797295594, - -1.5251294005982539 - ], - [ - 152.49584713714452, - 3.737997039525666 - ], - [ - 146.0412138327473, - 9.337403004260793 + 188.34948976520272, + -27.51312056415722 ] ], "speed": 11.0, - "interval": 2.6 + "interval": 1.8 }, { "pts": [ [ - 299.8317240905086, - -131.8999654994903 + 258.4805413787254, + -92.28866542876725 ], [ - 302.3544939757109, - -134.11003589468572 + 262.2737122591504, + -88.11954201221562 ], [ - 328.3524909104502, - -156.3039359135965 + 266.8273793037693, + -83.1100839184909 ], [ - 335.8998393149425, - -162.7566931458765 + 270.87333374760516, + -78.65784777138674 ], [ - 376.4710012181815, - -198.14867992704066 + 298.08928934155523, + -47.30427629341134 ] ], "speed": 12.0, - "interval": 2.2 + "interval": 1.5 }, { "pts": [ [ - 380.02079919024186, - -194.079412872944 + 254.70925209424783, + -92.50690682734246 ], [ - 339.4433211293332, - -158.68192485417183 + 259.70303711688206, + -96.8600914480509 ], [ - 331.85925322106283, - -152.19752408611663 + 263.66545095997486, + -100.3447248693 ], [ - 305.86522961245754, - -130.0070205048928 - ], - [ - 303.390286276, - -127.8383604999869 + 288.61557906385735, + -122.1176793727558 ] ], - "speed": 11.0, - "interval": 2.6 + "speed": 12.0, + "interval": 1.5 } ], "targets": [ diff --git a/levels/queen_st_mall/level.glb b/levels/queen_st_mall/level.glb index 1ae809e4306d36d573a9214ddd59f53911b9d910..07c7e833cc985f6addef666ca41f4ddf8b0d55d3 100644 GIT binary patch delta 1436 zcmajf)nAig9ENd*pn#MLDvC8*=Y02WZw$M;u|=JQfm3lRir9)>vkSW$ySux)0J|Ie zdj0|rzSqHXa({j&&-0(@J`4V&`vgYEL{|6o@bCy9n%CeOxFyif-`8r=6dUZxVgCJY z`c{o~L^p9cVqNY>-9wC;%_gamWVI`@B&#-)WRlEwtENe^BuN&V#bR>@FAfQkWxH9k z@TzEvs$1Q^Qo{=AW=)l3yQXQ1tm=xQL?xKwjZR0;&c+C%SgGdhBsOaNf5WOrnT)Pj zhqG^t!_}>q(-_g(BB^G{qN}>is%x^ONmlo}lu&oO)O^X4Q@lI_6fRPsK_8ivmY9@g zfG0A*3*N|xOz?p({E!*`2-umF7MMIeF)}U)!N`KF$c7MvB0F**C&G{mxe<;$$cuc) zj{-2FAPS){il8Wpp*TvQBupp;GfG2(1v0F#L4gVlI_!u*8I(milt%?rL?u*46;wqv zR7VZeL@m@t9n?iV)JFp}L?bjt6GWmZnxQ#bpe0(NHQFEwZP5@~< zh)I}?DVU0Bm<~5)U?yf^Hs)Y1=3zb-U?CPE35&4=OR)^gu>vcx3ahaOYq1XNu>l*g z37fG6Tak=y*p408iCx%@J=lwV*pCA^h(kDxBRGmTh(~ygCrCjm((n||@EkAj60h(YZ}1lH@E#xV5uflG zU+@**@Ez&+fuHz=-}r;S6C&dbQy}6gl0n2v#9JhzNG1^<5nmBMk<23gA^{?SB0(a- nB3VSTiewWB5eXH^E|Nner%0GcE|J_K;Uakq2l*TfGra!+8f8+g delta 1237 zcmWmEXHk#D1URr7+QfWLnCpR-EH$s#G#1cnA3K36X5-36TthwTb1eF=&Ilf3B%>J3BnrFx)i>XXwIy0EbEM_x@xy)le3wV}=EMhTBSjuxO<9S}-MP6b#D_F_P zyuz!jVl}U^hPABYb=LC+8`#L3Wb+oA*vuBTvW@M$%{%O1C%br;-Mq&h_VPX-@F5?u zkNteiCw$5QKI0&V_?$2Jk{rI`Fh}^BZ}^t)_?{p5k)JrqF^+SBljQOWR zILmMR&L5oPPyXU>{^4K#BcJnJ;3Ai}%>UD~G9t4HiG~yii4BPhDHu{HBtE2YNJ2=F xki?LrkmQh}A;m(9hm;5@8B!{wbV!+yvLPuU*K@Sk?dl diff --git a/levels/queen_st_mall/level.glb.import b/levels/queen_st_mall/level.glb.import index 517edae..ee4dd34 100644 --- a/levels/queen_st_mall/level.glb.import +++ b/levels/queen_st_mall/level.glb.import @@ -3,7 +3,7 @@ importer="scene" importer_version=1 type="PackedScene" -uid="uid://bo4jjuhgers8e" +uid="uid://djsujf76tbo3k" path="res://.godot/imported/level.glb-f093d0a503a60c4e982eefdc88ad879e.scn" [deps] diff --git a/levels/southbank/junctions.json b/levels/southbank/junctions.json index 3bbeb59..4da7554 100644 --- a/levels/southbank/junctions.json +++ b/levels/southbank/junctions.json @@ -2,25 +2,17 @@ { "name": "Melbourne x Merivale", "spawn": { - "x": -590.6178507503562, - "z": -217.37742659981197, - "dx": 0.8530074497663933, - "dz": -0.5218987360044418 + "x": -598.2786454342968, + "z": -212.65545062278278, + "dx": 0.8528773292725991, + "dz": -0.5221113494388324 }, "streams": [ { "pts": [ [ - -592.80332396911, - -218.96289397888663 - ], - [ - -586.6662380952372, - -227.08658029182723 - ], - [ - -522.2006803362468, - -263.3161280710875 + -522.2431469994345, + -263.2916099068527 ], [ -513.2984743659174, @@ -36,7 +28,37 @@ ] ], "speed": 12.0, - "interval": 2.2 + "interval": 1.5 + }, + { + "pts": [ + [ + -518.3319604245614, + -316.20816625786284 + ], + [ + -502.53998920863535, + -289.11772214349674 + ], + [ + -501.0310720126822, + -286.5339989119062 + ], + [ + -499.3074920997309, + -283.76972490400925 + ], + [ + -498.93054530357944, + -283.2413778417831 + ], + [ + -494.34501447411924, + -276.414559193545 + ] + ], + "speed": 12.0, + "interval": 1.5 }, { "pts": [ @@ -53,20 +75,42 @@ -263.8934194939837 ], [ - -519.5432562746829, - -258.6152667290545 - ], - [ - -583.8043810107387, - -222.50730370835848 - ], - [ - -588.4323775316024, - -215.7919592207373 + -519.5007896114952, + -258.6397848932893 ] ], "speed": 11.0, - "interval": 2.6 + "interval": 1.8 + }, + { + "pts": [ + [ + -498.82766047156883, + -273.4035984064291 + ], + [ + -503.4072640108861, + -280.2216113581537 + ], + [ + -503.85991989590116, + -280.8653486954825 + ], + [ + -505.6522751490968, + -283.7403486882755 + ], + [ + -507.20498688872703, + -286.3978330566516 + ], + [ + -522.997174842978, + -313.4886489421985 + ] + ], + "speed": 11.0, + "interval": 1.8 }, { "pts": [ @@ -96,89 +140,29 @@ ] ], "speed": 12.0, - "interval": 2.2 + "interval": 1.5 }, { "pts": [ [ - -425.28794348242786, - -315.13222698127333 + -490.9127030755138, + -271.2416907037699 ], [ - -435.78284848324455, - -308.49056735250105 + -487.07948135301893, + -265.2321814476218 ], [ - -450.4943046193153, - -298.8163541564887 + -460.79611403958603, + -221.7057389459239 ], [ - -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 + -449.78766148749827, + -203.62165684766288 ] ], "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 + "interval": 1.5 } ], "targets": [ @@ -190,10 +174,10 @@ { "name": "Grey x Glenelg", "spawn": { - "x": -132.09447432867904, - "z": -92.67206939992113, - "dx": 0.5885516813877528, - "dz": 0.8084595959821672 + "x": -135.06485953132193, + "z": -97.50127625771249, + "dx": 0.5279471843098005, + "dz": 0.8492772047920241 }, "streams": [ { @@ -215,26 +199,52 @@ -1.692678191469608 ], [ - -82.8231789453831, - -8.34834069992463 - ], - [ - -134.38713557935665, - -91.24598932165014 + -82.82069253186671, + -8.344352365976752 ] ], "speed": 12.0, - "interval": 2.2 + "interval": 1.5 }, { "pts": [ [ - -129.80181307800143, - -94.09814947819211 + -33.05020815528413, + -14.758620186231095 ], [ - -78.23825562104584, - -11.201142499900957 + -35.06751528318885, + -14.599409411489315 + ], + [ + -37.27001626186296, + -13.996146050544787 + ], + [ + -43.35852481930847, + -10.760668273887504 + ], + [ + -53.694918113870564, + -4.204520462618362 + ], + [ + -56.331462588402815, + -2.4702458690280507 + ], + [ + -63.091151948462084, + 2.2185465904455017 + ] + ], + "speed": 12.0, + "interval": 1.5 + }, + { + "pts": [ + [ + -78.24074203456223, + -11.205130833848836 ], [ -74.07981104388904, @@ -254,7 +264,41 @@ ] ], "speed": 11.0, - "interval": 2.6 + "interval": 1.8 + }, + { + "pts": [ + [ + -66.1688465571084, + -2.2185465904455017 + ], + [ + -59.37835881390333, + -6.9285441308125515 + ], + [ + -56.602733425687326, + -8.754752337189586 + ], + [ + -46.12004993919494, + -15.401140126214159 + ], + [ + -39.50773651812935, + -18.910676349026538 + ], + [ + -36.1005431869604, + -19.899678588618844 + ], + [ + -33.51553068408273, + -20.138534213814854 + ] + ], + "speed": 11.0, + "interval": 1.8 }, { "pts": [ @@ -267,74 +311,30 @@ 7.176315693408943 ], [ - -31.591830618167315, - 46.425585467880346 - ], - [ - 5.553177653998906, - 108.18727469018124 + -31.608294574960304, + 46.399012606383145 ] ], "speed": 12.0, - "interval": 2.2 + "interval": 1.5 }, { "pts": [ [ - 0.9256242241840709, - 110.9703933104001 + -105.44415325837058, + 17.29338138921854 ], [ - -36.19855979655569, - 49.24303933209625 + -124.85549758561747, + 29.02803771609806 ], [ - -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 + -145.9651989732041, + 41.680234308480806 ] ], "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 + "interval": 1.5 } ], "targets": [ diff --git a/levels/southbank/level.glb b/levels/southbank/level.glb index 2158e2e2f2de5904e00f5459d41536fc4182fb03..c838ef828743f5ed4397767433d7602466e0bf25 100644 GIT binary patch delta 1416 zcmbW%WkXX@0EcmgPBv6T1hE5K$Gp4uZuibEup2vPVIV5bE=*Kx4D9X#u?xlS?k>bG z?0n8w@WtUKrBZ4SF4IV7h|v9+@VxuaZ> z{UcoN9(^K%%C(Vot1LTYRdH&vrs;}2>p&h)NsBu)G|)?|heUP}+cp1ZJ6Kj!tIetE zc2!dximcmM5AX|??W$FioeHO_k}gY1($jru#<;78Z6=;3Lm6vKi7}=a;E4>#2qQAV z3#MJh6!T0uCO+K^Z)8RmWQ7m1!57)#haB)n0CFM_xsV%qkQe!o9|celg-{qlD1xFW zhT4JC<6Z1V~q7fRS37R4d&Cnbz&=RfC8g0-P?a&?_&=D?lLT7YAS9C)-x}yhrq8B32 z8jnNo`u^5N(n1C2e#3W2cET&*8reQi} zU?yfE4zn=_b1@I`n2!Zmh(%b8C0L4OSdJA~iB(vQHCT&vSdR_Zh)vjxE!c_#Bw`zq zupK+F6T7e*d$1S#upb9-5Xm@%!$`pq9K|sl#|fOoDV)X`oW(hu#|2!(C0xc8T*Wn9 z#|_-XE!@T(+{HcI#{)dXBRs|vJjFAl;yKdr0x$6juki+N@ec3t0Uz-R>G+H<_=<1% zjvx4mU-*qb_!}FaZkP@cPmv5F8AXgDnMAxqOd@6xZ;{L*Swymm_=sc^@fFD~;wO?r T#9t&pB&SHAA(?N%kQVcj)%8Zj^RnW35uzkggd~zFO3KoBUTi{ov|6>S{K6Q; zC{77Vl1eE`Q--pXqdXO;NF^?!GF7;ks$9aQTt?WhP7N;S3a;cTYEp}rOy1=^-sb~8;-JfuWO$&l2LQX!>7%7l~+DHl>cq(Vr=kV=tUw-DJk`F~Jx4mAJ( diff --git a/levels/southbank/level.glb.import b/levels/southbank/level.glb.import index 8da64ad..75a1581 100644 --- a/levels/southbank/level.glb.import +++ b/levels/southbank/level.glb.import @@ -3,7 +3,7 @@ importer="scene" importer_version=1 type="PackedScene" -uid="uid://ce6dmcne00017" +uid="uid://dw52qeb8csi30" path="res://.godot/imported/level.glb-17af9cdeb05fd15a816097b7ae0fb96c.scn" [deps] diff --git a/levels/story_bridge/junctions.json b/levels/story_bridge/junctions.json index c7bea8e..b8919e3 100644 --- a/levels/story_bridge/junctions.json +++ b/levels/story_bridge/junctions.json @@ -2,191 +2,179 @@ { "name": "Bridge Approach", "spawn": { - "x": 8.830464544568363, - "z": 869.4323045999147, - "dx": 0.07444215026982055, - "dz": -0.9972253337451898 + "x": 32.83372074498513, + "z": -118.28842427027045, + "dx": -0.11409585864571734, + "dz": -0.9934697454074263 }, "streams": [ { "pts": [ [ - 6.138933818706997, - 869.2186170903494 + 21.82446567810751, + -237.81387509878263 ], [ - 7.71576357448385, - 849.3684911078449 + 23.145720027383163, + -226.45520770652828 ], [ - 10.638374835867648, - 826.3950177708408 + 24.788209612143817, + -213.84815371354867 ], [ - 13.290793665081416, - 807.4161707785927 - ], - [ - 16.464164221809273, - 786.8204240256596 + 26.145569174952232, + -200.29741976993301 ] ], "speed": 12.0, - "interval": 2.2 + "interval": 1.5 }, { "pts": [ [ - 21.80118214360166, - 787.6427619747212 + 2.199941588527042, + -240.32717119002737 ], [ - 18.633212388594718, - 808.2026580211177 + -2.5248172223755736, + -243.91950536774928 ], [ - 15.991303480558578, - 827.1064634292346 + -6.325349530807717, + -247.60965512377 ], [ - 13.086203238107553, - 849.932740892406 + -15.183983237537262, + -255.2613583327868 ], [ - 11.521995270429729, - 869.64599210948 - ] - ], - "speed": 11.0, - "interval": 2.6 - }, - { - "pts": [ - [ - 18.99248078399424, - 762.39669386784 + -19.822722086570074, + -258.6729952966697 ], [ - 25.743285889885765, - 718.3587501785464 + -24.279515332983824, + -261.70335578638884 ], [ - 28.769387854660618, - 692.1866319902829 - ], - [ - 30.0890967826306, - 666.9831668179552 - ], - [ - 31.42169559470248, - 651.6049662957291 - ], - [ - 31.81270185839052, - 638.4108005958807 - ], - [ - 32.07909068323288, - 624.4813765582306 + -29.011437974400625, + -263.89076106496833 ] ], "speed": 12.0, - "interval": 2.2 + "interval": 1.5 }, { "pts": [ [ - 37.478103203918025, - 624.5846422421965 + 20.772447151780117, + -199.75931223039953 ], [ - 37.211108262175465, - 638.5419810043787 + 19.423377981138138, + -213.23286388675078 ], [ - 36.81191635813593, - 651.9298049040424 + 17.78654515972297, + -225.7924522932427 ], [ - 35.47759634851261, - 667.3354059817195 - ], - [ - 34.150141407857646, - 692.6421440096167 - ], - [ - 31.09171746204034, - 719.1032498212462 - ], - [ - 24.33013387059421, - 763.2148989320567 + 16.46063564037367, + -237.18991410136036 ] ], "speed": 11.0, - "interval": 2.6 + "interval": 1.8 }, { "pts": [ [ - 46.61372413580953, - 629.091352780875 + -26.737042902514784, + -268.7884265350654 ], [ - 46.0791068558773, - 644.9124964101077 + -21.61124337794858, + -266.3980682135746 ], [ - 45.148393336775854, - 661.3968152993656 + -16.704188313209045, + -263.0814811029762 ], [ - 43.42646160590967, - 688.423534995915 + -11.761774350471743, + -259.43849686691397 ], [ - 41.167633319989406, - 712.1819019166054 + -2.7224195143774796, + -251.63195487598963 ], [ - 39.13812925040989, - 728.9192605875077 + 0.9839307874462617, + -248.02422063236523 + ], + [ + 5.4649806575202335, + -244.6282780101317 + ] + ], + "speed": 11.0, + "interval": 1.8 + }, + { + "pts": [ + [ + 11.27070084290225, + -197.84784324166512 + ], + [ + 10.896175804718348, + -204.76641770228525 + ], + [ + 10.476477879675205, + -211.20975788808624 + ], + [ + 9.92304078347784, + -216.20700696593718 + ], + [ + 8.932958301853615, + -221.62531766816238 + ], + [ + 7.508738699199866, + -226.8653717079938 ] ], "speed": 12.0, - "interval": 2.2 + "interval": 1.5 }, { "pts": [ [ - 33.77740686081252, - 728.2691410126066 + 28.875188928696993, + -229.49242546244793 ], [ - 35.797668406451436, - 711.6131516837517 + 28.72548025209394, + -214.05366159277472 ], [ - 38.04296969005352, - 688.0016170039884 + 28.150874632761848, + -209.45956029627345 ], [ - 39.75839999553985, - 661.068224700852 + 27.279147353059784, + -204.7812989955361 ], [ - 40.68465217733579, - 644.6678359900116 - ], - [ - 41.21680242495871, - 628.9090452194655 + 26.094879529030933, + -199.44340263065618 ] ], - "speed": 11.0, - "interval": 2.6 + "speed": 12.0, + "interval": 1.5 } ], "targets": [ diff --git a/levels/story_bridge/level.glb b/levels/story_bridge/level.glb index 677d61ed8f1ad69ee2a9dc79280a940d3235325c..4fb3a091c700769d03fe88c0853a2c0283c5f8a6 100644 GIT binary patch delta 1661 zcmbW%dw7mz9Ki9t8@6|54x2G&k2B9+&+|Tq`+4S&8H!42O3~ugY*zEqyhxHV%&Alh ziFDK%X~ilflu~ae9qH(>&Q2AzPVhp2mV*Q5&zUjR%m3B-BHFG(a*^ z5JV~(A`Oj@j>c%Rr8eGl?5fKA;%3M|CYmD)EzlC#Xoc2jgSKdg_UM3)(9j8;(FI-6 z4c*ZLJ<$s~au7mq7%&lr1se`rD0uLZi$3U!e&~-2FaUWNh9xClcr3>V`P zT#Dhi43}dBu0R1s;!0eFQMek{U^K?yS`^|sjKw%aFdo;V2oq3@5==xXZorKw!z4s8 z8B=f*$}tr&Ov7}{z|EM6TQCc^Vm5BW9NdmOFc%e=hxu55N-V@8EXEQn#hthd%WyaD z!M&)$eOQhaSc&_w3J+j4*5E;`#Y1=)kKj=}hR3lEPvA*Bg{Schp2c%mj}6#}=dlSd zpc*gYC2YnP)Zk^jf>*H>uij`#5aKEy}(7<=#u zKE-GF9ADr|?8QEOg|G1qzQuR=9{X_s2XP3s_yIrSC;W_G@GE}9VI09x9K&(^jz91x zPT(Z|!r!RFKRAWcID>!j-;(^|z%ob@B}tNclKPScl4MDWBq&LhG?b)C8cEV6jU`Pa zO(o4F8Inv%b4iw@g`}kTGB?+R?<$=UeZC*QKCsYNjgiqNV-b8NxDmVNP0?o pNpwk$BqZrAF(jrWEU_fE#F4lXCGjM_Bv;Z$(l@Y;zax-$?0=`aZWjOm delta 1410 zcmWmE_k#`u9DwnA@2umT&1*Y*9B#k&B9T(kPTDkGg|s^@mD4GOLPd%8lyrqkRJ4@S zE~V0rmUh?ve1G^nf5NkNWnFgFin{FFiYenpXJ%w%RE-~=D44V=cUDejy0~U(&h&LP zBR8+k$}7oFm6uXRigMEVsdNE_6wOT5Rwru{WRgWa>eGORG$NbEG$Dtkr62tnKt2N*#9#_2q=;fl zD5Z=P<)j(HP=>Jw!`YJ&jASoHu{ZlLntj=iG3?J+4&Xoz;$RNpP!8j8#&HDWIg+C| znqxSY<2arPOymSkN*| zOSz00T+S6-$xN!4#Z^=@o2$8oIb6$i%w-<)xt<$Xz(N*rBR6p~i&?@g+{#jJV;Rf2 zojbUbySSTsxR?95p9gr5hgiYGtmF|^@hCMs#^bDJ4QqLVCwYo>Jk5GG@C?uL92?oh z^Sr=|yu{0F<`rIL3$<+JHD2cp-sCOb<{jSUJ+|>aAF!Pd`G}9%!6$smXMD~Ve92dQ z%}#dl4d3z|-}3{z`H`RanP2#o-}s$BsN+xm;&1-pU;dj@S&^7WBr}o~sTZjqX%J}` zX%xwhG>$Zh$w>Q1he*drr%2~Wmq^!0w@6;3 zd!$FCXQWr8ccf3GZ=_$Oe`G);KQb^fC^9%w5GjllMT#ROk void: + if target and not wrecked: + _hunt(delta) + super(delta) + +func _hunt(delta: float) -> void: + var to := target.global_position - global_position + var flat := Vector3(to.x, 0.0, to.z) + var dist := flat.length() + var local := to_local(target.global_position) + var angle := atan2(-local.x, -local.z) + + if _unstick > 0.0: + # reversing out of whatever we buried ourselves in + _unstick -= delta + throttle_in = -1.0 + steer_in = signf(angle) + drift_in = false + boost_in = false + return + + steer_in = clampf(angle * 1.7, -1.0, 1.0) + throttle_in = 0.4 if absf(angle) > 1.3 else 1.0 + drift_in = absf(angle) > 0.8 and linear_velocity.length() > 15.0 + # close the gap hard, but don't burn boost nose-to-tail + boost_in = boost > 0.3 and absf(angle) < 0.35 and dist > 14.0 + power_scale = power_boost + + if linear_velocity.length() < 2.0: + _stuck += delta + if _stuck > 1.6: + _stuck = 0.0 + _unstick = 1.1 + else: + _stuck = 0.0 diff --git a/src/cop.gd.uid b/src/cop.gd.uid new file mode 100644 index 0000000..a64d5be --- /dev/null +++ b/src/cop.gd.uid @@ -0,0 +1 @@ +uid://bbwscf3nqqpe5 diff --git a/src/cop.tscn b/src/cop.tscn new file mode 100644 index 0000000..09b44b3 --- /dev/null +++ b/src/cop.tscn @@ -0,0 +1,8 @@ +[gd_scene load_steps=3 format=3] + +[ext_resource type="PackedScene" path="res://src/car.tscn" id="1"] +[ext_resource type="Script" path="res://src/cop.gd" id="2"] + +[node name="Cop" instance=ExtResource("1")] +script = ExtResource("2") +is_player = false diff --git a/src/deform.gd b/src/deform.gd index ed6b9d0..79c3966 100644 --- a/src/deform.gd +++ b/src/deform.gd @@ -23,16 +23,36 @@ const LATTICE := Vector3i(3, 3, 5) # x, y, z resolution of the hull const MAX_PULL := 0.42 # metres any control point may travel, total const DENT_RADIUS := 1.5 # how far along the shell an impact is felt +const SCRATCH := "res://assets/textures/scratches.jpg" +## setup() is O(verts x control points) on the main thread, and derby spawns five +## opponents at once. The procedural fleet is ~1k verts (~40k ops, imperceptible), +## but an imported model can be far denser -- past this budget the car simply goes +## without deformation rather than stalling the frame everyone spawns in. +const VERT_BUDGET := 6000 + var _meshes: Array = [] # one entry per MeshInstance3D, all its surfaces together var _cp := PackedVector3Array() var _disp := PackedVector3Array() var _ok := false +var _skins: Array = [] # {mat, albedo, rough}: per-car material copies to dirty up +var damage := 0.0 # 0..1, drives the paint getting scratched and dulled func setup(root: Node3D) -> bool: ## Snapshot every mesh under `root`, then build the control hull around them. var pending: Array = [] var bounds := AABB() var first := true + var budget := 0 + for n in root.find_children("*", "MeshInstance3D", true, false): + var am0 := (n as MeshInstance3D).mesh as ArrayMesh + if am0: + for s in am0.get_surface_count(): + budget += (am0.surface_get_arrays(s)[Mesh.ARRAY_VERTEX] as PackedVector3Array).size() + if budget > VERT_BUDGET: + push_warning("deform: %d verts over the %d budget, skipping" % [budget, VERT_BUDGET]) + return false + + var seen_mats := {} for n in root.find_children("*", "MeshInstance3D", true, false): var mi := n as MeshInstance3D var am := mi.mesh as ArrayMesh @@ -52,8 +72,32 @@ func setup(root: Node3D) -> bool: first = false else: bounds = bounds.expand(base[i]) - surfs.append({"arrays": arr, "base": base, - "mat": am.surface_get_material(s)}) + # the GLB's materials are shared too -- copy them or denting one + # Kingswood scuffs the paint on every other Kingswood + var mat := am.surface_get_material(s) + if mat is BaseMaterial3D: + # one copy per unique source material, not per surface: a model + # sharing one paint material across 20 surfaces would otherwise + # get 20 copies, all being re-tuned on every impact + if seen_mats.has(mat): + surfs.append({"arrays": arr, "base": base, "mat": seen_mats[mat]}) + continue + var src := mat + mat = (mat as BaseMaterial3D).duplicate() + seen_mats[src] = mat + var bm := mat as BaseMaterial3D + var skin := {"mat": bm, "albedo": bm.albedo_color, "rough": bm.roughness} + if ResourceLoader.exists(SCRATCH): + bm.detail_enabled = false + # MUL, not MIX: detail_mask defaults to white, so MIX makes the + # scratch texture *replace* the paint outright -- the car turns + # grey instead of getting scuffed. MUL darkens it instead. + bm.detail_blend_mode = BaseMaterial3D.BLEND_MODE_MUL + bm.detail_albedo = load(SCRATCH) + bm.detail_uv_layer = BaseMaterial3D.DETAIL_UV_1 + skin["scratch"] = true + _skins.append(skin) + surfs.append({"arrays": arr, "base": base, "mat": mat}) pending.append({"mi": mi, "xform": xform, "surfs": surfs}) if pending.is_empty() or bounds.size.length() < 0.1: return false @@ -131,6 +175,27 @@ func impact(local_point: Vector3, local_dir: Vector3, strength: float) -> void: moved = true if moved: _write() + _wear() + +func _wear() -> void: + ## Paint damage tracks how bent the car actually is, so the scratches and the + ## crumple always agree -- no separate damage counter to drift out of sync. + var total := 0.0 + for d in _disp: + total += d.length() + damage = clampf(total / (_cp.size() * MAX_PULL * 0.28), 0.0, 1.0) + for s in _skins: + var m: BaseMaterial3D = s["mat"] + var base: Color = s["albedo"] + # dull and dirty rather than recolour: a smashed shitbox is still its + # own colour, just filthier + m.albedo_color = base.lerp(Color(0.34, 0.31, 0.28), damage * 0.5) + m.roughness = lerpf(s["rough"], minf(s["rough"] + 0.45, 1.0), damage) + # StandardMaterial3D's detail layer has no blend-strength dial without a + # mask texture, so gate it on real damage: light scuffs only dull the + # paint, a properly bent car gets visible scratches over the top + if s.has("scratch"): + m.detail_enabled = damage > 0.35 func _write() -> void: for m in _meshes: diff --git a/src/game.gd b/src/game.gd index d88b41e..13438e8 100644 --- a/src/game.gd +++ b/src/game.gd @@ -15,7 +15,10 @@ static var junction: Dictionary = {} const LAPS := 3 const CRASH_TIME := 60.0 const RIVAL_COUNT := 3 -const TRAFFIC_COUNT := {"cruise": 8, "crash": 12, "race": 4} +## drag and derby are explicitly 0: both fall through to the RacePath traffic +## branch otherwise (every level GLB has a RacePath), littering a drag strip and +## a derby ring with loop traffic that belongs to neither event. +const TRAFFIC_COUNT := {"cruise": 8, "crash": 12, "race": 4, "drag": 0, "derby": 0} const PARKED_MAX := 50 # PP_ markers filled with shuntable fleet cars ## Street furniture that topples. Marker prefix -> size, mass, colour. ## Deliberately physics, not scenery: clipping a lamp post at 120 and watching @@ -28,6 +31,27 @@ const SMASHABLE := { 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 const ROAM_RIVALS := 2 # challengers circling the RacePath in cruise +## Pursuit (Simpsons Hit & Run's meter, our code): wreck enough street furniture +## and the law turns up. Heat decays if you behave, so it's a choice not a timer. +const HEAT_PER_SMASH := 0.14 +const HEAT_DECAY := 0.055 # per second while not smashing anything +const HEAT_COOLDOWN := 20.0 # grace after an escape before heat can build again +const COPS_MIN := 2 +const COPS_MAX := 4 +const BUST_RANGE := 7.0 # a cop this close, this long, and you're done +const BUST_TIME := 4.0 +const ESCAPE_RANGE := 220.0 +const ESCAPE_TIME := 6.0 +## Drag (NFSU2's, our code): the whole event is the gearbox. Revs climb, you +## shift in the window, and a blown shift costs you the race in a straight line +## where there's nothing else to get wrong. +const DRAG_GEARS := 5 +const SHIFT_LO := 0.74 # revs below this = shifted early, lose drive +const REDLINE := 0.99 # hit it without shifting and the engine bogs +const DRAG_REV_RATE := [0.0, 0.62, 0.50, 0.40, 0.32, 0.26] # per gear, revs/sec +const DERBY_CARS := 5 # opponents in the arena, last one running wins +const DERBY_ARENA := 90.0 # leave the ring this long and you forfeit +const DERBY_OUT := 8.0 const OUTRUN_GAP := 250.0 # escape distance that decides an outrun (NFSU2 rule) const OUTRUN_TIME := 90.0 const OUTRUN_START_D := 12.0 # pull alongside within this, at speed, to trigger @@ -36,6 +60,27 @@ var jstate := "" # junction: "run" -> "settle" -> "done" var breaker_used := false var spinners: Array[Node3D] = [] # ferris wheels etc, turned in _process +var heat := 0.0 +var heat_cd := 0.0 +var cops: Array[Cop] = [] +var bust_t := 0.0 +var escape_t := 0.0 +var busts := 0 +var escapes := 0 + +var drag_end: Vector3 +var drag_len := 0.0 +var gear := 1 +var revs := 0.0 +var bog := 0.0 # seconds of lost drive from a blown shift +var clean_shifts := 0 +var blown_shifts := 0 +var drag_t := 0.0 + +var derby: Array[Cop] = [] # arena opponents; Cop AI already hunts a target +var derby_centre: Vector3 +var derby_out_t := 0.0 +var derby_kills := 0 var outrun_rival: Rival = null var outrun_t := 0.0 var outrun_cd := 0.0 # cooldown so a finished outrun doesn't instantly re-arm @@ -55,9 +100,11 @@ var near_cd := {} # traffic -> near-miss cooldown var speed_label: Label var boost_bar: ProgressBar +var heat_bar: ProgressBar var info_label: Label var msg_label: Label var msg_timer: SceneTreeTimer +var _sticky := false # a result screen is up; transient messages must not clobber it func _ready() -> void: var level: Node = (load(level_path) as PackedScene).instantiate() @@ -99,8 +146,21 @@ func _ready() -> void: 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.transform = Transform3D(Basis.looking_at(jf, Vector3.UP), + _clear_spawn(Vector3(sp["x"], 0.7, sp["z"]), jf)) car.hold_wreck = true + # The marker test lives in the branch condition, not inside it: as an `elif` + # with the test inside, a level without a drag strip (the code-built carpark) + # matched the branch, set no transform, and left the car at the world origin + # half-sunk in the ground slab with no way to end the run. + elif mode == "drag" and _drag_markers(level).size() == 2: + var mk := _drag_markers(level) + drag_end = Vector3(mk[1].global_position.x, 0.0, mk[1].global_position.z) + var from := Vector3(mk[0].global_position.x, 0.7, mk[0].global_position.z) + var dir := (drag_end - Vector3(from.x, 0, from.z)).normalized() + drag_len = Vector3(from.x, 0, from.z).distance_to(drag_end) + car.transform = Transform3D(Basis.looking_at(dir, Vector3.UP), + _clear_spawn(from, dir) - dir.cross(Vector3.UP) * 2.0) elif mode == "race" and race_path: car.transform = _grid_xform(0.0) else: @@ -134,6 +194,52 @@ func _ready() -> void: prev_off[r] = off r.wreck_started.connect(_rival_wrecked.bind(r)) rivals.append(r) + elif mode == "derby": + # Cop AI is exactly what a derby wants: hunts a target, reverses out of + # wedges. Each one chases a different car so it's a brawl, not a mob. + derby_centre = Vector3(car.global_position.x, 0.0, car.global_position.z) + var pool := Registry.cars().values() + for i in DERBY_CARS: + var d: Cop = (load("res://src/cop.tscn") as PackedScene).instantiate() + d.stats = load(pool[randi() % pool.size()]) + d.power_boost = 1.0 + # a wreck is an elimination: without this car.gd's 2.2 s _recover + # timer stands them back up and the field never empties + d.hold_wreck = true + add_child(d) + var a := TAU * i / DERBY_CARS + var at := derby_centre + Vector3(cos(a), 0, sin(a)) * 26.0 + Vector3.UP * 0.75 + d.global_transform = Transform3D( + Basis.looking_at((derby_centre - at).normalized() * Vector3(1, 0, 1), Vector3.UP), at) + d.wreck_started.connect(_derby_down.bind(d)) + derby.append(d) + for i in derby.size(): + derby[i].target = car if i % 2 == 0 else derby[(i + 1) % derby.size()] + elif mode == "drag" and drag_len > 0.0: + # one opponent, one straight -- a Path3D of two points is all the AI needs + # Run the rival down its OWN lane. Anchoring the strip on the player put + # the AI's racing line on top of the player and they simply collided. + var fwd := -car.global_basis.z + fwd = Vector3(fwd.x, 0.0, fwd.z).normalized() + var lane := fwd.cross(Vector3.UP).normalized() * 4.0 + var strip := Path3D.new() + var sc := Curve3D.new() + sc.add_point(car.global_position + lane) + sc.add_point(drag_end + Vector3.UP * 0.7 + lane) + # a 2-point curve makes rival.gd's fmod() lookahead wrap to the start once + # it nears the end, spinning the car around -- extend past the line instead + sc.add_point(drag_end + Vector3.UP * 0.7 + lane + fwd * 120.0) + strip.curve = sc + add_child(strip) + var pool := Registry.cars().values() + var r: Rival = (load("res://src/rival.tscn") as PackedScene).instantiate() + r.stats = load(pool[randi() % pool.size()]) + r.path = strip + add_child(r) + r.global_transform = Transform3D(Basis.looking_at(fwd, Vector3.UP), + car.global_position + lane) + r.progress = 0.0 + rivals.append(r) elif mode == "cruise" and race_path: # roaming challengers: they lap the circuit until you pick a fight var pool := Registry.cars().values() @@ -163,7 +269,7 @@ func _ready() -> void: 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) + var n: int = clampi(int(L / headway), 1, 14) for i in n: var t := TrafficCar.new() t.path = path @@ -203,6 +309,16 @@ func _ready() -> void: boost_bar.size = Vector2(160, 18) boost_bar.show_percentage = false hud.add_child(boost_bar) + heat_bar = ProgressBar.new() + heat_bar.position = Vector2(24, 96) + heat_bar.size = Vector2(160, 10) + heat_bar.show_percentage = false + heat_bar.max_value = 1.0 + heat_bar.visible = false + var fill := StyleBoxFlat.new() + fill.bg_color = Color(0.85, 0.16, 0.12) + heat_bar.add_theme_stylebox_override("fill", fill) + hud.add_child(heat_bar) info_label = Label.new() info_label.anchor_left = 1.0 info_label.anchor_right = 1.0 @@ -220,6 +336,54 @@ func _ready() -> void: msg_label.add_theme_font_size_override("font_size", 48) hud.add_child(msg_label) +func _drag_markers(level: Node) -> Array[Node3D]: + var out: Array[Node3D] = [] + var s := level.find_child("DragStart", true, false) as Node3D + var e := level.find_child("DragEnd", true, false) as Node3D + if s and e: + out = [s, e] + return out + +func _clear_spawn(pos: Vector3, fwd: Vector3) -> Vector3: + ## The generator places launch points from street centrelines alone, so one + ## can land inside a building or a mall canopy leg -- Albert x Adelaide was + ## wedged at its start line, 1 m driven in 25 s. Back off along the approach + ## until the car actually fits. + var space := get_world_3d().direct_space_state + var q := PhysicsShapeQueryParameters3D.new() + var box := BoxShape3D.new() + # must clear the ground slab: its top face is y=0 and the spawn sits at 0.7, + # so a 1.4-tall box centred there touches the ground at every candidate and + # the whole search silently fails + box.size = Vector3(2.2, 0.9, 4.8) + q.shape = box + var lift := Vector3(0, 0.25, 0) + var side := Vector3.UP.cross(fwd).normalized() + # nearest-first: try the exact spot, then widening back/side offsets. A clear + # box isn't enough -- Albert x Adelaide fitted fine but faced a wall 2.7 m + # on, so the launch corridor has to be clear too. + var tries: Array[Vector3] = [Vector3.ZERO] + # nearest-first, and allow sliding forward as well as back: Albert x Adelaide's + # generated launch sits inside a CBD building block, where nothing behind it + # is clear either. A shortened run-up beats an unplayable event. + for along in [0.0, -6.0, 6.0, -12.0, 12.0, -20.0, 20.0, -30.0, 30.0, + 45.0, 60.0, 75.0, 90.0, 105.0]: + for lat in [0.0, 4.0, -4.0, 8.0, -8.0, 13.0, -13.0]: + tries.append(fwd * along + side * lat) + for off in tries: + var at := pos + off + q.transform = Transform3D(Basis.IDENTITY, at + lift) + if not space.intersect_shape(q, 1).is_empty(): + continue + var ray := PhysicsRayQueryParameters3D.create(at + lift, at + lift + fwd * 22.0) + if space.intersect_ray(ray).is_empty(): + if off.length() > 0.5: + push_warning("junction spawn blocked; moved %.0f m to clear the run-up" + % off.length()) + return at + push_warning("junction spawn has no clear launch corridor") + return pos + func _spawn_smashables(level: Node) -> void: ## Fill the level's PP_/WB_ markers with shuntable parked cars and wheelie ## bins. Real fleet models, asleep until rammed. @@ -400,6 +564,12 @@ func _physics_process(delta: float) -> void: _race_tick() if mode == "cruise" and race_path: _outrun_tick(delta) + if mode == "cruise": + _pursuit_tick(delta) + if mode == "drag" and not over: + _drag_tick(delta) + if mode == "derby" and not over: + _derby_tick(delta) if mode == "junction" and jstate == "settle": if Input.is_action_just_pressed("boost") and not breaker_used: _crashbreaker() @@ -412,6 +582,194 @@ func _physics_process(delta: float) -> void: + fwd.normalized() * Input.get_axis("brake", "throttle") car.apply_central_force(push * car.mass * 9.0) +func _derby_down(d: Cop) -> void: + if over: + return # no bounties after the result screen is up + if not car.wrecked and car.global_position.distance_to(d.global_position) < 22.0: + derby_kills += 1 + car.boost_max = minf(car.boost_max + 0.5, Car.BOOST_CAP) + car.boost = car.boost_max + _msg("WRECKED 'EM! x%d" % derby_kills, 46) + +func _derby_tick(delta: float) -> void: + var alive: Array[Cop] = [] + for d in derby: + if is_instance_valid(d) and not d.wrecked: + alive.append(d) + # retarget anyone hunting a car that's already out, or they circle a corpse + for d in alive: + if d.target == null or not is_instance_valid(d.target) \ + or (d.target is Cop and (d.target as Cop).wrecked): + d.target = car if alive.size() < 2 else alive[randi() % alive.size()] + if d.target == d: + d.target = car + + var flat := Vector3(car.global_position.x, 0.0, car.global_position.z) + var from_centre := flat.distance_to(derby_centre) + if from_centre > DERBY_ARENA: + derby_out_t += delta + else: + derby_out_t = maxf(derby_out_t - delta, 0.0) + info_label.text = "DERBY %d left %s" % [ + alive.size(), + "GET BACK IN %ds" % ceili(DERBY_OUT - derby_out_t) if derby_out_t > 0.5 else "%d wrecked" % derby_kills] + + if car.wrecked: + over = true + _msg("WRECKED\n%d of %d taken out\nR to retry, Esc for menu" + % [derby_kills, DERBY_CARS], 44, true) + elif derby_out_t >= DERBY_OUT: + over = true + _msg("RAN AWAY\nR to retry, Esc for menu", 44, true) + elif alive.is_empty(): + over = true + _msg("LAST ONE RUNNING\n%d wrecked\nR to retry, Esc for menu" % derby_kills, 48, true) + +func _drag_tick(delta: float) -> void: + if drag_len <= 0.0: + info_label.text = "no drag strip in this level" + return + drag_t += delta + bog = maxf(bog - delta, 0.0) + + if gear <= DRAG_GEARS: + revs = minf(revs + DRAG_REV_RATE[gear] * delta * (1.0 if bog == 0.0 else 0.25), 1.0) + if Input.is_action_just_pressed("boost"): + if revs >= SHIFT_LO and revs < REDLINE: + clean_shifts += 1 + _msg("CLEAN SHIFT", 34) + else: + blown_shifts += 1 + bog = 0.9 + _msg("MISSED IT", 34) + gear += 1 + revs = 0.28 if gear <= DRAG_GEARS else 1.0 + elif revs >= REDLINE: + bog = maxf(bog, 0.35) # sitting on the limiter goes nowhere + + # drive comes from the box, not the throttle: clean shifts pay, bogging hurts + var tune := 1.0 + clean_shifts * 0.07 - blown_shifts * 0.05 + # clamp the gear term: gear runs to DRAG_GEARS+1 as the "past top" sentinel, + # and un-clamped that paid 6/5 -- more grunt than top gear for shifting into nothing + car.power_scale = (0.35 if bog > 0.0 else tune) \ + * (0.75 + 0.25 * mini(gear, DRAG_GEARS) / float(DRAG_GEARS)) + + var flat := Vector3(car.global_position.x, 0.0, car.global_position.z) + var left := flat.distance_to(drag_end) + var rival_left := INF + if not rivals.is_empty(): + var rf := Vector3(rivals[0].global_position.x, 0.0, rivals[0].global_position.z) + rival_left = rf.distance_to(drag_end) + info_label.text = "GEAR %d %s %dm %.1fs" % [ + mini(gear, DRAG_GEARS), + ("BOGGED" if bog > 0.0 else ("SHIFT!" if revs >= SHIFT_LO else "%d%%" % int(revs * 100))), + int(left), drag_t] + # the rival crossing has to end it too, or losing means the run never stops + if left < 12.0 or rival_left < 12.0: + over = true + car.power_scale = 1.0 # _drag_tick stops on `over`, so reset it here + var won := left <= rival_left + _msg("%s %.2fs\n%d clean / %d blown\nR to retry, Esc for menu" + % ["WON" if won else "LOST", drag_t, clean_shifts, blown_shifts], 44, true) + +func add_heat(amount: float) -> void: + if mode != "cruise" or heat_cd > 0.0 or not cops.is_empty(): + return + heat = minf(heat + amount, 1.0) + if heat >= 1.0: + _start_pursuit() + +func _start_pursuit() -> void: + var pool := Registry.cars().values() + var n := COPS_MIN + (busts + escapes) / 2 # they escalate as the night goes on + for i in mini(n, COPS_MAX): + var c: Cop = (load("res://src/cop.tscn") as PackedScene).instantiate() + c.stats = load(pool[randi() % pool.size()]) + c.target = car + c.power_boost = 1.12 + c.hold_wreck = true # wrecked cop stays out of the chase + add_child(c) + # Drop them in behind, spread across the road, clear of the player. + # Flattened: multiplying by the full basis while the player is pitched + # (mid-jump, up a kerb) spawns cops in the air or under the road. + var flat_b := Basis.looking_at( + Vector3(-car.global_basis.z.x, 0.0, -car.global_basis.z.z).normalized(), Vector3.UP) + var back := flat_b * Vector3((i - 0.5) * 4.0, 0.0, 22.0 + i * 5.0) + c.global_transform = Transform3D( + Basis.looking_at(-back.normalized(), Vector3.UP), car.global_position + back) + c.wreck_started.connect(_cop_wrecked.bind(c)) + cops.append(c) + bust_t = 0.0 + escape_t = 0.0 + _msg("WANTED!\nlose them", 48) + +func _cop_wrecked(c: Cop) -> void: + # taking one out is worth the same as any takedown + if not car.wrecked and car.global_position.distance_to(c.global_position) < 25.0: + car.boost_max = minf(car.boost_max + 0.5, Car.BOOST_CAP) + car.boost = car.boost_max + _msg("COP DOWN!", 48) + +func _end_pursuit(escaped: bool) -> void: + for c in cops: + if is_instance_valid(c): + c.queue_free() + cops.clear() + heat = 0.0 + heat_cd = HEAT_COOLDOWN + info_label.text = "" # nothing else writes this in cruise, so "WANTED" would stick + if escaped: + escapes += 1 + car.boost_max = minf(car.boost_max + 0.5, Car.BOOST_CAP) + car.boost = car.boost_max + _msg("LOST 'EM! x%d" % escapes, 52) + else: + busts += 1 + _msg("BUSTED", 52) + +func _pursuit_tick(delta: float) -> void: + heat_cd = maxf(heat_cd - delta, 0.0) + if cops.is_empty(): + if heat > 0.0: + heat = maxf(heat - HEAT_DECAY * delta, 0.0) + return + + cops = cops.filter(func(c): return is_instance_valid(c)) + if cops.is_empty(): + _end_pursuit(true) + return + + var nearest := INF + for c in cops: + if not c.wrecked: + nearest = minf(nearest, car.global_position.distance_to(c.global_position)) + if nearest == INF: # every cop wrecked = you won + _end_pursuit(true) + return + + if nearest < BUST_RANGE and not car.wrecked: + bust_t += delta + escape_t = 0.0 + else: + bust_t = maxf(bust_t - delta * 0.5, 0.0) + if nearest > ESCAPE_RANGE: + escape_t += delta + else: + escape_t = 0.0 + + info_label.text = "WANTED %dm %s" % [ + int(nearest), + ("BUSTED IN %ds" % ceili(BUST_TIME - bust_t)) if bust_t > 1.0 + else ("LOSING THEM %ds" % ceili(ESCAPE_TIME - escape_t)) if escape_t > 0.5 + else "%d cars" % cops.size()] + + if car.wrecked and nearest < 40.0: + _end_pursuit(false) + elif bust_t >= BUST_TIME: + _end_pursuit(false) + elif escape_t >= ESCAPE_TIME: + _end_pursuit(true) + func _outrun_tick(delta: float) -> void: ## NFSU2-style impromptu 1v1: pull alongside a roaming rival at speed and it's ## on. Straight-line distance decides WHEN it ends (so ducking down a side @@ -510,6 +868,8 @@ func _nearest_off(pos: Vector3, seed_off: float, L: float) -> float: func _player_crash(dv: float) -> void: if mode in ["crash", "junction"] and not over: score += minf(dv, 30.0) * 120.0 + if dv > 3.0: + add_heat(HEAT_PER_SMASH * minf(dv / 6.0, 2.0)) if mode == "junction" and jstate == "run" and dv > 5.0: jstate = "settle" time_left = JCT_SETTLE @@ -536,6 +896,7 @@ func _traffic_smashed(impulse: float) -> void: score += impulse * 400.0 _msg("$%d" % int(impulse * 400.0), 40) car.earn_boost(0.2) + add_heat(HEAT_PER_SMASH) func _crashbreaker() -> void: breaker_used = true @@ -594,6 +955,11 @@ func _jct_tally() -> void: _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: + # a sticky message is a result screen; don't let a stray "NEAR MISS" from a + # still-rolling wreck overwrite it a moment later + if _sticky and not sticky: + return + _sticky = sticky msg_label.text = text msg_label.add_theme_font_size_override("font_size", size) if sticky: @@ -622,7 +988,16 @@ func _process(delta: float) -> void: 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"): + # drag and derby both tell you "R to retry", so they have to honour it too + if mode == "race" and Input.is_action_just_pressed("reset"): + # car.gd teleports the player home on R; without resyncing the lap + # tracker, _nearest_off's 25 m local window walks the offset around the + # loop and silently books phantom laps + for c in laps: + prev_off[c] = 0.0 if c == car else (c as Rival).progress + laps[car] = 0 + if over and mode in ["crash", "junction", "drag", "derby"] \ + and Input.is_action_just_pressed("reset"): get_tree().reload_current_scene() return for sp in spinners: @@ -635,3 +1010,5 @@ func _process(delta: float) -> void: boost_bar.size.x = 160.0 * car.boost_max boost_bar.max_value = 100.0 * car.boost_max boost_bar.value = car.boost * 100.0 + heat_bar.visible = mode == "cruise" and heat > 0.02 and cops.is_empty() + heat_bar.value = heat diff --git a/src/main.gd b/src/main.gd index cd17192..3c464da 100644 --- a/src/main.gd +++ b/src/main.gd @@ -1,7 +1,8 @@ extends Control ## Menu: pick a car, a level and a mode from whatever exists, hit RACE. -const MODES := [["cruise", "Cruise"], ["crash", "Crash Mode"], ["race", "Race"]] +const MODES := [["cruise", "Cruise"], ["crash", "Crash Mode"], ["race", "Race"], + ["drag", "Drag"], ["derby", "Derby"]] @onready var car_list: ItemList = $VBox/HBox/Cars @onready var level_list: ItemList = $VBox/HBox/Levels diff --git a/tests/smoke.gd b/tests/smoke.gd index e656703..9c6717a 100644 --- a/tests/smoke.gd +++ b/tests/smoke.gd @@ -10,34 +10,36 @@ func _initialize() -> void: func _run() -> void: var cars := Registry.cars() var levels := Registry.levels() - assert(not cars.is_empty(), "no cars found") - assert(not levels.is_empty(), "no levels found") + _check(not cars.is_empty(), "no cars found") + _check(not levels.is_empty(), "no levels found") for p in cars.values(): var s: CarStats = load(p) - assert(s != null and s.engine_power > 0.0, "bad stats: %s" % p) + _check(s != null and s.engine_power > 0.0, "bad stats: %s" % p) if s.model_path != "": - assert(load(s.model_path) is PackedScene, "bad model: %s" % s.model_path) + _check(load(s.model_path) is PackedScene, "bad model: %s" % s.model_path) # datto is procedural (has det_* parts); imported models legitimately have none Game.car_path = cars.get("datto_120y", cars.values()[0]) # the carpark is the controlled arena; city levels have walls that complicate the ram test Game.level_path = levels.get("woolies_carpark", levels.values()[0]) # cruise: throttle 3s, car must move + print(" .. cruise") var game: Game = await _boot("cruise") if game.car.stats.model_path != "": - assert(game.car._parts.size() >= 8, "model has no detachable det_* parts") + _check(game.car._parts.size() >= 8, "model has no detachable det_* parts") var start: Vector3 = game.car.global_position Input.action_press("throttle") for i in 180: await physics_frame var dist := start.distance_to(game.car.global_position) - assert(dist > 5.0, "car didn't move (%.1f m)" % dist) + _check(dist > 5.0, "car didn't move (%.1f m)" % dist) Input.action_release("throttle") game.free() # crash mode: traffic spawned, ram the nearest one, score must rise + print(" .. crash") game = await _boot("crash") - assert(game.traffic.size() == Game.TRAFFIC_COUNT["crash"], "traffic missing") + _check(game.traffic.size() == Game.TRAFFIC_COUNT["crash"], "traffic missing") var victim: TrafficCar = game.traffic[0] await physics_frame # traffic takes position on its first physics tick Input.action_press("throttle") @@ -54,18 +56,19 @@ func _run() -> void: if not victim.live: break Input.action_release("throttle") - assert(not victim.live, "rammed traffic never went dynamic") - assert(game.score > 0.0, "crash mode scored nothing") + _check(not victim.live, "rammed traffic never went dynamic") + _check(game.score > 0.0, "crash mode scored nothing") 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") + _check(not jcts.is_empty(), "no woolies junction event") Game.junction = jcts[0].data + print(" .. junction") game = await _boot("junction") - assert(game.traffic.size() >= 8, "junction convoys missing (%d)" % game.traffic.size()) + _check(game.traffic.size() >= 8, "junction convoys missing (%d)" % game.traffic.size()) Input.action_press("throttle") for i in 950: await physics_frame @@ -78,9 +81,9 @@ func _run() -> void: 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)) + _check(game.jstate != "run", "never crashed into the convoy") + _check(game.breaker_used, "crashbreaker never fired") + _check(game.over and game.score > 0.0, "junction never tallied (score %d)" % int(game.score)) var jct_score := game.score Game.junction = {} game.free() @@ -88,7 +91,7 @@ func _run() -> void: # outrun: roaming rivals exist in cruise; pulling alongside at speed arms the # duel, and the win path pays out and clears state game = await _boot("cruise") - assert(game.rivals.size() == Game.ROAM_RIVALS, "no roaming rivals in cruise") + _check(game.rivals.size() == Game.ROAM_RIVALS, "no roaming rivals in cruise") var rr: Rival = game.rivals[0] await physics_frame game.car.global_position = rr.global_position + Vector3(3, 0.3, 0) @@ -97,17 +100,72 @@ func _run() -> void: await physics_frame if game.outrun_rival != null: break - assert(game.outrun_rival == rr, "outrun didn't arm on proximity") + _check(game.outrun_rival == rr, "outrun didn't arm on proximity") var bm0: float = game.car.boost_max game._outrun_end(true, "test") - assert(game.outrun_rival == null and game.outruns_won == 1, "outrun win didn't settle") - assert(game.car.boost_max > bm0, "outrun win paid no boost") - assert(game.outrun_cd > 0.0, "no re-arm cooldown after outrun") + _check(game.outrun_rival == null and game.outruns_won == 1, "outrun win didn't settle") + _check(game.car.boost_max > bm0, "outrun win paid no boost") + _check(game.outrun_cd > 0.0, "no re-arm cooldown after outrun") + game.free() + + # pursuit: smashing raises heat, heat summons cops, fleeing loses them + print(" .. pursuit") + game = await _boot("cruise") + for i in 10: + game.add_heat(Game.HEAT_PER_SMASH) + _check(game.cops.size() >= Game.COPS_MIN, "heat didn't summon cops") + game.car.global_position += Vector3(900, 0, 900) + for i in 700: + await physics_frame + if game.cops.is_empty(): + break + _check(game.escapes == 1 and game.cops.is_empty(), "escape never resolved") + _check(game.heat_cd > 0.0, "no cooldown after escape") + game.free() + + # drag: needs a level with DragStart/DragEnd, so not the code-built carpark + Game.level_path = levels["southbank"] + print(" .. drag") + game = await _boot("drag") + _check(game.drag_len > 100.0, "no drag strip found (%.0f m)" % game.drag_len) + _check(game.rivals.size() == 1, "drag had no opponent") + Input.action_press("throttle") + for i in 10: + await physics_frame + _check(game.revs > 0.0, "revs never climbed") + var g0: int = game.gear + # shift the moment revs enter the window. First gear redlines in ~1.6 s, so + # idling here first pins revs at 1.0 and the shift correctly scores as blown. + var spin := 0 + while game.revs < Game.SHIFT_LO and spin < 600: # bounded: never hang the suite + await physics_frame + spin += 1 + _check(game.revs >= Game.SHIFT_LO and game.revs < Game.REDLINE, + "revs %.2f not inside the shift window [%.2f, %.2f)" % [game.revs, Game.SHIFT_LO, Game.REDLINE]) + Input.action_press("boost") + await physics_frame + Input.action_release("boost") + await physics_frame + _check(game.gear == g0 + 1, "shift didn't change gear") + _check(game.clean_shifts == 1 and game.blown_shifts == 0, + "in-window shift scored as blown (%d/%d)" % [game.clean_shifts, game.blown_shifts]) + Input.action_release("throttle") + game.free() + Game.level_path = levels.get("woolies_carpark", levels.values()[0]) + + # derby: opponents spawn in a ring and each hunts something + print(" .. derby") + game = await _boot("derby") + _check(game.derby.size() == Game.DERBY_CARS, "derby field wrong size") + await physics_frame + for d in game.derby: + _check(d.target != null and d.target != d, "derby car has no/self target") game.free() # race: rivals spawn and make progress along the path + print(" .. race") game = await _boot("race") - assert(game.rivals.size() == Game.RIVAL_COUNT, "rivals missing") + _check(game.rivals.size() == Game.RIVAL_COUNT, "rivals missing") var p0: float = game.rivals[0].progress for i in 240: await physics_frame @@ -115,13 +173,23 @@ func _run() -> void: for r in game.rivals: if absf(r.progress - p0) > 3.0 or r.linear_velocity.length() > 3.0: moved = true - assert(moved, "no rival is driving") + _check(moved, "no rival is driving") game.free() - print("smoke ok: %d cars, %d levels | cruise %.1f m | crash $%d | junction $%d | outrun arms+pays | race AI driving" % - [cars.size(), levels.size(), dist, int(crash_score), int(jct_score)]) + print("smoke ok: %d cars, %d levels | cruise %.1f m | crash $%d | junction $%d | outrun arms+pays | pursuit spawns+escapes | drag shifts | derby %d cars | race AI driving" % + [cars.size(), levels.size(), dist, int(crash_score), int(jct_score), Game.DERBY_CARS]) quit(0) +func _check(cond: bool, msg: String) -> void: + ## NOT assert(): a failed assert in a headless SceneTree script halts the + ## script but leaves the tree running, so the process spins forever instead + ## of failing. One bad assertion cost 53 minutes of wall clock before this. + if cond: + return + push_error(msg) + print("SMOKE FAIL: %s" % msg) + quit(1) + func _boot(mode: String) -> Game: Game.mode = mode var game: Game = (load("res://src/game.tscn") as PackedScene).instantiate() diff --git a/tools/build_all_junctions.sh b/tools/build_all_junctions.sh new file mode 100755 index 0000000..d08707e --- /dev/null +++ b/tools/build_all_junctions.sh @@ -0,0 +1,23 @@ +#!/bin/bash +# Regenerate every OSM crash-junction event. The spec strings live here because +# they were previously nowhere -- the original invocations weren't recorded, and +# recovering "which two streets is this event?" meant re-deriving them from the +# generated geometry. Don't let that happen twice. +# +# woolies_carpark/junctions.json is hand-authored (Trolley Run) and not touched. +set -e +cd "$(dirname "$0")/.." +C="${OSM_CACHE:-$HOME/.cache/burnout_osm}" +[ -d "$C" ] || { echo "no OSM cache at $C -- see README"; exit 1; } +while IFS='|' read -r id json lat lon specs; do + [ -z "$id" ] && continue + IFS=';' read -ra SPEC <<< "$specs" + python3 tools/osm_junction.py "$C/$json" "levels/$id/junctions.json" "$lat" "$lon" "${SPEC[@]}" +done <<'EOF' +queen_st_mall|brisbane_cbd.json|-27.4699|153.0251|Albert x Adelaide|Albert Street|Adelaide Street;Edward x Elizabeth|Edward Street|Elizabeth Street +southbank|southbank.json|-27.4775|153.0215|Melbourne x Merivale|Melbourne Street|Merivale Street;Grey x Glenelg|Grey Street|Glenelg Street +albert_parklands|albert_parklands.json|-27.4740|153.0280|Alice x George|Alice Street|George Street;Margaret x Edward|Margaret Street|Edward Street +story_bridge|story_bridge.json|-27.4635|153.0355|Bridge Approach|Bradfield Highway|Kemp Place +kangaroo_point|kangaroo_point.json|-27.4780|153.0345|Cliffs Corner|River Terrace|Main Street +EOF +echo "junctions rebuilt (woolies_carpark left alone -- hand-authored)" diff --git a/tools/build_level_osm.py b/tools/build_level_osm.py index f7fc29c..2554abf 100644 --- a/tools/build_level_osm.py +++ b/tools/build_level_osm.py @@ -373,7 +373,7 @@ def add_canopy(bm, at, fwd): d = json.load(open(src)) counts = dict.fromkeys( ("bld", "road", "path", "park", "planter", "water", "cliff", "tree", "prop", - "cross", "signal", "pole", "pp", "sl", "bo", "wb"), 0) + "cross", "signal", "pole", "pp", "sl", "bo", "wb", "drag_m"), 0) GREEN_LEISURE = ("park", "recreation_ground", "pitch", "playground", "golf_course", "nature_reserve") GREEN_LANDUSE = ("grass", "recreation_ground", "village_green", "meadow", "forest") PLANTER_LEISURE = ("garden",) @@ -397,6 +397,7 @@ sig_bm, lens_bm = bmesh.new(), bmesh.new() pole_bm, wire_bm = bmesh.new(), bmesh.new() pp_spots, wb_spots = [], [] # (x, y, yaw): parked-car / wheelie-bin markers for game.gd to fill smash_spots = {"SL": [], "BO": [], "WB": []} # street lamps / bollards / bins off OSM +drag_candidates = [] # wide road polylines; the straightest run becomes the drag strip PP_CAP, WB_CAP = 70, 40 SMASH_CAP = {"SL": 90, "BO": 90, "WB": 60} PARK_STREETS = ("residential", "tertiary", "secondary", "unclassified") @@ -651,6 +652,8 @@ for e in d["elements"]: acc += 28.0 acc -= segL road_ways.append((pts, wide, hw)) + if hw not in ("service", "busway") and wide >= 7.0: + drag_candidates.append(pts) # no footpath strip on a pedestrian mall -- the mall paving IS the surface if hw != "pedestrian" and wide >= 7.0 and hw not in ("busway",): # dashed centre line for i in range(len(pts) - 1): @@ -879,6 +882,40 @@ for prefix, spots, cap in batches: bpy.context.collection.objects.link(emp) counts[prefix.lower()] = min(len(spots), cap) +# Drag strip: the longest genuinely straight run of wide road in the level. +# Emitted as a marker pair so drag mode doesn't need hand-authored coordinates +# per level -- a strip that bends is a strip you crash on rather than race. +best_run = None +for pts in drag_candidates: + i = 0 + while i < len(pts) - 1: + ax, az = pts[i + 1][0] - pts[i][0], pts[i + 1][1] - pts[i][1] + aL = math.hypot(ax, az) + if aL < 1.0: + i += 1 + continue + ax, az = ax / aL, az / aL + j, run = i + 1, aL + while j < len(pts) - 1: + bx, bz = pts[j + 1][0] - pts[j][0], pts[j + 1][1] - pts[j][1] + bL = math.hypot(bx, bz) + if bL < 1.0 or (bx / bL) * ax + (bz / bL) * az < 0.985: # ~10 deg + break + run += bL + j += 1 + if best_run is None or run > best_run[0]: + best_run = (run, pts[i], pts[j]) + i = max(j, i + 1) +if best_run and best_run[0] >= 150.0: + run, p0, p1 = best_run + yaw = math.atan2(p1[1] - p0[1], p1[0] - p0[0]) + for nm, p in (("DragStart", p0), ("DragEnd", p1)): + emp = bpy.data.objects.new(nm, None) + emp.location = (p[0], p[1], 0.1) + emp.rotation_euler = (0, 0, yaw) + bpy.context.collection.objects.link(emp) + counts["drag_m"] = int(run) + if spawn_arg: slat, slon, sbear = [float(v) for v in spawn_arg.split(",")] sx, sy = xy(slat, slon) diff --git a/tools/gen_textures.sh b/tools/gen_textures.sh index c066803..d815ec1 100755 --- a/tools/gen_textures.sh +++ b/tools/gen_textures.sh @@ -45,6 +45,7 @@ signage_strip|768|seamless tiling texture, a long horizontal row of colourful fi billboard_ad|768|single large fictional advertising billboard poster, bold colourful retro soft drink advertisement with invented gibberish brand name lettering and a sunburst, flat frontal view, even lighting|colourful fictional poster advertisement with made up unreadable brand lettering, bold shapes, flat view|0 billboard_ad2|768|single large fictional advertising billboard poster, 1980s Australian beer advertisement style, bold blue and gold blocks with invented gibberish brand lettering, flat frontal view, even lighting|fictional poster ad with blue and gold blocks and made up unreadable lettering, flat frontal view|0 billboard_ad3|768|single large fictional advertising billboard poster, bright red and cream used car yard sale advertisement with invented gibberish lettering and starburst price tags, flat frontal view, even lighting|fictional red and cream sale poster with made up unreadable lettering and starbursts, flat frontal view|0 +scratches|512|seamless tiling texture of scratched and scuffed dull grey car paint, fine directional scratches, small dents and primer showing through in patches, mid grey, flat even lighting|repeating scuffed scratched grey painted metal panel surface with worn patches, even light|150 EOF ) diff --git a/tools/osm_junction.py b/tools/osm_junction.py index e26aa8a..b83982d 100644 --- a/tools/osm_junction.py +++ b/tools/osm_junction.py @@ -5,14 +5,23 @@ Usage: osm_junction.py "Name|Stre 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). +copy lane-offset the other way). Spawn point sits SPAWN_DIST up Street A aimed +at the junction. Coordinates are level-local Godot metres (x east, z south). + +RADIUS is the whole event. Clip too wide and the convoy spreads thin over +hundreds of metres, so you arrive at the intersection through a 20 m gap and +score nothing -- "Margaret x Edward" was unwinnable that way, 27 convoy cars +alive and zero collisions. Keep the streams hugging the junction so the cars +are actually crossing when you get there. """ import json import math import sys +from itertools import zip_longest -RADIUS = 170.0 +RADIUS = 95.0 # convoy clip window around the junction +SPAWN_DIST = 120.0 # launch distance up street A +MIN_STREAM = 28.0 # shorter than this and a "stream" is a couple of parked cars LANE = 2.7 TARGETS = [15000, 35000, 60000] @@ -41,6 +50,45 @@ def offset(pts, d): outp.append([p[0] + dz / L * d, p[1] - dx / L * d]) return outp +def launch_point(pts, jpt): + """Walk street A outward from the junction to SPAWN_DIST, aimed back at it. + + Taking the farthest clipped point instead (the old way) tied the launch + distance to RADIUS, so tightening the convoy window would have dragged the + start line in on top of the intersection. + """ + i = min(range(len(pts)), key=lambda k: math.hypot(pts[k][0] - jpt[0], pts[k][1] - jpt[1])) + best = None + for step in (1, -1): + run, prev, k, end, hit = 0.0, pts[i], i + step, pts[i], False + while 0 <= k < len(pts): + seg = math.hypot(pts[k][0] - prev[0], pts[k][1] - prev[1]) + if run + seg >= SPAWN_DIST and seg > 0.01: + # land exactly on SPAWN_DIST -- OSM segments can be hundreds of + # metres, so stopping at the far vertex overshot by 100m+ + f = (SPAWN_DIST - run) / seg + end = [prev[0] + (pts[k][0] - prev[0]) * f, + prev[1] + (pts[k][1] - prev[1]) * f] + run, hit = SPAWN_DIST, True + break + run += seg + prev, end, k = pts[k], pts[k], k + step + if best is None or (hit and not best[3]) or (hit == best[3] and run > best[0]): + best = (run, end, step, hit) + run, end, step, hit = best + if not hit and run > 1.0: + # street ran out -- extend along its last heading so every event + # launches from the same distance regardless of how OSM split the way + j = max(min(i + step, len(pts) - 1), 0) + dx, dz = end[0] - pts[j][0], end[1] - pts[j][1] + L = math.hypot(dx, dz) + if L > 0.01: + grow = SPAWN_DIST - run + end = [end[0] + dx / L * grow, end[1] + dz / L * grow] + dx, dz = jpt[0] - end[0], jpt[1] - end[1] + L = math.hypot(dx, dz) or 1.0 + return {"x": end[0], "z": end[1], "dx": dx / L, "dz": dz / L} + events = [] for spec in sys.argv[5:]: name, a, b = spec.split("|")[:3] @@ -50,14 +98,32 @@ for spec in sys.argv[5:]: 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 = [] + # Two named streets often share several nodes (OSM splits ways at every + # change of tag), and the lowest node id is arbitrary -- for Albert x + # Adelaide it landed inside a CBD building block with the convoy 60 m away + # and no drivable run-up. Pick the shared node with the most street geometry + # around it: the busiest crossing is the best crash junction anyway. + def node_pt(n): + for street in (a, b): + for w in ways[street]: + if n in w["nodes"]: + g = w["geometry"][w["nodes"].index(n)] + return xz(g["lat"], g["lon"]) + return None + + def busyness(n): + p = node_pt(n) + if p is None: + return -1 + return sum(1 for w in ways.get(a, []) + ways.get(b, []) + for g in w["geometry"] + if math.hypot(*[c - d for c, d in zip(xz(g["lat"], g["lon"]), p)]) < RADIUS) + + jn = max(sorted(shared), key=busyness) + jpt = node_pt(jn) + by_street = {a: [], b: []} spawn = None + spawn_d = 1e9 for street in (a, b): for w in ways[street]: pts = [xz(g["lat"], g["lon"]) for g in w["geometry"]] @@ -66,21 +132,37 @@ for spec in sys.argv[5:]: 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: + if length < MIN_STREAM: 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} + by_street[street].append({"pts": offset(near, LANE), "speed": 12.0, "interval": 1.5}) + by_street[street].append({"pts": offset(list(reversed(near)), LANE), + "speed": 11.0, "interval": 1.8}) + if street == a: + # OSM splits a street into many ways; only the one that actually + # reaches the junction can define the launch, or we start 40 m + # off the approach and never aim at the intersection + d_near = min(math.hypot(p[0] - jpt[0], p[1] - jpt[1]) for p in pts) + if d_near < spawn_d: + spawn, spawn_d = launch_point(pts, jpt), d_near + # Interleave the two streets before capping. Appending A's streams then B's + # and slicing [:6] could drop street B entirely -- leaving an "intersection" + # with traffic only on the road you launch down, and nothing to hit. + streams = [] + for pair in zip_longest(by_street[a], by_street[b]): + for s in pair: + if s is not None: + streams.append(s) + streams = streams[:6] 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]))) + cross = sum(1 for s in streams if s in by_street[b]) + events.append({"name": name, "spawn": spawn, "streams": streams, "targets": TARGETS}) + print("OK %s: %d streams (%d crossing), spawn %.0fm out" + % (name, len(streams), cross, 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))) +if len(events) < len(sys.argv[5:]): + sys.exit("FAILED: %d of %d specs produced nothing -- refusing to ship a level " + "with missing junctions" % (len(sys.argv[5:]) - len(events), len(sys.argv[5:]))) diff --git a/tools/tune_junctions.gd b/tools/tune_junctions.gd new file mode 100644 index 0000000..f7fbc43 --- /dev/null +++ b/tools/tune_junctions.gd @@ -0,0 +1,104 @@ +extends SceneTree +## Measure what each crash junction is actually worth, then write per-event +## medal targets back into levels//junctions.json. +## +## Godot --headless --path . --script tools/tune_junctions.gd -- runs=12 +## +## Every junction shipped with the same 15/35/60k guess, which is meaningless: +## a six-lane approach with four convoy streams pays several times what a +## carpark aisle does. This drives each event headless with a spread of launch +## speeds and steering biases, takes the damage distribution, and sets +## bronze/silver/gold at the 35th/65th/88th percentile of what was achieved. +## +## Percentiles, not max: gold should be a good run, not a freak one. Runs are +## seeded per (event, attempt) so the numbers reproduce. + +const SETTLE_FRAMES := 1500 + +func _initialize() -> void: + _run() + +func _run() -> void: + var runs := 12 + for a in OS.get_cmdline_user_args(): + if a.begins_with("runs="): + runs = int(a.split("=")[1]) + + var by_level := {} + for j in Registry.junctions(): + by_level.get_or_add(j.level_id, []).append(j) + + for level_id in by_level: + var path := "res://levels/%s/junctions.json" % level_id + var events: Array = JSON.parse_string(FileAccess.get_file_as_string(path)) + for j in by_level[level_id]: + var scores: Array[float] = [] + for attempt in runs: + scores.append(await _play(j.data, attempt)) + scores.sort() + var pick := func(p: float) -> int: + var v: float = scores[clampi(int(round(p * (scores.size() - 1))), 0, scores.size() - 1)] + return int(round(v / 500.0)) * 500 # tidy numbers, not $47,213 + var targets := [pick.call(0.35), pick.call(0.65), pick.call(0.88)] + # guarantee a real gap between medals even on a flat distribution + targets[0] = maxi(targets[0], 2000) + targets[1] = maxi(targets[1], targets[0] + 2500) + targets[2] = maxi(targets[2], targets[1] + 4000) + var duds := 0 + for s in scores: + if s <= 0.0: + duds += 1 + if duds * 2 >= scores.size(): + push_warning("%s: %d/%d runs scored nothing -- event may be unwinnable" + % [j.name, duds, scores.size()]) + print(" !! %s scored nothing in %d/%d runs" % [j.name, duds, scores.size()]) + for ev in events: + if ev.get("name") == j.name: + ev["targets"] = targets + print("%-18s %-24s min $%d med $%d max $%d -> %s" + % [level_id, j.name, int(scores[0]), int(scores[scores.size() / 2]), + int(scores[-1]), str(targets)]) + var f := FileAccess.open(path, FileAccess.WRITE) + f.store_string(JSON.stringify(events, " ") + "\n") + f.close() + print("tuned: wrote targets for %d levels" % by_level.size()) + quit(0) + +func _play(data: Dictionary, attempt: int) -> float: + seed(hash(str(data.get("name", "")) + str(attempt))) + Game.car_path = Registry.cars()["kingswood_hz"] + Game.level_path = Registry.levels()[_level_of(data)] + Game.mode = "junction" + Game.junction = data + var game: Game = (load("res://src/game.tscn") as PackedScene).instantiate() + root.add_child(game) + await process_frame + # vary the launch so the spread reflects skill, not one scripted line + var lean := (attempt % 5 - 2) * 0.16 + var boosty := attempt % 3 == 0 + Input.action_press("throttle") + if boosty: + Input.action_press("boost") + var fired := false + for i in SETTLE_FRAMES: + await physics_frame + if i < 90: + game.car.steer_in = lean + if game.jstate == "settle" and not fired: + Input.action_release("throttle") + Input.action_press("boost") + fired = true + if game.over: + break + Input.action_release("throttle") + Input.action_release("boost") + var s: float = game.score + game.free() + await process_frame + return s + +func _level_of(data: Dictionary) -> String: + for j in Registry.junctions(): + if j.data == data: + return j.level_id + return "" diff --git a/tools/tune_junctions.gd.uid b/tools/tune_junctions.gd.uid new file mode 100644 index 0000000..09b24b7 --- /dev/null +++ b/tools/tune_junctions.gd.uid @@ -0,0 +1 @@ +uid://dkj574xr62aff