Compare commits
No commits in common. "types" and "main" have entirely different histories.
1
.gitattributes
vendored
1
.gitattributes
vendored
@ -1 +0,0 @@
|
||||
*.txt linguist-language=Lua
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@ -3,4 +3,4 @@
|
||||
!/koptilnya
|
||||
!README.md
|
||||
!.gitignore
|
||||
!.gitattributes
|
||||
!.lua-format
|
||||
12
.lua-format
Normal file
12
.lua-format
Normal file
@ -0,0 +1,12 @@
|
||||
# Install vs-code extension: koihik.vscode-lua-format
|
||||
# Then set the config file
|
||||
|
||||
column_limit: 280
|
||||
line_breaks_after_function_body: 2
|
||||
keep_simple_control_block_one_line: false
|
||||
keep_simple_function_one_line: false
|
||||
break_after_functioncall_lp: false
|
||||
break_before_functioncall_rp: false
|
||||
use_tab: false
|
||||
continuation_indent_width: 1
|
||||
|
||||
@ -8,274 +8,171 @@ local Differential = require('/koptilnya/engine_remastered/powertrain/differenti
|
||||
|
||||
-- Michelin Pilot Sport 4S (High-Performance Road Tire)
|
||||
local WheelConfig = {
|
||||
DEBUG = false,
|
||||
BrakePower = 1200,
|
||||
CustomWheel = {
|
||||
Mass = 30,
|
||||
LateralFrictionPreset = {
|
||||
B = 12.0,
|
||||
C = 1.3,
|
||||
D = 1.8,
|
||||
E = -1.8,
|
||||
},
|
||||
LongitudinalFrictionPreset = {
|
||||
B = 18.0,
|
||||
C = 1.5,
|
||||
D = 1.5,
|
||||
E = 0.3,
|
||||
},
|
||||
AligningFrictionPreset = {
|
||||
B = 2.8,
|
||||
C = 2.1,
|
||||
D = 0.10,
|
||||
E = -2.5,
|
||||
},
|
||||
-- AligningFrictionPreset = {
|
||||
-- B = 13,
|
||||
-- C = 2.4,
|
||||
-- D = 1,
|
||||
-- E = 0.48
|
||||
-- }
|
||||
},
|
||||
Model = 'models/sprops/trans/wheel_a/wheel25.mdl',
|
||||
}
|
||||
local FrontWheelsConfig = table.merge(table.copy(WheelConfig), {
|
||||
BrakePower = 1600,
|
||||
CustomWheel = {
|
||||
CasterAngle = 7,
|
||||
CamberAngle = -3,
|
||||
},
|
||||
})
|
||||
DEBUG = true,
|
||||
BrakePower = 1200,
|
||||
CustomWheel = {
|
||||
Mass = 30,
|
||||
|
||||
local RearWheelsConfig = table.merge(table.copy(WheelConfig), {
|
||||
BrakePower = 0,
|
||||
HandbrakePower = 2200,
|
||||
CustomWheel = {
|
||||
CamberAngle = -2,
|
||||
LongitudinalFrictionPreset = nil,
|
||||
LateralFrictionPreset = nil,
|
||||
-- LongitudinalFrictionPreset = {
|
||||
-- B = 0.1,
|
||||
-- C = 1,
|
||||
-- D = 0.9,
|
||||
-- E = 0.9,
|
||||
-- },
|
||||
-- LateralFrictionPreset = {
|
||||
-- B = 0.1,
|
||||
-- C = 1,
|
||||
-- D = 0.9,
|
||||
-- E = 0.9,
|
||||
-- },
|
||||
},
|
||||
})
|
||||
LateralFrictionPreset = {
|
||||
B = 12.0,
|
||||
C = 1.3,
|
||||
D = 1.8,
|
||||
E = -1.8
|
||||
},
|
||||
LongitudinalFrictionPreset = {
|
||||
B = 18.0,
|
||||
C = 1.5,
|
||||
D = 1.5,
|
||||
E = 0.3
|
||||
},
|
||||
AligningFrictionPreset = {
|
||||
B = 2.8,
|
||||
C = 2.1,
|
||||
D = 0.10,
|
||||
E = -2.5
|
||||
},
|
||||
-- AligningFrictionPreset = {
|
||||
-- B = 13,
|
||||
-- C = 2.4,
|
||||
-- D = 1,
|
||||
-- E = 0.48
|
||||
-- }
|
||||
},
|
||||
Model = 'models/sprops/trans/wheel_a/wheel25.mdl'
|
||||
}
|
||||
local FrontWheelsConfig = table.merge(
|
||||
table.copy(WheelConfig),
|
||||
{
|
||||
CustomWheel = {
|
||||
CasterAngle = 7,
|
||||
CamberAngle = -3,
|
||||
ToeAngle = 0.5
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
local RearWheelsConfig = table.merge(
|
||||
table.copy(WheelConfig),
|
||||
{
|
||||
HandbrakePower = 2200,
|
||||
CustomWheel = {
|
||||
CamberAngle = -2,
|
||||
ToeAngle = 0.5
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
Vehicle:new({
|
||||
{
|
||||
Name = 'Engine',
|
||||
Type = POWERTRAIN_COMPONENT.Engine,
|
||||
Config = {
|
||||
IdleRPM = 900,
|
||||
MaxRPM = 7000,
|
||||
Inertia = 0.151,
|
||||
StartFriction = -10,
|
||||
FrictionCoeff = 0.01,
|
||||
LimiterDuration = 0.06,
|
||||
TorqueMap = {
|
||||
118.89918444138,
|
||||
122.0751393736,
|
||||
125.25109430583,
|
||||
128.42704923806,
|
||||
131.60300417029,
|
||||
134.77895910251,
|
||||
137.95491403474,
|
||||
141.13086896697,
|
||||
144.3068238992,
|
||||
147.48277883143,
|
||||
150.65873376365,
|
||||
153.83468869588,
|
||||
157.01064362811,
|
||||
160.18659856034,
|
||||
163.36255349256,
|
||||
166.53850842479,
|
||||
169.71446335702,
|
||||
172.89041828925,
|
||||
176.06637322148,
|
||||
179.2423281537,
|
||||
182.41828308593,
|
||||
185.59423801816,
|
||||
188.77019295039,
|
||||
191.94614788261,
|
||||
195.12210281484,
|
||||
198.29805774707,
|
||||
201.4740126793,
|
||||
204.64996761153,
|
||||
207.82592254375,
|
||||
211.00187747598,
|
||||
214.17783240821,
|
||||
217.35378734044,
|
||||
220.52974227266,
|
||||
223.70569720489,
|
||||
226.88165213712,
|
||||
227.9621903219,
|
||||
229.04272850669,
|
||||
230.12326669147,
|
||||
231.20380487625,
|
||||
232.28434306104,
|
||||
233.36488124582,
|
||||
234.4454194306,
|
||||
235.52595761538,
|
||||
236.60649580017,
|
||||
237.68703398495,
|
||||
238.76757216973,
|
||||
239.84811035452,
|
||||
240.9286485393,
|
||||
242.00918672408,
|
||||
243.08972490886,
|
||||
244.17026309365,
|
||||
245.25080127843,
|
||||
246.33133946321,
|
||||
247.411877648,
|
||||
248.49241583278,
|
||||
249.57295401756,
|
||||
250.65349220234,
|
||||
251.73403038713,
|
||||
252.81456857191,
|
||||
253.89510675669,
|
||||
254.97564494148,
|
||||
256.05618312626,
|
||||
257.13672131104,
|
||||
258.21725949582,
|
||||
259.29779768061,
|
||||
260.37833586539,
|
||||
261.29278066787,
|
||||
262.20722547034,
|
||||
263.12167027282,
|
||||
264.0361150753,
|
||||
264.95055987777,
|
||||
265.86500468025,
|
||||
266.77944948273,
|
||||
267.6938942852,
|
||||
268.60833908768,
|
||||
269.52278389016,
|
||||
270.43722869263,
|
||||
271.35167349511,
|
||||
272.26611829758,
|
||||
273.18056310006,
|
||||
274.09500790254,
|
||||
275.00945270501,
|
||||
275.92389750749,
|
||||
276.83834230997,
|
||||
275.50329427594,
|
||||
274.16824624192,
|
||||
272.8331982079,
|
||||
271.49815017388,
|
||||
270.16310213985,
|
||||
268.82805410583,
|
||||
267.49300607181,
|
||||
266.15795803778,
|
||||
264.82291000376,
|
||||
263.48786196974,
|
||||
262.15281393572,
|
||||
260.81776590169,
|
||||
259.48271786767,
|
||||
250.23203287321,
|
||||
240.98134787874,
|
||||
231.73066288428,
|
||||
222.47997788981,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name = 'Clutch',
|
||||
Input = 'Engine',
|
||||
Type = POWERTRAIN_COMPONENT.Clutch,
|
||||
Config = {
|
||||
Inertia = 0.002,
|
||||
SlipTorque = 1000,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name = 'Gearbox',
|
||||
Input = 'Clutch',
|
||||
Type = POWERTRAIN_COMPONENT.Gearbox,
|
||||
Config = {
|
||||
Type = 'MANUAL',
|
||||
ShiftTime = 3,
|
||||
Inertia = 0.01,
|
||||
Ratios = { 3.626, 2.200, 1.541, 1.213, 1.000, 0.767 },
|
||||
Reverse = 3.437,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name = 'AxleFront',
|
||||
Input = 'Axle',
|
||||
Type = POWERTRAIN_COMPONENT.Differential,
|
||||
Config = {
|
||||
Type = Differential.TYPES.Open,
|
||||
FinalDrive = 3.392,
|
||||
Inertia = 0.01,
|
||||
BiasAB = 0.5,
|
||||
CoastRamp = 1,
|
||||
PowerRamp = 1,
|
||||
Stiffness = 0.1,
|
||||
SlipTorque = 0,
|
||||
SteerLock = 50,
|
||||
ToeAngle = 0.5,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name = 'WheelFL',
|
||||
Type = POWERTRAIN_COMPONENT.Wheel,
|
||||
Input = 'AxleFront',
|
||||
Config = table.merge(table.copy(FrontWheelsConfig), { Offset = 0 }),
|
||||
},
|
||||
{
|
||||
Name = 'WheelFR',
|
||||
Type = POWERTRAIN_COMPONENT.Wheel,
|
||||
Input = 'AxleFront',
|
||||
Config = table.merge(table.copy(FrontWheelsConfig), { Offset = 180 }),
|
||||
},
|
||||
{
|
||||
Name = 'AxleBack',
|
||||
Input = 'Axle',
|
||||
Type = POWERTRAIN_COMPONENT.Differential,
|
||||
Config = {
|
||||
Type = Differential.TYPES.Open,
|
||||
FinalDrive = 3.392,
|
||||
Inertia = 0.01,
|
||||
BiasAB = 0.5,
|
||||
CoastRamp = 1,
|
||||
PowerRamp = 1,
|
||||
Stiffness = 1,
|
||||
SlipTorque = 1000,
|
||||
ToeAngle = 0.5,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name = 'Axle',
|
||||
Input = 'Gearbox',
|
||||
Type = POWERTRAIN_COMPONENT.Differential,
|
||||
Config = {
|
||||
Type = Differential.TYPES.Locking,
|
||||
FinalDrive = 1,
|
||||
Inertia = 0.01,
|
||||
BiasAB = 0.5,
|
||||
CoastRamp = 1,
|
||||
PowerRamp = 1,
|
||||
Stiffness = 1,
|
||||
SlipTorque = 1000,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name = 'WheelRL',
|
||||
Input = 'AxleBack',
|
||||
Type = POWERTRAIN_COMPONENT.Wheel,
|
||||
Config = table.merge(table.copy(RearWheelsConfig), { Offset = 0 }),
|
||||
},
|
||||
{
|
||||
Name = 'WheelRR',
|
||||
Input = 'AxleBack',
|
||||
Type = POWERTRAIN_COMPONENT.Wheel,
|
||||
Config = table.merge(table.copy(RearWheelsConfig), { Offset = 180 }),
|
||||
},
|
||||
{
|
||||
Name = 'Engine',
|
||||
Type = POWERTRAIN_COMPONENT.Engine,
|
||||
Config = {
|
||||
IdleRPM = 900,
|
||||
MaxRPM = 7000,
|
||||
Inertia = 0.151,
|
||||
StartFriction = -10,
|
||||
FrictionCoeff = 0.01,
|
||||
LimiterDuration = 0.06,
|
||||
TorqueMap = {
|
||||
118.89918444138, 122.0751393736, 125.25109430583, 128.42704923806, 131.60300417029, 134.77895910251, 137.95491403474, 141.13086896697, 144.3068238992, 147.48277883143, 150.65873376365, 153.83468869588, 157.01064362811, 160.18659856034, 163.36255349256,
|
||||
166.53850842479, 169.71446335702, 172.89041828925, 176.06637322148, 179.2423281537, 182.41828308593, 185.59423801816, 188.77019295039, 191.94614788261, 195.12210281484, 198.29805774707, 201.4740126793, 204.64996761153, 207.82592254375, 211.00187747598,
|
||||
214.17783240821, 217.35378734044, 220.52974227266, 223.70569720489, 226.88165213712, 227.9621903219, 229.04272850669, 230.12326669147, 231.20380487625, 232.28434306104, 233.36488124582, 234.4454194306, 235.52595761538, 236.60649580017, 237.68703398495,
|
||||
238.76757216973, 239.84811035452, 240.9286485393, 242.00918672408, 243.08972490886, 244.17026309365, 245.25080127843, 246.33133946321, 247.411877648, 248.49241583278, 249.57295401756, 250.65349220234, 251.73403038713, 252.81456857191, 253.89510675669, 254.97564494148,
|
||||
256.05618312626, 257.13672131104, 258.21725949582, 259.29779768061, 260.37833586539, 261.29278066787, 262.20722547034, 263.12167027282, 264.0361150753, 264.95055987777, 265.86500468025, 266.77944948273, 267.6938942852, 268.60833908768, 269.52278389016,
|
||||
270.43722869263, 271.35167349511, 272.26611829758, 273.18056310006, 274.09500790254, 275.00945270501, 275.92389750749, 276.83834230997, 275.50329427594, 274.16824624192, 272.8331982079, 271.49815017388, 270.16310213985, 268.82805410583, 267.49300607181,
|
||||
266.15795803778, 264.82291000376, 263.48786196974, 262.15281393572, 260.81776590169, 259.48271786767, 250.23203287321, 240.98134787874, 231.73066288428, 222.47997788981
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
Name = 'Clutch',
|
||||
Input = 'Engine',
|
||||
Type = POWERTRAIN_COMPONENT.Clutch,
|
||||
Config = {
|
||||
Inertia = 0.002,
|
||||
SlipTorque = 1000
|
||||
}
|
||||
},
|
||||
{
|
||||
Name = 'Gearbox',
|
||||
Input = 'Clutch',
|
||||
Type = POWERTRAIN_COMPONENT.Gearbox,
|
||||
Config = {
|
||||
Type = 'MANUAL',
|
||||
ShiftTime = 3,
|
||||
Inertia = 0.01,
|
||||
Ratios = { 3.626, 2.200, 1.541, 1.213, 1.000, 0.767 },
|
||||
Reverse = 3.437
|
||||
}
|
||||
},
|
||||
{
|
||||
Name = 'AxleFront',
|
||||
Input = 'Axle',
|
||||
Type = POWERTRAIN_COMPONENT.Differential,
|
||||
Config = {
|
||||
Type = Differential.TYPES.Open,
|
||||
FinalDrive = 3.392,
|
||||
Inertia = 0.01,
|
||||
BiasAB = 0.5,
|
||||
CoastRamp = 1,
|
||||
PowerRamp = 1,
|
||||
Stiffness = 0.1,
|
||||
SlipTorque = 0,
|
||||
SteerLock = 45
|
||||
}
|
||||
},
|
||||
{
|
||||
Name = 'WheelFL',
|
||||
Type = POWERTRAIN_COMPONENT.Wheel,
|
||||
Input = 'AxleFront',
|
||||
Config = table.merge(table.copy(FrontWheelsConfig), { Offset = 0 })
|
||||
},
|
||||
{
|
||||
Name = 'WheelFR',
|
||||
Type = POWERTRAIN_COMPONENT.Wheel,
|
||||
Input = 'AxleFront',
|
||||
Config = table.merge(table.copy(FrontWheelsConfig), { Offset = 180 })
|
||||
},
|
||||
{
|
||||
Name = 'AxleBack',
|
||||
Input = 'Axle',
|
||||
Type = POWERTRAIN_COMPONENT.Differential,
|
||||
Config = {
|
||||
Type = Differential.TYPES.Open,
|
||||
FinalDrive = 3.392,
|
||||
Inertia = 0.01,
|
||||
BiasAB = 0.5,
|
||||
CoastRamp = 1,
|
||||
PowerRamp = 1,
|
||||
Stiffness = 0.1,
|
||||
SlipTorque = 0,
|
||||
}
|
||||
},
|
||||
{
|
||||
Name = 'Axle',
|
||||
Input = 'Gearbox',
|
||||
Type = POWERTRAIN_COMPONENT.Differential,
|
||||
Config = {
|
||||
Type = Differential.TYPES.Open,
|
||||
FinalDrive = 1,
|
||||
Inertia = 0.01,
|
||||
BiasAB = 0.5,
|
||||
CoastRamp = 1,
|
||||
PowerRamp = 1,
|
||||
Stiffness = 1,
|
||||
SlipTorque = 0
|
||||
}
|
||||
},
|
||||
{
|
||||
Name = 'WheelRL',
|
||||
Input = 'AxleBack',
|
||||
Type = POWERTRAIN_COMPONENT.Wheel,
|
||||
Config = table.merge(table.copy(RearWheelsConfig), { Offset = 0 })
|
||||
},
|
||||
{
|
||||
Name = 'WheelRR',
|
||||
Input = 'AxleBack',
|
||||
Type = POWERTRAIN_COMPONENT.Wheel,
|
||||
Config = table.merge(table.copy(RearWheelsConfig), { Offset = 180 })
|
||||
}
|
||||
})
|
||||
|
||||
@ -1,83 +1,85 @@
|
||||
--@client
|
||||
local fontArial92 = render.createFont('Arial', 92, 250, true, false, false, false, 0, false, 0)
|
||||
local fontArial46 = render.createFont('Arial', 46, 250, true, false, false, false, 0, false, 0)
|
||||
ENGINE_RPM, CAR_SPEED, GEARBOX_GEAR = 0, 'SPD', 0
|
||||
local fontArial92 = render.createFont("Arial", 92, 250, true, false, false, false, 0, false, 0)
|
||||
local fontArial46 = render.createFont("Arial", 46, 250, true, false, false, false, 0, false, 0)
|
||||
ENGINE_RPM, CAR_SPEED, GEARBOX_GEAR = 0, 123, 0
|
||||
local resx, resy = render.getGameResolution()
|
||||
local linesmatrix = Matrix()
|
||||
local linesposx, linesposy = resx - 200, resy - 150
|
||||
linesmatrix:setTranslation(Vector(linesposx, linesposy, 0))
|
||||
linesmatrix:setAngles(Angle(0, 15, 0))
|
||||
linesmatrix:setScale(Vector(0.7))
|
||||
hook.add('DrawHud', 'CARHUD', function()
|
||||
render.pushMatrix(linesmatrix)
|
||||
render.enableScissorRect(linesposx - 50, linesposy + 30, linesposx + 208, linesposy + 50)
|
||||
for x = 0, 208, 4 do
|
||||
if ENGINE_RPM > x / 220 then
|
||||
col = 55 + x / 220 * 200
|
||||
render.setRGBA(col, col, col, 250)
|
||||
else
|
||||
render.setRGBA(100, 100, 100, 250)
|
||||
end
|
||||
hook.add("DrawHud", "CARHUD", function()
|
||||
render.pushMatrix(linesmatrix)
|
||||
render.enableScissorRect(linesposx - 50, linesposy + 30, linesposx + 208, linesposy + 50)
|
||||
for x = 0, 208, 4 do
|
||||
|
||||
render.drawRectFast(x, -x / 2, 2, 150)
|
||||
end
|
||||
render.disableScissorRect()
|
||||
render.enableScissorRect(linesposx - 50, linesposy + 20, linesposx + 212, linesposy + 50)
|
||||
for x = 212, 220, 4 do
|
||||
if ENGINE_RPM > x / 220 then
|
||||
render.setRGBA(200, 71, 71, 200)
|
||||
else
|
||||
render.setRGBA(100, 100, 100, 200)
|
||||
end
|
||||
if ENGINE_RPM > x / 220 then
|
||||
col = 55 + x / 220 * 200
|
||||
render.setRGBA(col, col, col, 250)
|
||||
else
|
||||
render.setRGBA(100, 100, 100, 250)
|
||||
end
|
||||
|
||||
render.drawRectFast(x, -x / 2, 2, 150)
|
||||
end
|
||||
render.disableScissorRect()
|
||||
render.enableScissorRect(linesposx - 50, linesposy + 20, linesposx + 208, linesposy + 25)
|
||||
render.drawRectFast(0, -256, 208, 512)
|
||||
render.disableScissorRect()
|
||||
render.popMatrix()
|
||||
render.setRGBA(151, 151, 151, 220)
|
||||
render.drawRectFast(x, -x / 2, 2, 150)
|
||||
end
|
||||
render.disableScissorRect()
|
||||
render.enableScissorRect(linesposx - 50, linesposy + 20, linesposx + 212, linesposy + 50)
|
||||
for x = 212, 220, 4 do
|
||||
|
||||
render.setFont(fontArial92)
|
||||
local str = string.rep('0', 3 - #tostring(CAR_SPEED)) .. CAR_SPEED
|
||||
render.setRGBA(51, 51, 51, 256)
|
||||
if ENGINE_RPM > x / 220 then
|
||||
render.setRGBA(200, 71, 71, 200)
|
||||
else
|
||||
render.setRGBA(100, 100, 100, 200)
|
||||
end
|
||||
|
||||
for k = 1, 3 do
|
||||
local num = string.sub(str, k, k)
|
||||
if num ~= '0' then
|
||||
render.setRGBA(255, 255, 255, 250)
|
||||
end
|
||||
render.drawText(linesposx - 60 + k * 46, resy - 130 - 80, num)
|
||||
end
|
||||
render.setFont(fontArial46)
|
||||
render.setRGBA(200, 51, 51, 256)
|
||||
local t = 'N'
|
||||
if GEARBOX_GEAR == -1 then
|
||||
t = 'R'
|
||||
elseif GEARBOX_GEAR == 0 then
|
||||
t = 'N'
|
||||
else
|
||||
t = GEARBOX_GEAR
|
||||
end
|
||||
render.drawText(linesposx - 35 + 164, resy - 130 - 40, t)
|
||||
render.drawRectFast(x, -x / 2, 2, 150)
|
||||
end
|
||||
render.disableScissorRect()
|
||||
render.enableScissorRect(linesposx - 50, linesposy + 20, linesposx + 208, linesposy + 25)
|
||||
render.drawRectFast(0, -256, 208, 512)
|
||||
render.disableScissorRect()
|
||||
render.popMatrix()
|
||||
render.setRGBA(151, 151, 151, 220)
|
||||
|
||||
render.setFont(fontArial92)
|
||||
local str = string.rep("0", 3 - #tostring(CAR_SPEED)) .. CAR_SPEED
|
||||
render.setRGBA(51, 51, 51, 256)
|
||||
|
||||
for k = 1, 3 do
|
||||
local num = string.sub(str, k, k)
|
||||
if num ~= "0" then
|
||||
render.setRGBA(255, 255, 255, 250)
|
||||
end
|
||||
render.drawText(linesposx - 60 + k * 46, resy - 130 - 80, num)
|
||||
end
|
||||
render.setFont(fontArial46)
|
||||
render.setRGBA(200, 51, 51, 256)
|
||||
local t = "N"
|
||||
if GEARBOX_GEAR == -1 then
|
||||
t = "R"
|
||||
elseif GEARBOX_GEAR == 0 then
|
||||
t = "N"
|
||||
else
|
||||
t = GEARBOX_GEAR
|
||||
end
|
||||
render.drawText(linesposx - 35 + 164, resy - 130 - 40, t)
|
||||
end)
|
||||
|
||||
net.receive('ENGINE_RPM', function()
|
||||
local rpm = net.readFloat()
|
||||
if rpm then
|
||||
ENGINE_RPM = rpm
|
||||
end
|
||||
net.receive("ENGINE_RPM", function()
|
||||
local rpm = net.readFloat()
|
||||
if rpm then
|
||||
ENGINE_RPM = rpm
|
||||
end
|
||||
end)
|
||||
net.receive('CAR_SPEED', function()
|
||||
local speed = net.readUInt(12)
|
||||
if speed then
|
||||
CAR_SPEED = math.clamp(speed, 0, 999)
|
||||
end
|
||||
end)
|
||||
net.receive('GEARBOX_GEAR', function()
|
||||
local gear = net.readInt(5)
|
||||
if gear then
|
||||
GEARBOX_GEAR = gear
|
||||
end
|
||||
net.receive("CAR_SPEED", function()
|
||||
local speed = net.readUInt(12)
|
||||
if speed then
|
||||
CAR_SPEED = math.clamp(speed, 0, 999)
|
||||
end
|
||||
end)
|
||||
net.receive("GEARBOX_GEAR", function()
|
||||
local gear = net.readInt(5)
|
||||
if gear then
|
||||
GEARBOX_GEAR = gear
|
||||
end
|
||||
end)
|
||||
@ -1,95 +1,72 @@
|
||||
--@include ./powertrain_component.txt
|
||||
--@include /koptilnya/libs/constants.txt
|
||||
|
||||
---@type PowertrainComponent
|
||||
local PowertrainComponent = require('./powertrain_component.txt')
|
||||
|
||||
require('/koptilnya/libs/constants.txt')
|
||||
|
||||
---@class ClutchConfig: PowertrainComponentConfig
|
||||
---@field SlipTorque? number
|
||||
|
||||
---@class Clutch: PowertrainComponent
|
||||
---@field slipTorque number
|
||||
local Clutch = class('Clutch', PowertrainComponent)
|
||||
|
||||
---@private
|
||||
---@param vehicle KPTLVehicle
|
||||
---@param name string
|
||||
---@param config ClutchConfig
|
||||
function Clutch:initialize(vehicle, name, config)
|
||||
PowertrainComponent.initialize(self, vehicle, name, config)
|
||||
PowertrainComponent.initialize(self, vehicle, name, config)
|
||||
|
||||
if CLIENT then
|
||||
return
|
||||
end
|
||||
if CLIENT then return end
|
||||
|
||||
self.wireInputs = {
|
||||
Clutch = 'number',
|
||||
}
|
||||
self.wireOutputs = {
|
||||
Clutch_Torque = 'number',
|
||||
}
|
||||
self.wireInputs = {
|
||||
Clutch = 'number'
|
||||
}
|
||||
self.wireOutputs = {
|
||||
Clutch_Torque = 'number'
|
||||
}
|
||||
|
||||
self.inertia = config.Inertia or 0.1
|
||||
self.slipTorque = config.SlipTorque or 1000
|
||||
self.inertia = config.Inertia or 0.1
|
||||
self.slipTorque = config.SlipTorque or 1000
|
||||
end
|
||||
|
||||
---@return nil
|
||||
function Clutch:updateWireOutputs()
|
||||
PowertrainComponent.updateWireOutputs(self)
|
||||
|
||||
wire.ports.Clutch_Torque = self.torque
|
||||
PowertrainComponent.updateWireOutputs(self)
|
||||
|
||||
wire.ports.Clutch_Torque = self.torque
|
||||
end
|
||||
|
||||
---@return number
|
||||
function Clutch:getPress()
|
||||
return 1 - wire.ports.Clutch
|
||||
return 1 - wire.ports.Clutch
|
||||
end
|
||||
|
||||
---@return number
|
||||
function Clutch:queryInertia()
|
||||
if self.output == nil then
|
||||
return self.inertia
|
||||
end
|
||||
if self.output == nil then
|
||||
return self.inertia
|
||||
end
|
||||
|
||||
return self.inertia + self.output:queryInertia() * self:getPress()
|
||||
return self.inertia + self.output:queryInertia() * self:getPress()
|
||||
end
|
||||
|
||||
---@param angularVelocity number
|
||||
---@return number
|
||||
function Clutch:queryAngularVelocity(angularVelocity)
|
||||
self.angularVelocity = angularVelocity
|
||||
self.angularVelocity = angularVelocity
|
||||
|
||||
if self.output == nil then
|
||||
return angularVelocity
|
||||
end
|
||||
if self.output == nil then
|
||||
return angularVelocity
|
||||
end
|
||||
|
||||
local outputW = self.output:queryAngularVelocity(angularVelocity) * self:getPress()
|
||||
local inputW = angularVelocity * (1 - self:getPress())
|
||||
local outputW = self.output:queryAngularVelocity(angularVelocity) * self:getPress()
|
||||
local inputW = angularVelocity * (1 - self:getPress())
|
||||
|
||||
return outputW + inputW
|
||||
return outputW + inputW
|
||||
end
|
||||
|
||||
---@param torque number
|
||||
---@param inertia number
|
||||
---@return number
|
||||
function Clutch:forwardStep(torque, inertia)
|
||||
if self.output == nil then
|
||||
return torque
|
||||
end
|
||||
if self.output == nil then
|
||||
return torque
|
||||
end
|
||||
|
||||
local press = self:getPress()
|
||||
local press = self:getPress()
|
||||
|
||||
self.torque = math.clamp(torque, -self.slipTorque, self.slipTorque)
|
||||
self.torque = self.torque * (1 - (1 - math.pow(press, 0.3)))
|
||||
self.torque = math.clamp(torque, -self.slipTorque, self.slipTorque)
|
||||
self.torque = self.torque * (1 - (1 - math.pow(press, 0.3)))
|
||||
|
||||
local returnTorque = self.output:forwardStep(self.torque, inertia * press + self.inertia) * press
|
||||
local returnTorque = self.output:forwardStep(self.torque, inertia * press + self.inertia) * press
|
||||
|
||||
return math.clamp(returnTorque, -self.slipTorque, self.slipTorque)
|
||||
return math.clamp(returnTorque, -self.slipTorque, self.slipTorque)
|
||||
end
|
||||
|
||||
---@type fun(vehicle: KPTLVehicle, name?: string, config?: ClutchConfig): Clutch
|
||||
Clutch.new = Clutch.new
|
||||
|
||||
return Clutch
|
||||
|
||||
@ -1,352 +1,237 @@
|
||||
--@include /koptilnya/libs/constants.txt
|
||||
--@include ./powertrain_component.txt
|
||||
|
||||
---@type PowertrainComponent
|
||||
local PowertrainComponent = require('./powertrain_component.txt')
|
||||
local WheelComponent = require('./wheel.txt')
|
||||
|
||||
require('/koptilnya/libs/constants.txt')
|
||||
|
||||
---@alias DifferentialSplitStrategyFn fun(tq: number, aW: number, bW: number, aI: number, bI: number, biasAB: number, preload: number, stiffness: number, powerRamp: number, coastRamp: number, slipTorque: number): number, number
|
||||
|
||||
---@class DifferentialConfig: PowertrainComponentConfig
|
||||
---@field Type? string
|
||||
---@field FinalDrive? number
|
||||
---@field Bias? number
|
||||
---@field CoastRamp? number
|
||||
---@field PowerRamp? number
|
||||
---@field Preload? number
|
||||
---@field Stiffness? number
|
||||
---@field SlipTorque? number
|
||||
---@field SteerLock? number
|
||||
---@field ToeAngle? number
|
||||
|
||||
---@class Differential: PowertrainComponent
|
||||
---@field finalDrive number
|
||||
---@field biasAB number
|
||||
---@field coastRamp number
|
||||
---@field powerRamp number
|
||||
---@field preload number
|
||||
---@field stiffness number
|
||||
---@field slipTorque number
|
||||
---@field private splitStrategy DifferentialSplitStrategyFn
|
||||
---@field steerLock number
|
||||
---@field toeAngle number
|
||||
---@field steerAngle number
|
||||
---@field private mzDiff number
|
||||
local Differential = class('Differential', PowertrainComponent)
|
||||
|
||||
---@private
|
||||
---@param vehicle KPTLVehicle
|
||||
---@param name string
|
||||
---@param config DifferentialConfig
|
||||
function Differential.getSplitStrategy(type)
|
||||
return SPLIT_STRATEGIES[type] or SPLIT_STRATEGIES[Differential.TYPES.Open]
|
||||
end
|
||||
|
||||
function Differential:initialize(vehicle, name, config)
|
||||
PowertrainComponent.initialize(self, vehicle, name, config)
|
||||
PowertrainComponent.initialize(self, vehicle, name, config)
|
||||
|
||||
if CLIENT then
|
||||
return
|
||||
end
|
||||
if CLIENT then return end
|
||||
|
||||
self.wireOutputs = {
|
||||
[string.format('%s_Torque', self.name)] = 'number',
|
||||
[string.format('%s_RPMDiff', self.name)] = 'number',
|
||||
}
|
||||
self.wireOutputs = {
|
||||
[string.format('%s_Torque', self.name)] = 'number'
|
||||
}
|
||||
|
||||
self.finalDrive = config.FinalDrive or 4
|
||||
self.inertia = config.Inertia or 0.2
|
||||
self.biasAB = config.Bias or 0.5
|
||||
self.coastRamp = config.CoastRamp or 0.5
|
||||
self.powerRamp = config.PowerRamp or 1
|
||||
self.preload = config.Preload or 10
|
||||
self.stiffness = config.Stiffness or 0.1
|
||||
self.slipTorque = config.SlipTorque or 1000
|
||||
self.splitStrategy = Differential.getSplitStrategy(config.Type or DIFFERENTIAL_TYPES.Open)
|
||||
self.finalDrive = config.FinalDrive or 4
|
||||
self.inertia = config.Inertia or 0.2
|
||||
self.biasAB = config.Bias or 0.5
|
||||
self.coastRamp = config.CoastRamp or 0.5
|
||||
self.powerRamp = config.PowerRamp or 1
|
||||
self.preload = config.Preload or 10
|
||||
self.stiffness = config.Stiffness or 0.1
|
||||
self.slipTorque = config.SlipTorque or 1000
|
||||
self.splitStrategy = Differential.getSplitStrategy(config.Type or Differential.TYPES.Open)
|
||||
|
||||
self.steerLock = config.SteerLock or 0
|
||||
self.toeAngle = config.ToeAngle or 0
|
||||
self.steerAngle = 0
|
||||
|
||||
self.mzDiff = 0
|
||||
self.rpmDiff = 0
|
||||
self.steerLock = config.SteerLock or 0
|
||||
self.steerAngle = 0
|
||||
end
|
||||
|
||||
---@param component PowertrainComponent
|
||||
---@return nil
|
||||
function Differential:linkComponent(component)
|
||||
---@diagnostic disable-next-line: undefined-field
|
||||
if not component:isInstanceOf(PowertrainComponent) then
|
||||
return
|
||||
end
|
||||
if not component:isInstanceOf(PowertrainComponent) then
|
||||
return
|
||||
end
|
||||
|
||||
if self.outputA == nil then
|
||||
self.outputA = component
|
||||
component.input = self
|
||||
elseif self.outputB == nil then
|
||||
self.outputB = component
|
||||
component.input = self
|
||||
end
|
||||
if self.outputA == nil then
|
||||
self.outputA = component
|
||||
component.input = self
|
||||
elseif self.outputB == nil then
|
||||
self.outputB = component
|
||||
component.input = self
|
||||
end
|
||||
end
|
||||
|
||||
---@return nil
|
||||
function Differential:updateWireOutputs()
|
||||
PowertrainComponent.updateWireOutputs(self)
|
||||
PowertrainComponent.updateWireOutputs(self)
|
||||
|
||||
wire.ports[string.format('%s_Torque', self.name)] = self.torque
|
||||
|
||||
wire.ports[string.format('%s_Torque', self.name)] = self.torque
|
||||
wire.ports[string.format('%s_RPMDiff', self.name)] = self.rpmDiff
|
||||
if self.outputA ~= nil then
|
||||
self.outputA:updateWireOutputs()
|
||||
end
|
||||
|
||||
if self.outputA ~= nil then
|
||||
self.outputA:updateWireOutputs()
|
||||
end
|
||||
|
||||
if self.outputB ~= nil then
|
||||
self.outputB:updateWireOutputs()
|
||||
end
|
||||
if self.outputB ~= nil then
|
||||
self.outputB:updateWireOutputs()
|
||||
end
|
||||
end
|
||||
|
||||
---@return number
|
||||
function Differential:queryInertia()
|
||||
if self.outputA == nil or self.outputB == nil then
|
||||
return self.inertia
|
||||
end
|
||||
if self.outputA == nil or self.outputB == nil then
|
||||
return self.inertia
|
||||
end
|
||||
|
||||
return self.inertia + (self.outputA:queryInertia() + self.outputB:queryInertia()) / math.pow(self.finalDrive, 2)
|
||||
return self.inertia + (self.outputA:queryInertia() + self.outputB:queryInertia()) / math.pow(self.finalDrive, 2)
|
||||
end
|
||||
|
||||
---@return number
|
||||
function Differential:queryAngularVelocity(angularVelocity)
|
||||
self.angularVelocity = angularVelocity
|
||||
self.angularVelocity = angularVelocity
|
||||
|
||||
if self.outputA == nil or self.outputB == nil then
|
||||
return angularVelocity
|
||||
end
|
||||
if self.outputA == nil or self.outputB == nil then
|
||||
return angularVelocity
|
||||
end
|
||||
|
||||
local aW = self.outputA:queryAngularVelocity(angularVelocity)
|
||||
local bW = self.outputB:queryAngularVelocity(angularVelocity)
|
||||
local aW = self.outputA:queryAngularVelocity(angularVelocity)
|
||||
local bW = self.outputB:queryAngularVelocity(angularVelocity)
|
||||
|
||||
return (aW + bW) * self.finalDrive * 0.5
|
||||
return (aW + bW) * self.finalDrive * 0.5
|
||||
end
|
||||
|
||||
---@param torque number
|
||||
---@param inertia number
|
||||
---@return number
|
||||
function Differential:forwardStep(torque, inertia)
|
||||
if self.outputA == nil or self.outputB == nil then
|
||||
return torque
|
||||
end
|
||||
if self.outputA == nil or self.outputB == nil then
|
||||
return torque
|
||||
end
|
||||
|
||||
local aW = self.outputA:queryAngularVelocity(self.angularVelocity)
|
||||
local bW = self.outputB:queryAngularVelocity(self.angularVelocity)
|
||||
local aI = self.outputA:queryInertia()
|
||||
local bI = self.outputB:queryInertia()
|
||||
local aW = self.outputA:queryAngularVelocity(self.angularVelocity)
|
||||
local bW = self.outputB:queryAngularVelocity(self.angularVelocity)
|
||||
local aI = self.outputA:queryInertia()
|
||||
local bI = self.outputB:queryInertia()
|
||||
|
||||
self.torque = torque * self.finalDrive
|
||||
self.torque = torque * self.finalDrive
|
||||
|
||||
local tqA, tqB = self.splitStrategy(
|
||||
self.torque,
|
||||
aW,
|
||||
bW,
|
||||
aI,
|
||||
bI,
|
||||
self.biasAB,
|
||||
self.preload,
|
||||
self.stiffness,
|
||||
self.powerRamp,
|
||||
self.coastRamp,
|
||||
self.slipTorque
|
||||
)
|
||||
local tqA, tqB = self:splitStrategy(
|
||||
self.torque,
|
||||
aW,
|
||||
bW,
|
||||
aI,
|
||||
bI,
|
||||
self.biasAB,
|
||||
self.preload,
|
||||
self.stiffness,
|
||||
self.powerRamp,
|
||||
self.coastRamp,
|
||||
self.slipTorque
|
||||
)
|
||||
|
||||
tqA = self.outputA:forwardStep(tqA, inertia * 0.5 * math.pow(self.finalDrive, 2) + aI)
|
||||
tqB = self.outputB:forwardStep(tqB, inertia * 0.5 * math.pow(self.finalDrive, 2) + bI)
|
||||
tqA = self.outputA:forwardStep(tqA, inertia * 0.5 * math.pow(self.finalDrive, 2) + aI)
|
||||
tqB = self.outputB:forwardStep(tqB, inertia * 0.5 * math.pow(self.finalDrive, 2) + bI)
|
||||
|
||||
if self.CONFIG.Type == DIFFERENTIAL_TYPES.Locking then
|
||||
local avg = (self.outputA.angularVelocity + self.outputB.angularVelocity) / 2
|
||||
-- // REFACTOR
|
||||
if self.steerLock ~= 0 then
|
||||
local steerInertia = (aI + bI) / 2
|
||||
local driverInput = 5
|
||||
|
||||
self.outputA:queryAngularVelocity(avg)
|
||||
self.outputB:queryAngularVelocity(avg)
|
||||
end
|
||||
local localVelocityLength = chip():getVelocity():getLength()
|
||||
local MPH = localVelocityLength * (15 / 352)
|
||||
local KPH = MPH * 1.609
|
||||
|
||||
self.rpmDiff = self.outputA:getRPM() - self.outputB:getRPM()
|
||||
local assist = math.clamp(10.0 / math.sqrt(KPH / 3), 2.0, 10.0)
|
||||
local inputForce = driverInput * assist
|
||||
|
||||
-- // REFACTOR
|
||||
if self.outputA.customWheel ~= nil and self.outputB.customWheel ~= nil then
|
||||
if self.steerLock ~= 0 then
|
||||
local outputA = self.outputA --[[@as Wheel]]
|
||||
local outputB = self.outputB --[[@as Wheel]]
|
||||
local maxSteerSpeed = math.rad(1337)
|
||||
|
||||
local steerInertia = (aI + bI)
|
||||
local driverInput = 3
|
||||
local inputTorque = self.vehicle.steer * inputForce
|
||||
|
||||
local localVelocityLength = chip():getVelocity():getLength()
|
||||
local MPH = localVelocityLength * (15 / 352)
|
||||
local KPH = MPH * 1.609
|
||||
local avgSteerAngle = (self.outputA.customWheel.steerAngle + self.outputB.customWheel.steerAngle) / 2
|
||||
local mz = self.outputA.customWheel.mz + self.outputB.customWheel.mz
|
||||
local mzDiff = self.outputA.customWheel.mz - self.outputB.customWheel.mz
|
||||
local avgMz = (self.outputA.customWheel.mz + self.outputB.customWheel.mz) / 2
|
||||
local maxMz = math.max(self.outputA.customWheel.mz + self.outputB.customWheel.mz)
|
||||
local minMz = math.min(self.outputA.customWheel.mz + self.outputB.customWheel.mz)
|
||||
|
||||
local assist = math.clamp(10.0 / math.sqrt(KPH / 3), 2.0, 10.0) / 2
|
||||
local steerTorque = mzDiff / 2 * -1 + inputTorque
|
||||
|
||||
local inputForce = driverInput * assist
|
||||
local steerAngularAccel = steerTorque / steerInertia
|
||||
|
||||
local inputTorque = self.vehicle.steer * inputForce
|
||||
self.steerAngle = math.clamp(
|
||||
self.steerAngle + steerAngularAccel * TICK_INTERVAL,
|
||||
-self.steerLock,
|
||||
self.steerLock
|
||||
)
|
||||
|
||||
local mzDiff = (outputB.customWheel.mz - outputA.customWheel.mz) * TICK_INTERVAL / 2
|
||||
local wheelbase = 2.05
|
||||
local trackWidth = 1.124
|
||||
local radius = wheelbase / math.tan(math.rad(self.steerAngle))
|
||||
|
||||
self.mzDiff = math.lerp(0.1, self.mzDiff, mzDiff)
|
||||
local innerAngle = math.deg(math.atan(wheelbase / (radius - (trackWidth / 2))))
|
||||
local outerAngle = math.deg(math.atan(wheelbase / (radius + (trackWidth / 2))))
|
||||
|
||||
local steerTorque = self.mzDiff * 1 + inputTorque
|
||||
self.outputA.customWheel.steerAngle = outerAngle
|
||||
self.outputB.customWheel.steerAngle = innerAngle
|
||||
end
|
||||
|
||||
local steerAngularAccel = steerTorque / steerInertia
|
||||
|
||||
self.steerAngle = math.clamp(self.steerAngle + math.deg(steerAngularAccel) * TICK_INTERVAL, -self.steerLock, self.steerLock)
|
||||
|
||||
local wheelbase = 2.05
|
||||
local trackWidth = 1.124
|
||||
local radius = wheelbase / math.tan(math.rad(self.steerAngle))
|
||||
|
||||
local innerAngle = math.deg(math.atan(wheelbase / (radius - (trackWidth / 2))))
|
||||
local outerAngle = math.deg(math.atan(wheelbase / (radius + (trackWidth / 2))))
|
||||
|
||||
outputA.customWheel.steerAngle = outerAngle + self.toeAngle * outputA.customWheel.direction
|
||||
outputB.customWheel.steerAngle = innerAngle + self.toeAngle * outputB.customWheel.direction
|
||||
else
|
||||
local outputA = self.outputA --[[@as Wheel]]
|
||||
local outputB = self.outputB --[[@as Wheel]]
|
||||
|
||||
outputA.customWheel.steerAngle = self.toeAngle * outputA.customWheel.direction
|
||||
outputB.customWheel.steerAngle = self.toeAngle * outputB.customWheel.direction
|
||||
end
|
||||
end
|
||||
|
||||
return tqA + tqB
|
||||
return tqA + tqB
|
||||
end
|
||||
|
||||
---@return number, number
|
||||
local function _openDiffTorqueSplit(tq, aW, bW, aI, bI, biasAB, preload, stiffness, powerRamp, coastRamp, slipTorque)
|
||||
return tq * (1 - biasAB), tq * biasAB
|
||||
function Differential:_openDiffTorqueSplit(tq, aW, bW, aI, bI, biasAB, preload, stiffness, powerRamp, coastRamp, slipTorque)
|
||||
return tq * (1 - biasAB), tq * biasAB;
|
||||
end
|
||||
|
||||
---@return number, number
|
||||
local function _lockingDiffTorqueSplit(tq, aW, bW, aI, bI, biasAB, preload, stiffness, powerRamp, coastRamp, slipTorque)
|
||||
return tq * 0.5, tq * 0.5
|
||||
-- -- Разность скоростей между выходами
|
||||
-- local deltaW = aW - bW
|
||||
function Differential:_lockingDiffTorqueSplit(tq, aW, bW, aI, bI, biasAB, preload, stiffness, powerRamp, coastRamp, slipTorque)
|
||||
Ta = tq * (1 - biasAB);
|
||||
Tb = tq * biasAB;
|
||||
|
||||
-- -- Расчет "проскальзывающего" момента между сторонами
|
||||
-- local slip = math.abs(deltaW)
|
||||
local syncTorque = (aW - bW) * stiffness * (aI + bI) * 0.5 / TICK_INTERVAL;
|
||||
|
||||
-- -- Применяем преднагрузку и жесткость, чтобы определить базовый момент блокировки
|
||||
-- local lockingTorque = preload + stiffness * deltaW
|
||||
Ta = Ta - syncTorque;
|
||||
Tb = Tb + syncTorque;
|
||||
|
||||
-- -- Определение направления нагрузки: ускорение или торможение
|
||||
-- local ramp = tq >= 0 and powerRamp or coastRamp
|
||||
return Ta, Tb
|
||||
-- local sumI = aI + bI
|
||||
-- local w = aI / sumI * aW + bI / sumI * bW
|
||||
-- local aTqCorr = (w - aW) * aI -- / dt
|
||||
-- aTqCorr = aTqCorr * stiffness
|
||||
|
||||
-- -- Применяем ramp к разности скоростей, усиливая/ослабляя блокировку
|
||||
-- local rampTorque = slip * ramp
|
||||
-- local bTqCorr = (w - bW) * bI -- / dt
|
||||
-- bTqCorr = bTqCorr * stiffness
|
||||
|
||||
-- -- Итоговый момент передачи между сторонами
|
||||
-- local transferTorque = lockingTorque + rampTorque
|
||||
-- local biasA = math.clamp(0.5 + (bW - aW) * 0.1 * stiffness, 0, 1)
|
||||
|
||||
-- -- Ограничиваем передачу момента порогом проскальзывания
|
||||
-- if transferTorque > slipTorque then
|
||||
-- transferTorque = slipTorque
|
||||
-- end
|
||||
|
||||
-- -- Учитываем биас (смещение баланса)
|
||||
-- local maxBias = biasAB
|
||||
-- local biasLimit = tq * (maxBias - 1) / (maxBias + 1)
|
||||
|
||||
-- -- Финальный момент, передаваемый между сторонами, с учетом направления
|
||||
-- local direction = deltaW > 0 and -1 or 1
|
||||
-- local finalTorque = direction * transferTorque
|
||||
|
||||
-- -- Применяем финальный момент к каждой стороне, ограничивая его bias'ом
|
||||
-- local aTorque = tq * 0.5 + math.min(finalTorque, biasLimit)
|
||||
-- local bTorque = tq * 0.5 - math.min(finalTorque, biasLimit)
|
||||
|
||||
-- -- Возвращаем обновленные значения угловых скоростей
|
||||
-- return aTorque, bTorque
|
||||
|
||||
-- local aTq = tq * 0.5
|
||||
-- local bTq = tq * 0.5
|
||||
|
||||
-- local syncTorque = (aW - bW) * 1e6 * (aI + bI) / TICK_INTERVAL
|
||||
|
||||
-- aTq = aTq - syncTorque
|
||||
-- bTq = bTq + syncTorque
|
||||
|
||||
-- -- print(aTq, bTq)
|
||||
|
||||
-- return aTq, bTq
|
||||
|
||||
-- local sumI = aI + bI
|
||||
-- local w = aI / sumI * aW + bI / sumI * bW
|
||||
-- local aTqCorr = (w - aW) * aI / TICK_INTERVAL
|
||||
-- aTqCorr = aTqCorr * stiffness
|
||||
|
||||
-- local bTqCorr = (w - bW) * bI / TICK_INTERVAL
|
||||
-- bTqCorr = bTqCorr * stiffness
|
||||
|
||||
-- local biasA = math.clamp(0.5 + (bW - aW) * 0.1 * stiffness, 0, 1)
|
||||
|
||||
-- return tq * biasA + aTqCorr, tq * (1 - biasA) * bTqCorr
|
||||
-- return tq * biasA + aTqCorr, tq * (1 - biasA) * bTqCorr
|
||||
end
|
||||
|
||||
---@return number, number
|
||||
local function _VLSDTorqueSplit(tq, aW, bW, aI, bI, biasAB, preload, stiffness, powerRamp, coastRamp, slipTorque)
|
||||
if aW < 0 or bW < 0 then
|
||||
return tq * (1 - biasAB), tq * biasAB
|
||||
end
|
||||
function Differential:_VLSDTorqueSplit(tq, aW, bW, aI, bI, biasAB, preload, stiffness, powerRamp, coastRamp, slipTorque)
|
||||
if aW < 0 or bW < 0 then
|
||||
return tq * (1 - biasAB), tq * biasAB
|
||||
end
|
||||
|
||||
local c = tq > 0 and powerRamp or coastRamp
|
||||
local totalW = math.abs(aW) + math.abs(bW)
|
||||
local slip = 0
|
||||
if aW > 0 or bW > 0 then
|
||||
slip = (aW - bW) / totalW
|
||||
end
|
||||
local dTq = slip * stiffness * c * slipTorque
|
||||
local c = tq > 0 and powerRamp or coastRamp
|
||||
local totalW = math.abs(aW) + math.abs(bW)
|
||||
local slip = 0
|
||||
if aW > 0 or bW > 0 then
|
||||
slip = (aW - bW) / totalW
|
||||
end
|
||||
local dTq = slip * stiffness * c * slipTorque
|
||||
|
||||
return tq * (1 - biasAB) - dTq, tq * biasAB + dTq
|
||||
return tq * (1 - biasAB) - dTq, tq * biasAB + dTq
|
||||
end
|
||||
|
||||
---@return number, number
|
||||
local function _HLSDTorqueSplit(tq, aW, bW, aI, bI, biasAB, preload, stiffness, powerRamp, coastRamp, slipTorque)
|
||||
if aW < 0 or bW < 0 then
|
||||
return tq * (1 - biasAB), tq * biasAB
|
||||
end
|
||||
function Differential:_HLSDTorqueSplit(tq, aW, bW, aI, bI, biasAB, preload, stiffness, powerRamp, coastRamp, slipTorque)
|
||||
if aW < 0 or bW < 0 then
|
||||
return tq * (1 - biasAB), tq * biasAB
|
||||
end
|
||||
|
||||
local c = tq > 0 and powerRamp or coastRamp
|
||||
local tqFactor = math.clamp(math.abs(tq) / slipTorque, -1, 1)
|
||||
local totalW = math.abs(aW) + math.abs(bW)
|
||||
local slip = 0
|
||||
if aW > 0 or bW > 0 then
|
||||
slip = (aW - bW) / totalW
|
||||
end
|
||||
local dTq = slip * stiffness * c * slipTorque * tqFactor
|
||||
local c = tq > 0 and powerRamp or coastRamp
|
||||
local tqFactor = math.clamp(math.abs(tq) / slipTorque, -1, 1)
|
||||
local totalW = math.abs(aW) + math.abs(bW)
|
||||
local slip = 0
|
||||
if aW > 0 or bW > 0 then
|
||||
slip = (aW - bW) / totalW
|
||||
end
|
||||
local dTq = slip * stiffness * c * slipTorque * tqFactor
|
||||
|
||||
return tq * (1 - biasAB) - dTq, tq * biasAB + dTq
|
||||
return tq * (1 - biasAB) - dTq, tq * biasAB + dTq
|
||||
end
|
||||
|
||||
---@enum DIFFERENTIAL_TYPE
|
||||
DIFFERENTIAL_TYPES = {
|
||||
Open = 'Open',
|
||||
Locking = 'Locking',
|
||||
VLSD = 'VLSD',
|
||||
HLSD = 'HLSD',
|
||||
Differential.TYPES = {
|
||||
Open = 'Open',
|
||||
Locking = 'Locking',
|
||||
VLSD = 'VLSD',
|
||||
HLSD = 'HLSD',
|
||||
}
|
||||
|
||||
---@type { [DIFFERENTIAL_TYPE]: DifferentialSplitStrategyFn }
|
||||
local SPLIT_STRATEGIES = {
|
||||
[DIFFERENTIAL_TYPES.Open] = _openDiffTorqueSplit,
|
||||
[DIFFERENTIAL_TYPES.Locking] = _lockingDiffTorqueSplit,
|
||||
[DIFFERENTIAL_TYPES.VLSD] = _VLSDTorqueSplit,
|
||||
[DIFFERENTIAL_TYPES.HLSD] = _HLSDTorqueSplit,
|
||||
SPLIT_STRATEGIES = {
|
||||
[Differential.TYPES.Open] = Differential._openDiffTorqueSplit,
|
||||
[Differential.TYPES.Locking] = Differential._lockingDiffTorqueSplit,
|
||||
[Differential.TYPES.VLSD] = Differential._VLSDTorqueSplit,
|
||||
[Differential.TYPES.HLSD] = Differential._HLSDTorqueSplit,
|
||||
}
|
||||
|
||||
---@param type DIFFERENTIAL_TYPE
|
||||
---@return DifferentialSplitStrategyFn
|
||||
Differential.getSplitStrategy = function(type)
|
||||
return SPLIT_STRATEGIES[type] or SPLIT_STRATEGIES[DIFFERENTIAL_TYPES.Open]
|
||||
end
|
||||
|
||||
Differential.TYPES = DIFFERENTIAL_TYPES
|
||||
|
||||
---@type fun(vehicle: KPTLVehicle, name?: string, config?: DifferentialConfig): Differential
|
||||
Differential.new = Differential.new
|
||||
|
||||
return Differential
|
||||
|
||||
@ -1,171 +1,132 @@
|
||||
--@include ./powertrain_component.txt
|
||||
--@include /koptilnya/libs/constants.txt
|
||||
|
||||
---@type PowertrainComponent
|
||||
local PowertrainComponent = require('./powertrain_component.txt')
|
||||
|
||||
|
||||
|
||||
require('/koptilnya/libs/constants.txt')
|
||||
|
||||
---@class EngineConfig: PowertrainComponentConfig
|
||||
---@field IdleRPM? number
|
||||
---@field MaxRPM? number
|
||||
---@field StartFriction? number
|
||||
---@field FrictionCoeff? number
|
||||
---@field LimiterDuration? number
|
||||
---@field TorqueMap? number[]
|
||||
|
||||
---@class Engine: PowertrainComponent
|
||||
---@field idleRPM number
|
||||
---@field maxRPM number
|
||||
---@field startFriction number
|
||||
---@field frictionCoeff number
|
||||
---@field limiterDuration number
|
||||
---@field torqueMap number[]
|
||||
---@field friction number
|
||||
---@field masterThrottle number
|
||||
---@field finalTorque number
|
||||
---@field reactTorque number
|
||||
---@field returnedTorque number
|
||||
---@field private limiterTimer number
|
||||
local Engine = class('Engine', PowertrainComponent)
|
||||
|
||||
---@private
|
||||
---@param vehicle KPTLVehicle
|
||||
---@param name string
|
||||
---@param config EngineConfig
|
||||
function Engine:initialize(vehicle, name, config)
|
||||
PowertrainComponent.initialize(self, vehicle, name, config)
|
||||
PowertrainComponent.initialize(self, vehicle, name, config)
|
||||
|
||||
self.wireInputs = {
|
||||
Throttle = 'number',
|
||||
}
|
||||
self.wireOutputs = {
|
||||
Engine_RPM = 'number',
|
||||
Engine_Torque = 'number',
|
||||
Engine_MasterThrottle = 'number',
|
||||
Engine_ReactTorque = 'number',
|
||||
Engine_ReturnedTorque = 'number',
|
||||
Engine_FinalTorque = 'number',
|
||||
}
|
||||
self.wireInputs = {
|
||||
Throttle = 'number'
|
||||
}
|
||||
self.wireOutputs = {
|
||||
Engine_RPM = 'number',
|
||||
Engine_Torque = 'number',
|
||||
Engine_MasterThrottle = 'number',
|
||||
Engine_ReactTorque = 'number',
|
||||
Engine_ReturnedTorque = 'number',
|
||||
Engine_FinalTorque = 'number'
|
||||
}
|
||||
|
||||
self.idleRPM = config.IdleRPM or 900
|
||||
self.maxRPM = config.MaxRPM or 7000
|
||||
self.inertia = config.Inertia or 0.4
|
||||
self.startFriction = config.StartFriction or -50
|
||||
self.frictionCoeff = config.FrictionCoeff or 0.02
|
||||
self.limiterDuration = config.LimiterDuration or 0.05
|
||||
self.torqueMap = config.TorqueMap or {}
|
||||
self.idleRPM = config.IdleRPM or 900
|
||||
self.maxRPM = config.MaxRPM or 7000
|
||||
self.inertia = config.Inertia or 0.4
|
||||
self.startFriction = config.StartFriction or -50
|
||||
self.frictionCoeff = config.FrictionCoeff or 0.02
|
||||
self.limiterDuration = config.LimiterDuration or 0.05
|
||||
self.torqueMap = config.TorqueMap or {}
|
||||
|
||||
self.friction = 0
|
||||
self.masterThrottle = 0
|
||||
self.friction = 0
|
||||
self.masterThrottle = 0
|
||||
|
||||
self.limiterTimer = 0
|
||||
self.limiterTimer = 0
|
||||
|
||||
self.finalTorque = 0
|
||||
self.reactTorque = 0
|
||||
self.returnedTorque = 0
|
||||
self.reactTorque = 0
|
||||
self.returnedTorque = 0
|
||||
|
||||
if CLIENT then
|
||||
--@include /koptilnya/engine_sound_2.txt
|
||||
local Sound = require('/koptilnya/engine_sound_2.txt')
|
||||
if CLIENT then
|
||||
--@include /koptilnya/engine_sound_2.txt
|
||||
local Sound = require('/koptilnya/engine_sound_2.txt')
|
||||
|
||||
Sound(self.maxRPM, chip())
|
||||
end
|
||||
Sound(config.MaxRPM or 7000, chip())
|
||||
end
|
||||
end
|
||||
|
||||
---@return nil
|
||||
function Engine:updateWireOutputs()
|
||||
PowertrainComponent.updateWireOutputs(self)
|
||||
|
||||
wire.ports.Engine_RPM = self:getRPM()
|
||||
wire.ports.Engine_Torque = self.torque
|
||||
wire.ports.Engine_MasterThrottle = self.masterThrottle
|
||||
wire.ports.Engine_ReactTorque = self.reactTorque
|
||||
wire.ports.Engine_ReturnedTorque = self.returnedTorque
|
||||
wire.ports.Engine_FinalTorque = self.finalTorque
|
||||
PowertrainComponent.updateWireOutputs(self)
|
||||
|
||||
wire.ports.Engine_RPM = self:getRPM()
|
||||
wire.ports.Engine_Torque = self.torque
|
||||
wire.ports.Engine_MasterThrottle = self.masterThrottle
|
||||
wire.ports.Engine_ReactTorque = self.reactTorque
|
||||
wire.ports.Engine_ReturnedTorque = self.returnedTorque
|
||||
wire.ports.Engine_FinalTorque = self.finalTorque
|
||||
end
|
||||
|
||||
---@return number
|
||||
function Engine:getThrottle()
|
||||
return wire.ports.Throttle
|
||||
return wire.ports.Throttle
|
||||
end
|
||||
|
||||
---@private
|
||||
---@return number
|
||||
function Engine:generateTorque()
|
||||
local throttle = self:getThrottle()
|
||||
local rpm = self:getRPM()
|
||||
function Engine:_generateTorque()
|
||||
local throttle = self:getThrottle()
|
||||
local rpm = self:getRPM()
|
||||
|
||||
local rpmFrac = math.clamp((rpm - self.idleRPM) / (self.maxRPM - self.idleRPM), 0, 1)
|
||||
self.friction = self.startFriction - self:getRPM() * self.frictionCoeff
|
||||
local rpmFrac = math.clamp((rpm - self.idleRPM) / (self.maxRPM - self.idleRPM), 0, 1)
|
||||
self.friction = self.startFriction - self:getRPM() * self.frictionCoeff
|
||||
|
||||
local tqIdx = math.clamp(math.floor(rpmFrac * #self.torqueMap), 1, #self.torqueMap)
|
||||
local maxInitialTorque = self.torqueMap[tqIdx] - self.friction
|
||||
local idleFadeStart = math.clamp(math.remap(rpm, self.idleRPM - 300, self.idleRPM, 1, 0), 0, 1)
|
||||
local idleFadeEnd = math.clamp(math.remap(rpm, self.idleRPM, self.idleRPM + 600, 1, 0), 0, 1)
|
||||
local additionalEnergySupply = idleFadeEnd * (-self.friction / maxInitialTorque) + idleFadeStart
|
||||
local tqIdx = math.clamp(math.floor(rpmFrac * #self.torqueMap), 1, #self.torqueMap)
|
||||
local maxInitialTorque = self.torqueMap[tqIdx] - self.friction
|
||||
local idleFadeStart = math.clamp(math.remap(rpm, self.idleRPM - 300, self.idleRPM, 1, 0), 0, 1)
|
||||
local idleFadeEnd = math.clamp(math.remap(rpm, self.idleRPM, self.idleRPM + 600, 1, 0), 0, 1)
|
||||
local additionalEnergySupply = idleFadeEnd * (-self.friction / maxInitialTorque) + idleFadeStart
|
||||
|
||||
if rpm > self.maxRPM then
|
||||
throttle = 0
|
||||
self.limiterTimer = timer.systime()
|
||||
else
|
||||
throttle = timer.systime() >= self.limiterTimer + self.limiterDuration and throttle or 0
|
||||
end
|
||||
if rpm > self.maxRPM then
|
||||
throttle = 0
|
||||
self.limiterTimer = timer.systime()
|
||||
else
|
||||
throttle = timer.systime() >= self.limiterTimer + self.limiterDuration and throttle or 0
|
||||
end
|
||||
|
||||
self.masterThrottle = math.clamp(additionalEnergySupply + throttle, 0, 1)
|
||||
self.masterThrottle = math.clamp(additionalEnergySupply + throttle, 0, 1)
|
||||
|
||||
local realInitialTorque = maxInitialTorque * self.masterThrottle
|
||||
local realInitialTorque = maxInitialTorque * self.masterThrottle
|
||||
|
||||
self.torque = realInitialTorque + self.friction
|
||||
self.torque = realInitialTorque + self.friction
|
||||
|
||||
return self.torque
|
||||
return self.torque
|
||||
end
|
||||
|
||||
---@param torque number
|
||||
---@param inertia number
|
||||
---@return number
|
||||
function Engine:forwardStep(torque, inertia)
|
||||
if self.output == nil then
|
||||
local generatedTorque = self:generateTorque()
|
||||
if self.output == nil then
|
||||
local generatedTorque = self:_generateTorque()
|
||||
|
||||
self.angularVelocity = self.angularVelocity + generatedTorque / self.inertia * TICK_INTERVAL
|
||||
self.angularVelocity = math.max(self.angularVelocity, 0)
|
||||
self.angularVelocity = self.angularVelocity + generatedTorque / self.inertia * TICK_INTERVAL
|
||||
self.angularVelocity = math.max(self.angularVelocity, 0)
|
||||
end
|
||||
|
||||
return 0
|
||||
end
|
||||
local outputInertia = self.output:queryInertia()
|
||||
local inertiaSum = self.inertia + outputInertia
|
||||
local outputW = self.output:queryAngularVelocity(self.angularVelocity)
|
||||
local targetW = self.inertia / inertiaSum * self.angularVelocity + outputInertia / inertiaSum * outputW
|
||||
|
||||
local outputInertia = self.output:queryInertia()
|
||||
local inertiaSum = self.inertia + outputInertia
|
||||
local outputW = self.output:queryAngularVelocity(self.angularVelocity)
|
||||
local targetW = self.inertia / inertiaSum * self.angularVelocity + outputInertia / inertiaSum * outputW
|
||||
local generatedTorque = self:_generateTorque()
|
||||
local reactTorque = (targetW - self.angularVelocity) * self.inertia / TICK_INTERVAL
|
||||
local returnedTorque = self.output:forwardStep(torque + generatedTorque - reactTorque, inertia + self.inertia)
|
||||
|
||||
local generatedTorque = self:generateTorque()
|
||||
local reactTorque = (targetW - self.angularVelocity) * self.inertia / TICK_INTERVAL
|
||||
local returnedTorque = self.output:forwardStep(generatedTorque - reactTorque, 0)
|
||||
self.reactTorque = reactTorque
|
||||
self.returnedTorque = returnedTorque
|
||||
|
||||
self.reactTorque = reactTorque
|
||||
self.returnedTorque = returnedTorque
|
||||
local finalTorque = generatedTorque + reactTorque + returnedTorque
|
||||
|
||||
local finalTorque = generatedTorque + reactTorque + returnedTorque
|
||||
self.finalTorque = finalTorque
|
||||
|
||||
self.finalTorque = finalTorque
|
||||
self.angularVelocity = self.angularVelocity + finalTorque / inertiaSum * TICK_INTERVAL
|
||||
self.angularVelocity = math.max(self.angularVelocity, 0)
|
||||
|
||||
self.angularVelocity = self.angularVelocity + finalTorque / inertiaSum * TICK_INTERVAL
|
||||
self.angularVelocity = math.max(self.angularVelocity, 0)
|
||||
-- net.start("ENGINE_RPM")
|
||||
-- net.writeFloat(self:getRPM() / self.maxRPM)
|
||||
-- net.send(self.vehicle.playersConnectedToHUD, true)
|
||||
|
||||
net.start('ENGINE_RPM')
|
||||
net.writeFloat(self:getRPM() / self.maxRPM)
|
||||
net.send(self.vehicle.playersConnectedToHUD, true)
|
||||
|
||||
net.start('ENGINE_FULLRPM')
|
||||
net.writeUInt(self:getRPM(), 16)
|
||||
net.writeFloat(self.masterThrottle)
|
||||
net.send(nil, true)
|
||||
|
||||
return 0
|
||||
-- net.start("ENGINE_FULLRPM")
|
||||
-- net.writeUInt(self:getRPM(), 16)
|
||||
-- net.writeFloat(self.masterThrottle)
|
||||
-- net.send(nil, true)
|
||||
end
|
||||
|
||||
---@type fun(vehicle: KPTLVehicle, name?: string, config?: EngineConfig): Engine
|
||||
Engine.new = Engine.new
|
||||
|
||||
return Engine
|
||||
|
||||
@ -5,68 +5,66 @@ local PowertrainComponent = require('../powertrain_component.txt')
|
||||
local Gearbox = class('Gearbox', PowertrainComponent)
|
||||
|
||||
function Gearbox:initialize(vehicle, name, config)
|
||||
PowertrainComponent.initialize(self, vehicle, name, config)
|
||||
PowertrainComponent.initialize(self, vehicle, name, config)
|
||||
|
||||
if CLIENT then
|
||||
return
|
||||
end
|
||||
if CLIENT then return end
|
||||
|
||||
self.wireInputs = {
|
||||
Upshift = 'number',
|
||||
Downshift = 'number',
|
||||
}
|
||||
self.wireOutputs = {
|
||||
[string.format('%s_RPM', self.name)] = 'number',
|
||||
[string.format('%s_Torque', self.name)] = 'number',
|
||||
[string.format('%s_Ratio', self.name)] = 'number',
|
||||
}
|
||||
self.wireInputs = {
|
||||
Upshift = 'number',
|
||||
Downshift = 'number'
|
||||
}
|
||||
self.wireOutputs = {
|
||||
[string.format('%s_RPM', self.name)] = 'number',
|
||||
[string.format('%s_Torque', self.name)] = 'number',
|
||||
[string.format('%s_Ratio', self.name)] = 'number'
|
||||
}
|
||||
|
||||
self.type = config.Type or 'MANUAL'
|
||||
self.inertia = config.Inertia or 1000
|
||||
self.type = config.Type or 'MANUAL'
|
||||
self.inertia = config.Inertia or 1000
|
||||
|
||||
self.ratio = 0
|
||||
self.ratio = 0
|
||||
end
|
||||
|
||||
function Gearbox:updateWireOutputs()
|
||||
PowertrainComponent.updateWireOutputs(self)
|
||||
|
||||
wire.ports[string.format('%s_RPM', self.name)] = self:getRPM()
|
||||
wire.ports[string.format('%s_Torque', self.name)] = self.torque
|
||||
wire.ports[string.format('%s_Ratio', self.name)] = self.ratio
|
||||
PowertrainComponent.updateWireOutputs(self)
|
||||
|
||||
wire.ports[string.format('%s_RPM', self.name)] = self:getRPM()
|
||||
wire.ports[string.format('%s_Torque', self.name)] = self.torque
|
||||
wire.ports[string.format('%s_Ratio', self.name)] = self.ratio
|
||||
end
|
||||
|
||||
function Gearbox:queryInertia()
|
||||
if self.output == nil or self.ratio == 0 then
|
||||
return self.inertia
|
||||
end
|
||||
if self.output == nil or self.ratio == 0 then
|
||||
return self.inertia
|
||||
end
|
||||
|
||||
return self.inertia + self.output:queryInertia() / math.pow(self.ratio, 2)
|
||||
return self.inertia + self.output:queryInertia() / math.pow(self.ratio, 2)
|
||||
end
|
||||
|
||||
function Gearbox:queryAngularVelocity(angularVelocity)
|
||||
self.angularVelocity = angularVelocity
|
||||
self.angularVelocity = angularVelocity
|
||||
|
||||
if self.output == nil or self.ratio == 0 then
|
||||
return angularVelocity
|
||||
end
|
||||
if self.output == nil or self.ratio == 0 then
|
||||
return angularVelocity
|
||||
end
|
||||
|
||||
return self.output:queryAngularVelocity(angularVelocity) * self.ratio
|
||||
return self.output:queryAngularVelocity(angularVelocity) * self.ratio
|
||||
end
|
||||
|
||||
function Gearbox:forwardStep(torque, inertia)
|
||||
self.torque = torque * self.ratio
|
||||
self.torque = torque * self.ratio
|
||||
|
||||
if self.output == nil then
|
||||
return torque
|
||||
end
|
||||
if self.output == nil then
|
||||
return torque
|
||||
end
|
||||
|
||||
if self.ratio == 0 then
|
||||
self.output:forwardStep(0, self.inertia * 0.5)
|
||||
if self.ratio == 0 then
|
||||
self.output:forwardStep(0, self.inertia * 0.5)
|
||||
|
||||
return torque
|
||||
end
|
||||
return torque
|
||||
end
|
||||
|
||||
return self.output:forwardStep(self.torque, (inertia + self.inertia) * math.pow(self.ratio, 2)) / self.ratio
|
||||
return self.output:forwardStep(self.torque, (inertia + self.inertia) * math.pow(self.ratio, 2)) / self.ratio
|
||||
end
|
||||
|
||||
return Gearbox
|
||||
|
||||
@ -10,78 +10,74 @@ require('/koptilnya/libs/utils.txt')
|
||||
local ManualGearbox = class('ManualGearbox', BaseGearbox)
|
||||
|
||||
function ManualGearbox:initialize(vehicle, name, config)
|
||||
BaseGearbox.initialize(self, vehicle, name, config)
|
||||
BaseGearbox.initialize(self, vehicle, name, config)
|
||||
|
||||
if CLIENT then
|
||||
return
|
||||
end
|
||||
if CLIENT then return end
|
||||
|
||||
table.merge(self.wireOutputs, {
|
||||
[string.format('%s_Gear', self.name)] = 'number',
|
||||
})
|
||||
table.merge(self.wireOutputs, {
|
||||
[string.format('%s_Gear', self.name)] = 'number'
|
||||
})
|
||||
|
||||
self.ratios = config.Ratios or { 3.6, 2.2, 1.5, 1.2, 1.0, 0.8 }
|
||||
self.reverse = config.Reverse or 3.4
|
||||
self.ratios = config.Ratios or { 3.6, 2.2, 1.5, 1.2, 1.0, 0.8}
|
||||
self.reverse = config.Reverse or 3.4
|
||||
|
||||
self.gear = 0
|
||||
self.gear = 0
|
||||
|
||||
function shiftFunc()
|
||||
if wire.ports.Clutch == 0 then
|
||||
return 0
|
||||
end
|
||||
function shiftFunc()
|
||||
if wire.ports.Clutch == 0 then return 0 end
|
||||
|
||||
local upshift = wire.ports.Upshift or 0
|
||||
local downshift = wire.ports.Downshift or 0
|
||||
local upshift = wire.ports.Upshift or 0
|
||||
local downshift = wire.ports.Downshift or 0
|
||||
|
||||
return upshift - downshift
|
||||
end
|
||||
return upshift - downshift
|
||||
end
|
||||
|
||||
self.shiftWatcher = watcher(shiftFunc, function(val)
|
||||
if val ~= 0 then
|
||||
self:shift(val)
|
||||
end
|
||||
end)
|
||||
self.shiftWatcher = watcher(shiftFunc, function(val)
|
||||
if val ~= 0 then
|
||||
self:shift(val)
|
||||
end
|
||||
end)
|
||||
|
||||
self:recalcRatio()
|
||||
self:recalcRatio()
|
||||
end
|
||||
|
||||
function ManualGearbox:updateWireOutputs()
|
||||
BaseGearbox.updateWireOutputs(self)
|
||||
BaseGearbox.updateWireOutputs(self)
|
||||
|
||||
wire.ports[string.format('%s_Gear', self.name)] = self.gear
|
||||
wire.ports[string.format('%s_Gear', self.name)] = self.gear
|
||||
end
|
||||
|
||||
function ManualGearbox:setGear(gear)
|
||||
if gear >= -1 and gear <= #self.ratios then
|
||||
self.gear = gear
|
||||
self:recalcRatio()
|
||||
if gear >= -1 and gear <= #self.ratios then
|
||||
self.gear = gear
|
||||
self:recalcRatio()
|
||||
|
||||
net.start('GEARBOX_GEAR')
|
||||
net.writeInt(gear, 5)
|
||||
net.send(self.vehicle.playersConnectedToHUD, true)
|
||||
end
|
||||
net.start('GEARBOX_GEAR')
|
||||
net.writeInt(gear, 5)
|
||||
net.send(self.vehicle.playersConnectedToHUD, true)
|
||||
end
|
||||
end
|
||||
|
||||
function ManualGearbox:recalcRatio()
|
||||
if self.gear == -1 then
|
||||
self.ratio = -self.reverse
|
||||
elseif self.gear == 0 then
|
||||
self.ratio = 0
|
||||
else
|
||||
self.ratio = self.ratios[self.gear]
|
||||
end
|
||||
if self.gear == -1 then
|
||||
self.ratio = -self.reverse
|
||||
elseif self.gear == 0 then
|
||||
self.ratio = 0
|
||||
else
|
||||
self.ratio = self.ratios[self.gear]
|
||||
end
|
||||
end
|
||||
|
||||
function ManualGearbox:shift(dir)
|
||||
self:setGear(self.gear + dir)
|
||||
self:setGear(self.gear + dir)
|
||||
end
|
||||
|
||||
function ManualGearbox:forwardStep(torque, inertia)
|
||||
self.shiftWatcher()
|
||||
self.shiftWatcher()
|
||||
|
||||
local result = BaseGearbox.forwardStep(self, torque, inertia)
|
||||
|
||||
local result = BaseGearbox.forwardStep(self, torque, inertia)
|
||||
|
||||
return result
|
||||
return result
|
||||
end
|
||||
|
||||
return ManualGearbox
|
||||
|
||||
@ -1,135 +1,104 @@
|
||||
--@include /koptilnya/libs/constants.txt
|
||||
--@include /koptilnya/libs/utils.txt
|
||||
--@include ../wire_component.txt
|
||||
--@include ../vehicle.txt
|
||||
|
||||
local WireComponent = require('../wire_component.txt')
|
||||
|
||||
require('/koptilnya/libs/constants.txt')
|
||||
require('/koptilnya/libs/utils.txt')
|
||||
|
||||
---@class PowertrainComponentConfig
|
||||
---@field Name? string
|
||||
---@field DEBUG? boolean
|
||||
---@field Inertia? number
|
||||
|
||||
---@class PowertrainComponent: KPTLWireComponent
|
||||
---@field vehicle KPTLVehicle
|
||||
---@field name string
|
||||
---@field CONFIG PowertrainComponentConfig
|
||||
---@field DEBUG boolean
|
||||
---@field input? PowertrainComponent
|
||||
---@field output? PowertrainComponent
|
||||
---@field inertia number
|
||||
---@field angularVelocity number
|
||||
---@field torque number
|
||||
---@field DEBUG_DATA? table
|
||||
local PowertrainComponent = class('PowertrainComponent', WireComponent)
|
||||
|
||||
---@protected
|
||||
---@param vehicle KPTLVehicle
|
||||
---@param name? string
|
||||
---@param config? PowertrainComponentConfig
|
||||
function PowertrainComponent:initialize(vehicle, name, config)
|
||||
config = config or {}
|
||||
config = config or {}
|
||||
|
||||
WireComponent.initialize(self)
|
||||
WireComponent.initialize(self)
|
||||
|
||||
self.vehicle = vehicle
|
||||
self.name = name or 'PowertrainComponent'
|
||||
self.CONFIG = config
|
||||
self.DEBUG = config.DEBUG or false
|
||||
self.input = nil
|
||||
self.output = nil
|
||||
self.inertia = 0.02
|
||||
self.angularVelocity = 0
|
||||
self.torque = 0
|
||||
self.vehicle = vehicle
|
||||
self.name = name or 'PowertrainComponent'
|
||||
self.CONFIG = config
|
||||
self.DEBUG = config.DEBUG or false
|
||||
self.input = nil
|
||||
self.output = nil
|
||||
|
||||
if self.DEBUG then
|
||||
if CLIENT then
|
||||
self.DEBUG_DATA = {}
|
||||
self.inertia = 0.02
|
||||
self.angularVelocity = 0
|
||||
self.torque = 0
|
||||
|
||||
net.receive('DEBUG_' .. self.name, function()
|
||||
self:deserializeDebugData(self.DEBUG_DATA)
|
||||
end)
|
||||
end
|
||||
|
||||
self.DEBUG_SEND_DATA_DEBOUNCED = debounce(function()
|
||||
net.start('DEBUG_' .. self.name)
|
||||
self:serializeDebugData()
|
||||
net.send(self.vehicle.playersConnectedToHUD, true)
|
||||
end, 1 / 10)
|
||||
end
|
||||
if self.DEBUG then
|
||||
if CLIENT then
|
||||
self.DEBUG_DATA = {}
|
||||
|
||||
net.receive('DEBUG_' .. self.name, function()
|
||||
self:deserializeDebugData(self.DEBUG_DATA)
|
||||
end)
|
||||
end
|
||||
|
||||
self.DEBUG_SEND_DATA_DEBOUNCED = debounce(function()
|
||||
net.start("DEBUG_" .. self.name)
|
||||
self:serializeDebugData()
|
||||
net.send(self.vehicle.playersConnectedToHUD, true)
|
||||
end, 1 / 10)
|
||||
end
|
||||
end
|
||||
|
||||
function PowertrainComponent:start()
|
||||
end
|
||||
|
||||
---@param component PowertrainComponent
|
||||
---@return nil
|
||||
function PowertrainComponent:linkComponent(component)
|
||||
---@diagnostic disable-next-line: undefined-field
|
||||
if not component:isInstanceOf(PowertrainComponent) then
|
||||
return
|
||||
end
|
||||
if not component:isInstanceOf(PowertrainComponent) then
|
||||
return
|
||||
end
|
||||
|
||||
if self.output == nil then
|
||||
self.output = component
|
||||
component.input = self
|
||||
end
|
||||
if self.output == nil then
|
||||
self.output = component
|
||||
component.input = self
|
||||
end
|
||||
end
|
||||
|
||||
---@return number
|
||||
function PowertrainComponent:getRPM()
|
||||
return self.angularVelocity * RAD_TO_RPM
|
||||
return self.angularVelocity * RAD_TO_RPM
|
||||
end
|
||||
|
||||
---@return number
|
||||
function PowertrainComponent:queryInertia()
|
||||
if self.output == nil then
|
||||
return self.inertia
|
||||
end
|
||||
if self.output == nil then
|
||||
return self.inertia
|
||||
end
|
||||
|
||||
return self.inertia + self.output:queryInertia()
|
||||
return self.inertia + self.output:queryInertia()
|
||||
end
|
||||
|
||||
---@param angularVelocity number
|
||||
---@return number
|
||||
function PowertrainComponent:queryAngularVelocity(angularVelocity)
|
||||
self.angularVelocity = angularVelocity
|
||||
self.angularVelocity = angularVelocity
|
||||
|
||||
if self.output == nil then
|
||||
return 0
|
||||
end
|
||||
if self.output == nil then
|
||||
return 0
|
||||
end
|
||||
|
||||
return self.output:queryAngularVelocity(angularVelocity)
|
||||
return self.output:queryAngularVelocity(angularVelocity)
|
||||
end
|
||||
|
||||
---@param torque number
|
||||
---@param inertia number
|
||||
---@return number
|
||||
function PowertrainComponent:forwardStep(torque, inertia)
|
||||
if self.output == nil then
|
||||
return self.torque
|
||||
end
|
||||
if self.output == nil then
|
||||
return self.torque
|
||||
end
|
||||
|
||||
return self.output:forwardStep(self.torque, self.inertia + inertia)
|
||||
return self.output:forwardStep(self.torque, self.inertia + inertia)
|
||||
end
|
||||
|
||||
---@return nil
|
||||
function PowertrainComponent:updateWireOutputs()
|
||||
WireComponent.updateWireOutputs(self)
|
||||
WireComponent.updateWireOutputs(self)
|
||||
|
||||
if self.DEBUG then
|
||||
self:DEBUG_SEND_DATA_DEBOUNCED()
|
||||
end
|
||||
if self.DEBUG then
|
||||
self:DEBUG_SEND_DATA_DEBOUNCED()
|
||||
end
|
||||
end
|
||||
|
||||
---@return nil
|
||||
function PowertrainComponent:serializeDebugData() end
|
||||
function PowertrainComponent:serializeDebugData()
|
||||
end
|
||||
|
||||
---@param result table
|
||||
---@return nil
|
||||
function PowertrainComponent:deserializeDebugData(result) end
|
||||
|
||||
---@type fun(vehicle: KPTLVehicle, name?: string, config?: PowertrainComponentConfig): PowertrainComponent
|
||||
PowertrainComponent.new = PowertrainComponent.new
|
||||
function PowertrainComponent:deserializeDebugData(result)
|
||||
end
|
||||
|
||||
return PowertrainComponent
|
||||
|
||||
@ -3,251 +3,190 @@
|
||||
--@include /koptilnya/libs/constants.txt
|
||||
--@include /koptilnya/libs/entity.txt
|
||||
|
||||
---@type PowertrainComponent
|
||||
local PowertrainComponent = require('./powertrain_component.txt')
|
||||
|
||||
---@type CustomWheel
|
||||
local CustomWheel = require('../wheel/wheel.txt')
|
||||
|
||||
require('/koptilnya/libs/constants.txt')
|
||||
require('/koptilnya/libs/entity.txt')
|
||||
|
||||
---@class WheelConfig: PowertrainComponentConfig
|
||||
---@field Entity? Entity
|
||||
---@field BrakePower? number
|
||||
---@field HandbrakePower? number
|
||||
---@field Model? string
|
||||
---@field Offset? number In degrees
|
||||
---@field CustomWheel? CustomWheelConfig
|
||||
|
||||
---@class Wheel: PowertrainComponent
|
||||
---@field brakePower number
|
||||
---@field handbrakePower number
|
||||
---@field direction integer
|
||||
---@field private rot number
|
||||
---@field model string
|
||||
---@field customWheel CustomWheel
|
||||
local Wheel = class('Wheel', PowertrainComponent)
|
||||
|
||||
---@private
|
||||
---@param vehicle KPTLVehicle
|
||||
---@param name string
|
||||
---@param config WheelConfig
|
||||
local DEBUG = true
|
||||
|
||||
function Wheel:initialize(vehicle, name, config)
|
||||
PowertrainComponent.initialize(self, vehicle, name, config)
|
||||
PowertrainComponent.initialize(self, vehicle, name, config)
|
||||
|
||||
if CLIENT and self.DEBUG then
|
||||
local mat = render.createMaterial('models/debug/debugwhite')
|
||||
if CLIENT and self.DEBUG then
|
||||
local scale = 0.1
|
||||
local font = render.createFont("Roboto", 256, 400, true)
|
||||
local mat = render.createMaterial('models/debug/debugwhite')
|
||||
|
||||
local lerpLoad = 0
|
||||
local lerpForwardFrictionForce = 0
|
||||
local lerpSideFrictionForce = 0
|
||||
local lerpLoad = 0
|
||||
local lerpForwardFrictionForce = 0
|
||||
local lerpSideFrictionForce = 0
|
||||
hook.add("PostDrawTranslucentRenderables", "DEBUG_RENDER_" .. self.name, function()
|
||||
if next(self.DEBUG_DATA) == nil then return end
|
||||
if not isValid(self.DEBUG_DATA.entity) then return end
|
||||
|
||||
hook.add('PostDrawTranslucentRenderables', 'DEBUG_RENDER_' .. self.name, function()
|
||||
if next(self.DEBUG_DATA) == nil then
|
||||
return
|
||||
end
|
||||
if not isValid(self.DEBUG_DATA.entity) then
|
||||
return
|
||||
end
|
||||
local pos = self.DEBUG_DATA.entity:getPos()
|
||||
|
||||
render.setMaterial(mat)
|
||||
render.setColor(Color(255, 0, 0, 200))
|
||||
lerpForwardFrictionForce = math.lerp(0.1, lerpForwardFrictionForce, self.DEBUG_DATA.forwardFrictionForce)
|
||||
render.draw3DBeam(pos, pos + ((lerpForwardFrictionForce / 500 + 16) * self.DEBUG_DATA.forward), 1, 0, 0)
|
||||
|
||||
local pos = self.DEBUG_DATA.entity:localToWorld(Vector(0, 0, -self.DEBUG_DATA.radius * UNITS_PER_METER))
|
||||
render.setColor(Color(0, 255, 0, 255))
|
||||
lerpSideFrictionForce = math.lerp(0.1, lerpSideFrictionForce, self.DEBUG_DATA.sideFrictionForce)
|
||||
render.draw3DBeam(pos, pos + ((lerpSideFrictionForce / 500 + 16) * self.DEBUG_DATA.right), 1, 0, 0)
|
||||
|
||||
render.setMaterial(mat)
|
||||
render.setColor(Color(255, 0, 0, 200))
|
||||
lerpForwardFrictionForce = math.lerp(0.1, lerpForwardFrictionForce, self.DEBUG_DATA.forwardFrictionForce)
|
||||
render.draw3DBeam(pos, pos + ((lerpForwardFrictionForce / 500 + 0) * self.DEBUG_DATA.forward), 1, 0, 0)
|
||||
render.setColor(Color(0, 0, 255, 200))
|
||||
lerpLoad = math.lerp(0.1, lerpLoad, self.DEBUG_DATA.load)
|
||||
render.draw3DBeam(pos, pos + ((lerpLoad * 4 + 16) * self.DEBUG_DATA.up), 1, 0, 0)
|
||||
end)
|
||||
|
||||
render.setColor(Color(0, 255, 0, 255))
|
||||
lerpSideFrictionForce = math.lerp(0.1, lerpSideFrictionForce, self.DEBUG_DATA.sideFrictionForce)
|
||||
render.draw3DBeam(pos, pos + ((lerpSideFrictionForce / 500 + 0) * self.DEBUG_DATA.right), 1, 0, 0)
|
||||
if player() == owner() and not render.isHUDActive() then
|
||||
enableHud(nil, true)
|
||||
end
|
||||
|
||||
render.setColor(Color(0, 0, 255, 200))
|
||||
lerpLoad = math.lerp(0.1, lerpLoad, self.DEBUG_DATA.load)
|
||||
render.draw3DBeam(pos, pos + ((lerpLoad / 1000 * 4 + 16) * self.DEBUG_DATA.up), 1, 0, 0)
|
||||
end)
|
||||
return
|
||||
end
|
||||
|
||||
if player() == owner() and not render.isHUDActive() then
|
||||
enableHud(owner(), true)
|
||||
end
|
||||
return
|
||||
end
|
||||
self.steerLock = config.SteerLock or 0
|
||||
self.brakePower = config.BrakePower or 0
|
||||
self.handbrakePower = config.HandbrakePower or 0
|
||||
|
||||
self.brakePower = config.BrakePower or 0
|
||||
self.handbrakePower = config.HandbrakePower or 0
|
||||
self.model = config.Model or ''
|
||||
self.direction = math.sign(math.cos(math.rad(config.Offset or 0)))
|
||||
|
||||
self.direction = math.sign(math.cos(math.rad(config.Offset or 0)))
|
||||
self.wireInputs = {
|
||||
[self.name] = 'entity'
|
||||
}
|
||||
self.wireOutputs = {
|
||||
[string.format('%s_RPM', self.name)] = 'number',
|
||||
[string.format('%s_W', self.name)] = 'number'
|
||||
}
|
||||
|
||||
self.wireInputs = {
|
||||
[self.name] = 'entity',
|
||||
}
|
||||
self.wireOutputs = {
|
||||
[string.format('%s_RPM', self.name)] = 'number',
|
||||
}
|
||||
self.rot = 0
|
||||
|
||||
self.rot = 0
|
||||
self.entity = config.Entity or NULL_ENTITY
|
||||
|
||||
---@type Entity?
|
||||
self.entity = nil
|
||||
self.holo = self:createHolo(self.entity)
|
||||
|
||||
---@type Hologram?
|
||||
self.holo = nil
|
||||
self.customWheel = CustomWheel:new(table.merge(table.copy(config.CustomWheel), { Direction = self.direction }))
|
||||
|
||||
self.customWheel = CustomWheel:new(
|
||||
table.merge(
|
||||
table.copy(config.CustomWheel),
|
||||
{ Direction = self.direction, Name = self.name, ParentPhysObj = vehicle.basePhysObject }
|
||||
)
|
||||
)
|
||||
hook.add('input', 'vehicle_wheel_update' .. self.name, function(name, value)
|
||||
if name == self.name then
|
||||
self.entity = value
|
||||
self.customWheel:setEntity(value)
|
||||
|
||||
hook.add('Input', 'vehicle_wheel_update' .. self.name, function(name, value)
|
||||
if name == self.name then
|
||||
self:setEntity(value)
|
||||
end
|
||||
end)
|
||||
if isValid(self.holo) then
|
||||
self.holo:remove()
|
||||
end
|
||||
|
||||
self.holo = self:createHolo(self.entity)
|
||||
|
||||
if not config.Radius then
|
||||
self.customWheel.radius = self:getEntityRadius()
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
self.steerVelocity = 0
|
||||
end
|
||||
|
||||
---@override
|
||||
---@return nil
|
||||
function Wheel:onWirePortsReady()
|
||||
self:setEntity(self.CONFIG.Entity or wire.ports[self.name])
|
||||
end
|
||||
|
||||
---@private
|
||||
---@param entity Entity
|
||||
---@return nil
|
||||
function Wheel:setEntity(entity)
|
||||
self.entity = entity
|
||||
self.customWheel:setEntity(entity)
|
||||
self.customWheel.radius = self:getEntityRadius()
|
||||
|
||||
if isValid(self.holo) then
|
||||
self.holo:remove()
|
||||
end
|
||||
|
||||
self.holo = self:createHolo(self.entity)
|
||||
end
|
||||
|
||||
---@return number
|
||||
function Wheel:getEntityRadius()
|
||||
if not isValid(self.entity) then
|
||||
return 0
|
||||
end
|
||||
if not isValid(self.entity) then
|
||||
return 0
|
||||
end
|
||||
|
||||
return self.entity:getModelRadius() * UNITS_TO_METERS
|
||||
return self.entity:getModelRadius() * UNITS_TO_METERS
|
||||
end
|
||||
|
||||
---@return Hologram?
|
||||
function Wheel:createHolo(entity)
|
||||
if not isValid(entity) then
|
||||
return nil
|
||||
end
|
||||
if not isValid(entity) then
|
||||
return NULL_ENTITY
|
||||
end
|
||||
|
||||
local holo = hologram.create(entity:getPos(), entity:getAngles(), self.model or '')
|
||||
local holo = holograms.create(
|
||||
entity:getPos(),
|
||||
entity:getAngles(),
|
||||
self.CONFIG.Model or ''
|
||||
)
|
||||
|
||||
holo:setParent(entity)
|
||||
|
||||
if holo == nil or not isValid(holo) then
|
||||
return nil
|
||||
end
|
||||
entity:setColor(Color(0,0,0,0))
|
||||
|
||||
holo:setParent(entity)
|
||||
|
||||
entity:setColor(Color(0, 0, 0, 0))
|
||||
|
||||
return holo
|
||||
return holo
|
||||
end
|
||||
|
||||
---@return nil
|
||||
function Wheel:updateWireOutputs()
|
||||
PowertrainComponent.updateWireOutputs(self)
|
||||
PowertrainComponent.updateWireOutputs(self)
|
||||
|
||||
wire.ports[string.format('%s_RPM', self.name)] = self.customWheel:getRPM()
|
||||
wire.ports[string.format('%s_RPM', self.name)] = self.customWheel:getRPM()
|
||||
wire.ports[string.format('%s_W', self.name)] = self.customWheel.angularVelocity
|
||||
end
|
||||
|
||||
---@return number
|
||||
function Wheel:queryInertia()
|
||||
return self.customWheel.inertia
|
||||
return self.customWheel.inertia
|
||||
end
|
||||
|
||||
---@return number
|
||||
function Wheel:queryAngularVelocity(angularVelocity)
|
||||
self.customWheel.angularVelocity = angularVelocity
|
||||
|
||||
return self.customWheel.angularVelocity
|
||||
function Wheel:queryAngularVelocity()
|
||||
return self.angularVelocity
|
||||
end
|
||||
|
||||
---@param torque number
|
||||
---@param inertia number
|
||||
---@return number
|
||||
function Wheel:forwardStep(torque, inertia)
|
||||
if not isValid(self.customWheel) then
|
||||
return 0
|
||||
end
|
||||
if not isValid(self.customWheel) then
|
||||
return 0
|
||||
end
|
||||
|
||||
self.customWheel.motorTorque = torque
|
||||
self.customWheel.brakeTorque = math.max(self.brakePower * self.vehicle.brake, self.handbrakePower * self.vehicle.handbrake)
|
||||
|
||||
self.customWheel.motorTorque = torque
|
||||
self.customWheel.brakeTorque = math.max(self.brakePower * self.vehicle.brake, self.handbrakePower * self.vehicle.handbrake)
|
||||
self.customWheel:update()
|
||||
|
||||
self.customWheel:update()
|
||||
self.angularVelocity = self.customWheel.angularVelocity
|
||||
|
||||
self.angularVelocity = self.customWheel.angularVelocity
|
||||
if self.customWheel.hasHit and isValid(self.vehicle.basePhysObject) then
|
||||
local surfaceForceVector = self.customWheel.right * self.customWheel.sideFriction.force + self.customWheel.forward * self.customWheel.forwardFriction.force
|
||||
|
||||
if self.customWheel.hasHit and isValid(self.vehicle.basePhysObject) then
|
||||
local surfaceForceVector = self.customWheel.right * self.customWheel.sideFriction.force
|
||||
+ self.customWheel.forward * self.customWheel.forwardFriction.force
|
||||
self.vehicle.basePhysObject:applyForceOffset(surfaceForceVector, self.entity:localToWorld(Vector(0, 0, -self.entity:getModelRadius())))
|
||||
end
|
||||
|
||||
self.vehicle.basePhysObject:applyForceOffset(
|
||||
surfaceForceVector,
|
||||
self.entity:localToWorld(Vector(0, 0, -self.customWheel.radius * UNITS_PER_METER))
|
||||
)
|
||||
end
|
||||
if isValid(self.holo) then
|
||||
local entityAngles = self.entity:getAngles()
|
||||
|
||||
if isValid(self.holo) then
|
||||
-- Step 1: Update rotational state
|
||||
self.rot = self.rot + self.angularVelocity * TICK_INTERVAL * self.direction
|
||||
self.rot = self.rot % (2 * math.pi)
|
||||
self.rot = self.rot + self.angularVelocity * TICK_INTERVAL
|
||||
|
||||
-- Step 2: Apply spin (rolling) to forward and up directions
|
||||
local spunFwd = self.customWheel.forward:rotateAroundAxis(self.customWheel.right, nil, self.rot)
|
||||
local spunUp = self.customWheel.up:rotateAroundAxis(self.customWheel.right, nil, self.rot)
|
||||
local steerRotated = entityAngles:rotateAroundAxis(self.customWheel.up, nil, math.rad(-self.customWheel.steerAngle) + self.customWheel.toeAngle)
|
||||
local camberRotated = steerRotated:rotateAroundAxis(self.customWheel.forward, nil, -self.customWheel.camberAngle * self.direction)
|
||||
local angularVelocityRotated = camberRotated:rotateAroundAxis(self.customWheel.right, nil, self.rot)
|
||||
|
||||
-- Step 3: Get quaternion from spun directions
|
||||
local visualQuat = spunFwd:getQuaternion(spunUp)
|
||||
self.holo:setAngles(angularVelocityRotated)
|
||||
end
|
||||
|
||||
-- Step 4: Apply orientation to holo
|
||||
self.holo:setAngles(visualQuat:getEulerAngle())
|
||||
end
|
||||
|
||||
return self.customWheel.counterTorque
|
||||
return self.customWheel.counterTorque
|
||||
end
|
||||
|
||||
---@return nil
|
||||
function Wheel:serializeDebugData()
|
||||
net.writeEntity(self.entity)
|
||||
net.writeFloat(self.customWheel.radius)
|
||||
net.writeVector(self.customWheel.forward)
|
||||
net.writeVector(self.customWheel.right)
|
||||
net.writeVector(self.customWheel.up)
|
||||
net.writeFloat(self.customWheel.load)
|
||||
net.writeFloat(self.customWheel.forwardFriction.force)
|
||||
net.writeFloat(self.customWheel.sideFriction.force)
|
||||
net.writeEntity(self.entity)
|
||||
net.writeVector(self.customWheel.forward)
|
||||
net.writeVector(self.customWheel.right)
|
||||
net.writeVector(self.customWheel.up)
|
||||
net.writeFloat(self.customWheel.load)
|
||||
net.writeFloat(self.customWheel.forwardFriction.force)
|
||||
net.writeFloat(self.customWheel.sideFriction.force)
|
||||
end
|
||||
|
||||
---@param result table
|
||||
---@return nil
|
||||
function Wheel:deserializeDebugData(result)
|
||||
net.readEntity(function(ent)
|
||||
result.entity = ent
|
||||
end)
|
||||
result.radius = net.readFloat()
|
||||
result.forward = net.readVector()
|
||||
result.right = net.readVector()
|
||||
result.up = net.readVector()
|
||||
result.load = net.readFloat()
|
||||
result.forwardFrictionForce = net.readFloat()
|
||||
result.sideFrictionForce = net.readFloat()
|
||||
net.readEntity(function(ent)
|
||||
result.entity = ent
|
||||
end)
|
||||
result.forward = net.readVector()
|
||||
result.right = net.readVector()
|
||||
result.up = net.readVector()
|
||||
result.load = net.readFloat()
|
||||
result.forwardFrictionForce = net.readFloat()
|
||||
result.sideFrictionForce = net.readFloat()
|
||||
end
|
||||
|
||||
---@type fun(vehicle: KPTLVehicle, name?: string, config?: WheelConfig): Wheel
|
||||
Wheel.new = Wheel.new
|
||||
|
||||
return Wheel
|
||||
|
||||
@ -6,291 +6,155 @@
|
||||
local Task = require('/libs/task.txt')
|
||||
|
||||
local PowertrainComponentFactory = require('./factories/powertrain_component.txt')
|
||||
local CustomWheel = require('./wheel/wheel.txt')
|
||||
|
||||
local POWERTRAIN_COMPONENT = require('./enums/powertrain_component.txt')
|
||||
|
||||
require('/koptilnya/libs/table.txt')
|
||||
require('/koptilnya/libs/constants.txt')
|
||||
|
||||
---@class KPTLVehicle
|
||||
---@field config { Name: string, Input?: string, Config?: table }[]
|
||||
---@field components PowertrainComponent[]
|
||||
---@field headComponents PowertrainComponent[]
|
||||
---@field steer number
|
||||
---@field brake number
|
||||
---@field handbrake number
|
||||
---@field playersConnectedToHUD Player[]
|
||||
---@field base? Entity
|
||||
---@field basePhysObject? PhysObj
|
||||
---@field initialized boolean
|
||||
---@field initializedPlayers Player[]
|
||||
local Vehicle = class('Vehicle')
|
||||
|
||||
---@param config { [string]: any }
|
||||
function Vehicle:initialize(config)
|
||||
if not Vehicle:validateConfig(config) then
|
||||
throw('Please check your vehicle configuration!')
|
||||
end
|
||||
if not Vehicle:validateConfig(config) then
|
||||
throw('Please check your vehicle configuration!')
|
||||
end
|
||||
|
||||
self.initialized = false
|
||||
self.config = config
|
||||
self.components = {}
|
||||
self.headComponents = {}
|
||||
|
||||
self.config = config
|
||||
self.components = {}
|
||||
self.headComponents = {}
|
||||
self:createComponents()
|
||||
self:linkComponents()
|
||||
|
||||
self.steer = 0
|
||||
self.brake = 0
|
||||
self.handbrake = 0
|
||||
self.initializedPlayers = {}
|
||||
self.playersConnectedToHUD = {}
|
||||
if SERVER then
|
||||
self:createIO()
|
||||
end
|
||||
|
||||
if SERVER then
|
||||
self.playersConnectedToHUD = find.allPlayers(function(ply)
|
||||
return isValid(ply) and ply:isHUDActive()
|
||||
end)
|
||||
if SERVER then
|
||||
self.steer = 0
|
||||
self.brake = 0
|
||||
self.handbrake = 0
|
||||
self.playersConnectedToHUD = {}
|
||||
|
||||
hook.add('Input', 'vehicle_wire_input', function(name, value)
|
||||
self:handleWireInput(name, value)
|
||||
end)
|
||||
hook.add('input', 'vehicle_wire_input', function(name, value)
|
||||
self:handleWireInput(name, value)
|
||||
end)
|
||||
|
||||
---@diagnostic disable-next-line: param-type-mismatch
|
||||
hook.add('Tick', 'vehicle_update', function()
|
||||
self:update()
|
||||
end)
|
||||
hook.add('tick', 'vehicle_update', function()
|
||||
self:update()
|
||||
end)
|
||||
|
||||
hook.add('HUDConnected', 'vehicle_hudconnected', function(ent, ply)
|
||||
table.insert(self.playersConnectedToHUD, ply)
|
||||
end)
|
||||
hook.add('HUDConnected', 'vehicle_hudconnected', function(ent, ply)
|
||||
table.insert(self.playersConnectedToHUD, ply)
|
||||
end)
|
||||
|
||||
hook.add('HUDDisconnected', 'vehicle_huddisconnected', function(ent, ply)
|
||||
table.removeByValue(self.playersConnectedToHUD, ply)
|
||||
end)
|
||||
|
||||
hook.add('PlayerDisconnected', 'vehicle_huddisconnected', function(ply)
|
||||
table.removeByValue(self.playersConnectedToHUD, ply)
|
||||
end)
|
||||
|
||||
hook.add('ClientInitialized', 'vehicle_clientinitialized', function(ply)
|
||||
table.insert(self.initializedPlayers, ply)
|
||||
|
||||
if self.initialized then
|
||||
net.start('VEHICLE_READY')
|
||||
net.send(ply, true)
|
||||
end
|
||||
end)
|
||||
else
|
||||
--@include ./hud.txt
|
||||
require('./hud.txt')
|
||||
|
||||
net.receive('VEHICLE_READY', function()
|
||||
self:start()
|
||||
end)
|
||||
end
|
||||
|
||||
hook.add('HUDDisconnected', 'vehicle_huddisconnected', function(ent, ply)
|
||||
table.removeByValue(self.playersConnectedToHUD, ply)
|
||||
end)
|
||||
else
|
||||
--@include ./hud.txt
|
||||
require("./hud.txt")
|
||||
end
|
||||
end
|
||||
|
||||
---@return nil
|
||||
function Vehicle:start()
|
||||
self:createComponents()
|
||||
self:linkComponents()
|
||||
|
||||
if SERVER then
|
||||
self:createIO()
|
||||
end
|
||||
|
||||
self.initialized = true
|
||||
|
||||
if SERVER then
|
||||
net.start('VEHICLE_READY')
|
||||
net.send(self.initializedPlayers, true)
|
||||
end
|
||||
function Vehicle.static:validateConfig(config)
|
||||
return type(config) == 'table'
|
||||
end
|
||||
|
||||
---@param config any
|
||||
---@return boolean
|
||||
function Vehicle:validateConfig(config)
|
||||
return type(config) == 'table'
|
||||
end
|
||||
|
||||
---@param name string
|
||||
---@return PowertrainComponent
|
||||
function Vehicle:getComponentByName(name)
|
||||
return table.find(self.components, function(component)
|
||||
return component.name == name
|
||||
end)
|
||||
return table.find(self.components, function(component)
|
||||
return component.name == name
|
||||
end)
|
||||
end
|
||||
|
||||
-- ---@param type string
|
||||
-- ---@return PowertrainComponent[]
|
||||
-- function Vehicle:getComponentsByType(type)
|
||||
-- return table.find(self.components, function(component)
|
||||
-- return component.name == name
|
||||
-- end)
|
||||
-- end
|
||||
|
||||
---@return nil
|
||||
function Vehicle:createComponents()
|
||||
for _, componentConfig in pairs(self.config) do
|
||||
local component = PowertrainComponentFactory:create(self, componentConfig.Type, componentConfig.Name, componentConfig.Config)
|
||||
for _, componentConfig in pairs(self.config) do
|
||||
local component = PowertrainComponentFactory:create(self, componentConfig.Type, componentConfig.Name, componentConfig.Config)
|
||||
|
||||
table.insert(self.components, component)
|
||||
end
|
||||
table.insert(self.components, component)
|
||||
end
|
||||
end
|
||||
|
||||
---@return nil
|
||||
function Vehicle:linkComponents()
|
||||
for _, componentConfig in pairs(self.config) do
|
||||
local component = self:getComponentByName(componentConfig.Name)
|
||||
for _, componentConfig in pairs(self.config) do
|
||||
local component = self:getComponentByName(componentConfig.Name)
|
||||
|
||||
if componentConfig.Input == nil then
|
||||
table.insert(self.headComponents, component)
|
||||
else
|
||||
local inputComponent = self:getComponentByName(componentConfig.Input)
|
||||
if componentConfig.Input == nil then
|
||||
table.insert(self.headComponents, component)
|
||||
else
|
||||
local inputComponent = self:getComponentByName(componentConfig.Input)
|
||||
|
||||
if inputComponent ~= nil then
|
||||
inputComponent:linkComponent(component)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if SERVER then
|
||||
print(Color(0, 255, 0), 'Powertrain tree:')
|
||||
for _, component in pairs(self.headComponents) do
|
||||
PrintTree(component)
|
||||
print(' ')
|
||||
end
|
||||
end
|
||||
if inputComponent ~= nil then
|
||||
inputComponent:linkComponent(component)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@return nil
|
||||
function Vehicle:createIO()
|
||||
local inputs = {
|
||||
Base = 'entity',
|
||||
Steer = 'number',
|
||||
Brake = 'number',
|
||||
Handbrake = 'number',
|
||||
}
|
||||
local outputs = {}
|
||||
local inputs = {
|
||||
Base = 'entity',
|
||||
Steer = 'number',
|
||||
Brake = 'number',
|
||||
Handbrake = 'number'
|
||||
}
|
||||
local outputs = {}
|
||||
|
||||
for _, component in ipairs(self.components) do
|
||||
inputs = table.merge(inputs, component.wireInputs)
|
||||
outputs = table.merge(outputs, component.wireOutputs)
|
||||
end
|
||||
for _, component in ipairs(self.components) do
|
||||
inputs = table.merge(inputs, component.wireInputs)
|
||||
outputs = table.merge(outputs, component.wireOutputs)
|
||||
end
|
||||
|
||||
wire.adjustPorts(inputs, outputs)
|
||||
|
||||
for _, component in ipairs(self.components) do
|
||||
component:onWirePortsReady()
|
||||
end
|
||||
wire.adjustPorts(inputs, outputs)
|
||||
end
|
||||
|
||||
function Vehicle:getRootComponent()
|
||||
return table.find(self.components, function(component)
|
||||
return component.input == nil
|
||||
end)
|
||||
end
|
||||
|
||||
---@return nil
|
||||
function Vehicle:handleWireInput(name, value)
|
||||
if name == 'Base' then
|
||||
self.base = value
|
||||
self.basePhysObject = isValid(value) and value:getPhysicsObject() or nil
|
||||
elseif name == 'Steer' then
|
||||
self.steer = value
|
||||
elseif name == 'Brake' then
|
||||
self.brake = value
|
||||
elseif name == 'Handbrake' then
|
||||
self.handbrake = value
|
||||
end
|
||||
|
||||
if not self.initialized and self.base ~= nil and self.basePhysObject ~= nil then
|
||||
self:start()
|
||||
end
|
||||
if name == 'Base' then
|
||||
self.base = value
|
||||
self.basePhysObject = isValid(value) and value:getPhysicsObject() or nil
|
||||
elseif name == 'Steer' then
|
||||
self.steer = value
|
||||
elseif name == 'Brake' then
|
||||
self.brake = value
|
||||
elseif name == 'Handbrake' then
|
||||
self.handbrake = value
|
||||
end
|
||||
end
|
||||
|
||||
---@return nil
|
||||
function Vehicle:update()
|
||||
if SERVER then
|
||||
local base = wire.ports.Base
|
||||
if SERVER then
|
||||
local base = wire.ports.Base
|
||||
|
||||
for _, component in pairs(self.headComponents) do
|
||||
component:forwardStep(0, 0)
|
||||
end
|
||||
for _, component in pairs(self.headComponents) do
|
||||
component:forwardStep(0, 0)
|
||||
end
|
||||
|
||||
for _, component in pairs(self.components) do
|
||||
component:updateWireOutputs()
|
||||
end
|
||||
for _, component in pairs(self.components) do
|
||||
component:updateWireOutputs()
|
||||
end
|
||||
|
||||
-- net.start("CAR_SPEED")
|
||||
-- net.writeUInt(base:getVelocity():getLength()* 1.905 / 100000 * 3600, 12)
|
||||
-- net.send(self.playersConnectedToHUD, true)
|
||||
end
|
||||
-- net.start("CAR_SPEED")
|
||||
-- net.writeUInt(base:getVelocity():getLength()* 1.905 / 100000 * 3600, 12)
|
||||
-- net.send(self.playersConnectedToHUD, true)
|
||||
end
|
||||
|
||||
--if isValid(base) and (self.clutch:getPress() == 1 or self.gearbox.ratio == 0) then
|
||||
-- local tiltForce = self.engine.torque * (-1 + self.engine.masterThrottle * 2)
|
||||
--
|
||||
-- base:applyForceOffset(base:getUp() * tiltForce, base:getMassCenter() + base:getRight() * UNITS_PER_METER)
|
||||
-- base:applyForceOffset(-base:getUp() * tiltForce, base:getMassCenter() - base:getRight() * UNITS_PER_METER)
|
||||
--end
|
||||
end
|
||||
|
||||
---@return nil
|
||||
function PrintTree(root)
|
||||
if root == nil then
|
||||
return
|
||||
end
|
||||
|
||||
local lines = { { Color(255, 255, 255), root.name } }
|
||||
local subtreeLines = PrintSubtree(root, '')
|
||||
for _, line in ipairs(subtreeLines) do
|
||||
table.insert(lines, line)
|
||||
end
|
||||
|
||||
for _, line in ipairs(lines) do
|
||||
print(unpack(line))
|
||||
end
|
||||
end
|
||||
|
||||
---@return (string | Color)[]
|
||||
function PrintSubtree(root, prefix)
|
||||
if root == nil then
|
||||
return {}
|
||||
end
|
||||
|
||||
---@type (string | Color)[]
|
||||
local lines = {}
|
||||
|
||||
local left = root.outputB
|
||||
local right = root.output or root.outputA
|
||||
|
||||
local hasLeft = left ~= nil
|
||||
local hasRight = right ~= nil
|
||||
|
||||
if not hasLeft and not hasRight then
|
||||
return lines
|
||||
end
|
||||
|
||||
if hasRight then
|
||||
local printStrand = hasLeft and (right.output ~= nil or right.outputA ~= nil or right.outputB ~= nil)
|
||||
local newPrefix = prefix .. (printStrand and '│ \t' or '\t\t')
|
||||
|
||||
table.insert(
|
||||
lines,
|
||||
{ Color(80, 80, 80), prefix .. (hasLeft and '├── ' or '└── '), Color(255, 255, 255), tostring(right.name) }
|
||||
)
|
||||
|
||||
local rightLines = PrintSubtree(right, newPrefix)
|
||||
for _, line in ipairs(rightLines) do
|
||||
table.insert(lines, line)
|
||||
end
|
||||
end
|
||||
|
||||
if hasLeft then
|
||||
table.insert(lines, { Color(80, 80, 80), (hasRight and prefix or '') .. '└── ', Color(255, 255, 255), tostring(left.name) })
|
||||
|
||||
local leftLines = PrintSubtree(left, prefix .. '\t\t')
|
||||
for _, line in ipairs(leftLines) do
|
||||
table.insert(lines, line)
|
||||
end
|
||||
end
|
||||
|
||||
return lines
|
||||
--if isValid(base) and (self.clutch:getPress() == 1 or self.gearbox.ratio == 0) then
|
||||
-- local tiltForce = self.engine.torque * (-1 + self.engine.masterThrottle * 2)
|
||||
--
|
||||
-- base:applyForceOffset(base:getUp() * tiltForce, base:getMassCenter() + base:getRight() * UNITS_PER_METER)
|
||||
-- base:applyForceOffset(-base:getUp() * tiltForce, base:getMassCenter() - base:getRight() * UNITS_PER_METER)
|
||||
--end
|
||||
end
|
||||
|
||||
return {
|
||||
Vehicle,
|
||||
POWERTRAIN_COMPONENT,
|
||||
}
|
||||
Vehicle,
|
||||
POWERTRAIN_COMPONENT
|
||||
}
|
||||
@ -1,16 +1,14 @@
|
||||
---@class Friction
|
||||
---@field force number
|
||||
---@field slip number
|
||||
---@field speed number
|
||||
local Friction = class('Friction')
|
||||
|
||||
function Friction:initialize()
|
||||
self.force = 0
|
||||
self.slip = 0
|
||||
self.speed = 0
|
||||
function Friction:initialize(config)
|
||||
config = config or {}
|
||||
|
||||
self.slipCoefficient = config.SlipCoefficient or 0.8
|
||||
self.forceCoefficient = config.ForceCoefficient or 1.2
|
||||
|
||||
self.force = 0
|
||||
self.slip = 0
|
||||
self.speed = 0
|
||||
end
|
||||
|
||||
---@type fun(): Friction
|
||||
Friction.new = Friction.new
|
||||
|
||||
return Friction
|
||||
|
||||
@ -1,35 +1,18 @@
|
||||
---@class FrictionPresetConfig
|
||||
---@field B? number
|
||||
---@field C? number
|
||||
---@field D? number
|
||||
---@field E? number
|
||||
|
||||
---@class FrictionPreset
|
||||
---@field B number
|
||||
---@field C number
|
||||
---@field D number
|
||||
---@field E number
|
||||
local FrictionPreset = class('FrictionPreset')
|
||||
|
||||
---@param config FrictionPresetConfig
|
||||
function FrictionPreset:initialize(config)
|
||||
config = config or {}
|
||||
config = config or {}
|
||||
|
||||
self.B = config.B or 10.86
|
||||
self.C = config.C or 2.15
|
||||
self.D = config.D or 0.933
|
||||
self.E = config.E or 0.992
|
||||
self.B = config.B or 10.86
|
||||
self.C = config.C or 2.15
|
||||
self.D = config.D or 0.933
|
||||
self.E = config.E or 0.992
|
||||
end
|
||||
|
||||
---@param slip number
|
||||
---@return number
|
||||
function FrictionPreset:evaluate(slip)
|
||||
local t = math.abs(slip)
|
||||
local t = math.abs(slip)
|
||||
|
||||
return self.D * math.sin(self.C * math.atan(self.B * t - self.E * (self.B * t - math.atan(self.B * t))))
|
||||
return self.D * math.sin(self.C * math.atan(self.B * t - self.E * (self.B * t - math.atan(self.B * t))))
|
||||
end
|
||||
|
||||
---@type fun(config?: FrictionPresetConfig): FrictionPreset
|
||||
FrictionPreset.new = FrictionPreset.new
|
||||
|
||||
return FrictionPreset
|
||||
|
||||
@ -1,405 +1,289 @@
|
||||
--@include ./friction.txt
|
||||
--@include ./friction_preset.txt
|
||||
--@include /koptilnya/libs/constants.txt
|
||||
--@include /koptilnya/libs/utils.txt
|
||||
|
||||
---@type Friction
|
||||
local Friction = require('./friction.txt')
|
||||
|
||||
---@type FrictionPreset
|
||||
local FrictionPreset = require('./friction_preset.txt')
|
||||
|
||||
require('/koptilnya/libs/constants.txt')
|
||||
require('/koptilnya/libs/utils.txt')
|
||||
|
||||
---@class CustomWheelConfig
|
||||
---@field ParentPhysObj PhysObj
|
||||
---@field Name? string
|
||||
---@field Direction? integer
|
||||
---@field LateralFrictionPreset? FrictionPresetConfig
|
||||
---@field LongitudinalFrictionPreset? FrictionPresetConfig
|
||||
---@field AligningFrictionPreset? FrictionPresetConfig
|
||||
|
||||
---@class CustomWheel
|
||||
local Wheel = class('Wheel')
|
||||
|
||||
---@param config CustomWheelConfig
|
||||
function Wheel:initialize(config)
|
||||
config = config or {}
|
||||
config = config or {}
|
||||
|
||||
---@type string
|
||||
self.name = config.Name
|
||||
self.direction = config.Direction or 1
|
||||
self.mass = config.Mass or 20
|
||||
self.radius = config.Radius or 0.27
|
||||
self.rollingResistance = config.RollingResistance or 20
|
||||
self.squat = config.Squat or 0.1
|
||||
self.slipCircleShape = config.SlipCircleShape or 1.05
|
||||
self.casterAngle = math.rad(config.CasterAngle or 0) * self.direction
|
||||
self.camberAngle = math.rad(config.CamberAngle or 0)
|
||||
self.toeAngle = math.rad(config.ToeAngle or 0) * -self.direction
|
||||
|
||||
self.direction = config.Direction or 1
|
||||
self.mass = config.Mass or 20
|
||||
self.radius = config.Radius or 0.27
|
||||
self.rollingResistance = config.RollingResistance or 20
|
||||
self.forwardFriction = Friction:new(config.ForwardFriction)
|
||||
self.sideFriction = Friction:new(config.SideFriction)
|
||||
|
||||
self.casterAngle = math.rad(config.CasterAngle or 0)
|
||||
self.camberAngle = math.rad(config.CamberAngle or 0)
|
||||
self.toeAngle = math.rad(config.ToeAngle or 0)
|
||||
self.lateralFrictionPreset = FrictionPreset:new(config.LateralFrictionPreset)
|
||||
self.longitudinalFrictionPreset = FrictionPreset:new(config.LongitudinalFrictionPreset)
|
||||
self.aligningFrictionPreset = FrictionPreset:new(config.AligningFrictionPreset)
|
||||
|
||||
self.forwardFriction = Friction:new()
|
||||
self.sideFriction = Friction:new()
|
||||
self.motorTorque = 0
|
||||
self.brakeTorque = 0
|
||||
self.steerAngle = 0
|
||||
self.hasHit = false
|
||||
self.counterTorque = 0
|
||||
self.inertia = 0
|
||||
self.load = 0
|
||||
self.angularVelocity = 0
|
||||
self.mz = 0
|
||||
|
||||
self.lateralFrictionPreset = FrictionPreset:new(config.LateralFrictionPreset)
|
||||
|
||||
self.longitudinalFrictionPreset = FrictionPreset:new(config.LongitudinalFrictionPreset)
|
||||
|
||||
self.aligningFrictionPreset = FrictionPreset:new(config.AligningFrictionPreset)
|
||||
self.slipCircleShape = config.SlipCircleShape or 1.05
|
||||
|
||||
self.motorTorque = 0
|
||||
self.brakeTorque = 0
|
||||
self.steerAngle = 0
|
||||
self.hasHit = false
|
||||
self.counterTorque = 0
|
||||
self.inertia = 0
|
||||
self.load = 0
|
||||
self.angularVelocity = 0
|
||||
self.mz = 0
|
||||
|
||||
self.forward = Vector(0)
|
||||
self.right = Vector(0)
|
||||
self.up = Vector(0)
|
||||
self.entity = NULL_ENTITY
|
||||
|
||||
---@type PhysObj?
|
||||
self.physObj = nil
|
||||
self.baseInertia = 0.6 * self.mass * math.pow(self.radius, 2)
|
||||
self.inertia = self.baseInertia
|
||||
|
||||
self.printDebounced = debounce(function(...)
|
||||
print(...)
|
||||
end, 1)
|
||||
|
||||
self.parentPhysObj = config.ParentPhysObj
|
||||
self.forward = Vector(0)
|
||||
self.right = Vector(0)
|
||||
self.up = Vector(0)
|
||||
self.entity = NULL_ENTITY
|
||||
self.physObj = nil
|
||||
self.baseInertia = 0.5 * self.mass * math.pow(self.radius, 2)
|
||||
self.inertia = self.baseInertia
|
||||
end
|
||||
|
||||
---@param entity Entity
|
||||
function Wheel:setEntity(entity)
|
||||
self.entity = entity
|
||||
self.entity:setNoDraw(false)
|
||||
self.entity = entity
|
||||
self.entity:setNoDraw(false)
|
||||
|
||||
self.physObj = isValid(entity) and entity:getPhysicsObject() or nil
|
||||
self.physObj = isValid(entity) and entity:getPhysicsObject() or nil
|
||||
|
||||
if isValid(self.physObj) then
|
||||
self.physObj:setMaterial('friction_00')
|
||||
self.physObj:setMass(self.mass)
|
||||
self.physObj:enableDrag(false)
|
||||
self.physObj:setDragCoefficient(0)
|
||||
self.physObj:setAngleDragCoefficient(0)
|
||||
self:setInertia(0.6 * self.mass * math.pow(self.radius, 2))
|
||||
end
|
||||
if isValid(self.physObj) then
|
||||
self.physObj:setMaterial('friction_00')
|
||||
self.physObj:setMass(self.mass)
|
||||
self.physObj:enableDrag(false)
|
||||
self.physObj:setDragCoefficient(0)
|
||||
self.physObj:setAngleDragCoefficient(0)
|
||||
end
|
||||
end
|
||||
|
||||
function Wheel:setInertia(inertia)
|
||||
if isValid(self.physObj) then
|
||||
self.physObj:setInertia(Vector(inertia))
|
||||
self.inertia = inertia
|
||||
end
|
||||
if isValid(self.physObj) then
|
||||
print(inertia)
|
||||
self.physObj:setInertia(Vector(inertia))
|
||||
self.inertia = inertia
|
||||
end
|
||||
end
|
||||
|
||||
function Wheel:isValid()
|
||||
return isValid(self.entity) and isValid(self.physObj)
|
||||
return isValid(self.entity) and isValid(self.physObj)
|
||||
end
|
||||
|
||||
function Wheel:getRPM()
|
||||
return self.angularVelocity * RAD_TO_RPM
|
||||
return self.angularVelocity * RAD_TO_RPM
|
||||
end
|
||||
|
||||
function Wheel:getLongitudinalLoadCoefficient(load)
|
||||
return 11000 * (1 - math.exp(-0.00014 * load))
|
||||
return 11000 * (1 - math.exp(-0.00014 * load));
|
||||
end
|
||||
|
||||
function Wheel:getLateralLoadCoefficient(load)
|
||||
return 18000 * (1 - math.exp(-0.0001 * load))
|
||||
return 18000 * (1 - math.exp(-0.0001 * load));
|
||||
end
|
||||
|
||||
function Wheel:stepLongitudinal(Tm, Tb, Vx, W, Lc, R, I)
|
||||
local Winit = W
|
||||
local VxAbs = math.abs(Vx)
|
||||
local Sx = 0
|
||||
function Wheel:stepLongitudinal(Tm, Tb, Vx, W, Lc, R, I, kFx, kSx)
|
||||
local Winit = W
|
||||
local VxAbs = math.abs(Vx)
|
||||
local Sx = 0
|
||||
|
||||
if Lc < 0.01 then
|
||||
Sx = 0
|
||||
elseif VxAbs >= 0.01 then
|
||||
Sx = (W * R - Vx) / VxAbs
|
||||
else
|
||||
Sx = (W * R - Vx) * 0.6
|
||||
end
|
||||
if VxAbs >= 0.1 then
|
||||
Sx = (Vx - W * R) / VxAbs
|
||||
else
|
||||
Sx = (Vx - W * R) * 0.6
|
||||
end
|
||||
|
||||
Sx = math.clamp(Sx, -1, 1)
|
||||
Sx = Sx * kSx;
|
||||
Sx = math.clamp(Sx, -1, 1)
|
||||
|
||||
W = W + Tm / I * TICK_INTERVAL
|
||||
W = W + Tm / I * TICK_INTERVAL
|
||||
|
||||
if self.name == 'WheelRL' or self.name == 'WheelRR' then
|
||||
self.printDebounced(Color(255, 255, 0), string.format('[%s] ', self.name), Color(255, 255, 255), string.format('W: %.2f', W))
|
||||
end
|
||||
Tb = Tb * (W > 0 and -1 or 1)
|
||||
local TbCap = math.abs(W) * I / TICK_INTERVAL
|
||||
local Tbr = math.abs(Tb) - math.abs(TbCap)
|
||||
Tbr = math.max(Tbr, 0)
|
||||
Tb = math.clamp(Tb, -TbCap, TbCap)
|
||||
W = W + Tb / I * TICK_INTERVAL
|
||||
|
||||
Tb = Tb * (W > 0 and -1 or 1)
|
||||
local TbCap = math.abs(W) * I / TICK_INTERVAL
|
||||
local Tbr = math.abs(Tb) - math.abs(TbCap)
|
||||
Tbr = math.max(Tbr, 0)
|
||||
Tb = math.clamp(Tb, -TbCap, TbCap)
|
||||
W = W + Tb / I * TICK_INTERVAL
|
||||
local maxTorque = self.longitudinalFrictionPreset:evaluate(math.abs(Sx)) * Lc * kFx * R
|
||||
local errorTorque = (W - Vx / R) * I / TICK_INTERVAL
|
||||
local surfaceTorque = math.clamp(errorTorque, -maxTorque, maxTorque)
|
||||
|
||||
local maxTorque = self.longitudinalFrictionPreset:evaluate(math.abs(Sx)) * Lc * R
|
||||
W = W - surfaceTorque / I * TICK_INTERVAL
|
||||
local Fx = surfaceTorque / R
|
||||
|
||||
-- local inertiaSum = (I + self.vehicle.basePhysObject:getInertia():getLength())
|
||||
Tbr = Tbr * (W > 0 and -1 or 1)
|
||||
local TbCap2 = math.abs(W) * I / TICK_INTERVAL
|
||||
local Tb2 = math.clamp(Tbr, -TbCap2, TbCap2)
|
||||
W = W + Tb2 / I * TICK_INTERVAL
|
||||
|
||||
local errorTorque = (W - Vx / R) * I / TICK_INTERVAL
|
||||
local surfaceTorque = math.clamp(errorTorque, -maxTorque, maxTorque)
|
||||
local deltaOmegaTorque = (W - Winit) * I / TICK_INTERVAL
|
||||
local Tcnt = -surfaceTorque + Tb + Tb2 - deltaOmegaTorque
|
||||
|
||||
W = W - surfaceTorque / I * TICK_INTERVAL
|
||||
local Fx = surfaceTorque / R
|
||||
if Lc < 0.001 then
|
||||
Sx = 0
|
||||
end
|
||||
|
||||
Tbr = Tbr * (W > 0 and -1 or 1)
|
||||
local TbCap2 = math.abs(W) * I / TICK_INTERVAL
|
||||
local Tb2 = math.clamp(Tbr, -TbCap2, TbCap2)
|
||||
W = W + Tb2 / I * TICK_INTERVAL
|
||||
|
||||
local deltaOmegaTorque = (W - Winit) * I / TICK_INTERVAL
|
||||
local Tcnt = -surfaceTorque + Tb + Tb2 - deltaOmegaTorque
|
||||
|
||||
return W, Sx, Fx, Tcnt
|
||||
return W, Sx, Fx, Tcnt
|
||||
end
|
||||
|
||||
function Wheel:stepLongitudinal2(Tm, Tb, Vx, W, Lc, R, I)
|
||||
-- Параметры Pacejka (примерные значения, можно калибровать под задачу)
|
||||
local B = 10.0 -- stiffness factor
|
||||
local C = 1.9 -- shape factor
|
||||
local D = Lc -- peak factor (максимальное сцепление зависит от нагрузки)
|
||||
local E = 0.97 -- curvature factor
|
||||
function Wheel:stepLateral(Vx, Vy, Lc, kFy, kSy)
|
||||
local VxAbs = math.abs(Vx)
|
||||
local Sy = 0
|
||||
|
||||
-- Угловая скорость колеса и радиус
|
||||
local wheelSpeed = W * R
|
||||
if VxAbs > 0.1 then
|
||||
Sy = math.deg(math.atan(Vy / VxAbs))
|
||||
Sy = Sy / 50
|
||||
else
|
||||
Sy = Vy * (0.003 / TICK_INTERVAL)
|
||||
end
|
||||
|
||||
-- Slip ratio (проверка на деление на 0)
|
||||
local slip
|
||||
if math.abs(Vx) < 0.1 then
|
||||
slip = 0.0
|
||||
else
|
||||
slip = (wheelSpeed - Vx) / math.abs(Vx)
|
||||
end
|
||||
Sy = Sy * kSy * 0.95
|
||||
Sy = math.clamp(Sy, -1, 1)
|
||||
local slipSign = Sy < 0 and -1 or 1
|
||||
local Fy = -slipSign * self.lateralFrictionPreset:evaluate(math.abs(Sy)) * Lc * kFy
|
||||
|
||||
-- Формула Pacejka для расчета силы тяги (longitudinal force)
|
||||
local Fx = D * math.sin(C * math.atan(B * slip - E * (B * slip - math.atan(B * slip))))
|
||||
if Lc < 0.0001 then
|
||||
Sy = 0
|
||||
end
|
||||
|
||||
-- Добавим моторный момент и торможение, сопротивление качению
|
||||
local driveForce = Tm / R
|
||||
local brakeForce = Tb / R
|
||||
local rollingResistanceTorque = self.rollingResistance * R
|
||||
|
||||
local tractionTorque = Fx * R
|
||||
|
||||
-- Суммарный момент, действующий на колесо
|
||||
local netTorque = Tm - Tb - rollingResistanceTorque - tractionTorque
|
||||
|
||||
-- Угловое ускорение
|
||||
local angularAcceleration = netTorque / I
|
||||
|
||||
-- Обновляем угловую скорость
|
||||
local newAngularVelocity = W + angularAcceleration * TICK_INTERVAL
|
||||
|
||||
-- Суммарная продольная сила (на автомобиль)
|
||||
local totalLongitudinalForce = Fx + (Tm - Tb - rollingResistanceTorque) / R
|
||||
|
||||
-- print(newAngularVelocity, slip, totalLongitudinalForce, -tractionTorque)
|
||||
|
||||
return newAngularVelocity, slip, totalLongitudinalForce, tractionTorque
|
||||
end
|
||||
|
||||
function Wheel:stepLateral(Vx, Vy, Lc)
|
||||
local VxAbs = math.abs(Vx)
|
||||
local Sy = 0
|
||||
|
||||
if Lc < 0.01 then
|
||||
Sy = 0
|
||||
elseif VxAbs > 0.1 then
|
||||
Sy = math.deg(math.atan(Vy / VxAbs))
|
||||
Sy = Sy / 50
|
||||
else
|
||||
Sy = Vy * (0.003 / TICK_INTERVAL)
|
||||
end
|
||||
|
||||
Sy = Sy -- * 0.95
|
||||
Sy = math.clamp(Sy, -1, 1)
|
||||
local slipSign = Sy < 0 and -1 or 1
|
||||
local Fy = -slipSign * self.lateralFrictionPreset:evaluate(math.abs(Sy)) * Lc
|
||||
|
||||
return Sy, Fy
|
||||
return Sy, Fy
|
||||
end
|
||||
|
||||
function Wheel:slipCircle(Sx, Sy, Fx, Fy, slipCircleShape)
|
||||
local SxAbs = math.abs(Sx)
|
||||
local SxAbs = math.abs(Sx)
|
||||
|
||||
if SxAbs > 0.01 then
|
||||
local SxClamped = math.clamp(Sx, -1, 1)
|
||||
local SyClamped = math.clamp(Sy, -1, 1)
|
||||
local combinedSlip = Vector2(SxClamped * slipCircleShape, SyClamped)
|
||||
local slipDir = combinedSlip:getNormalized()
|
||||
if SxAbs > 0.01 then
|
||||
local SxClamped = math.clamp(Sx, -1, 1)
|
||||
local SyClamped = math.clamp(Sy, -1, 1)
|
||||
local combinedSlip = Vector2(
|
||||
SxClamped * slipCircleShape,
|
||||
SyClamped
|
||||
)
|
||||
local slipDir = combinedSlip:getNormalized()
|
||||
|
||||
local F = math.sqrt(Fx * Fx + Fy * Fy)
|
||||
local absSlipDirY = math.abs(slipDir.y)
|
||||
local F = math.sqrt(Fx * Fx + Fy * Fy)
|
||||
local absSlipDirY = math.abs(slipDir.y)
|
||||
|
||||
Fy = F * absSlipDirY * (Fy < 0 and -1 or 1)
|
||||
end
|
||||
Fy = F * absSlipDirY * (Fy < 0 and -1 or 1)
|
||||
end
|
||||
|
||||
return Sx, Sy, Fx, Fy
|
||||
return Sx, Sy, Fx, Fy
|
||||
end
|
||||
|
||||
function Wheel:selfAligningTorque(Sy, Fy, load)
|
||||
if math.abs(Sy) < 0.001 or load < 0.001 then
|
||||
return 0
|
||||
end
|
||||
function Wheel:selfAligningTorque(Sy, load)
|
||||
if math.abs(Sy) < 0.001 or load < 0.001 then
|
||||
return 0
|
||||
end
|
||||
|
||||
local B = self.aligningFrictionPreset.B
|
||||
local C = self.aligningFrictionPreset.C
|
||||
local D = self.aligningFrictionPreset.D
|
||||
local E = self.aligningFrictionPreset.E
|
||||
local B = self.aligningFrictionPreset.B
|
||||
local C = self.aligningFrictionPreset.C
|
||||
local D = self.aligningFrictionPreset.D
|
||||
local E = self.aligningFrictionPreset.E
|
||||
|
||||
local entityWheelDown = self.entity:localToWorld(Vector(0, 0, -self.radius * UNITS_PER_METER))
|
||||
local physWheelDown = self.entity:localToWorld(-self.up * self.radius * UNITS_PER_METER)
|
||||
local loadScale = load * 1000
|
||||
local mechanicalTrail = 0.15
|
||||
local casterEffect = math.tan(self.casterAngle)
|
||||
local effectiveTrail = mechanicalTrail + casterEffect * self.radius
|
||||
local D_scaled = D * loadScale * effectiveTrail
|
||||
|
||||
local mechanicalTrail = entityWheelDown:getDistance(physWheelDown) * UNITS_TO_METERS
|
||||
local pneumaticTrail = 0.2 * math.exp(-5 * math.abs(Sy))
|
||||
local camberEffect = 1 + 0.5 * math.abs(self.camberAngle)
|
||||
local toeEffect = 1 + 0.3 * math.abs(self.toeAngle)
|
||||
|
||||
local Mz = -Fy * (mechanicalTrail + pneumaticTrail)
|
||||
local term = B * Sy - E * (B * Sy - math.atan(B * Sy))
|
||||
local Mz = D_scaled * camberEffect * toeEffect * math.sin(C * math.atan(term))
|
||||
|
||||
if self.name == 'WheelFL' or self.name == 'WheelFR' then
|
||||
-- self.printDebounced(
|
||||
-- Color(255, 255, 0),
|
||||
-- string.format('[%s] ', self.name),
|
||||
-- Color(255, 255, 255),
|
||||
-- string.format('Mz: %.2f | Sy: %.2f | Fy: %.2f | mechanicalTrail: %.3f', Mz, Sy, Fy, mechanicalTrail)
|
||||
-- )
|
||||
end
|
||||
|
||||
return Mz -- TICK_INTERVAL
|
||||
return Mz
|
||||
end
|
||||
|
||||
function Wheel:rotateAxes()
|
||||
local ang = self.physObj:getAngles()
|
||||
function Wheel:rotateVector(vector, jopa)
|
||||
local ang = self.entity:getAngles()
|
||||
local baseForward = ang:getForward()
|
||||
local baseUp = ang:getUp()
|
||||
local baseRight = -ang:getRight()
|
||||
|
||||
-- Base directions
|
||||
---@type Vector
|
||||
local forward = ang:getForward() * self.direction
|
||||
local up = ang:getUp()
|
||||
---@type Vector
|
||||
local right = -ang:getRight() * self.direction
|
||||
local steerRotated = vector:rotateAroundAxis(baseUp, nil, math.rad(-self.steerAngle) + self.toeAngle)
|
||||
local camberRotated = steerRotated:rotateAroundAxis(baseForward, nil, -self.camberAngle)
|
||||
local casterRotated = camberRotated:rotateAroundAxis(baseRight, nil, -self.casterAngle)
|
||||
|
||||
-- Step 1: Steer + Toe (rotate forward/right around up)
|
||||
local steerAngle = -self.steerAngle
|
||||
forward = forward:rotateAroundAxis(up, steerAngle)
|
||||
right = right:rotateAroundAxis(up, steerAngle)
|
||||
|
||||
-- Step 2: Caster (rotate forward/up around right)
|
||||
local casterAngle = -self.casterAngle -- * self.direction
|
||||
forward = forward:rotateAroundAxis(right, nil, casterAngle)
|
||||
up = up:rotateAroundAxis(right, nil, casterAngle)
|
||||
|
||||
-- Step 3: Camber (rotate up/right around forward)
|
||||
local camberAngle = -self.camberAngle * self.direction
|
||||
up = up:rotateAroundAxis(forward, nil, camberAngle)
|
||||
right = right:rotateAroundAxis(forward, nil, camberAngle)
|
||||
|
||||
-- Normalize final vectors
|
||||
self.forward = forward:getNormalized()
|
||||
self.right = right:getNormalized() * self.direction
|
||||
self.up = up:getNormalized()
|
||||
return casterRotated:getNormalized()
|
||||
end
|
||||
|
||||
function Wheel:update()
|
||||
if not isValid(self.entity) or not isValid(self.physObj) then
|
||||
return
|
||||
end
|
||||
if not isValid(self.entity) or not isValid(self.physObj) then
|
||||
return
|
||||
end
|
||||
|
||||
local frictionSnapshot = self.physObj:getFrictionSnapshot()
|
||||
local externalStress, internalStress = self.physObj:getStress()
|
||||
local externalStress = self.physObj:getStress()
|
||||
local velocity = self.physObj:getVelocity() * UNITS_TO_METERS
|
||||
|
||||
self.hasHit = #frictionSnapshot > 0
|
||||
self.load = self.hasHit and externalStress * KG_TO_N or 0
|
||||
self.hasHit = externalStress ~= 0
|
||||
self.load = externalStress * KG_TO_KN
|
||||
|
||||
---@type PhysObj?
|
||||
local frictionSnapshotItem = self.hasHit and frictionSnapshot[1] or nil
|
||||
self.longitudinalLoadCoefficient = self:getLongitudinalLoadCoefficient(self.load * 1000)
|
||||
self.lateralLoadCoefficient = self:getLateralLoadCoefficient(self.load * 1000)
|
||||
|
||||
local surfaceVelocity = Vector(0, 0, 0)
|
||||
local ang = self.entity:getAngles()
|
||||
local baseForward = ang:getForward() * self.direction
|
||||
local baseUp = ang:getUp()
|
||||
local baseRight = -ang:getRight() * self.direction
|
||||
|
||||
self.forward = self:rotateVector(baseForward)
|
||||
self.right = self:rotateVector(baseRight)
|
||||
self.up = self:rotateVector(baseUp)
|
||||
|
||||
if frictionSnapshotItem ~= nil then
|
||||
surfaceVelocity = frictionSnapshotItem.Other:getVelocityAtPoint(frictionSnapshotItem.ContactPoint)
|
||||
end
|
||||
local forwardSpeed = 0
|
||||
local sideSpeed = 0
|
||||
|
||||
local velocity = (self.parentPhysObj:getVelocityAtPoint(self.entity:getPos()) - surfaceVelocity) * UNITS_TO_METERS
|
||||
-- local velocity = self.physObj:getVelocity() * UNITS_TO_METERS
|
||||
if self.hasHit then
|
||||
forwardSpeed = velocity:dot(self.forward)
|
||||
sideSpeed = velocity:dot(self.right)
|
||||
end
|
||||
|
||||
-- self.load = frictionSnapshotItem and frictionSnapshotItem.NormalForce / KG_TO_N or 0
|
||||
local W, Sx, Fx, CounterTq = self:stepLongitudinal(
|
||||
self.motorTorque,
|
||||
self.brakeTorque + self.rollingResistance,
|
||||
forwardSpeed,
|
||||
self.angularVelocity,
|
||||
self.longitudinalLoadCoefficient,
|
||||
self.radius,
|
||||
self.inertia,
|
||||
0.95,
|
||||
0.9
|
||||
)
|
||||
|
||||
self.longitudinalLoadCoefficient = self:getLongitudinalLoadCoefficient(self.load)
|
||||
self.lateralLoadCoefficient = self:getLateralLoadCoefficient(self.load)
|
||||
local Sy, Fy = self:stepLateral(
|
||||
forwardSpeed,
|
||||
sideSpeed,
|
||||
self.lateralLoadCoefficient,
|
||||
0.95,
|
||||
0.9
|
||||
|
||||
-- if self.name == 'WheelRL' or self.name == 'WheelRR' then
|
||||
-- self.printDebounced(
|
||||
-- Color(255, 255, 0),
|
||||
-- string.format('[%s] ', self.name),
|
||||
-- Color(255, 255, 255),
|
||||
-- string.format(
|
||||
-- 'Load: %.2f | LonLc: %.2f | LaLc: %.2f | externalStress: %.2f',
|
||||
-- self.load,
|
||||
-- self.longitudinalLoadCoefficient,
|
||||
-- self.lateralLoadCoefficient,
|
||||
-- externalStress
|
||||
-- )
|
||||
-- )
|
||||
-- end
|
||||
)
|
||||
|
||||
self:rotateAxes()
|
||||
Sx, Sy, Fx, Fy = self:slipCircle(
|
||||
Sx,
|
||||
Sy,
|
||||
Fx,
|
||||
Fy,
|
||||
1.05
|
||||
)
|
||||
|
||||
local forwardSpeed = 0
|
||||
local sideSpeed = 0
|
||||
self.mz = self:selfAligningTorque(Sy, self.load)
|
||||
|
||||
if self.hasHit then
|
||||
forwardSpeed = velocity:dot(self.forward)
|
||||
sideSpeed = velocity:dot(self.right)
|
||||
end
|
||||
|
||||
local W, Sx, Fx, CounterTq = self:stepLongitudinal(
|
||||
self.motorTorque,
|
||||
self.brakeTorque + self.rollingResistance,
|
||||
forwardSpeed,
|
||||
self.angularVelocity,
|
||||
self.longitudinalLoadCoefficient,
|
||||
self.radius,
|
||||
self.inertia
|
||||
)
|
||||
|
||||
local Sy, Fy = self:stepLateral(forwardSpeed, sideSpeed, self.lateralLoadCoefficient)
|
||||
|
||||
Sx, Sy, Fx, Fy = self:slipCircle(Sx, Sy, Fx, Fy, self.slipCircleShape)
|
||||
|
||||
-- Traction circle: ограничение по максимальной силе сцепления
|
||||
-- local mu = 1.0 -- коэффициент трения (может быть в пресете)
|
||||
-- local F_max = self.load * mu
|
||||
-- local F_total = math.sqrt(Fx * Fx + Fy * Fy)
|
||||
-- if F_total > F_max and F_total > 0 then
|
||||
-- local scale = F_max / F_total
|
||||
-- Fx = Fx * scale
|
||||
-- Fy = Fy * scale
|
||||
-- end
|
||||
|
||||
self.mz = self:selfAligningTorque(Sy, Fy, self.load)
|
||||
|
||||
self.angularVelocity = W
|
||||
self.counterTorque = CounterTq * self.direction
|
||||
self.forwardFriction.slip = Sx
|
||||
self.forwardFriction.force = Fx
|
||||
self.forwardFriction.speed = forwardSpeed
|
||||
self.sideFriction.slip = Sy
|
||||
self.sideFriction.force = Fy
|
||||
self.sideFriction.speed = sideSpeed
|
||||
self.angularVelocity = W
|
||||
self.counterTorque = CounterTq
|
||||
self.forwardFriction.slip = Sx
|
||||
self.forwardFriction.force = Fx
|
||||
self.forwardFriction.speed = forwardSpeed
|
||||
self.sideFriction.slip = Sy
|
||||
self.sideFriction.force = Fy
|
||||
self.sideFriction.speed = sideSpeed
|
||||
end
|
||||
|
||||
---@type fun(config: CustomWheelConfig): CustomWheel
|
||||
Wheel.new = Wheel.new
|
||||
|
||||
return Wheel
|
||||
|
||||
@ -1,28 +1,13 @@
|
||||
---@alias KPTLWirePortType
|
||||
---| "number"
|
||||
---| "normal"
|
||||
---| "string"
|
||||
---| "entity"
|
||||
|
||||
---@class KPTLWireComponent
|
||||
---@field wireInputs { [string]: KPTLWirePortType }
|
||||
---@field wireOutputs { [string]: KPTLWirePortType }
|
||||
local WireComponent = class('WireComponent')
|
||||
|
||||
---@return nil
|
||||
function WireComponent:initialize()
|
||||
if CLIENT then
|
||||
return
|
||||
end
|
||||
|
||||
self.wireInputs = {}
|
||||
self.wireOutputs = {}
|
||||
if CLIENT then return end
|
||||
|
||||
self.wireInputs = {}
|
||||
self.wireOutputs = {}
|
||||
end
|
||||
|
||||
---@return nil
|
||||
function WireComponent:onWirePortsReady() end
|
||||
|
||||
---@return nil
|
||||
function WireComponent:updateWireOutputs() end
|
||||
function WireComponent:updateWireOutputs()
|
||||
end
|
||||
|
||||
return WireComponent
|
||||
|
||||
@ -2,87 +2,88 @@
|
||||
--@include /libs/task.txt
|
||||
local Task = require('/libs/task.txt')
|
||||
|
||||
local Sound = class('Sound')
|
||||
local Sound = class("Sound")
|
||||
|
||||
local function map(x, a, b, c, d)
|
||||
return (x - a) / (b - a) * (d - c) + c
|
||||
return (x - a) / (b - a) * (d - c) + c
|
||||
end
|
||||
|
||||
local function fade(n, min, mid, max)
|
||||
if n < min or n > max then
|
||||
return 0
|
||||
end
|
||||
if n < min or n > max then
|
||||
return 0
|
||||
end
|
||||
|
||||
if n > mid then
|
||||
min = mid - (max - mid)
|
||||
end
|
||||
if n > mid then
|
||||
min = mid - (max - mid)
|
||||
end
|
||||
|
||||
return math.cos((1 - ((n - min) / (mid - min))) * (math.pi / 2))
|
||||
return math.cos((1 - ((n - min) / (mid - min))) * (math.pi / 2))
|
||||
end
|
||||
|
||||
function Sound:initialize(redline, parent, sounds)
|
||||
local sounds = sounds
|
||||
or {
|
||||
[900] = 'https://raw.githubusercontent.com/koptilnya/gmod-data/main/engine_sounds/bmw_s54/ext_e30s54_idle.ogg',
|
||||
[2500] = 'https://raw.githubusercontent.com/koptilnya/gmod-data/main/engine_sounds/bmw_s54/ext_e30s54_on_2500.ogg',
|
||||
[4000] = 'https://raw.githubusercontent.com/koptilnya/gmod-data/main/engine_sounds/bmw_s54/ext_e30s54_on_4000.ogg',
|
||||
[6750] = 'https://raw.githubusercontent.com/koptilnya/gmod-data/main/engine_sounds/bmw_s54/ext_e30s54_on_6750.ogg',
|
||||
[8500] = 'https://raw.githubusercontent.com/koptilnya/gmod-data/main/engine_sounds/bmw_s54/ext_e30s54_on_8500.ogg',
|
||||
}
|
||||
local redline = redline or 7000
|
||||
self.active = true
|
||||
local soundObjects = {}
|
||||
local soundRpms = {}
|
||||
local maxValue = 0
|
||||
local throttle = 0
|
||||
local engineRpm = 0
|
||||
local smoothRpm = 0
|
||||
local smoothThrottle = 0
|
||||
local sounds = sounds or {
|
||||
[900] = "https://raw.githubusercontent.com/koptilnya/gmod-data/main/engine_sounds/bmw_s54/ext_e30s54_idle.ogg",
|
||||
[2500] = "https://raw.githubusercontent.com/koptilnya/gmod-data/main/engine_sounds/bmw_s54/ext_e30s54_on_2500.ogg",
|
||||
[4000] = "https://raw.githubusercontent.com/koptilnya/gmod-data/main/engine_sounds/bmw_s54/ext_e30s54_on_4000.ogg",
|
||||
[6750] = "https://raw.githubusercontent.com/koptilnya/gmod-data/main/engine_sounds/bmw_s54/ext_e30s54_on_6750.ogg",
|
||||
[8500] = "https://raw.githubusercontent.com/koptilnya/gmod-data/main/engine_sounds/bmw_s54/ext_e30s54_on_8500.ogg"
|
||||
}
|
||||
local redline = redline or 7000
|
||||
self.active = false
|
||||
local soundObjects = {}
|
||||
local soundRpms = {}
|
||||
local maxValue = 0
|
||||
local throttle = 0
|
||||
local engineRpm = 0
|
||||
local smoothRpm = 0
|
||||
local smoothThrottle = 0
|
||||
|
||||
Task.run(function()
|
||||
for soundRpm, soundPath in pairs(sounds) do
|
||||
local sound = await * soundLoad(soundPath, '3d noblock noplay')
|
||||
soundObjects[soundRpm] = sound
|
||||
table.insert(soundRpms, soundRpm)
|
||||
if maxValue < soundRpm then
|
||||
maxValue = soundRpm
|
||||
end
|
||||
end
|
||||
Task.run(function()
|
||||
for soundRpm, soundPath in pairs(sounds) do
|
||||
local sound = await* soundLoad(soundPath, "3d noblock noplay")
|
||||
soundObjects[soundRpm] = sound
|
||||
table.insert(soundRpms,soundRpm)
|
||||
if maxValue < soundRpm then
|
||||
maxValue = soundRpm
|
||||
end
|
||||
end
|
||||
|
||||
table.sort(soundRpms)
|
||||
table.sort(soundRpms)
|
||||
|
||||
hook.add("think", table.address({}), function()
|
||||
if not self.active then
|
||||
return
|
||||
end
|
||||
|
||||
hook.add('think', table.address({}), function()
|
||||
if not self.active then
|
||||
return
|
||||
end
|
||||
smoothRpm = smoothRpm * (1 - 0.2) + engineRpm * 0.2
|
||||
smoothThrottle = smoothThrottle * (1 - 0.1) + throttle * 0.1
|
||||
|
||||
for n, rpm in ipairs(soundRpms) do
|
||||
if not soundObjects[rpm] or not soundObjects[rpm].Bass then
|
||||
goto CONTINUE
|
||||
end
|
||||
local min = n == 1 and -100000 or soundRpms[n - 1]
|
||||
local max = n == #soundRpms and 100000 or soundRpms[n + 1]
|
||||
local c = fade(smoothRpm, min - 10, rpm, max + 10)
|
||||
local vol = c * map(smoothThrottle, 0, 1, 0.5, 1)
|
||||
local soundObject = soundObjects[rpm].Bass
|
||||
soundObject:setVolume(vol)
|
||||
soundObject:setPitch(smoothRpm / rpm)
|
||||
soundObject:setPos(parent:getPos())
|
||||
soundObject:pause()
|
||||
soundObject:play()
|
||||
::CONTINUE::
|
||||
end
|
||||
end)
|
||||
end)
|
||||
|
||||
smoothRpm = smoothRpm * (1 - 0.2) + engineRpm * 0.2
|
||||
smoothThrottle = smoothThrottle * (1 - 0.1) + throttle * 0.1
|
||||
|
||||
for n, rpm in ipairs(soundRpms) do
|
||||
if not soundObjects[rpm] or not soundObjects[rpm].Bass then
|
||||
goto CONTINUE
|
||||
end
|
||||
local min = n == 1 and -100000 or soundRpms[n - 1]
|
||||
local max = n == #soundRpms and 100000 or soundRpms[n + 1]
|
||||
local c = fade(smoothRpm, min - 10, rpm, max + 10)
|
||||
local vol = c * map(smoothThrottle, 0, 1, 0.5, 1)
|
||||
local soundObject = soundObjects[rpm].Bass
|
||||
soundObject:setVolume(vol)
|
||||
soundObject:setPitch(smoothRpm / rpm)
|
||||
soundObject:setPos(parent:getPos())
|
||||
soundObject:pause()
|
||||
soundObject:play()
|
||||
::CONTINUE::
|
||||
end
|
||||
end)
|
||||
end)
|
||||
|
||||
net.receive('ENGINE_FULLRPM', function()
|
||||
local rpm = net.readUInt(16)
|
||||
engineRpm = rpm * (maxValue / redline)
|
||||
throttle = math.max(net.readFloat(), 0)
|
||||
end)
|
||||
net.receive("ENGINE_FULLRPM", function()
|
||||
local rpm = net.readUInt(16)
|
||||
engineRpm = rpm * (maxValue / redline)
|
||||
throttle = math.max(net.readFloat(), 0)
|
||||
end)
|
||||
end
|
||||
|
||||
return Sound
|
||||
|
||||
|
||||
|
||||
@ -1,86 +1,86 @@
|
||||
-- @name koptilnya/libs/utils
|
||||
function checkVarClass(var, class, message)
|
||||
if type(var) ~= 'table' or var['class'] == nil or not var:isInstanceOf(class) then
|
||||
throw(message == nil and 'Wrong variable class.' or message, 1, true)
|
||||
end
|
||||
if type(var) ~= "table" or var["class"] == nil or not var:isInstanceOf(class) then
|
||||
throw(message == nil and "Wrong variable class." or message, 1, true)
|
||||
end
|
||||
end
|
||||
|
||||
function accessorFunc(tbl, varName, name, defaultValue)
|
||||
tbl[varName] = defaultValue
|
||||
tbl['get' .. name] = function(self)
|
||||
return self[varName]
|
||||
end
|
||||
tbl['set' .. name] = function(self, value)
|
||||
self[varName] = value
|
||||
end
|
||||
tbl[varName] = defaultValue
|
||||
tbl["get" .. name] = function(self)
|
||||
return self[varName]
|
||||
end
|
||||
tbl["set" .. name] = function(self, value)
|
||||
self[varName] = value
|
||||
end
|
||||
end
|
||||
|
||||
function rotateAround(entity, pivot, angles)
|
||||
local pos = entity:getPos()
|
||||
local localPivotPos = entity:worldToLocal(pivot)
|
||||
local pos = entity:getPos()
|
||||
local localPivotPos = entity:worldToLocal(pivot)
|
||||
|
||||
entity:setAngles(angles)
|
||||
pos = pos + (pivot - entity:localToWorld(localPivotPos))
|
||||
entity:setPos(pos)
|
||||
entity:setAngles(angles)
|
||||
pos = pos + (pivot - entity:localToWorld(localPivotPos))
|
||||
entity:setPos(pos)
|
||||
end
|
||||
|
||||
function tobase(number, base)
|
||||
local ret = ''
|
||||
local ret = ""
|
||||
|
||||
if base < 2 or base > 36 or number == 0 then
|
||||
return '0'
|
||||
end
|
||||
if base < 2 or base > 36 or number == 0 then
|
||||
return "0"
|
||||
end
|
||||
|
||||
if base == 10 then
|
||||
return tostring(number)
|
||||
end
|
||||
if base == 10 then
|
||||
return tostring(number)
|
||||
end
|
||||
|
||||
local chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'
|
||||
local loops = 0
|
||||
while number > 0 do
|
||||
loops = loops + 1
|
||||
number, d = math.floor(number / base), (number % base) + 1
|
||||
ret = string.sub(chars, d, d) .. ret
|
||||
if loops > 32000 then
|
||||
break
|
||||
end
|
||||
end
|
||||
return ret
|
||||
local chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
local loops = 0
|
||||
while number > 0 do
|
||||
loops = loops + 1
|
||||
number, d = math.floor(number / base), (number % base) + 1
|
||||
ret = string.sub(chars, d, d) .. ret
|
||||
if (loops > 32000) then
|
||||
break
|
||||
end
|
||||
end
|
||||
return ret
|
||||
end
|
||||
|
||||
function bytesToHex(bytes)
|
||||
local hex = '0x'
|
||||
local hex = "0x"
|
||||
|
||||
for _, v in pairs(bytes) do
|
||||
hex = hex .. bit.tohex(v, 2)
|
||||
end
|
||||
for _, v in pairs(bytes) do
|
||||
hex = hex .. bit.tohex(v, 2)
|
||||
end
|
||||
|
||||
return hex
|
||||
return hex
|
||||
end
|
||||
|
||||
function byteTable(str, start, length)
|
||||
local result = {}
|
||||
local result = {}
|
||||
|
||||
for i = 1, length do
|
||||
result[i] = string.byte(str, i + start - 1)
|
||||
end
|
||||
for i = 1, length do
|
||||
result[i] = string.byte(str, i + start - 1)
|
||||
end
|
||||
|
||||
return result
|
||||
return result
|
||||
end
|
||||
|
||||
function isURL(str)
|
||||
local _1, _2, prefix = str:find('^(%w-):')
|
||||
local _1, _2, prefix = str:find("^(%w-):")
|
||||
|
||||
return prefix == 'http' or prefix == 'https' or prefix == 'data'
|
||||
return prefix == "http" or prefix == "https" or prefix == "data"
|
||||
end
|
||||
|
||||
function debounce(func, delay)
|
||||
local lastCall = 0
|
||||
return function(...)
|
||||
local now = timer.systime()
|
||||
if now - lastCall >= delay then
|
||||
lastCall = now
|
||||
return func(...)
|
||||
end
|
||||
end
|
||||
end
|
||||
local lastCall = 0
|
||||
return function(...)
|
||||
local now = timer.systime()
|
||||
if now - lastCall >= delay then
|
||||
lastCall = now
|
||||
return func(...)
|
||||
end
|
||||
end
|
||||
end
|
||||
Loading…
x
Reference in New Issue
Block a user