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 <noreply@anthropic.com>
This commit is contained in:
parent
59202c222c
commit
0c13c82eac
43
README.md
43
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
|
- **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)
|
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
|
and detonate the Crashbreaker (space). Damage tally + bronze/silver/gold. Events live in
|
||||||
`levels/<id>/junctions.json`; generate more from real intersections with
|
`levels/<id>/junctions.json`, regenerated in one go by
|
||||||
`tools/osm_junction.py <area.json> <out.json> <lat0> <lon0> "Name|Street A|Street B"`.
|
[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`.
|
- **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
|
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).
|
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.
|
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
|
[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
|
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;
|
instead of folding through itself. Only the player and rivals carry a deformer;
|
||||||
traffic and parked cars are plain bodies.
|
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
|
## Cars
|
||||||
|
|
||||||
A car is a folder: `cars/<id>/stats.tres` + optional `car.glb`. Models are built
|
A car is a folder: `cars/<id>/stats.tres` + optional `car.glb`. Models are built
|
||||||
|
|||||||
BIN
assets/textures/scratches.jpg
Normal file
BIN
assets/textures/scratches.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 40 KiB |
40
assets/textures/scratches.jpg.import
Normal file
40
assets/textures/scratches.jpg.import
Normal file
@ -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
|
||||||
@ -2,10 +2,10 @@
|
|||||||
{
|
{
|
||||||
"name": "Alice x George",
|
"name": "Alice x George",
|
||||||
"spawn": {
|
"spawn": {
|
||||||
"x": -143.83199552181551,
|
"x": -165.3192678228276,
|
||||||
"z": 142.01018819980536,
|
"z": 159.9955423589618,
|
||||||
"dx": 0.7668348571083106,
|
"dx": 0.7668331983120437,
|
||||||
"dz": -0.6418444530598335
|
"dz": -0.6418464348787193
|
||||||
},
|
},
|
||||||
"streams": [
|
"streams": [
|
||||||
{
|
{
|
||||||
@ -56,7 +56,29 @@
|
|||||||
]
|
]
|
||||||
],
|
],
|
||||||
"speed": 12.0,
|
"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": [
|
"pts": [
|
||||||
@ -106,65 +128,7 @@
|
|||||||
]
|
]
|
||||||
],
|
],
|
||||||
"speed": 11.0,
|
"speed": 11.0,
|
||||||
"interval": 2.6
|
"interval": 1.8
|
||||||
},
|
|
||||||
{
|
|
||||||
"pts": [
|
|
||||||
[
|
|
||||||
29.76081815300818,
|
|
||||||
-3.9437773782503602
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0.8522815554834703,
|
|
||||||
21.169938953797764
|
|
||||||
],
|
|
||||||
[
|
|
||||||
-51.1224413806449,
|
|
||||||
67.0201498038004
|
|
||||||
]
|
|
||||||
],
|
|
||||||
"speed": 12.0,
|
|
||||||
"interval": 2.2
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"pts": [
|
|
||||||
[
|
|
||||||
-54.694766613638436,
|
|
||||||
62.97064459578822
|
|
||||||
],
|
|
||||||
[
|
|
||||||
-2.7090703425578857,
|
|
||||||
17.11077984650507
|
|
||||||
],
|
|
||||||
[
|
|
||||||
26.219388577689813,
|
|
||||||
-8.020329421713114
|
|
||||||
]
|
|
||||||
],
|
|
||||||
"speed": 11.0,
|
|
||||||
"interval": 2.6
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"pts": [
|
|
||||||
[
|
|
||||||
-75.361941989609,
|
|
||||||
84.82292365250544
|
|
||||||
],
|
|
||||||
[
|
|
||||||
-80.93953886827424,
|
|
||||||
78.61163770120406
|
|
||||||
],
|
|
||||||
[
|
|
||||||
-82.56599989409156,
|
|
||||||
76.7348960854059
|
|
||||||
],
|
|
||||||
[
|
|
||||||
-115.40558143561937,
|
|
||||||
38.74155522137386
|
|
||||||
]
|
|
||||||
],
|
|
||||||
"speed": 12.0,
|
|
||||||
"interval": 2.2
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"pts": [
|
"pts": [
|
||||||
@ -186,7 +150,147 @@
|
|||||||
]
|
]
|
||||||
],
|
],
|
||||||
"speed": 11.0,
|
"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": [
|
"targets": [
|
||||||
@ -198,64 +302,12 @@
|
|||||||
{
|
{
|
||||||
"name": "Margaret x Edward",
|
"name": "Margaret x Edward",
|
||||||
"spawn": {
|
"spawn": {
|
||||||
"x": 88.92833122889411,
|
"x": 276.7158840463856,
|
||||||
"z": -205.50177900009658,
|
"z": -371.11587218595514,
|
||||||
"dx": 0.7566215209215891,
|
"dx": -0.7427057931255263,
|
||||||
"dz": -0.6538530982401947
|
"dz": 0.6696178797327494
|
||||||
},
|
},
|
||||||
"streams": [
|
"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": [
|
"pts": [
|
||||||
[
|
[
|
||||||
@ -284,7 +336,41 @@
|
|||||||
]
|
]
|
||||||
],
|
],
|
||||||
"speed": 12.0,
|
"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": [
|
"pts": [
|
||||||
@ -314,71 +400,13 @@
|
|||||||
]
|
]
|
||||||
],
|
],
|
||||||
"speed": 11.0,
|
"speed": 11.0,
|
||||||
"interval": 2.6
|
"interval": 1.8
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"pts": [
|
"pts": [
|
||||||
[
|
[
|
||||||
116.8650308963045,
|
240.4674984004257,
|
||||||
-376.11651856442666
|
-225.74901403845038
|
||||||
],
|
|
||||||
[
|
|
||||||
122.97019010007931,
|
|
||||||
-369.0715631207818
|
|
||||||
],
|
|
||||||
[
|
|
||||||
179.81860554472172,
|
|
||||||
-304.1656834379468
|
|
||||||
],
|
|
||||||
[
|
|
||||||
181.35983927566346,
|
|
||||||
-302.4069925620872
|
|
||||||
],
|
|
||||||
[
|
|
||||||
184.2133034529848,
|
|
||||||
-299.1448137854387
|
|
||||||
],
|
|
||||||
[
|
|
||||||
189.71555797124375,
|
|
||||||
-292.5416508365839
|
|
||||||
],
|
|
||||||
[
|
|
||||||
194.9234531376747,
|
|
||||||
-286.3366793395667
|
|
||||||
],
|
|
||||||
[
|
|
||||||
234.05675616721896,
|
|
||||||
-241.1315253602194
|
|
||||||
],
|
|
||||||
[
|
|
||||||
244.51403825990147,
|
|
||||||
-229.32469253293408
|
|
||||||
],
|
|
||||||
[
|
|
||||||
255.05386474930117,
|
|
||||||
-217.31435380098776
|
|
||||||
],
|
|
||||||
[
|
|
||||||
256.9119332285974,
|
|
||||||
-215.07700198364842
|
|
||||||
]
|
|
||||||
],
|
|
||||||
"speed": 12.0,
|
|
||||||
"interval": 2.2
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"pts": [
|
|
||||||
[
|
|
||||||
252.7568367319326,
|
|
||||||
-211.628064016186
|
|
||||||
],
|
|
||||||
[
|
|
||||||
250.9803399157384,
|
|
||||||
-213.769442598948
|
|
||||||
],
|
|
||||||
[
|
|
||||||
240.4633914001549,
|
|
||||||
-225.75366186691826
|
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
229.982416914263,
|
229.982416914263,
|
||||||
@ -401,20 +429,12 @@
|
|||||||
-298.8501898379666
|
-298.8501898379666
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
175.7564478621494,
|
175.7569318305437,
|
||||||
-300.60775216234316
|
-300.6071997580377
|
||||||
],
|
|
||||||
[
|
|
||||||
118.90617949034443,
|
|
||||||
-365.5157484793863
|
|
||||||
],
|
|
||||||
[
|
|
||||||
112.7841869787125,
|
|
||||||
-372.58003543563154
|
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
"speed": 11.0,
|
"speed": 11.0,
|
||||||
"interval": 2.6
|
"interval": 1.8
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"targets": [
|
"targets": [
|
||||||
|
|||||||
Binary file not shown.
@ -3,7 +3,7 @@
|
|||||||
importer="scene"
|
importer="scene"
|
||||||
importer_version=1
|
importer_version=1
|
||||||
type="PackedScene"
|
type="PackedScene"
|
||||||
uid="uid://lekue6r6vs5u"
|
uid="uid://dm4gmb5igf0n5"
|
||||||
path="res://.godot/imported/level.glb-4aa597fe23a02438c7c91aac8329adb8.scn"
|
path="res://.godot/imported/level.glb-4aa597fe23a02438c7c91aac8329adb8.scn"
|
||||||
|
|
||||||
[deps]
|
[deps]
|
||||||
|
|||||||
@ -2,21 +2,17 @@
|
|||||||
{
|
{
|
||||||
"name": "Cliffs Corner",
|
"name": "Cliffs Corner",
|
||||||
"spawn": {
|
"spawn": {
|
||||||
"x": 49.98333461330541,
|
"x": 19.82910866163153,
|
||||||
"z": -68.64433920000972,
|
"z": -91.88863639399733,
|
||||||
"dx": 0.4680499904857942,
|
"dx": 0.7116289476302671,
|
||||||
"dz": -0.8837019895905226
|
"dz": -0.7025555073406219
|
||||||
},
|
},
|
||||||
"streams": [
|
"streams": [
|
||||||
{
|
{
|
||||||
"pts": [
|
"pts": [
|
||||||
[
|
[
|
||||||
47.4831595229508,
|
63.92619388342862,
|
||||||
-69.66371377177926
|
-109.98937518013152
|
||||||
],
|
|
||||||
[
|
|
||||||
63.94149741985269,
|
|
||||||
-110.02953643297833
|
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
78.3750416498091,
|
78.3750416498091,
|
||||||
@ -24,7 +20,21 @@
|
|||||||
]
|
]
|
||||||
],
|
],
|
||||||
"speed": 12.0,
|
"speed": 12.0,
|
||||||
"interval": 2.2
|
"interval": 1.5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pts": [
|
||||||
|
[
|
||||||
|
106.13660245700423,
|
||||||
|
-179.68219050814878
|
||||||
|
],
|
||||||
|
[
|
||||||
|
105.14898470103478,
|
||||||
|
-213.87167130817437
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"speed": 12.0,
|
||||||
|
"interval": 1.5
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"pts": [
|
"pts": [
|
||||||
@ -33,96 +43,102 @@
|
|||||||
-146.95142862004474
|
-146.95142862004474
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
68.97210013309922,
|
68.98740366952329,
|
||||||
-108.06662116698321
|
-108.10678241983003
|
||||||
],
|
|
||||||
[
|
|
||||||
52.48350970366002,
|
|
||||||
-67.62496462824019
|
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
"speed": 11.0,
|
"speed": 11.0,
|
||||||
"interval": 2.6
|
"interval": 1.8
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"pts": [
|
"pts": [
|
||||||
[
|
[
|
||||||
114.66387685267605,
|
110.54673313420321,
|
||||||
-57.53027727981913
|
-214.02759389225906
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
109.86666098023801,
|
111.53435089017266,
|
||||||
-74.50891216449172
|
-179.83811309223347
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"speed": 11.0,
|
||||||
|
"interval": 1.8
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pts": [
|
||||||
|
[
|
||||||
|
110.03793689684629,
|
||||||
|
-168.49583563849748
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
107.12471250932794,
|
105.07701815835642,
|
||||||
-155.39569742995997
|
-166.64105720795808
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
106.88904877360271,
|
102.76621908994164,
|
||||||
-160.41551590552262
|
-165.6886852292976
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
106.3952482221942,
|
100.0562323833477,
|
||||||
-170.89775425802756
|
-163.76233037299872
|
||||||
|
],
|
||||||
|
[
|
||||||
|
96.9873892973125,
|
||||||
|
-160.78656603278262
|
||||||
|
],
|
||||||
|
[
|
||||||
|
94.50499850150788,
|
||||||
|
-158.03413164414619
|
||||||
|
],
|
||||||
|
[
|
||||||
|
91.4342291831665,
|
||||||
|
-153.71805517412884
|
||||||
|
],
|
||||||
|
[
|
||||||
|
82.70848350683406,
|
||||||
|
-145.88280488205245
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
"speed": 12.0,
|
"speed": 12.0,
|
||||||
"interval": 2.2
|
"interval": 1.5
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"pts": [
|
"pts": [
|
||||||
[
|
[
|
||||||
111.78926635550437,
|
79.10280957887883,
|
||||||
-171.15185734246612
|
-149.90264511833848
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
112.28308355725834,
|
87.56161285248508,
|
||||||
-160.66926529510476
|
-157.4814104264721
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
112.52147634417389,
|
90.25853122720883,
|
||||||
-155.58262017041363
|
-161.369924756482
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
115.2509302403802,
|
93.10927630970947,
|
||||||
-74.92079143598734
|
-164.5442567671812
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
119.8558354049959,
|
96.57846276532892,
|
||||||
-59.01471872056136
|
-167.89332522702423
|
||||||
|
],
|
||||||
|
[
|
||||||
|
100.07071556655998,
|
||||||
|
-170.36781557108517
|
||||||
|
],
|
||||||
|
[
|
||||||
|
103.11280473039093,
|
||||||
|
-171.67115319207213
|
||||||
|
],
|
||||||
|
[
|
||||||
|
108.14657768085227,
|
||||||
|
-173.5537759619962
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
"speed": 11.0,
|
"speed": 11.0,
|
||||||
"interval": 2.6
|
"interval": 1.8
|
||||||
},
|
|
||||||
{
|
|
||||||
"pts": [
|
|
||||||
[
|
|
||||||
105.14797091687734,
|
|
||||||
-213.92504025483834
|
|
||||||
],
|
|
||||||
[
|
|
||||||
104.12084845022,
|
|
||||||
-326.6884054546313
|
|
||||||
]
|
|
||||||
],
|
|
||||||
"speed": 12.0,
|
|
||||||
"interval": 2.2
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"pts": [
|
|
||||||
[
|
|
||||||
109.5206244517033,
|
|
||||||
-326.7375901453881
|
|
||||||
],
|
|
||||||
[
|
|
||||||
110.54774691836064,
|
|
||||||
-213.9742249455951
|
|
||||||
]
|
|
||||||
],
|
|
||||||
"speed": 11.0,
|
|
||||||
"interval": 2.6
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"targets": [
|
"targets": [
|
||||||
|
|||||||
Binary file not shown.
@ -3,7 +3,7 @@
|
|||||||
importer="scene"
|
importer="scene"
|
||||||
importer_version=1
|
importer_version=1
|
||||||
type="PackedScene"
|
type="PackedScene"
|
||||||
uid="uid://cik0c7knt33ep"
|
uid="uid://dik381ld32een"
|
||||||
path="res://.godot/imported/level.glb-30461b4810ef38297351fb89cbe9aa18.scn"
|
path="res://.godot/imported/level.glb-30461b4810ef38297351fb89cbe9aa18.scn"
|
||||||
|
|
||||||
[deps]
|
[deps]
|
||||||
|
|||||||
@ -2,155 +2,33 @@
|
|||||||
{
|
{
|
||||||
"name": "Albert x Adelaide",
|
"name": "Albert x Adelaide",
|
||||||
"spawn": {
|
"spawn": {
|
||||||
"x": -141.55578237343948,
|
"x": 18.671705882497893,
|
||||||
"z": -187.08015060009473,
|
"z": -22.919507778110138,
|
||||||
"dx": 0.735035843445057,
|
"dx": -0.6633868531476461,
|
||||||
"dz": 0.6780282507764803
|
"dz": -0.7482766086621067
|
||||||
},
|
},
|
||||||
"streams": [
|
"streams": [
|
||||||
{
|
{
|
||||||
"pts": [
|
"pts": [
|
||||||
[
|
[
|
||||||
-63.049013950748005,
|
-58.84614846630014,
|
||||||
-111.03404120330288
|
-114.42417420924829
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
-67.67311197746825,
|
-53.880474905008,
|
||||||
-116.88556835863362
|
-108.39185473942744
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
-69.33640432781439,
|
-51.24051503498836,
|
||||||
-119.01651536179543
|
-105.52578654474546
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
-72.74965354025018,
|
-20.352984240690695,
|
||||||
-122.55658461736515
|
-70.76962163514554
|
||||||
],
|
|
||||||
[
|
|
||||||
-98.24408864239251,
|
|
||||||
-142.51997682349207
|
|
||||||
],
|
|
||||||
[
|
|
||||||
-106.03262882232805,
|
|
||||||
-149.46281622691504
|
|
||||||
],
|
|
||||||
[
|
|
||||||
-127.33652985317063,
|
|
||||||
-167.58066870505155
|
|
||||||
],
|
|
||||||
[
|
|
||||||
-136.23707672722756,
|
|
||||||
-177.3382472125045
|
|
||||||
],
|
|
||||||
[
|
|
||||||
-138.0495681108421,
|
|
||||||
-179.37835778679877
|
|
||||||
],
|
|
||||||
[
|
|
||||||
-139.33017336122177,
|
|
||||||
-180.76880262140824
|
|
||||||
],
|
|
||||||
[
|
|
||||||
-143.5234010906565,
|
|
||||||
-185.23123832968892
|
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
"speed": 12.0,
|
"speed": 12.0,
|
||||||
"interval": 2.2
|
"interval": 1.5
|
||||||
},
|
|
||||||
{
|
|
||||||
"pts": [
|
|
||||||
[
|
|
||||||
-139.58816365622246,
|
|
||||||
-188.92906287050053
|
|
||||||
],
|
|
||||||
[
|
|
||||||
-135.38602332573416,
|
|
||||||
-184.45711937808235
|
|
||||||
],
|
|
||||||
[
|
|
||||||
-134.03937221770562,
|
|
||||||
-182.99475501313762
|
|
||||||
],
|
|
||||||
[
|
|
||||||
-132.23691688556315,
|
|
||||||
-180.9657423870203
|
|
||||||
],
|
|
||||||
[
|
|
||||||
-123.67509820126826,
|
|
||||||
-171.5497892951127
|
|
||||||
],
|
|
||||||
[
|
|
||||||
-102.50831375382124,
|
|
||||||
-153.55417337313145
|
|
||||||
],
|
|
||||||
[
|
|
||||||
-94.84937670776534,
|
|
||||||
-146.71949237661244
|
|
||||||
],
|
|
||||||
[
|
|
||||||
-69.3394816295263,
|
|
||||||
-126.74355578269574
|
|
||||||
],
|
|
||||||
[
|
|
||||||
-65.32529933336386,
|
|
||||||
-122.63190423762802
|
|
||||||
],
|
|
||||||
[
|
|
||||||
-63.432906391639634,
|
|
||||||
-120.22931724123647
|
|
||||||
],
|
|
||||||
[
|
|
||||||
-58.81222264887106,
|
|
||||||
-114.38211519656227
|
|
||||||
]
|
|
||||||
],
|
|
||||||
"speed": 11.0,
|
|
||||||
"interval": 2.6
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"pts": [
|
|
||||||
[
|
|
||||||
48.262595664052256,
|
|
||||||
14.981139500351844
|
|
||||||
],
|
|
||||||
[
|
|
||||||
44.3597027328176,
|
|
||||||
10.810878499923838
|
|
||||||
],
|
|
||||||
[
|
|
||||||
12.513337259306525,
|
|
||||||
-23.281770877978737
|
|
||||||
],
|
|
||||||
[
|
|
||||||
-24.437975734562166,
|
|
||||||
-67.23871181646255
|
|
||||||
]
|
|
||||||
],
|
|
||||||
"speed": 12.0,
|
|
||||||
"interval": 2.2
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"pts": [
|
|
||||||
[
|
|
||||||
-20.30439756562987,
|
|
||||||
-70.71341058345321
|
|
||||||
],
|
|
||||||
[
|
|
||||||
16.56426693178581,
|
|
||||||
-26.85248072178107
|
|
||||||
],
|
|
||||||
[
|
|
||||||
48.305406828335755,
|
|
||||||
7.124224299892156
|
|
||||||
],
|
|
||||||
[
|
|
||||||
52.205267737000426,
|
|
||||||
11.291242899470646
|
|
||||||
]
|
|
||||||
],
|
|
||||||
"speed": 11.0,
|
|
||||||
"interval": 2.6
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"pts": [
|
"pts": [
|
||||||
@ -191,66 +69,40 @@
|
|||||||
-163.05430391717795
|
-163.05430391717795
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
7.529561567214451,
|
7.537426213222782,
|
||||||
-176.87932594543773
|
-176.88629254786764
|
||||||
],
|
|
||||||
[
|
|
||||||
21.92572970063743,
|
|
||||||
-189.79048578560977
|
|
||||||
],
|
|
||||||
[
|
|
||||||
25.62426293364586,
|
|
||||||
-193.04071169887433
|
|
||||||
],
|
|
||||||
[
|
|
||||||
26.615516895233576,
|
|
||||||
-193.87905456229723
|
|
||||||
],
|
|
||||||
[
|
|
||||||
27.606646263868104,
|
|
||||||
-194.72792646553222
|
|
||||||
],
|
|
||||||
[
|
|
||||||
32.11664235322457,
|
|
||||||
-198.8840883667601
|
|
||||||
],
|
|
||||||
[
|
|
||||||
57.105103286489076,
|
|
||||||
-221.92761298838238
|
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
"speed": 12.0,
|
"speed": 12.0,
|
||||||
"interval": 2.2
|
"interval": 1.5
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"pts": [
|
"pts": [
|
||||||
[
|
[
|
||||||
60.765864254843116,
|
-24.38938905950134,
|
||||||
-217.9578738114873
|
-67.18250076477023
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
35.77719298273772,
|
-55.272013374374126,
|
||||||
-194.91415523271732
|
-101.93315225504524
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
31.239945374075152,
|
-57.98533525129133,
|
||||||
-190.73303753393867
|
-104.88327646076375
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
30.11741737461444,
|
-63.01508813331892,
|
||||||
-189.76849583719522
|
-110.99198219061685
|
||||||
],
|
]
|
||||||
|
],
|
||||||
|
"speed": 11.0,
|
||||||
|
"interval": 1.8
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pts": [
|
||||||
[
|
[
|
||||||
29.172798227938216,
|
11.110167779562063,
|
||||||
-188.97034350113577
|
-172.83715465195058
|
||||||
],
|
|
||||||
[
|
|
||||||
25.522915182059183,
|
|
||||||
-185.76304781436195
|
|
||||||
],
|
|
||||||
[
|
|
||||||
11.118032425570394,
|
|
||||||
-172.8441212543805
|
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
-4.543064055336218,
|
-4.543064055336218,
|
||||||
@ -290,7 +142,75 @@
|
|||||||
]
|
]
|
||||||
],
|
],
|
||||||
"speed": 11.0,
|
"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": [
|
"targets": [
|
||||||
@ -302,105 +222,39 @@
|
|||||||
{
|
{
|
||||||
"name": "Edward x Elizabeth",
|
"name": "Edward x Elizabeth",
|
||||||
"spawn": {
|
"spawn": {
|
||||||
"x": 329.71079592281694,
|
"x": 173.87950117253465,
|
||||||
"z": -5.595044399780328,
|
"z": -177.50987452854054,
|
||||||
"dx": -0.6532357656212038,
|
"dx": 0.688388502507091,
|
||||||
"dz": -0.7571545644802516
|
"dz": 0.7253421741606128
|
||||||
},
|
},
|
||||||
"streams": [
|
"streams": [
|
||||||
{
|
{
|
||||||
"pts": [
|
"pts": [
|
||||||
[
|
[
|
||||||
258.4805413787254,
|
228.7426016851313,
|
||||||
-92.28866542876725
|
-123.80938841141814
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
262.2737122591504,
|
250.48785179439338,
|
||||||
-88.11954201221562
|
-101.12418691157934
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
266.8273793037693,
|
253.19184475779528,
|
||||||
-83.1100839184909
|
-98.13216139604938
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
270.87333374760516,
|
258.48528328991137,
|
||||||
-78.65784777138674
|
-92.28343983096748
|
||||||
],
|
|
||||||
[
|
|
||||||
298.1107780903454,
|
|
||||||
-47.27921270488467
|
|
||||||
],
|
|
||||||
[
|
|
||||||
323.17112576090676,
|
|
||||||
-16.89985163934848
|
|
||||||
],
|
|
||||||
[
|
|
||||||
325.81187978583756,
|
|
||||||
-13.69226732374769
|
|
||||||
],
|
|
||||||
[
|
|
||||||
331.6793038120916,
|
|
||||||
-7.443009954076085
|
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
"speed": 12.0,
|
"speed": 12.0,
|
||||||
"interval": 2.2
|
"interval": 1.5
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"pts": [
|
"pts": [
|
||||||
[
|
[
|
||||||
327.7422880335423,
|
184.8196819904455,
|
||||||
-3.7470788454845705
|
-31.59973983603264
|
||||||
],
|
|
||||||
[
|
|
||||||
321.7969353555861,
|
|
||||||
-10.08114267594156
|
|
||||||
],
|
|
||||||
[
|
|
||||||
319.0053923982548,
|
|
||||||
-13.463768760358409
|
|
||||||
],
|
|
||||||
[
|
|
||||||
293.9898388277115,
|
|
||||||
-43.789533694763236
|
|
||||||
],
|
|
||||||
[
|
|
||||||
266.80554434016864,
|
|
||||||
-75.10635662867679
|
|
||||||
],
|
|
||||||
[
|
|
||||||
262.8314530609969,
|
|
||||||
-79.47792568097934
|
|
||||||
],
|
|
||||||
[
|
|
||||||
258.27861498952456,
|
|
||||||
-84.48647198726033
|
|
||||||
],
|
|
||||||
[
|
|
||||||
254.4863239075441,
|
|
||||||
-88.6546281707152
|
|
||||||
]
|
|
||||||
],
|
|
||||||
"speed": 11.0,
|
|
||||||
"interval": 2.6
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"pts": [
|
|
||||||
[
|
|
||||||
142.50264789077926,
|
|
||||||
5.258364995815184
|
|
||||||
],
|
|
||||||
[
|
|
||||||
148.94725068367612,
|
|
||||||
-0.3323178390103432
|
|
||||||
],
|
|
||||||
[
|
|
||||||
154.9526328669413,
|
|
||||||
-5.595836199336438
|
|
||||||
],
|
|
||||||
[
|
|
||||||
184.81445478686962,
|
|
||||||
-31.59521315098408
|
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
200.9901724381037,
|
200.9901724381037,
|
||||||
@ -428,7 +282,29 @@
|
|||||||
]
|
]
|
||||||
],
|
],
|
||||||
"speed": 12.0,
|
"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": [
|
"pts": [
|
||||||
@ -457,76 +333,60 @@
|
|||||||
-41.47689161686228
|
-41.47689161686228
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
188.3547169687786,
|
188.34948976520272,
|
||||||
-27.51764724920578
|
-27.51312056415722
|
||||||
],
|
|
||||||
[
|
|
||||||
158.5007797295594,
|
|
||||||
-1.5251294005982539
|
|
||||||
],
|
|
||||||
[
|
|
||||||
152.49584713714452,
|
|
||||||
3.737997039525666
|
|
||||||
],
|
|
||||||
[
|
|
||||||
146.0412138327473,
|
|
||||||
9.337403004260793
|
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
"speed": 11.0,
|
"speed": 11.0,
|
||||||
"interval": 2.6
|
"interval": 1.8
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"pts": [
|
"pts": [
|
||||||
[
|
[
|
||||||
299.8317240905086,
|
258.4805413787254,
|
||||||
-131.8999654994903
|
-92.28866542876725
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
302.3544939757109,
|
262.2737122591504,
|
||||||
-134.11003589468572
|
-88.11954201221562
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
328.3524909104502,
|
266.8273793037693,
|
||||||
-156.3039359135965
|
-83.1100839184909
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
335.8998393149425,
|
270.87333374760516,
|
||||||
-162.7566931458765
|
-78.65784777138674
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
376.4710012181815,
|
298.08928934155523,
|
||||||
-198.14867992704066
|
-47.30427629341134
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
"speed": 12.0,
|
"speed": 12.0,
|
||||||
"interval": 2.2
|
"interval": 1.5
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"pts": [
|
"pts": [
|
||||||
[
|
[
|
||||||
380.02079919024186,
|
254.70925209424783,
|
||||||
-194.079412872944
|
-92.50690682734246
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
339.4433211293332,
|
259.70303711688206,
|
||||||
-158.68192485417183
|
-96.8600914480509
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
331.85925322106283,
|
263.66545095997486,
|
||||||
-152.19752408611663
|
-100.3447248693
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
305.86522961245754,
|
288.61557906385735,
|
||||||
-130.0070205048928
|
-122.1176793727558
|
||||||
],
|
|
||||||
[
|
|
||||||
303.390286276,
|
|
||||||
-127.8383604999869
|
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
"speed": 11.0,
|
"speed": 12.0,
|
||||||
"interval": 2.6
|
"interval": 1.5
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"targets": [
|
"targets": [
|
||||||
|
|||||||
Binary file not shown.
@ -3,7 +3,7 @@
|
|||||||
importer="scene"
|
importer="scene"
|
||||||
importer_version=1
|
importer_version=1
|
||||||
type="PackedScene"
|
type="PackedScene"
|
||||||
uid="uid://bo4jjuhgers8e"
|
uid="uid://djsujf76tbo3k"
|
||||||
path="res://.godot/imported/level.glb-f093d0a503a60c4e982eefdc88ad879e.scn"
|
path="res://.godot/imported/level.glb-f093d0a503a60c4e982eefdc88ad879e.scn"
|
||||||
|
|
||||||
[deps]
|
[deps]
|
||||||
|
|||||||
@ -2,25 +2,17 @@
|
|||||||
{
|
{
|
||||||
"name": "Melbourne x Merivale",
|
"name": "Melbourne x Merivale",
|
||||||
"spawn": {
|
"spawn": {
|
||||||
"x": -590.6178507503562,
|
"x": -598.2786454342968,
|
||||||
"z": -217.37742659981197,
|
"z": -212.65545062278278,
|
||||||
"dx": 0.8530074497663933,
|
"dx": 0.8528773292725991,
|
||||||
"dz": -0.5218987360044418
|
"dz": -0.5221113494388324
|
||||||
},
|
},
|
||||||
"streams": [
|
"streams": [
|
||||||
{
|
{
|
||||||
"pts": [
|
"pts": [
|
||||||
[
|
[
|
||||||
-592.80332396911,
|
-522.2431469994345,
|
||||||
-218.96289397888663
|
-263.2916099068527
|
||||||
],
|
|
||||||
[
|
|
||||||
-586.6662380952372,
|
|
||||||
-227.08658029182723
|
|
||||||
],
|
|
||||||
[
|
|
||||||
-522.2006803362468,
|
|
||||||
-263.3161280710875
|
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
-513.2984743659174,
|
-513.2984743659174,
|
||||||
@ -36,7 +28,37 @@
|
|||||||
]
|
]
|
||||||
],
|
],
|
||||||
"speed": 12.0,
|
"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": [
|
"pts": [
|
||||||
@ -53,20 +75,42 @@
|
|||||||
-263.8934194939837
|
-263.8934194939837
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
-519.5432562746829,
|
-519.5007896114952,
|
||||||
-258.6152667290545
|
-258.6397848932893
|
||||||
],
|
|
||||||
[
|
|
||||||
-583.8043810107387,
|
|
||||||
-222.50730370835848
|
|
||||||
],
|
|
||||||
[
|
|
||||||
-588.4323775316024,
|
|
||||||
-215.7919592207373
|
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
"speed": 11.0,
|
"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": [
|
"pts": [
|
||||||
@ -96,89 +140,29 @@
|
|||||||
]
|
]
|
||||||
],
|
],
|
||||||
"speed": 12.0,
|
"speed": 12.0,
|
||||||
"interval": 2.2
|
"interval": 1.5
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"pts": [
|
"pts": [
|
||||||
[
|
[
|
||||||
-425.28794348242786,
|
-490.9127030755138,
|
||||||
-315.13222698127333
|
-271.2416907037699
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
-435.78284848324455,
|
-487.07948135301893,
|
||||||
-308.49056735250105
|
-265.2321814476218
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
-450.4943046193153,
|
-460.79611403958603,
|
||||||
-298.8163541564887
|
-221.7057389459239
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
-467.3265789435252,
|
-449.78766148749827,
|
||||||
-288.3796882049204
|
-203.62165684766288
|
||||||
],
|
|
||||||
[
|
|
||||||
-486.6041919466704,
|
|
||||||
-276.5182512930966
|
|
||||||
],
|
|
||||||
[
|
|
||||||
-495.46295396354014,
|
|
||||||
-272.4538783008375
|
|
||||||
]
|
|
||||||
],
|
|
||||||
"speed": 11.0,
|
|
||||||
"interval": 2.6
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"pts": [
|
|
||||||
[
|
|
||||||
-511.28940466251,
|
|
||||||
-256.3197151971008
|
|
||||||
],
|
|
||||||
[
|
|
||||||
-539.5118789529853,
|
|
||||||
-239.934377879393
|
|
||||||
],
|
|
||||||
[
|
|
||||||
-576.453313765919,
|
|
||||||
-219.8022382435751
|
|
||||||
],
|
|
||||||
[
|
|
||||||
-581.3296752702165,
|
|
||||||
-217.31316962414144
|
|
||||||
],
|
|
||||||
[
|
|
||||||
-589.8527843522658,
|
|
||||||
-214.78808818884423
|
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
"speed": 12.0,
|
"speed": 12.0,
|
||||||
"interval": 2.2
|
"interval": 1.5
|
||||||
},
|
|
||||||
{
|
|
||||||
"pts": [
|
|
||||||
[
|
|
||||||
-591.3829171484466,
|
|
||||||
-219.9667650107797
|
|
||||||
],
|
|
||||||
[
|
|
||||||
-583.2152104060733,
|
|
||||||
-222.37328397575936
|
|
||||||
],
|
|
||||||
[
|
|
||||||
-579.0251997651409,
|
|
||||||
-224.55043815585836
|
|
||||||
],
|
|
||||||
[
|
|
||||||
-542.1517485157323,
|
|
||||||
-244.64511972030104
|
|
||||||
],
|
|
||||||
[
|
|
||||||
-514.0007454638794,
|
|
||||||
-260.98968640245414
|
|
||||||
]
|
|
||||||
],
|
|
||||||
"speed": 11.0,
|
|
||||||
"interval": 2.6
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"targets": [
|
"targets": [
|
||||||
@ -190,10 +174,10 @@
|
|||||||
{
|
{
|
||||||
"name": "Grey x Glenelg",
|
"name": "Grey x Glenelg",
|
||||||
"spawn": {
|
"spawn": {
|
||||||
"x": -132.09447432867904,
|
"x": -135.06485953132193,
|
||||||
"z": -92.67206939992113,
|
"z": -97.50127625771249,
|
||||||
"dx": 0.5885516813877528,
|
"dx": 0.5279471843098005,
|
||||||
"dz": 0.8084595959821672
|
"dz": 0.8492772047920241
|
||||||
},
|
},
|
||||||
"streams": [
|
"streams": [
|
||||||
{
|
{
|
||||||
@ -215,26 +199,52 @@
|
|||||||
-1.692678191469608
|
-1.692678191469608
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
-82.8231789453831,
|
-82.82069253186671,
|
||||||
-8.34834069992463
|
-8.344352365976752
|
||||||
],
|
|
||||||
[
|
|
||||||
-134.38713557935665,
|
|
||||||
-91.24598932165014
|
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
"speed": 12.0,
|
"speed": 12.0,
|
||||||
"interval": 2.2
|
"interval": 1.5
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"pts": [
|
"pts": [
|
||||||
[
|
[
|
||||||
-129.80181307800143,
|
-33.05020815528413,
|
||||||
-94.09814947819211
|
-14.758620186231095
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
-78.23825562104584,
|
-35.06751528318885,
|
||||||
-11.201142499900957
|
-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,
|
-74.07981104388904,
|
||||||
@ -254,7 +264,41 @@
|
|||||||
]
|
]
|
||||||
],
|
],
|
||||||
"speed": 11.0,
|
"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": [
|
"pts": [
|
||||||
@ -267,74 +311,30 @@
|
|||||||
7.176315693408943
|
7.176315693408943
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
-31.591830618167315,
|
-31.608294574960304,
|
||||||
46.425585467880346
|
46.399012606383145
|
||||||
],
|
|
||||||
[
|
|
||||||
5.553177653998906,
|
|
||||||
108.18727469018124
|
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
"speed": 12.0,
|
"speed": 12.0,
|
||||||
"interval": 2.2
|
"interval": 1.5
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"pts": [
|
"pts": [
|
||||||
[
|
[
|
||||||
0.9256242241840709,
|
-105.44415325837058,
|
||||||
110.9703933104001
|
17.29338138921854
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
-36.19855979655569,
|
-124.85549758561747,
|
||||||
49.24303933209625
|
29.02803771609806
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
-60.768388814750516,
|
-145.9651989732041,
|
||||||
10.095343106510734
|
41.680234308480806
|
||||||
],
|
|
||||||
[
|
|
||||||
-66.8313293113839,
|
|
||||||
1.5633764655737972
|
|
||||||
]
|
|
||||||
],
|
|
||||||
"speed": 11.0,
|
|
||||||
"interval": 2.6
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"pts": [
|
|
||||||
[
|
|
||||||
-146.4043520315188,
|
|
||||||
-133.05664071966743
|
|
||||||
],
|
|
||||||
[
|
|
||||||
-108.51077823239306,
|
|
||||||
-74.87735033479748
|
|
||||||
],
|
|
||||||
[
|
|
||||||
-83.23180011197921,
|
|
||||||
-33.360028872910426
|
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
"speed": 12.0,
|
"speed": 12.0,
|
||||||
"interval": 2.2
|
"interval": 1.5
|
||||||
},
|
|
||||||
{
|
|
||||||
"pts": [
|
|
||||||
[
|
|
||||||
-87.84412394798038,
|
|
||||||
-30.551743126791347
|
|
||||||
],
|
|
||||||
[
|
|
||||||
-113.07214707658274,
|
|
||||||
-71.98703646477304
|
|
||||||
],
|
|
||||||
[
|
|
||||||
-150.92919892918795,
|
|
||||||
-130.10947927979782
|
|
||||||
]
|
|
||||||
],
|
|
||||||
"speed": 11.0,
|
|
||||||
"interval": 2.6
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"targets": [
|
"targets": [
|
||||||
|
|||||||
Binary file not shown.
@ -3,7 +3,7 @@
|
|||||||
importer="scene"
|
importer="scene"
|
||||||
importer_version=1
|
importer_version=1
|
||||||
type="PackedScene"
|
type="PackedScene"
|
||||||
uid="uid://ce6dmcne00017"
|
uid="uid://dw52qeb8csi30"
|
||||||
path="res://.godot/imported/level.glb-17af9cdeb05fd15a816097b7ae0fb96c.scn"
|
path="res://.godot/imported/level.glb-17af9cdeb05fd15a816097b7ae0fb96c.scn"
|
||||||
|
|
||||||
[deps]
|
[deps]
|
||||||
|
|||||||
@ -2,191 +2,179 @@
|
|||||||
{
|
{
|
||||||
"name": "Bridge Approach",
|
"name": "Bridge Approach",
|
||||||
"spawn": {
|
"spawn": {
|
||||||
"x": 8.830464544568363,
|
"x": 32.83372074498513,
|
||||||
"z": 869.4323045999147,
|
"z": -118.28842427027045,
|
||||||
"dx": 0.07444215026982055,
|
"dx": -0.11409585864571734,
|
||||||
"dz": -0.9972253337451898
|
"dz": -0.9934697454074263
|
||||||
},
|
},
|
||||||
"streams": [
|
"streams": [
|
||||||
{
|
{
|
||||||
"pts": [
|
"pts": [
|
||||||
[
|
[
|
||||||
6.138933818706997,
|
21.82446567810751,
|
||||||
869.2186170903494
|
-237.81387509878263
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
7.71576357448385,
|
23.145720027383163,
|
||||||
849.3684911078449
|
-226.45520770652828
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
10.638374835867648,
|
24.788209612143817,
|
||||||
826.3950177708408
|
-213.84815371354867
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
13.290793665081416,
|
26.145569174952232,
|
||||||
807.4161707785927
|
-200.29741976993301
|
||||||
],
|
|
||||||
[
|
|
||||||
16.464164221809273,
|
|
||||||
786.8204240256596
|
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
"speed": 12.0,
|
"speed": 12.0,
|
||||||
"interval": 2.2
|
"interval": 1.5
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"pts": [
|
"pts": [
|
||||||
[
|
[
|
||||||
21.80118214360166,
|
2.199941588527042,
|
||||||
787.6427619747212
|
-240.32717119002737
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
18.633212388594718,
|
-2.5248172223755736,
|
||||||
808.2026580211177
|
-243.91950536774928
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
15.991303480558578,
|
-6.325349530807717,
|
||||||
827.1064634292346
|
-247.60965512377
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
13.086203238107553,
|
-15.183983237537262,
|
||||||
849.932740892406
|
-255.2613583327868
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
11.521995270429729,
|
-19.822722086570074,
|
||||||
869.64599210948
|
-258.6729952966697
|
||||||
]
|
|
||||||
],
|
|
||||||
"speed": 11.0,
|
|
||||||
"interval": 2.6
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"pts": [
|
|
||||||
[
|
|
||||||
18.99248078399424,
|
|
||||||
762.39669386784
|
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
25.743285889885765,
|
-24.279515332983824,
|
||||||
718.3587501785464
|
-261.70335578638884
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
28.769387854660618,
|
-29.011437974400625,
|
||||||
692.1866319902829
|
-263.89076106496833
|
||||||
],
|
|
||||||
[
|
|
||||||
30.0890967826306,
|
|
||||||
666.9831668179552
|
|
||||||
],
|
|
||||||
[
|
|
||||||
31.42169559470248,
|
|
||||||
651.6049662957291
|
|
||||||
],
|
|
||||||
[
|
|
||||||
31.81270185839052,
|
|
||||||
638.4108005958807
|
|
||||||
],
|
|
||||||
[
|
|
||||||
32.07909068323288,
|
|
||||||
624.4813765582306
|
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
"speed": 12.0,
|
"speed": 12.0,
|
||||||
"interval": 2.2
|
"interval": 1.5
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"pts": [
|
"pts": [
|
||||||
[
|
[
|
||||||
37.478103203918025,
|
20.772447151780117,
|
||||||
624.5846422421965
|
-199.75931223039953
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
37.211108262175465,
|
19.423377981138138,
|
||||||
638.5419810043787
|
-213.23286388675078
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
36.81191635813593,
|
17.78654515972297,
|
||||||
651.9298049040424
|
-225.7924522932427
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
35.47759634851261,
|
16.46063564037367,
|
||||||
667.3354059817195
|
-237.18991410136036
|
||||||
],
|
|
||||||
[
|
|
||||||
34.150141407857646,
|
|
||||||
692.6421440096167
|
|
||||||
],
|
|
||||||
[
|
|
||||||
31.09171746204034,
|
|
||||||
719.1032498212462
|
|
||||||
],
|
|
||||||
[
|
|
||||||
24.33013387059421,
|
|
||||||
763.2148989320567
|
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
"speed": 11.0,
|
"speed": 11.0,
|
||||||
"interval": 2.6
|
"interval": 1.8
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"pts": [
|
"pts": [
|
||||||
[
|
[
|
||||||
46.61372413580953,
|
-26.737042902514784,
|
||||||
629.091352780875
|
-268.7884265350654
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
46.0791068558773,
|
-21.61124337794858,
|
||||||
644.9124964101077
|
-266.3980682135746
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
45.148393336775854,
|
-16.704188313209045,
|
||||||
661.3968152993656
|
-263.0814811029762
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
43.42646160590967,
|
-11.761774350471743,
|
||||||
688.423534995915
|
-259.43849686691397
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
41.167633319989406,
|
-2.7224195143774796,
|
||||||
712.1819019166054
|
-251.63195487598963
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
39.13812925040989,
|
0.9839307874462617,
|
||||||
728.9192605875077
|
-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,
|
"speed": 12.0,
|
||||||
"interval": 2.2
|
"interval": 1.5
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"pts": [
|
"pts": [
|
||||||
[
|
[
|
||||||
33.77740686081252,
|
28.875188928696993,
|
||||||
728.2691410126066
|
-229.49242546244793
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
35.797668406451436,
|
28.72548025209394,
|
||||||
711.6131516837517
|
-214.05366159277472
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
38.04296969005352,
|
28.150874632761848,
|
||||||
688.0016170039884
|
-209.45956029627345
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
39.75839999553985,
|
27.279147353059784,
|
||||||
661.068224700852
|
-204.7812989955361
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
40.68465217733579,
|
26.094879529030933,
|
||||||
644.6678359900116
|
-199.44340263065618
|
||||||
],
|
|
||||||
[
|
|
||||||
41.21680242495871,
|
|
||||||
628.9090452194655
|
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
"speed": 11.0,
|
"speed": 12.0,
|
||||||
"interval": 2.6
|
"interval": 1.5
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"targets": [
|
"targets": [
|
||||||
|
|||||||
Binary file not shown.
@ -3,7 +3,7 @@
|
|||||||
importer="scene"
|
importer="scene"
|
||||||
importer_version=1
|
importer_version=1
|
||||||
type="PackedScene"
|
type="PackedScene"
|
||||||
uid="uid://dah3x674j2af"
|
uid="uid://bjd3utm2spgs0"
|
||||||
path="res://.godot/imported/level.glb-045fc5cf8c0e529ecdc2678ee7acde2a.scn"
|
path="res://.godot/imported/level.glb-045fc5cf8c0e529ecdc2678ee7acde2a.scn"
|
||||||
|
|
||||||
[deps]
|
[deps]
|
||||||
|
|||||||
50
src/cop.gd
Normal file
50
src/cop.gd
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
class_name Cop
|
||||||
|
extends Car
|
||||||
|
## Pursuit AI: hunts a target car directly instead of following a path, so it
|
||||||
|
## works anywhere in free roam rather than only on the RacePath.
|
||||||
|
##
|
||||||
|
## Deliberately dumber than a racing line -- it drives at where you are, not
|
||||||
|
## where you're going. That's what makes side streets and hard turns an actual
|
||||||
|
## escape rather than a formality (see Game._pursuit_tick).
|
||||||
|
|
||||||
|
var target: Node3D
|
||||||
|
var power_boost := 1.0 # scales with heat: more wanted = more committed
|
||||||
|
|
||||||
|
var _stuck := 0.0
|
||||||
|
var _unstick := 0.0
|
||||||
|
|
||||||
|
func _physics_process(delta: float) -> 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
|
||||||
1
src/cop.gd.uid
Normal file
1
src/cop.gd.uid
Normal file
@ -0,0 +1 @@
|
|||||||
|
uid://bbwscf3nqqpe5
|
||||||
8
src/cop.tscn
Normal file
8
src/cop.tscn
Normal file
@ -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
|
||||||
@ -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 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 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 _meshes: Array = [] # one entry per MeshInstance3D, all its surfaces together
|
||||||
var _cp := PackedVector3Array()
|
var _cp := PackedVector3Array()
|
||||||
var _disp := PackedVector3Array()
|
var _disp := PackedVector3Array()
|
||||||
var _ok := false
|
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:
|
func setup(root: Node3D) -> bool:
|
||||||
## Snapshot every mesh under `root`, then build the control hull around them.
|
## Snapshot every mesh under `root`, then build the control hull around them.
|
||||||
var pending: Array = []
|
var pending: Array = []
|
||||||
var bounds := AABB()
|
var bounds := AABB()
|
||||||
var first := true
|
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):
|
for n in root.find_children("*", "MeshInstance3D", true, false):
|
||||||
var mi := n as MeshInstance3D
|
var mi := n as MeshInstance3D
|
||||||
var am := mi.mesh as ArrayMesh
|
var am := mi.mesh as ArrayMesh
|
||||||
@ -52,8 +72,32 @@ func setup(root: Node3D) -> bool:
|
|||||||
first = false
|
first = false
|
||||||
else:
|
else:
|
||||||
bounds = bounds.expand(base[i])
|
bounds = bounds.expand(base[i])
|
||||||
surfs.append({"arrays": arr, "base": base,
|
# the GLB's materials are shared too -- copy them or denting one
|
||||||
"mat": am.surface_get_material(s)})
|
# 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})
|
pending.append({"mi": mi, "xform": xform, "surfs": surfs})
|
||||||
if pending.is_empty() or bounds.size.length() < 0.1:
|
if pending.is_empty() or bounds.size.length() < 0.1:
|
||||||
return false
|
return false
|
||||||
@ -131,6 +175,27 @@ func impact(local_point: Vector3, local_dir: Vector3, strength: float) -> void:
|
|||||||
moved = true
|
moved = true
|
||||||
if moved:
|
if moved:
|
||||||
_write()
|
_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:
|
func _write() -> void:
|
||||||
for m in _meshes:
|
for m in _meshes:
|
||||||
|
|||||||
385
src/game.gd
385
src/game.gd
@ -15,7 +15,10 @@ static var junction: Dictionary = {}
|
|||||||
const LAPS := 3
|
const LAPS := 3
|
||||||
const CRASH_TIME := 60.0
|
const CRASH_TIME := 60.0
|
||||||
const RIVAL_COUNT := 3
|
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
|
const PARKED_MAX := 50 # PP_ markers filled with shuntable fleet cars
|
||||||
## Street furniture that topples. Marker prefix -> size, mass, colour.
|
## Street furniture that topples. Marker prefix -> size, mass, colour.
|
||||||
## Deliberately physics, not scenery: clipping a lamp post at 120 and watching
|
## 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_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 JCT_SETTLE := 9.0 # aftertouch/crashbreaker window after the crash
|
||||||
const ROAM_RIVALS := 2 # challengers circling the RacePath in cruise
|
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_GAP := 250.0 # escape distance that decides an outrun (NFSU2 rule)
|
||||||
const OUTRUN_TIME := 90.0
|
const OUTRUN_TIME := 90.0
|
||||||
const OUTRUN_START_D := 12.0 # pull alongside within this, at speed, to trigger
|
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 breaker_used := false
|
||||||
|
|
||||||
var spinners: Array[Node3D] = [] # ferris wheels etc, turned in _process
|
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_rival: Rival = null
|
||||||
var outrun_t := 0.0
|
var outrun_t := 0.0
|
||||||
var outrun_cd := 0.0 # cooldown so a finished outrun doesn't instantly re-arm
|
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 speed_label: Label
|
||||||
var boost_bar: ProgressBar
|
var boost_bar: ProgressBar
|
||||||
|
var heat_bar: ProgressBar
|
||||||
var info_label: Label
|
var info_label: Label
|
||||||
var msg_label: Label
|
var msg_label: Label
|
||||||
var msg_timer: SceneTreeTimer
|
var msg_timer: SceneTreeTimer
|
||||||
|
var _sticky := false # a result screen is up; transient messages must not clobber it
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
var level: Node = (load(level_path) as PackedScene).instantiate()
|
var level: Node = (load(level_path) as PackedScene).instantiate()
|
||||||
@ -99,8 +146,21 @@ func _ready() -> void:
|
|||||||
if mode == "junction":
|
if mode == "junction":
|
||||||
var sp: Dictionary = junction["spawn"]
|
var sp: Dictionary = junction["spawn"]
|
||||||
var jf := Vector3(sp["dx"], 0, sp["dz"]).normalized()
|
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
|
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:
|
elif mode == "race" and race_path:
|
||||||
car.transform = _grid_xform(0.0)
|
car.transform = _grid_xform(0.0)
|
||||||
else:
|
else:
|
||||||
@ -134,6 +194,52 @@ func _ready() -> void:
|
|||||||
prev_off[r] = off
|
prev_off[r] = off
|
||||||
r.wreck_started.connect(_rival_wrecked.bind(r))
|
r.wreck_started.connect(_rival_wrecked.bind(r))
|
||||||
rivals.append(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:
|
elif mode == "cruise" and race_path:
|
||||||
# roaming challengers: they lap the circuit until you pick a fight
|
# roaming challengers: they lap the circuit until you pick a fight
|
||||||
var pool := Registry.cars().values()
|
var pool := Registry.cars().values()
|
||||||
@ -163,7 +269,7 @@ func _ready() -> void:
|
|||||||
add_child(path)
|
add_child(path)
|
||||||
var L := curve.get_baked_length()
|
var L := curve.get_baked_length()
|
||||||
var headway: float = stream["speed"] * stream["interval"]
|
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:
|
for i in n:
|
||||||
var t := TrafficCar.new()
|
var t := TrafficCar.new()
|
||||||
t.path = path
|
t.path = path
|
||||||
@ -203,6 +309,16 @@ func _ready() -> void:
|
|||||||
boost_bar.size = Vector2(160, 18)
|
boost_bar.size = Vector2(160, 18)
|
||||||
boost_bar.show_percentage = false
|
boost_bar.show_percentage = false
|
||||||
hud.add_child(boost_bar)
|
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 = Label.new()
|
||||||
info_label.anchor_left = 1.0
|
info_label.anchor_left = 1.0
|
||||||
info_label.anchor_right = 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)
|
msg_label.add_theme_font_size_override("font_size", 48)
|
||||||
hud.add_child(msg_label)
|
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:
|
func _spawn_smashables(level: Node) -> void:
|
||||||
## Fill the level's PP_/WB_ markers with shuntable parked cars and wheelie
|
## Fill the level's PP_/WB_ markers with shuntable parked cars and wheelie
|
||||||
## bins. Real fleet models, asleep until rammed.
|
## bins. Real fleet models, asleep until rammed.
|
||||||
@ -400,6 +564,12 @@ func _physics_process(delta: float) -> void:
|
|||||||
_race_tick()
|
_race_tick()
|
||||||
if mode == "cruise" and race_path:
|
if mode == "cruise" and race_path:
|
||||||
_outrun_tick(delta)
|
_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 mode == "junction" and jstate == "settle":
|
||||||
if Input.is_action_just_pressed("boost") and not breaker_used:
|
if Input.is_action_just_pressed("boost") and not breaker_used:
|
||||||
_crashbreaker()
|
_crashbreaker()
|
||||||
@ -412,6 +582,194 @@ func _physics_process(delta: float) -> void:
|
|||||||
+ fwd.normalized() * Input.get_axis("brake", "throttle")
|
+ fwd.normalized() * Input.get_axis("brake", "throttle")
|
||||||
car.apply_central_force(push * car.mass * 9.0)
|
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:
|
func _outrun_tick(delta: float) -> void:
|
||||||
## NFSU2-style impromptu 1v1: pull alongside a roaming rival at speed and it's
|
## 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
|
## 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:
|
func _player_crash(dv: float) -> void:
|
||||||
if mode in ["crash", "junction"] and not over:
|
if mode in ["crash", "junction"] and not over:
|
||||||
score += minf(dv, 30.0) * 120.0
|
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:
|
if mode == "junction" and jstate == "run" and dv > 5.0:
|
||||||
jstate = "settle"
|
jstate = "settle"
|
||||||
time_left = JCT_SETTLE
|
time_left = JCT_SETTLE
|
||||||
@ -536,6 +896,7 @@ func _traffic_smashed(impulse: float) -> void:
|
|||||||
score += impulse * 400.0
|
score += impulse * 400.0
|
||||||
_msg("$%d" % int(impulse * 400.0), 40)
|
_msg("$%d" % int(impulse * 400.0), 40)
|
||||||
car.earn_boost(0.2)
|
car.earn_boost(0.2)
|
||||||
|
add_heat(HEAT_PER_SMASH)
|
||||||
|
|
||||||
func _crashbreaker() -> void:
|
func _crashbreaker() -> void:
|
||||||
breaker_used = true
|
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)
|
_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:
|
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.text = text
|
||||||
msg_label.add_theme_font_size_override("font_size", size)
|
msg_label.add_theme_font_size_override("font_size", size)
|
||||||
if sticky:
|
if sticky:
|
||||||
@ -622,7 +988,16 @@ func _process(delta: float) -> void:
|
|||||||
if jstate == "run":
|
if jstate == "run":
|
||||||
_msg("Didn't even crash. Cooked.", 40)
|
_msg("Didn't even crash. Cooked.", 40)
|
||||||
_jct_tally()
|
_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()
|
get_tree().reload_current_scene()
|
||||||
return
|
return
|
||||||
for sp in spinners:
|
for sp in spinners:
|
||||||
@ -635,3 +1010,5 @@ func _process(delta: float) -> void:
|
|||||||
boost_bar.size.x = 160.0 * car.boost_max
|
boost_bar.size.x = 160.0 * car.boost_max
|
||||||
boost_bar.max_value = 100.0 * car.boost_max
|
boost_bar.max_value = 100.0 * car.boost_max
|
||||||
boost_bar.value = car.boost * 100.0
|
boost_bar.value = car.boost * 100.0
|
||||||
|
heat_bar.visible = mode == "cruise" and heat > 0.02 and cops.is_empty()
|
||||||
|
heat_bar.value = heat
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
extends Control
|
extends Control
|
||||||
## Menu: pick a car, a level and a mode from whatever exists, hit RACE.
|
## 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 car_list: ItemList = $VBox/HBox/Cars
|
||||||
@onready var level_list: ItemList = $VBox/HBox/Levels
|
@onready var level_list: ItemList = $VBox/HBox/Levels
|
||||||
|
|||||||
114
tests/smoke.gd
114
tests/smoke.gd
@ -10,34 +10,36 @@ func _initialize() -> void:
|
|||||||
func _run() -> void:
|
func _run() -> void:
|
||||||
var cars := Registry.cars()
|
var cars := Registry.cars()
|
||||||
var levels := Registry.levels()
|
var levels := Registry.levels()
|
||||||
assert(not cars.is_empty(), "no cars found")
|
_check(not cars.is_empty(), "no cars found")
|
||||||
assert(not levels.is_empty(), "no levels found")
|
_check(not levels.is_empty(), "no levels found")
|
||||||
for p in cars.values():
|
for p in cars.values():
|
||||||
var s: CarStats = load(p)
|
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 != "":
|
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
|
# datto is procedural (has det_* parts); imported models legitimately have none
|
||||||
Game.car_path = cars.get("datto_120y", cars.values()[0])
|
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
|
# 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])
|
Game.level_path = levels.get("woolies_carpark", levels.values()[0])
|
||||||
|
|
||||||
# cruise: throttle 3s, car must move
|
# cruise: throttle 3s, car must move
|
||||||
|
print(" .. cruise")
|
||||||
var game: Game = await _boot("cruise")
|
var game: Game = await _boot("cruise")
|
||||||
if game.car.stats.model_path != "":
|
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
|
var start: Vector3 = game.car.global_position
|
||||||
Input.action_press("throttle")
|
Input.action_press("throttle")
|
||||||
for i in 180:
|
for i in 180:
|
||||||
await physics_frame
|
await physics_frame
|
||||||
var dist := start.distance_to(game.car.global_position)
|
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")
|
Input.action_release("throttle")
|
||||||
game.free()
|
game.free()
|
||||||
|
|
||||||
# crash mode: traffic spawned, ram the nearest one, score must rise
|
# crash mode: traffic spawned, ram the nearest one, score must rise
|
||||||
|
print(" .. crash")
|
||||||
game = await _boot("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]
|
var victim: TrafficCar = game.traffic[0]
|
||||||
await physics_frame # traffic takes position on its first physics tick
|
await physics_frame # traffic takes position on its first physics tick
|
||||||
Input.action_press("throttle")
|
Input.action_press("throttle")
|
||||||
@ -54,18 +56,19 @@ func _run() -> void:
|
|||||||
if not victim.live:
|
if not victim.live:
|
||||||
break
|
break
|
||||||
Input.action_release("throttle")
|
Input.action_release("throttle")
|
||||||
assert(not victim.live, "rammed traffic never went dynamic")
|
_check(not victim.live, "rammed traffic never went dynamic")
|
||||||
assert(game.score > 0.0, "crash mode scored nothing")
|
_check(game.score > 0.0, "crash mode scored nothing")
|
||||||
var crash_score := game.score
|
var crash_score := game.score
|
||||||
game.free()
|
game.free()
|
||||||
|
|
||||||
# junction: convoys spawn, driving straight in causes the crash -> settle ->
|
# junction: convoys spawn, driving straight in causes the crash -> settle ->
|
||||||
# crashbreaker -> tally
|
# crashbreaker -> tally
|
||||||
var jcts := Registry.junctions().filter(func(j): return j.level_id == "woolies_carpark")
|
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
|
Game.junction = jcts[0].data
|
||||||
|
print(" .. junction")
|
||||||
game = await _boot("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")
|
Input.action_press("throttle")
|
||||||
for i in 950:
|
for i in 950:
|
||||||
await physics_frame
|
await physics_frame
|
||||||
@ -78,9 +81,9 @@ func _run() -> void:
|
|||||||
break
|
break
|
||||||
Input.action_release("throttle")
|
Input.action_release("throttle")
|
||||||
Input.action_release("boost")
|
Input.action_release("boost")
|
||||||
assert(game.jstate != "run", "never crashed into the convoy")
|
_check(game.jstate != "run", "never crashed into the convoy")
|
||||||
assert(game.breaker_used, "crashbreaker never fired")
|
_check(game.breaker_used, "crashbreaker never fired")
|
||||||
assert(game.over and game.score > 0.0, "junction never tallied (score %d)" % int(game.score))
|
_check(game.over and game.score > 0.0, "junction never tallied (score %d)" % int(game.score))
|
||||||
var jct_score := game.score
|
var jct_score := game.score
|
||||||
Game.junction = {}
|
Game.junction = {}
|
||||||
game.free()
|
game.free()
|
||||||
@ -88,7 +91,7 @@ func _run() -> void:
|
|||||||
# outrun: roaming rivals exist in cruise; pulling alongside at speed arms the
|
# outrun: roaming rivals exist in cruise; pulling alongside at speed arms the
|
||||||
# duel, and the win path pays out and clears state
|
# duel, and the win path pays out and clears state
|
||||||
game = await _boot("cruise")
|
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]
|
var rr: Rival = game.rivals[0]
|
||||||
await physics_frame
|
await physics_frame
|
||||||
game.car.global_position = rr.global_position + Vector3(3, 0.3, 0)
|
game.car.global_position = rr.global_position + Vector3(3, 0.3, 0)
|
||||||
@ -97,17 +100,72 @@ func _run() -> void:
|
|||||||
await physics_frame
|
await physics_frame
|
||||||
if game.outrun_rival != null:
|
if game.outrun_rival != null:
|
||||||
break
|
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
|
var bm0: float = game.car.boost_max
|
||||||
game._outrun_end(true, "test")
|
game._outrun_end(true, "test")
|
||||||
assert(game.outrun_rival == null and game.outruns_won == 1, "outrun win didn't settle")
|
_check(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")
|
_check(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_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()
|
game.free()
|
||||||
|
|
||||||
# race: rivals spawn and make progress along the path
|
# race: rivals spawn and make progress along the path
|
||||||
|
print(" .. race")
|
||||||
game = await _boot("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
|
var p0: float = game.rivals[0].progress
|
||||||
for i in 240:
|
for i in 240:
|
||||||
await physics_frame
|
await physics_frame
|
||||||
@ -115,13 +173,23 @@ func _run() -> void:
|
|||||||
for r in game.rivals:
|
for r in game.rivals:
|
||||||
if absf(r.progress - p0) > 3.0 or r.linear_velocity.length() > 3.0:
|
if absf(r.progress - p0) > 3.0 or r.linear_velocity.length() > 3.0:
|
||||||
moved = true
|
moved = true
|
||||||
assert(moved, "no rival is driving")
|
_check(moved, "no rival is driving")
|
||||||
game.free()
|
game.free()
|
||||||
|
|
||||||
print("smoke ok: %d cars, %d levels | cruise %.1f m | crash $%d | junction $%d | outrun arms+pays | race AI driving" %
|
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)])
|
[cars.size(), levels.size(), dist, int(crash_score), int(jct_score), Game.DERBY_CARS])
|
||||||
quit(0)
|
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:
|
func _boot(mode: String) -> Game:
|
||||||
Game.mode = mode
|
Game.mode = mode
|
||||||
var game: Game = (load("res://src/game.tscn") as PackedScene).instantiate()
|
var game: Game = (load("res://src/game.tscn") as PackedScene).instantiate()
|
||||||
|
|||||||
23
tools/build_all_junctions.sh
Executable file
23
tools/build_all_junctions.sh
Executable file
@ -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)"
|
||||||
@ -373,7 +373,7 @@ def add_canopy(bm, at, fwd):
|
|||||||
d = json.load(open(src))
|
d = json.load(open(src))
|
||||||
counts = dict.fromkeys(
|
counts = dict.fromkeys(
|
||||||
("bld", "road", "path", "park", "planter", "water", "cliff", "tree", "prop",
|
("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_LEISURE = ("park", "recreation_ground", "pitch", "playground", "golf_course", "nature_reserve")
|
||||||
GREEN_LANDUSE = ("grass", "recreation_ground", "village_green", "meadow", "forest")
|
GREEN_LANDUSE = ("grass", "recreation_ground", "village_green", "meadow", "forest")
|
||||||
PLANTER_LEISURE = ("garden",)
|
PLANTER_LEISURE = ("garden",)
|
||||||
@ -397,6 +397,7 @@ sig_bm, lens_bm = bmesh.new(), bmesh.new()
|
|||||||
pole_bm, wire_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
|
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
|
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
|
PP_CAP, WB_CAP = 70, 40
|
||||||
SMASH_CAP = {"SL": 90, "BO": 90, "WB": 60}
|
SMASH_CAP = {"SL": 90, "BO": 90, "WB": 60}
|
||||||
PARK_STREETS = ("residential", "tertiary", "secondary", "unclassified")
|
PARK_STREETS = ("residential", "tertiary", "secondary", "unclassified")
|
||||||
@ -651,6 +652,8 @@ for e in d["elements"]:
|
|||||||
acc += 28.0
|
acc += 28.0
|
||||||
acc -= segL
|
acc -= segL
|
||||||
road_ways.append((pts, wide, hw))
|
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
|
# 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
|
if hw != "pedestrian" and wide >= 7.0 and hw not in ("busway",): # dashed centre line
|
||||||
for i in range(len(pts) - 1):
|
for i in range(len(pts) - 1):
|
||||||
@ -879,6 +882,40 @@ for prefix, spots, cap in batches:
|
|||||||
bpy.context.collection.objects.link(emp)
|
bpy.context.collection.objects.link(emp)
|
||||||
counts[prefix.lower()] = min(len(spots), cap)
|
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:
|
if spawn_arg:
|
||||||
slat, slon, sbear = [float(v) for v in spawn_arg.split(",")]
|
slat, slon, sbear = [float(v) for v in spawn_arg.split(",")]
|
||||||
sx, sy = xy(slat, slon)
|
sx, sy = xy(slat, slon)
|
||||||
|
|||||||
@ -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_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_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
|
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
|
EOF
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -5,14 +5,23 @@ Usage: osm_junction.py <area.json> <out junctions.json> <lat0> <lon0> "Name|Stre
|
|||||||
|
|
||||||
For each spec: junction = shared node of the two streets. Every same-named way
|
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
|
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
|
copy lane-offset the other way). Spawn point sits SPAWN_DIST up Street A aimed
|
||||||
the junction. Coordinates are level-local Godot metres (x east, z south).
|
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 json
|
||||||
import math
|
import math
|
||||||
import sys
|
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
|
LANE = 2.7
|
||||||
TARGETS = [15000, 35000, 60000]
|
TARGETS = [15000, 35000, 60000]
|
||||||
|
|
||||||
@ -41,6 +50,45 @@ def offset(pts, d):
|
|||||||
outp.append([p[0] + dz / L * d, p[1] - dx / L * d])
|
outp.append([p[0] + dz / L * d, p[1] - dx / L * d])
|
||||||
return outp
|
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 = []
|
events = []
|
||||||
for spec in sys.argv[5:]:
|
for spec in sys.argv[5:]:
|
||||||
name, a, b = spec.split("|")[:3]
|
name, a, b = spec.split("|")[:3]
|
||||||
@ -50,14 +98,32 @@ for spec in sys.argv[5:]:
|
|||||||
if not shared:
|
if not shared:
|
||||||
print("SKIP %s: %s never meets %s" % (name, a, b))
|
print("SKIP %s: %s never meets %s" % (name, a, b))
|
||||||
continue
|
continue
|
||||||
jn = sorted(shared)[0]
|
# Two named streets often share several nodes (OSM splits ways at every
|
||||||
jpt = None
|
# change of tag), and the lowest node id is arbitrary -- for Albert x
|
||||||
for w in ways[a]:
|
# Adelaide it landed inside a CBD building block with the convoy 60 m away
|
||||||
if jn in w["nodes"]:
|
# and no drivable run-up. Pick the shared node with the most street geometry
|
||||||
g = w["geometry"][w["nodes"].index(jn)]
|
# around it: the busiest crossing is the best crash junction anyway.
|
||||||
jpt = xz(g["lat"], g["lon"])
|
def node_pt(n):
|
||||||
streams = []
|
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 = None
|
||||||
|
spawn_d = 1e9
|
||||||
for street in (a, b):
|
for street in (a, b):
|
||||||
for w in ways[street]:
|
for w in ways[street]:
|
||||||
pts = [xz(g["lat"], g["lon"]) for g in w["geometry"]]
|
pts = [xz(g["lat"], g["lon"]) for g in w["geometry"]]
|
||||||
@ -66,21 +132,37 @@ for spec in sys.argv[5:]:
|
|||||||
continue
|
continue
|
||||||
length = sum(math.hypot(near[i + 1][0] - near[i][0], near[i + 1][1] - near[i][1])
|
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))
|
for i in range(len(near) - 1))
|
||||||
if length < 60:
|
if length < MIN_STREAM:
|
||||||
continue
|
continue
|
||||||
streams.append({"pts": offset(near, LANE), "speed": 12.0, "interval": 2.2})
|
by_street[street].append({"pts": offset(near, LANE), "speed": 12.0, "interval": 1.5})
|
||||||
streams.append({"pts": offset(list(reversed(near)), LANE), "speed": 11.0, "interval": 2.6})
|
by_street[street].append({"pts": offset(list(reversed(near)), LANE),
|
||||||
if street == a and spawn is None:
|
"speed": 11.0, "interval": 1.8})
|
||||||
best = max(near, key=lambda p: math.hypot(p[0] - jpt[0], p[1] - jpt[1]))
|
if street == a:
|
||||||
dx, dz = jpt[0] - best[0], jpt[1] - best[1]
|
# OSM splits a street into many ways; only the one that actually
|
||||||
L = math.hypot(dx, dz) or 1.0
|
# reaches the junction can define the launch, or we start 40 m
|
||||||
spawn = {"x": best[0], "z": best[1], "dx": dx / L, "dz": dz / L}
|
# 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:
|
if not streams or spawn is None:
|
||||||
print("SKIP %s: no usable streams" % name)
|
print("SKIP %s: no usable streams" % name)
|
||||||
continue
|
continue
|
||||||
events.append({"name": name, "spawn": spawn, "streams": streams[:6], "targets": TARGETS})
|
cross = sum(1 for s in streams if s in by_street[b])
|
||||||
print("OK %s: %d streams, spawn %.0fm out" % (name, min(len(streams), 6),
|
events.append({"name": name, "spawn": spawn, "streams": streams, "targets": TARGETS})
|
||||||
math.hypot(spawn["x"] - jpt[0], spawn["z"] - jpt[1])))
|
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)
|
json.dump(events, open(out, "w"), indent=1)
|
||||||
print("wrote %s (%d events)" % (out, len(events)))
|
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:])))
|
||||||
|
|||||||
104
tools/tune_junctions.gd
Normal file
104
tools/tune_junctions.gd
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
extends SceneTree
|
||||||
|
## Measure what each crash junction is actually worth, then write per-event
|
||||||
|
## medal targets back into levels/<id>/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 ""
|
||||||
1
tools/tune_junctions.gd.uid
Normal file
1
tools/tune_junctions.gd.uid
Normal file
@ -0,0 +1 @@
|
|||||||
|
uid://dkj574xr62aff
|
||||||
Loading…
Reference in New Issue
Block a user