diff --git a/public/roadNetwork.json b/public/roadNetwork.json
index 5e5bc80..30a4c96 100644
--- a/public/roadNetwork.json
+++ b/public/roadNetwork.json
@@ -4,315 +4,454 @@
"x": -4.61,
"y": 7.13,
"z": -2.77,
- "connections": [2, 7]
+ "connections": [
+ 2,
+ 7
+ ]
},
{
"id": 2,
"x": -5.61,
"y": 7.08,
"z": -7.84,
- "connections": [1, 3]
+ "connections": [
+ 1,
+ 3
+ ]
},
{
"id": 3,
"x": -2.36,
"y": 6.96,
"z": -13.75,
- "connections": [2, 4]
+ "connections": [
+ 2,
+ 4
+ ]
},
{
"id": 4,
"x": -3.35,
"y": 6.48,
"z": -22.71,
- "connections": [3, 5, 69, 157]
+ "connections": [
+ 3,
+ 5,
+ 69,
+ 157
+ ]
},
{
"id": 5,
"x": -11.24,
"y": 6.45,
"z": -20.45,
- "connections": [4, 6]
+ "connections": [
+ 4,
+ 6
+ ]
},
{
"id": 6,
"x": -19.8,
"y": 6.46,
"z": -12.16,
- "connections": [5, 7, 8, 53]
+ "connections": [
+ 5,
+ 7,
+ 8,
+ 53
+ ]
},
{
"id": 7,
"x": -10.38,
"y": 6.99,
"z": -7.51,
- "connections": [6, 1]
+ "connections": [
+ 6,
+ 1
+ ]
},
{
"id": 8,
"x": -29.38,
"y": 5.69,
"z": -14.1,
- "connections": [6, 9, 15]
+ "connections": [
+ 6,
+ 9,
+ 15
+ ]
},
{
"id": 9,
"x": -34.88,
"y": 5.18,
"z": -14.73,
- "connections": [8, 10, 11]
+ "connections": [
+ 8,
+ 10,
+ 11
+ ]
},
{
"id": 10,
"x": -43.64,
"y": 4.25,
"z": -16.72,
- "connections": [9]
+ "connections": [
+ 9
+ ]
},
{
"id": 11,
"x": -34.62,
"y": 4.86,
"z": -21.94,
- "connections": [9, 12]
+ "connections": [
+ 9,
+ 12
+ ]
},
{
"id": 12,
"x": -39.38,
"y": 3.98,
"z": -29.69,
- "connections": [11, 13]
+ "connections": [
+ 11,
+ 13
+ ]
},
{
"id": 13,
"x": -46.05,
"y": 3.15,
"z": -34.04,
- "connections": [12, 14]
+ "connections": [
+ 12,
+ 14
+ ]
},
{
"id": 14,
"x": -55.16,
"y": 2.32,
"z": -36.22,
- "connections": [13]
+ "connections": [
+ 13
+ ]
},
{
"id": 15,
"x": -35.85,
"y": 5.37,
"z": -3.43,
- "connections": [8, 16, 18]
+ "connections": [
+ 8,
+ 16,
+ 18
+ ]
},
{
"id": 16,
"x": -37.61,
"y": 5.17,
"z": 5.14,
- "connections": [15, 17]
+ "connections": [
+ 15,
+ 17
+ ]
},
{
"id": 17,
"x": -44.96,
"y": 4.35,
"z": 8.67,
- "connections": [16]
+ "connections": [
+ 16
+ ]
},
{
"id": 18,
"x": -43.46,
"y": 4.57,
"z": -4.93,
- "connections": [15, 19]
+ "connections": [
+ 15,
+ 19
+ ]
},
{
"id": 19,
"x": -52.58,
"y": 3.6,
"z": -5.75,
- "connections": [18, 20, 23]
+ "connections": [
+ 18,
+ 20,
+ 23
+ ]
},
{
"id": 20,
"x": -58.7,
"y": 3.01,
"z": -0.58,
- "connections": [19, 21]
+ "connections": [
+ 19,
+ 21
+ ]
},
{
"id": 21,
"x": -64.82,
"y": 2.4,
"z": 5,
- "connections": [20, 22]
+ "connections": [
+ 20,
+ 22
+ ]
},
{
"id": 22,
"x": -70.26,
"y": 1.95,
"z": 4.73,
- "connections": [21]
+ "connections": [
+ 21
+ ]
},
{
"id": 23,
"x": -60.47,
"y": 2.73,
"z": -11.6,
- "connections": [19, 24, 25]
+ "connections": [
+ 19,
+ 24,
+ 25
+ ]
},
{
"id": 24,
"x": -64.69,
"y": 2.24,
"z": -16.49,
- "connections": [23]
+ "connections": [
+ 23
+ ]
},
{
"id": 25,
"x": -66.78,
"y": 2.17,
"z": -10.64,
- "connections": [23, 26]
+ "connections": [
+ 23,
+ 26
+ ]
},
{
"id": 26,
"x": -76.23,
"y": 1.39,
"z": -15.3,
- "connections": [25, 27, 29, 40]
+ "connections": [
+ 25,
+ 27,
+ 29,
+ 40
+ ]
},
{
"id": 27,
"x": -82.06,
"y": 0.97,
"z": -20.09,
- "connections": [26, 28]
+ "connections": [
+ 26,
+ 28
+ ]
},
{
"id": 28,
"x": -89.41,
"y": 0.54,
"z": -26.27,
- "connections": [27]
+ "connections": [
+ 27
+ ]
},
{
"id": 29,
"x": -82.06,
"y": 1.08,
"z": -11.22,
- "connections": [26, 30, 32]
+ "connections": [
+ 26,
+ 30,
+ 32
+ ]
},
{
"id": 30,
"x": -88.59,
"y": 0.74,
"z": -2.59,
- "connections": [29, 31]
+ "connections": [
+ 29,
+ 31
+ ]
},
{
"id": 31,
"x": -95.82,
"y": 0.45,
"z": 1.02,
- "connections": [30]
+ "connections": [
+ 30
+ ]
},
{
"id": 32,
"x": -90.92,
"y": 0.58,
"z": -14.14,
- "connections": [29, 33]
+ "connections": [
+ 29,
+ 33
+ ]
},
{
"id": 33,
"x": -97.45,
"y": 0.31,
"z": -17.87,
- "connections": [32, 34, 36]
+ "connections": [
+ 32,
+ 34,
+ 36
+ ]
},
{
"id": 34,
"x": -102.82,
"y": 0.14,
"z": -24.4,
- "connections": [33, 35]
+ "connections": [
+ 33,
+ 35
+ ]
},
{
"id": 35,
"x": -108.07,
"y": 0,
"z": -31.52,
- "connections": [34]
+ "connections": [
+ 34
+ ]
},
{
"id": 36,
"x": -102.94,
"y": 0.15,
"z": -14.96,
- "connections": [33, 37]
+ "connections": [
+ 33,
+ 37
+ ]
},
{
"id": 37,
"x": -107.37,
"y": 0.11,
"z": -9.01,
- "connections": [36, 38]
+ "connections": [
+ 36,
+ 38
+ ]
},
{
"id": 38,
"x": -110.75,
"y": 0.08,
"z": -2.59,
- "connections": [37, 39]
+ "connections": [
+ 37,
+ 39
+ ]
},
{
"id": 39,
"x": -116.58,
"y": 0,
"z": -0.38,
- "connections": [38]
+ "connections": [
+ 38
+ ]
},
{
"id": 40,
"x": -76.23,
"y": 1.27,
"z": -22.89,
- "connections": [26, 41]
+ "connections": [
+ 26,
+ 41
+ ]
},
{
"id": 41,
"x": -76.81,
"y": 1.09,
"z": -29.53,
- "connections": [40, 42, 47]
+ "connections": [
+ 40,
+ 42,
+ 47
+ ]
},
{
"id": 42,
"x": -73.43,
"y": 1.08,
"z": -37.23,
- "connections": [41, 43]
+ "connections": [
+ 41,
+ 43
+ ]
},
{
"id": 43,
"x": -71.91,
"y": 1.02,
"z": -42.83,
- "connections": [42, 44]
+ "connections": [
+ 42,
+ 44
+ ]
},
{
"id": 44,
"x": -72.61,
"y": 0.77,
"z": -49.48,
- "connections": [43, 45]
+ "connections": [
+ 43,
+ 45
+ ]
},
{
"id": 45,
"x": -77.51,
"y": 0.43,
"z": -56.36,
- "connections": [44]
+ "connections": [
+ 44
+ ]
},
{
"id": 46,
@@ -326,35 +465,49 @@
"x": -82.6,
"y": 0.64,
"z": -38.22,
- "connections": [41, 48]
+ "connections": [
+ 41,
+ 48
+ ]
},
{
"id": 48,
"x": -87.73,
"y": 0.32,
"z": -45.38,
- "connections": [47, 49]
+ "connections": [
+ 47,
+ 49
+ ]
},
{
"id": 49,
"x": -88.14,
"y": 0.17,
"z": -54.34,
- "connections": [48, 50]
+ "connections": [
+ 48,
+ 50
+ ]
},
{
"id": 50,
"x": -89.12,
"y": 0.09,
"z": -59.31,
- "connections": [49, 51]
+ "connections": [
+ 49,
+ 51
+ ]
},
{
"id": 51,
"x": -92.7,
"y": 0.01,
"z": -63.96,
- "connections": [50]
+ "connections": [
+ 50
+ ]
},
{
"id": 52,
@@ -368,189 +521,274 @@
"x": -23.13,
"y": 6.44,
"z": -4.02,
- "connections": [6, 54]
+ "connections": [
+ 6,
+ 54
+ ]
},
{
"id": 54,
"x": -23.62,
"y": 6.39,
"z": 5.67,
- "connections": [53, 55]
+ "connections": [
+ 53,
+ 55
+ ]
},
{
"id": 55,
"x": -18.16,
"y": 6.37,
"z": 16.59,
- "connections": [54, 56, 59, 156]
+ "connections": [
+ 54,
+ 56,
+ 59,
+ 174
+ ]
},
{
"id": 56,
"x": -14.25,
"y": 6.77,
"z": 11.21,
- "connections": [55, 57]
+ "connections": [
+ 55,
+ 57
+ ]
},
{
"id": 57,
"x": -13.93,
"y": 6.9,
"z": 6.81,
- "connections": [56, 58]
+ "connections": [
+ 56,
+ 58
+ ]
},
{
"id": 58,
"x": -11,
"y": 7.03,
"z": 3.23,
- "connections": [57]
+ "connections": [
+ 57
+ ]
},
{
"id": 59,
"x": -10.67,
"y": 6.41,
"z": 21.39,
- "connections": [55, 60]
+ "connections": [
+ 55,
+ 60
+ ]
},
{
"id": 60,
"x": -1.71,
"y": 6.38,
"z": 24.33,
- "connections": [59, 61]
+ "connections": [
+ 59,
+ 61
+ ]
},
{
"id": 61,
"x": 8.8,
"y": 6.36,
"z": 23.1,
- "connections": [60, 62]
+ "connections": [
+ 60,
+ 62
+ ]
},
{
"id": 62,
"x": 14.42,
"y": 6.42,
"z": 18.95,
- "connections": [61, 63, 64, 108]
+ "connections": [
+ 61,
+ 64,
+ 108,
+ 173
+ ]
},
{
"id": 63,
"x": 2.44,
"y": 7.13,
"z": 3.31,
- "connections": [62]
+ "connections": [
+ 172
+ ]
},
{
"id": 64,
"x": 20.12,
"y": 6.43,
"z": 12.52,
- "connections": [62, 65]
+ "connections": [
+ 62,
+ 65
+ ]
},
{
"id": 65,
"x": 22.48,
"y": 6.49,
"z": 3.88,
- "connections": [64, 66]
+ "connections": [
+ 64,
+ 66
+ ]
},
{
"id": 66,
"x": 21.34,
"y": 6.52,
"z": -6.79,
- "connections": [65, 67]
+ "connections": [
+ 65,
+ 67
+ ]
},
{
"id": 67,
"x": 18.25,
"y": 6.5,
"z": -13.47,
- "connections": [66, 68]
+ "connections": [
+ 66,
+ 68
+ ]
},
{
"id": 68,
"x": 15.23,
"y": 6.54,
"z": -15.99,
- "connections": [67, 69, 70, 71]
+ "connections": [
+ 67,
+ 69,
+ 70,
+ 71
+ ]
},
{
"id": 69,
"x": 5.78,
"y": 6.52,
"z": -21.53,
- "connections": [68, 4]
+ "connections": [
+ 68,
+ 4
+ ]
},
{
"id": 70,
"x": 11.08,
"y": 6.96,
"z": -8.17,
- "connections": [68]
+ "connections": [
+ 68
+ ]
},
{
"id": 71,
"x": 19.39,
"y": 6.07,
"z": -20.63,
- "connections": [68, 72, 91]
+ "connections": [
+ 68,
+ 72,
+ 91
+ ]
},
{
"id": 72,
"x": 21.18,
"y": 5.69,
"z": -24.87,
- "connections": [71, 73, 74]
+ "connections": [
+ 71,
+ 73,
+ 74
+ ]
},
{
"id": 73,
"x": 22.57,
"y": 4.58,
"z": -37.32,
- "connections": [72]
+ "connections": [
+ 72
+ ]
},
{
"id": 74,
"x": 28.76,
"y": 4.96,
"z": -27.8,
- "connections": [72, 75, 76]
+ "connections": [
+ 72,
+ 75,
+ 76
+ ]
},
{
"id": 75,
"x": 36.01,
"y": 4.45,
"z": -26.82,
- "connections": [74]
+ "connections": [
+ 74
+ ]
},
{
"id": 76,
"x": 39.1,
"y": 3.59,
"z": -35.78,
- "connections": [74, 77, 78]
+ "connections": [
+ 74,
+ 77,
+ 78
+ ]
},
{
"id": 77,
"x": 51.07,
"y": 2.37,
"z": -40.58,
- "connections": [76]
+ "connections": [
+ 76
+ ]
},
{
"id": 78,
"x": 39.26,
"y": 2.89,
"z": -45.14,
- "connections": [76, 79, 81]
+ "connections": [
+ 76,
+ 79,
+ 81
+ ]
},
{
"id": 79,
"x": 37.55,
"y": 2.11,
"z": -57.04,
- "connections": [78]
+ "connections": [
+ 78
+ ]
},
{
"id": 80,
@@ -564,63 +802,89 @@
"x": 47.25,
"y": 1.81,
"z": -54.26,
- "connections": [78, 82]
+ "connections": [
+ 78,
+ 82
+ ]
},
{
"id": 82,
"x": 60.2,
"y": 0.9,
"z": -60.53,
- "connections": [81, 83, 84]
+ "connections": [
+ 81,
+ 83,
+ 84
+ ]
},
{
"id": 83,
"x": 75.6,
"y": 0.3,
"z": -63.79,
- "connections": [82]
+ "connections": [
+ 82
+ ]
},
{
"id": 84,
"x": 71.69,
"y": 0.73,
"z": -52.06,
- "connections": [82, 85, 86]
+ "connections": [
+ 82,
+ 85,
+ 86
+ ]
},
{
"id": 85,
"x": 84.72,
"y": 0.41,
"z": -45.14,
- "connections": [84]
+ "connections": [
+ 84
+ ]
},
{
"id": 86,
"x": 72.83,
"y": 0.94,
"z": -43.18,
- "connections": [84, 87]
+ "connections": [
+ 84,
+ 87
+ ]
},
{
"id": 87,
"x": 82.52,
"y": 0.79,
"z": -29.01,
- "connections": [86, 88]
+ "connections": [
+ 86,
+ 88
+ ]
},
{
"id": 88,
"x": 92.95,
"y": 0.47,
"z": -18.1,
- "connections": [87, 89]
+ "connections": [
+ 87,
+ 89
+ ]
},
{
"id": 89,
"x": 100.77,
"y": 0.21,
"z": -16.55,
- "connections": [88]
+ "connections": [
+ 88
+ ]
},
{
"id": 90,
@@ -634,21 +898,32 @@
"x": 31.21,
"y": 5.49,
"z": -15.42,
- "connections": [71, 92, 102, 103]
+ "connections": [
+ 71,
+ 92,
+ 103,
+ 166
+ ]
},
{
"id": 92,
"x": 48.06,
"y": 3.69,
"z": -19.81,
- "connections": [91, 93, 95]
+ "connections": [
+ 91,
+ 93,
+ 95
+ ]
},
{
"id": 93,
"x": 59.71,
"y": 2.45,
"z": -24.13,
- "connections": [92]
+ "connections": [
+ 92
+ ]
},
{
"id": 94,
@@ -662,42 +937,59 @@
"x": 60.85,
"y": 2.78,
"z": -3.28,
- "connections": [92, 96, 97]
+ "connections": [
+ 92,
+ 96,
+ 97
+ ]
},
{
"id": 96,
"x": 68.43,
"y": 2.1,
"z": -2.47,
- "connections": [95]
+ "connections": [
+ 95
+ ]
},
{
"id": 97,
"x": 68.51,
"y": 2.04,
"z": 9.18,
- "connections": [95, 98, 99]
+ "connections": [
+ 95,
+ 98,
+ 99
+ ]
},
{
"id": 98,
"x": 67.86,
"y": 1.98,
"z": 16.59,
- "connections": [97]
+ "connections": [
+ 97
+ ]
},
{
"id": 99,
"x": 91.56,
"y": 0.56,
"z": 13.82,
- "connections": [97, 100]
+ "connections": [
+ 97,
+ 100
+ ]
},
{
"id": 100,
"x": 97.1,
"y": 0.31,
"z": 18.46,
- "connections": [99]
+ "connections": [
+ 99
+ ]
},
{
"id": 101,
@@ -706,103 +998,137 @@
"z": 6.58,
"connections": []
},
- {
- "id": 102,
- "x": 43.42,
- "y": 4.52,
- "z": -8.25,
- "connections": [91]
- },
{
"id": 103,
"x": 32.83,
"y": 5.63,
"z": -5.24,
- "connections": [91, 104]
+ "connections": [
+ 91,
+ 104
+ ]
},
{
"id": 104,
"x": 32.75,
"y": 5.62,
"z": 6.74,
- "connections": [103, 105]
+ "connections": [
+ 103,
+ 105
+ ]
},
{
"id": 105,
"x": 32.51,
"y": 5.42,
"z": 14.31,
- "connections": [104, 106]
+ "connections": [
+ 104,
+ 106
+ ]
},
{
"id": 106,
"x": 39.02,
"y": 4.65,
"z": 17.98,
- "connections": [105, 107]
+ "connections": [
+ 105,
+ 107
+ ]
},
{
"id": 107,
"x": 44.48,
"y": 4.26,
"z": 14.31,
- "connections": [106]
+ "connections": [
+ 106
+ ]
},
{
"id": 108,
"x": 19.31,
"y": 5.65,
"z": 26.84,
- "connections": [62, 109, 131]
+ "connections": [
+ 62,
+ 109,
+ 131,
+ 167
+ ]
},
{
"id": 109,
"x": 22.49,
"y": 5.23,
"z": 29.86,
- "connections": [108, 110, 127]
+ "connections": [
+ 108,
+ 110
+ ]
},
{
"id": 110,
"x": 32.91,
"y": 3.51,
"z": 42.4,
- "connections": [109, 111, 122]
+ "connections": [
+ 109,
+ 111,
+ 122
+ ]
},
{
"id": 111,
"x": 39.1,
"y": 2.75,
"z": 47.21,
- "connections": [110, 112]
+ "connections": [
+ 110,
+ 112
+ ]
},
{
"id": 112,
"x": 55.64,
"y": 1.54,
"z": 51.12,
- "connections": [111, 113, 115, 117]
+ "connections": [
+ 111,
+ 113,
+ 115,
+ 117
+ ]
},
{
"id": 113,
"x": 61.66,
"y": 1.26,
"z": 49.98,
- "connections": [112, 114]
+ "connections": [
+ 112,
+ 114
+ ]
},
{
"id": 114,
"x": 70.14,
"y": 0.98,
"z": 46.23,
- "connections": [113]
+ "connections": [
+ 113
+ ]
},
{
"id": 115,
"x": 56.45,
"y": 1.31,
"z": 54.86,
- "connections": [112]
+ "connections": [
+ 112
+ ]
},
{
"id": 116,
@@ -816,28 +1142,39 @@
"x": 61.42,
"y": 0.86,
"z": 60.39,
- "connections": [112, 118]
+ "connections": [
+ 112,
+ 118
+ ]
},
{
"id": 118,
"x": 60.85,
"y": 0.54,
"z": 70.01,
- "connections": [117, 119]
+ "connections": [
+ 117,
+ 119
+ ]
},
{
"id": 119,
"x": 56.7,
"y": 0.37,
"z": 78.56,
- "connections": [118, 120]
+ "connections": [
+ 118,
+ 120
+ ]
},
{
"id": 120,
"x": 57.11,
"y": 0.24,
"z": 83.2,
- "connections": [119]
+ "connections": [
+ 119
+ ]
},
{
"id": 121,
@@ -851,147 +1188,170 @@
"x": 31.12,
"y": 2.64,
"z": 54.13,
- "connections": [110, 123, 124]
+ "connections": [
+ 110,
+ 123,
+ 124
+ ]
},
{
"id": 123,
"x": 25.5,
"y": 2.37,
"z": 60.15,
- "connections": [122]
+ "connections": [
+ 122
+ ]
},
{
"id": 124,
"x": 32.1,
"y": 1.84,
"z": 64.06,
- "connections": [122, 125]
+ "connections": [
+ 122,
+ 125
+ ]
},
{
"id": 125,
"x": 26.07,
"y": 1.22,
"z": 75.79,
- "connections": [124, 126]
+ "connections": [
+ 124,
+ 126
+ ]
},
{
"id": 126,
"x": 26.07,
"y": 1,
"z": 79.54,
- "connections": [125]
- },
- {
- "id": 127,
- "x": 31.39,
- "y": 4.75,
- "z": 27.95,
- "connections": [109, 128]
- },
- {
- "id": 128,
- "x": 41.12,
- "y": 4.07,
- "z": 25.58,
- "connections": [127, 129]
- },
- {
- "id": 129,
- "x": 46.63,
- "y": 3.54,
- "z": 26.12,
- "connections": [128, 130]
- },
- {
- "id": 130,
- "x": 53.28,
- "y": 2.65,
- "z": 32.44,
- "connections": [129]
+ "connections": [
+ 125
+ ]
},
{
"id": 131,
"x": 19.13,
"y": 4.92,
"z": 35.57,
- "connections": [108, 132]
+ "connections": [
+ 108,
+ 132
+ ]
},
{
"id": 132,
"x": 16.75,
"y": 4.05,
"z": 45.62,
- "connections": [131, 133]
+ "connections": [
+ 131,
+ 133
+ ]
},
{
"id": 133,
"x": 11.18,
"y": 3.33,
"z": 54.32,
- "connections": [132, 134, 143]
+ "connections": [
+ 132,
+ 134,
+ 143
+ ]
},
{
"id": 134,
"x": 7.83,
"y": 2.97,
"z": 58.54,
- "connections": [133, 135, 139]
+ "connections": [
+ 133,
+ 135,
+ 139
+ ]
},
{
"id": 135,
"x": 2.7,
"y": 2.73,
"z": 61.4,
- "connections": [134, 136]
+ "connections": [
+ 134,
+ 136
+ ]
},
{
"id": 136,
"x": -2.22,
"y": 2.81,
"z": 60.59,
- "connections": [135, 137]
+ "connections": [
+ 135,
+ 137
+ ]
},
{
"id": 137,
"x": -2.98,
"y": 3.58,
"z": 52.97,
- "connections": [136]
+ "connections": [
+ 136
+ ]
},
{
"id": 139,
"x": 4.05,
"y": 1.58,
"z": 74.79,
- "connections": [134, 140]
+ "connections": [
+ 134,
+ 140
+ ]
},
{
"id": 140,
"x": -0.33,
"y": 1.21,
"z": 80.47,
- "connections": [139, 141]
+ "connections": [
+ 139,
+ 141
+ ]
},
{
"id": 141,
"x": -3.24,
"y": 1.24,
"z": 79.76,
- "connections": [140, 142]
+ "connections": [
+ 140,
+ 142
+ ]
},
{
"id": 142,
"x": -3.41,
"y": 1.73,
"z": 73.01,
- "connections": [141]
+ "connections": [
+ 141
+ ]
},
{
"id": 143,
"x": 11.78,
"y": 0.74,
"z": 87.87,
- "connections": [133, 145, 149]
+ "connections": [
+ 133,
+ 145,
+ 149
+ ]
},
{
"id": 144,
@@ -1005,63 +1365,88 @@
"x": 7.89,
"y": 0.44,
"z": 94.98,
- "connections": [143, 146]
+ "connections": [
+ 143,
+ 146
+ ]
},
{
"id": 146,
"x": 4.48,
"y": 0.31,
"z": 98.94,
- "connections": [145, 147]
+ "connections": [
+ 145,
+ 147
+ ]
},
{
"id": 147,
"x": -2.22,
"y": 0.27,
"z": 100.07,
- "connections": [146, 148]
+ "connections": [
+ 146,
+ 148
+ ]
},
{
"id": 148,
"x": -3.03,
"y": 0.47,
"z": 94.56,
- "connections": [147]
+ "connections": [
+ 147
+ ]
},
{
"id": 149,
"x": 11.78,
"y": 0.29,
"z": 98.56,
- "connections": [143, 150]
+ "connections": [
+ 143,
+ 150
+ ]
},
{
"id": 150,
"x": 9.61,
"y": 0.05,
"z": 108.88,
- "connections": [149, 151]
+ "connections": [
+ 149,
+ 151
+ ]
},
{
"id": 151,
"x": 5.08,
"y": 0,
"z": 117.04,
- "connections": [150, 152]
+ "connections": [
+ 150,
+ 152
+ ]
},
{
"id": 152,
"x": -0.49,
"y": 0,
"z": 119.26,
- "connections": [151, 153]
+ "connections": [
+ 151,
+ 153
+ ]
},
{
"id": 153,
"x": -5.3,
"y": 0,
"z": 116.93,
- "connections": [152]
+ "connections": [
+ 152
+ ]
},
{
"id": 154,
@@ -1070,67 +1455,237 @@
"z": 117.06,
"connections": []
},
- {
- "id": 156,
- "x": -45.93,
- "y": 2.77,
- "z": 40.28,
- "connections": [55]
- },
{
"id": 157,
"x": -7.58,
"y": 5.97,
"z": -28.56,
- "connections": [4, 158]
+ "connections": [
+ 4,
+ 179
+ ]
},
{
"id": 158,
"x": -17.19,
"y": 2.57,
"z": -60.82,
- "connections": [157, 159, 162]
+ "connections": [
+ 159,
+ 162,
+ 180
+ ]
},
{
"id": 159,
"x": -2.45,
"y": 2.37,
"z": -65.38,
- "connections": [158, 160]
+ "connections": [
+ 158,
+ 160
+ ]
},
{
"id": 160,
"x": 7.4,
"y": 2.46,
"z": -63.99,
- "connections": [159, 161]
+ "connections": [
+ 159,
+ 161
+ ]
},
{
"id": 161,
"x": 24.59,
"y": 2.71,
"z": -56.66,
- "connections": [160]
+ "connections": [
+ 160
+ ]
},
{
"id": 162,
"x": -31.53,
"y": 2.44,
"z": -56.42,
- "connections": [158, 163]
+ "connections": [
+ 158,
+ 163
+ ]
},
{
"id": 163,
"x": -39.67,
"y": 2.4,
"z": -51.61,
- "connections": [162, 164]
+ "connections": [
+ 162,
+ 164
+ ]
},
{
"id": 164,
"x": -49.61,
"y": 2.28,
"z": -44.28,
- "connections": [163]
+ "connections": [
+ 163
+ ]
+ },
+ {
+ "id": 165,
+ "x": 106.01,
+ "y": 0.1,
+ "z": 1.02,
+ "connections": []
+ },
+ {
+ "id": 166,
+ "x": 43.06,
+ "y": 4.57,
+ "z": -7.87,
+ "connections": [
+ 91
+ ]
+ },
+ {
+ "id": 167,
+ "x": 25.12,
+ "y": 5.18,
+ "z": 28.43,
+ "connections": [
+ 108,
+ 168
+ ]
+ },
+ {
+ "id": 168,
+ "x": 33.68,
+ "y": 4.56,
+ "z": 28.11,
+ "connections": [
+ 167,
+ 169
+ ]
+ },
+ {
+ "id": 169,
+ "x": 40.79,
+ "y": 4.09,
+ "z": 25.69,
+ "connections": [
+ 168,
+ 170
+ ]
+ },
+ {
+ "id": 170,
+ "x": 47.9,
+ "y": 3.41,
+ "z": 26.49,
+ "connections": [
+ 169,
+ 171
+ ]
+ },
+ {
+ "id": 171,
+ "x": 52.58,
+ "y": 2.73,
+ "z": 31.99,
+ "connections": [
+ 170
+ ]
+ },
+ {
+ "id": 172,
+ "x": 7.42,
+ "y": 7.03,
+ "z": 8.82,
+ "connections": [
+ 63,
+ 173
+ ]
+ },
+ {
+ "id": 173,
+ "x": 11,
+ "y": 6.74,
+ "z": 15.13,
+ "connections": [
+ 172,
+ 62
+ ]
+ },
+ {
+ "id": 174,
+ "x": -24.16,
+ "y": 5.72,
+ "z": 21.45,
+ "connections": [
+ 55,
+ 175
+ ]
+ },
+ {
+ "id": 175,
+ "x": -31.1,
+ "y": 4.88,
+ "z": 26.48,
+ "connections": [
+ 174,
+ 176
+ ]
+ },
+ {
+ "id": 176,
+ "x": -35.84,
+ "y": 4.17,
+ "z": 31.18,
+ "connections": [
+ 175,
+ 177
+ ]
+ },
+ {
+ "id": 177,
+ "x": -41.34,
+ "y": 3.37,
+ "z": 36.51,
+ "connections": [
+ 176,
+ 178
+ ]
+ },
+ {
+ "id": 178,
+ "x": -47.11,
+ "y": 2.66,
+ "z": 40.6,
+ "connections": [
+ 177
+ ]
+ },
+ {
+ "id": 179,
+ "x": -9.75,
+ "y": 5.03,
+ "z": -38.14,
+ "connections": [
+ 157,
+ 180
+ ]
+ },
+ {
+ "id": 180,
+ "x": -14.38,
+ "y": 3.62,
+ "z": -50.73,
+ "connections": [
+ 179,
+ 158
+ ]
}
-]
+]
\ No newline at end of file
diff --git a/src/components/ebike/Ebike.tsx b/src/components/ebike/Ebike.tsx
index 6eb0455..502df8c 100644
--- a/src/components/ebike/Ebike.tsx
+++ b/src/components/ebike/Ebike.tsx
@@ -2,7 +2,7 @@ import { useEffect, useRef, useState, useMemo, useCallback } from "react";
import * as THREE from "three";
import { useFrame, useThree } from "@react-three/fiber";
import { EbikeGPSMap } from "@/components/ebike/EbikeGPSMap";
-import { EbikeSpeedometer } from "@/components/ebike/EbikeSpeedometer";
+import { EbikeSpeedmeter } from "@/components/ebike/EbikeSpeedmeter";
import { InteractableObject } from "@/components/three/interaction/InteractableObject";
import { useLoggedGLTF } from "@/hooks/three/useLoggedGLTF";
import { useClonedObject } from "@/hooks/three/useClonedObject";
@@ -123,11 +123,35 @@ export function Ebike({ position }: EbikeProps): React.JSX.Element {
}, [movementMode, parkedPosition]);
useEffect(() => {
- if (model) {
- const fork = model.getObjectByName("fourche");
- if (fork) {
- forkRef.current = fork;
+ if (!model) return;
+
+ // Full recursive search — case-insensitive so it survives export renames.
+ // Also tries the exact path Moto > * > Fourche as a fallback.
+ let forkNode: THREE.Object3D | null = null;
+
+ model.traverse((child) => {
+ if (child.name.toLowerCase() === "fourche") {
+ forkNode = child;
}
+ });
+
+ if (forkNode) {
+ forkRef.current = forkNode;
+ console.log("[Ebike] Fork found:", (forkNode as THREE.Object3D).name);
+ } else {
+ // Print the full hierarchy tree so you can read the exact node names.
+ const lines: string[] = [];
+ function printTree(obj: THREE.Object3D, indent: number): void {
+ lines.push(" ".repeat(indent * 2) + (obj.name || "(unnamed)"));
+ for (const child of obj.children) {
+ printTree(child, indent + 1);
+ }
+ }
+ printTree(model, 0);
+ console.warn(
+ '[Ebike] No node matching "fourche" (case-insensitive) found.\nFull hierarchy:\n' +
+ lines.join("\n"),
+ );
}
}, [model]);
@@ -156,9 +180,11 @@ export function Ebike({ position }: EbikeProps): React.JSX.Element {
useFrame((_, delta) => {
if (groupRef.current) {
if (movementMode === "ebike") {
+ // Sound plays whenever the bike is actually moving (speedFactor > 5 %),
+ // not only while the input key is held.
updateEbikeSounds({
mounted: true,
- driving: window.ebikeDriveInputActive === true,
+ driving: (window.ebikeSpeedFactor ?? 0) > 0.05,
breakdown: window.ebikeBreakdownActive === true,
});
@@ -169,11 +195,11 @@ export function Ebike({ position }: EbikeProps): React.JSX.Element {
];
restingRotationRef.current = groupRef.current.rotation.y;
- // Smoothly rotate the front fork ("fourche") up to 15 degrees in its own Z axis
+ // Smoothly rotate the front fork ("fourche") on its local Z axis
const steerFactor = window.ebikeSteerFactor ?? 0;
if (forkRef.current) {
- // 15 degrees is 0.26 radians
- const targetForkRotation = steerFactor * 0.26;
+ // 10 degrees = 0.175 radians
+ const targetForkRotation = steerFactor * 0.175;
forkRef.current.rotation.z = THREE.MathUtils.lerp(
forkRef.current.rotation.z,
targetForkRotation,
@@ -329,6 +355,9 @@ export function Ebike({ position }: EbikeProps): React.JSX.Element {
scale={EBIKE_WORLD_SCALE}
>
+ {/* radius 20 → ~7 unités monde (scale 0.35).
+ Sphère omnidirectionnelle pour que le raycast fonctionne
+ quelle que soit l'orientation de la caméra (montée ou à pied). */}
-
-
+
+
- {/* Dynamic 3D GPS Dashboard Screen */}
-
+ {/* GPS + Speedmeter – same group so they are perfectly co-localised.
+ GPS: full circle (Fresnel mask), renderOrder 10 000
+ Speedmeter: upper-half arc overlay, renderOrder 10 001
+ rotation: Math.PI/2 radians = 90° (NOT the number 90 which = ~116.6°) */}
+
+
-
-
-
) : null}
{showCameraPoints && !repairGameOwnsEbikeModel && (
<>
-
+ {/*
-
+ */}
>
)}
>
diff --git a/src/components/ebike/EbikeGPSMap.tsx b/src/components/ebike/EbikeGPSMap.tsx
index 08fab3e..064bcc6 100644
--- a/src/components/ebike/EbikeGPSMap.tsx
+++ b/src/components/ebike/EbikeGPSMap.tsx
@@ -12,6 +12,28 @@ import {
} from "@/pathfinding/WaypointAStar";
import type { Waypoint } from "@/pathfinding/types";
import type { Vector3Tuple } from "@/types/three/three";
+
+const VERT_SHADER = /* glsl */ `
+ varying vec2 vUv;
+ void main() {
+ vUv = uv;
+ gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
+ }
+`;
+
+// Circular Fresnel mask: fully visible inside innerRadius, fades out to outerRadius
+const FRAG_SHADER = /* glsl */ `
+ uniform sampler2D map;
+ uniform float innerRadius;
+ uniform float outerRadius;
+ varying vec2 vUv;
+ void main() {
+ vec4 color = texture2D(map, vUv);
+ float dist = length(vUv - vec2(0.5));
+ float mask = 1.0 - smoothstep(innerRadius, outerRadius, dist);
+ gl_FragColor = vec4(color.rgb, color.a * mask);
+ }
+`;
function computeImageSource(
img: HTMLImageElement | HTMLCanvasElement,
baseBounds: { minX: number; maxX: number; minZ: number; maxZ: number },
@@ -126,19 +148,57 @@ export const EbikeGPSMap: React.FC = ({
// eslint-disable-next-line react-hooks/exhaustive-deps -- Canvas should only be created once
}, []);
- // Resize the canvas whenever canvasSize changes
- // Note: Modifying canvas dimensions is intentional and necessary for rendering
- useEffect(() => {
- // Use Object.assign to resize canvas - this is a necessary mutation for canvas rendering
- Object.assign(offscreenCanvas, { width: canvasSize, height: canvasSize });
- if (textureRef.current) {
- textureRef.current.needsUpdate = true;
- }
- }, [canvasSize, offscreenCanvas]);
-
- const textureRef = useRef(null);
const animTimeRef = useRef(0);
+ // Imperative CanvasTexture — must be declared before the resize effect below
+ const texture = useMemo(() => {
+ const tex = new THREE.CanvasTexture(offscreenCanvas);
+ tex.format = THREE.RGBAFormat;
+ tex.minFilter = THREE.LinearFilter;
+ tex.magFilter = THREE.LinearFilter;
+ return tex;
+ }, [offscreenCanvas]);
+
+ // ShaderMaterial with circular Fresnel mask (created once)
+ const shaderMat = useMemo(
+ () =>
+ new THREE.ShaderMaterial({
+ uniforms: {
+ map: { value: null },
+ innerRadius: { value: 0.45 },
+ outerRadius: { value: 0.5 },
+ },
+ vertexShader: VERT_SHADER,
+ fragmentShader: FRAG_SHADER,
+ transparent: true,
+ depthTest: false,
+ depthWrite: false,
+ side: THREE.DoubleSide,
+ toneMapped: false,
+ }),
+ [],
+ );
+
+ // Sync texture into uniform when it changes (canvas resize)
+ useEffect(() => {
+ shaderMat.uniforms.map.value = texture;
+ }, [shaderMat, texture]);
+
+ // Cleanup on unmount
+ useEffect(
+ () => () => {
+ shaderMat.dispose();
+ texture.dispose();
+ },
+ [shaderMat, texture],
+ );
+
+ // Resize the canvas whenever canvasSize changes (texture declared above)
+ useEffect(() => {
+ Object.assign(offscreenCanvas, { width: canvasSize, height: canvasSize });
+ texture.needsUpdate = true;
+ }, [canvasSize, offscreenCanvas, texture]);
+
// Load waypoints (localStorage with /roadNetwork.json fallback)
useEffect(() => {
let cancelled = false;
@@ -492,42 +552,20 @@ export const EbikeGPSMap: React.FC = ({
useEffect(() => {
let animId: number;
const tick = () => {
- animTimeRef.current += 0.004; // Slow, premium sweep speed
+ animTimeRef.current += 0.004;
if (animTimeRef.current > 1) animTimeRef.current = 0;
-
draw();
-
- // Update texture after draw
- if (textureRef.current) {
- textureRef.current.needsUpdate = true;
- }
-
+ texture.needsUpdate = true;
animId = requestAnimationFrame(tick);
};
animId = requestAnimationFrame(tick);
return () => cancelAnimationFrame(animId);
- }, [draw]);
+ }, [draw, texture]);
return (
-
-
-
+
);
};
diff --git a/src/components/ebike/EbikeSpeedmeter.tsx b/src/components/ebike/EbikeSpeedmeter.tsx
new file mode 100644
index 0000000..827ebf6
--- /dev/null
+++ b/src/components/ebike/EbikeSpeedmeter.tsx
@@ -0,0 +1,233 @@
+import { useEffect, useRef, useMemo } from "react";
+import { useFrame } from "@react-three/fiber";
+import { useTexture } from "@react-three/drei";
+import * as THREE from "three";
+import type { Vector3Tuple } from "@/types/three/three";
+import "@/types/ebike/ebikeWindow";
+
+const SPEEDOMETER_DIAL_TEXTURE = "/assets/world/gps/cadran.png";
+const SPEEDOMETER_NEEDLE_TEXTURE = "/assets/world/gps/fleche.png";
+
+export interface EbikeSpeedmeterProps {
+ width?: number;
+ height?: number;
+ /** Local position offset within the parent group. Default: [0, 0, 0] */
+ position?: Vector3Tuple;
+ /**
+ * Needle rotation.z when speedFactor = 0.
+ * Default: Math.PI / 2 (pointing left — 9 o'clock)
+ */
+ minAngle?: number;
+ /**
+ * Needle rotation.z when speedFactor = 1.
+ * Default: -Math.PI / 2 (pointing right — 3 o'clock)
+ */
+ maxAngle?: number;
+ renderOrder?: number;
+ /**
+ * Inner radius of the gauge-fill arc, as a fraction of the canvas half-width.
+ * Tune this to align the fill with the cadran.png track. Default: 0.33
+ */
+ gaugeInnerR?: number;
+ /**
+ * Outer radius of the gauge-fill arc, as a fraction of the canvas half-width.
+ * Tune this to align the fill with the cadran.png track. Default: 0.445
+ */
+ gaugeOuterR?: number;
+ /**
+ * Width of the gauge-fill plane. Defaults to `width` when omitted.
+ * Lets you resize the fill independently of the cadran/needle.
+ */
+ gaugeWidth?: number;
+ /**
+ * Height of the gauge-fill plane. Defaults to `height` when omitted.
+ * Lets you resize the fill independently of the cadran/needle.
+ */
+ gaugeHeight?: number;
+ /**
+ * Horizontal offset of the arc pivot from the canvas centre.
+ * Expressed as a fraction of the canvas size: -0.1 = shift 10 % to the left,
+ * +0.1 = shift 10 % to the right. Default: 0
+ */
+ gaugeOffsetX?: number;
+ /**
+ * Vertical offset of the arc pivot from its default position.
+ * Expressed as a fraction of the canvas size: -0.1 = shift upward (toward top
+ * of the plane), +0.1 = shift downward. Default: 0
+ */
+ gaugeOffsetY?: number;
+}
+
+// The needle pivot is always at -height*0.38 in local space,
+// which is always 12 % from the bottom of the plane (UV y = 0.12).
+// With Three.js flipY texture convention, canvas y = (1 - 0.12) * size = 0.88 * size.
+const NEEDLE_PIVOT_UV_Y = 0.12; // fraction from bottom
+
+export function EbikeSpeedmeter({
+ width = 0.8,
+ height = 0.8,
+ position = [0, 0, 0],
+ minAngle = Math.PI / 2,
+ maxAngle = -Math.PI / 2,
+ renderOrder = 1000,
+ gaugeInnerR = 0.33,
+ gaugeOuterR = 0.445,
+ gaugeWidth,
+ gaugeHeight,
+ gaugeOffsetX = 0,
+ gaugeOffsetY = 0,
+}: EbikeSpeedmeterProps): React.JSX.Element {
+ // Fall back to the main dimensions when gauge-specific ones aren't provided
+ const fillW = gaugeWidth ?? width;
+ const fillH = gaugeHeight ?? height;
+ const needleGroupRef = useRef(null);
+ const speedFactorRef = useRef(0);
+
+ // ── Dial & needle textures ──────────────────────────────────────────────────
+ const [dialTexture, needleTexture] = useTexture([
+ SPEEDOMETER_DIAL_TEXTURE,
+ SPEEDOMETER_NEEDLE_TEXTURE,
+ ]) as [THREE.Texture, THREE.Texture];
+
+ const needleWidth = width * 0.68;
+ const needleHeight = needleWidth / 2;
+
+ useEffect(() => {
+ [dialTexture, needleTexture].forEach((tex) => {
+ tex.colorSpace = THREE.SRGBColorSpace;
+ tex.needsUpdate = true;
+ });
+ }, [dialTexture, needleTexture]);
+
+ // ── Gauge-fill canvas ───────────────────────────────────────────────────────
+ const fillCanvas = useMemo(() => {
+ const c = document.createElement("canvas");
+ c.width = 256;
+ c.height = 256;
+ return c;
+ }, []);
+
+ const fillTexture = useMemo(() => {
+ const tex = new THREE.CanvasTexture(fillCanvas);
+ tex.format = THREE.RGBAFormat;
+ tex.minFilter = THREE.LinearFilter;
+ tex.magFilter = THREE.LinearFilter;
+ return tex;
+ }, [fillCanvas]);
+
+ useEffect(
+ () => () => {
+ fillTexture.dispose();
+ },
+ [fillTexture],
+ );
+
+ // ── Frame loop ──────────────────────────────────────────────────────────────
+ useFrame((_, delta) => {
+ // 1. Smooth speed factor
+ const target = THREE.MathUtils.clamp(window.ebikeSpeedFactor ?? 0, 0, 1);
+ speedFactorRef.current = THREE.MathUtils.lerp(
+ speedFactorRef.current,
+ target,
+ Math.min(1, delta * 10),
+ );
+
+ // 2. Needle rotation
+ if (needleGroupRef.current) {
+ needleGroupRef.current.rotation.z = THREE.MathUtils.lerp(
+ minAngle,
+ maxAngle,
+ speedFactorRef.current,
+ );
+ }
+
+ // 3. Draw gauge fill -------------------------------------------------------
+ const ctx = fillCanvas.getContext("2d", { alpha: true });
+ if (!ctx) return;
+
+ const size = fillCanvas.width;
+ ctx.clearRect(0, 0, size, size);
+
+ // Default centre: horizontal middle + needle-pivot height.
+ // gaugeOffsetX/Y shift the pivot so the arc aligns with cadran.png.
+ const cx = size * (0.5 + gaugeOffsetX);
+ const cy = size * ((1 - NEEDLE_PIVOT_UV_Y) + gaugeOffsetY); // default ≈ 0.88 × size
+
+ const outerR = size * gaugeOuterR;
+ const innerR = size * gaugeInnerR;
+
+ // Arc sweeps clockwise from π (left) to current needle angle
+ const arcStart = Math.PI;
+ const arcEnd = Math.PI + speedFactorRef.current * Math.PI;
+
+ if (speedFactorRef.current > 0.005) {
+ // Radial gradient using #3F67DD — slightly transparent at inner edge,
+ // fully solid at outer edge for a depth effect.
+ const radial = ctx.createRadialGradient(cx, cy, innerR, cx, cy, outerR);
+ radial.addColorStop(0, "rgba(191, 234, 255, 0)"); // inner edge
+ radial.addColorStop(0.7, "rgba(118, 152, 255, 0.95)"); // outer edge
+
+ // Annular sector shape (outer arc + inner arc reversed)
+ ctx.beginPath();
+ ctx.arc(cx, cy, outerR, arcStart, arcEnd, false);
+ ctx.arc(cx, cy, innerR, arcEnd, arcStart, true);
+ ctx.closePath();
+
+ ctx.fillStyle = radial;
+ ctx.shadowBlur = 16;
+ ctx.shadowColor = "#3F67DD";
+ ctx.fill();
+ ctx.shadowBlur = 0;
+ }
+
+ fillTexture.needsUpdate = true;
+ });
+
+ return (
+
+ {/* Gauge fill — behind the cadran frame (size controlled by gaugeWidth/gaugeHeight) */}
+
+
+
+
+
+ {/* Dial frame (cadran.png) */}
+
+
+
+
+
+ {/* Needle — pivot at bottom-centre of the arc */}
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/data/ebike/ebikeConfig.ts b/src/data/ebike/ebikeConfig.ts
index 577a7e6..d68fb23 100644
--- a/src/data/ebike/ebikeConfig.ts
+++ b/src/data/ebike/ebikeConfig.ts
@@ -6,7 +6,7 @@ export interface CameraTransform {
}
export const EBIKE_CAMERA_TRANSFORM: CameraTransform = {
- position: [-2.6, 4.5, 0],
+ position: [-1, 1, 0],
rotation: [-10, -90, 0],
};
diff --git a/src/world/player/PlayerController.tsx b/src/world/player/PlayerController.tsx
index 563b25f..a42c7f5 100644
--- a/src/world/player/PlayerController.tsx
+++ b/src/world/player/PlayerController.tsx
@@ -512,14 +512,29 @@ export function PlayerController({
);
window.ebikeSteerFactor = steerFactor;
+ // ── Ebike camera tuning ──────────────────────────────────────────────────
+ // All motion effects in one place — set to 0 to fully disable each one.
+ /** Lateral camera drift when steering (0 = no sway) */
+ const CAM_SWAY_SIDE = -0.5;
+ /** Vertical camera drop when steering (0 = no recoil) */
+ const CAM_SWAY_VERTICAL = 0;
+ /** Position lerp factor. 1 = instant snap, lower = more lag/trail */
+ const CAM_POS_LERP = 1;
+ /** FOV boost at full speed in degrees (0 = constant FOV) */
+ const CAM_FOV_BOOST = 0.15; // speed × 0.15, capped at 3° → subtle speed sensation
+ /** How fast FOV lerps toward target (lower = slower breathing) */
+ const CAM_FOV_LERP = 4;
+ /** Visual body lean in radians at max steer (20° = 0.349 rad) */
+ const BIKE_LEAN = THREE.MathUtils.degToRad(10);
+ // ─────────────────────────────────────────────────────────────────────────
+
const speed = velocity.current.length();
- const targetFov = 60 + Math.min(speed * 0.35, 9);
const perspectiveCam = camera as THREE.PerspectiveCamera;
// eslint-disable-next-line react-hooks/immutability -- Three.js camera.fov must be mutated directly for dynamic FOV changes during frame updates
perspectiveCam.fov = THREE.MathUtils.lerp(
perspectiveCam.fov,
- targetFov,
- 6 * dt,
+ 60 + Math.min(speed * CAM_FOV_BOOST, 3),
+ CAM_FOV_LERP * dt,
);
perspectiveCam.updateProjectionMatrix();
@@ -528,9 +543,8 @@ export function PlayerController({
);
cameraOffset.applyAxisAngle(_up, ebikeAngle.current);
- const swingX = -Math.abs(steerFactor) * 1.5;
- const swingZ = steerFactor > 0 ? steerFactor * 2.5 : steerFactor * 1.0;
-
+ const swingX = -Math.abs(steerFactor) * CAM_SWAY_VERTICAL;
+ const swingZ = steerFactor * CAM_SWAY_SIDE;
const cameraSwing = new THREE.Vector3(swingX, 0, swingZ);
cameraSwing.applyAxisAngle(_up, ebikeAngle.current);
cameraOffset.add(cameraSwing);
@@ -539,7 +553,7 @@ export function PlayerController({
.copy(capsule.current.end)
.add(cameraOffset);
- camera.position.lerp(targetCamPos, 12 * dt);
+ camera.position.lerp(targetCamPos, CAM_POS_LERP);
const pitchRad = THREE.MathUtils.degToRad(
EBIKE_CAMERA_TRANSFORM.rotation[0],
@@ -559,8 +573,12 @@ export function PlayerController({
capsule.current.end.y - PLAYER_EYE_HEIGHT,
capsule.current.end.z,
);
- const leanAngle = steerFactor * 0.26;
- ebikeVisual.rotation.set(0, ebikeAngle.current, leanAngle, "YXZ");
+ ebikeVisual.rotation.set(
+ steerFactor * -BIKE_LEAN,
+ ebikeAngle.current,
+ 0,
+ "YXZ",
+ );
}
} else {
camera.position.copy(capsule.current.end);