Насрал типами, но еще не до конца

This commit is contained in:
Никита Круглицкий 2025-05-16 07:11:48 +06:00
parent 9b50dbe33d
commit 84f591db59
18 changed files with 1629 additions and 1217 deletions

1
.gitattributes vendored Normal file
View File

@ -0,0 +1 @@
*.txt linguist-language=Lua

2
.gitignore vendored
View File

@ -3,4 +3,4 @@
!/koptilnya !/koptilnya
!README.md !README.md
!.gitignore !.gitignore
!.lua-format !.gitattributes

View File

@ -1,12 +0,0 @@
# 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

View File

@ -8,171 +8,267 @@ local Differential = require('/koptilnya/engine_remastered/powertrain/differenti
-- Michelin Pilot Sport 4S (High-Performance Road Tire) -- Michelin Pilot Sport 4S (High-Performance Road Tire)
local WheelConfig = { local WheelConfig = {
DEBUG = true, DEBUG = true,
BrakePower = 1200, BrakePower = 1200,
CustomWheel = { CustomWheel = {
Mass = 30, Mass = 30,
LateralFrictionPreset = { LateralFrictionPreset = {
B = 12.0, B = 12.0,
C = 1.3, C = 1.3,
D = 1.8, D = 1.8,
E = -1.8 E = -1.8,
}, },
LongitudinalFrictionPreset = { LongitudinalFrictionPreset = {
B = 18.0, B = 18.0,
C = 1.5, C = 1.5,
D = 1.5, D = 1.5,
E = 0.3 E = 0.3,
}, },
AligningFrictionPreset = { AligningFrictionPreset = {
B = 2.8, B = 2.8,
C = 2.1, C = 2.1,
D = 0.10, D = 0.10,
E = -2.5 E = -2.5,
}, },
-- AligningFrictionPreset = { -- AligningFrictionPreset = {
-- B = 13, -- B = 13,
-- C = 2.4, -- C = 2.4,
-- D = 1, -- D = 1,
-- E = 0.48 -- E = 0.48
-- } -- }
}, },
Model = 'models/sprops/trans/wheel_a/wheel25.mdl' Model = 'models/sprops/trans/wheel_a/wheel25.mdl',
} }
local FrontWheelsConfig = table.merge( local FrontWheelsConfig = table.merge(table.copy(WheelConfig), {
table.copy(WheelConfig), BrakePower = 1600,
{ CustomWheel = {
CustomWheel = { CasterAngle = 7,
CasterAngle = 7, CamberAngle = -3,
CamberAngle = -3, },
ToeAngle = 0.5 })
}
}
)
local RearWheelsConfig = table.merge( local RearWheelsConfig = table.merge(table.copy(WheelConfig), {
table.copy(WheelConfig), BrakePower = 600,
{ HandbrakePower = 2200,
HandbrakePower = 2200, CustomWheel = {
CustomWheel = { CamberAngle = -2,
CamberAngle = -2, -- LongitudinalFrictionPreset = {
ToeAngle = 0.5 -- B = 0.1,
} -- C = 1,
} -- D = 0.9,
) -- E = 0.9,
-- },
},
})
Vehicle:new({ Vehicle:new({
{ {
Name = 'Engine', Name = 'Engine',
Type = POWERTRAIN_COMPONENT.Engine, Type = POWERTRAIN_COMPONENT.Engine,
Config = { Config = {
IdleRPM = 900, IdleRPM = 900,
MaxRPM = 7000, MaxRPM = 7000,
Inertia = 0.151, Inertia = 0.151,
StartFriction = -10, StartFriction = -10,
FrictionCoeff = 0.01, FrictionCoeff = 0.01,
LimiterDuration = 0.06, LimiterDuration = 0.06,
TorqueMap = { 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, 118.89918444138,
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, 122.0751393736,
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, 125.25109430583,
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, 128.42704923806,
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, 131.60300417029,
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, 134.77895910251,
266.15795803778, 264.82291000376, 263.48786196974, 262.15281393572, 260.81776590169, 259.48271786767, 250.23203287321, 240.98134787874, 231.73066288428, 222.47997788981 137.95491403474,
} 141.13086896697,
} 144.3068238992,
}, 147.48277883143,
{ 150.65873376365,
Name = 'Clutch', 153.83468869588,
Input = 'Engine', 157.01064362811,
Type = POWERTRAIN_COMPONENT.Clutch, 160.18659856034,
Config = { 163.36255349256,
Inertia = 0.002, 166.53850842479,
SlipTorque = 1000 169.71446335702,
} 172.89041828925,
}, 176.06637322148,
{ 179.2423281537,
Name = 'Gearbox', 182.41828308593,
Input = 'Clutch', 185.59423801816,
Type = POWERTRAIN_COMPONENT.Gearbox, 188.77019295039,
Config = { 191.94614788261,
Type = 'MANUAL', 195.12210281484,
ShiftTime = 3, 198.29805774707,
Inertia = 0.01, 201.4740126793,
Ratios = { 3.626, 2.200, 1.541, 1.213, 1.000, 0.767 }, 204.64996761153,
Reverse = 3.437 207.82592254375,
} 211.00187747598,
}, 214.17783240821,
{ 217.35378734044,
Name = 'AxleFront', 220.52974227266,
Input = 'Axle', 223.70569720489,
Type = POWERTRAIN_COMPONENT.Differential, 226.88165213712,
Config = { 227.9621903219,
Type = Differential.TYPES.Open, 229.04272850669,
FinalDrive = 3.392, 230.12326669147,
Inertia = 0.01, 231.20380487625,
BiasAB = 0.5, 232.28434306104,
CoastRamp = 1, 233.36488124582,
PowerRamp = 1, 234.4454194306,
Stiffness = 0.1, 235.52595761538,
SlipTorque = 0, 236.60649580017,
SteerLock = 45 237.68703398495,
} 238.76757216973,
}, 239.84811035452,
{ 240.9286485393,
Name = 'WheelFL', 242.00918672408,
Type = POWERTRAIN_COMPONENT.Wheel, 243.08972490886,
Input = 'AxleFront', 244.17026309365,
Config = table.merge(table.copy(FrontWheelsConfig), { Offset = 0 }) 245.25080127843,
}, 246.33133946321,
{ 247.411877648,
Name = 'WheelFR', 248.49241583278,
Type = POWERTRAIN_COMPONENT.Wheel, 249.57295401756,
Input = 'AxleFront', 250.65349220234,
Config = table.merge(table.copy(FrontWheelsConfig), { Offset = 180 }) 251.73403038713,
}, 252.81456857191,
{ 253.89510675669,
Name = 'AxleBack', 254.97564494148,
Input = 'Axle', 256.05618312626,
Type = POWERTRAIN_COMPONENT.Differential, 257.13672131104,
Config = { 258.21725949582,
Type = Differential.TYPES.Open, 259.29779768061,
FinalDrive = 3.392, 260.37833586539,
Inertia = 0.01, 261.29278066787,
BiasAB = 0.5, 262.20722547034,
CoastRamp = 1, 263.12167027282,
PowerRamp = 1, 264.0361150753,
Stiffness = 0.1, 264.95055987777,
SlipTorque = 0, 265.86500468025,
} 266.77944948273,
}, 267.6938942852,
{ 268.60833908768,
Name = 'Axle', 269.52278389016,
Input = 'Gearbox', 270.43722869263,
Type = POWERTRAIN_COMPONENT.Differential, 271.35167349511,
Config = { 272.26611829758,
Type = Differential.TYPES.Open, 273.18056310006,
FinalDrive = 1, 274.09500790254,
Inertia = 0.01, 275.00945270501,
BiasAB = 0.5, 275.92389750749,
CoastRamp = 1, 276.83834230997,
PowerRamp = 1, 275.50329427594,
Stiffness = 1, 274.16824624192,
SlipTorque = 0 272.8331982079,
} 271.49815017388,
}, 270.16310213985,
{ 268.82805410583,
Name = 'WheelRL', 267.49300607181,
Input = 'AxleBack', 266.15795803778,
Type = POWERTRAIN_COMPONENT.Wheel, 264.82291000376,
Config = table.merge(table.copy(RearWheelsConfig), { Offset = 0 }) 263.48786196974,
}, 262.15281393572,
{ 260.81776590169,
Name = 'WheelRR', 259.48271786767,
Input = 'AxleBack', 250.23203287321,
Type = POWERTRAIN_COMPONENT.Wheel, 240.98134787874,
Config = table.merge(table.copy(RearWheelsConfig), { Offset = 180 }) 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 = 'Gearbox',
Type = POWERTRAIN_COMPONENT.Differential,
Config = {
Type = Differential.TYPES.Open,
FinalDrive = 3.392,
Inertia = 0.01,
BiasAB = 0.5,
CoastRamp = 1,
PowerRamp = 1,
Stiffness = 0.9,
SlipTorque = 1000,
ToeAngle = 0.5,
},
},
{
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 = 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 }),
},
}) })

View File

@ -1,85 +1,83 @@
--@client --@client
local fontArial92 = render.createFont("Arial", 92, 250, true, false, false, false, 0, false, 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) local fontArial46 = render.createFont('Arial', 46, 250, true, false, false, false, 0, false, 0)
ENGINE_RPM, CAR_SPEED, GEARBOX_GEAR = 0, 123, 0 ENGINE_RPM, CAR_SPEED, GEARBOX_GEAR = 0, 'SPD', 0
local resx, resy = render.getGameResolution() local resx, resy = render.getGameResolution()
local linesmatrix = Matrix() local linesmatrix = Matrix()
local linesposx, linesposy = resx - 200, resy - 150 local linesposx, linesposy = resx - 200, resy - 150
linesmatrix:setTranslation(Vector(linesposx, linesposy, 0)) linesmatrix:setTranslation(Vector(linesposx, linesposy, 0))
linesmatrix:setAngles(Angle(0, 15, 0)) linesmatrix:setAngles(Angle(0, 15, 0))
linesmatrix:setScale(Vector(0.7)) linesmatrix:setScale(Vector(0.7))
hook.add("DrawHud", "CARHUD", function() hook.add('DrawHud', 'CARHUD', function()
render.pushMatrix(linesmatrix) render.pushMatrix(linesmatrix)
render.enableScissorRect(linesposx - 50, linesposy + 30, linesposx + 208, linesposy + 50) render.enableScissorRect(linesposx - 50, linesposy + 30, linesposx + 208, linesposy + 50)
for x = 0, 208, 4 do 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
if ENGINE_RPM > x / 220 then render.drawRectFast(x, -x / 2, 2, 150)
col = 55 + x / 220 * 200 end
render.setRGBA(col, col, col, 250) render.disableScissorRect()
else render.enableScissorRect(linesposx - 50, linesposy + 20, linesposx + 212, linesposy + 50)
render.setRGBA(100, 100, 100, 250) for x = 212, 220, 4 do
end if ENGINE_RPM > x / 220 then
render.setRGBA(200, 71, 71, 200)
else
render.setRGBA(100, 100, 100, 200)
end
render.drawRectFast(x, -x / 2, 2, 150) render.drawRectFast(x, -x / 2, 2, 150)
end end
render.disableScissorRect() render.disableScissorRect()
render.enableScissorRect(linesposx - 50, linesposy + 20, linesposx + 212, linesposy + 50) render.enableScissorRect(linesposx - 50, linesposy + 20, linesposx + 208, linesposy + 25)
for x = 212, 220, 4 do render.drawRectFast(0, -256, 208, 512)
render.disableScissorRect()
render.popMatrix()
render.setRGBA(151, 151, 151, 220)
if ENGINE_RPM > x / 220 then render.setFont(fontArial92)
render.setRGBA(200, 71, 71, 200) local str = string.rep('0', 3 - #tostring(CAR_SPEED)) .. CAR_SPEED
else render.setRGBA(51, 51, 51, 256)
render.setRGBA(100, 100, 100, 200)
end
render.drawRectFast(x, -x / 2, 2, 150) for k = 1, 3 do
end local num = string.sub(str, k, k)
render.disableScissorRect() if num ~= '0' then
render.enableScissorRect(linesposx - 50, linesposy + 20, linesposx + 208, linesposy + 25) render.setRGBA(255, 255, 255, 250)
render.drawRectFast(0, -256, 208, 512) end
render.disableScissorRect() render.drawText(linesposx - 60 + k * 46, resy - 130 - 80, num)
render.popMatrix() end
render.setRGBA(151, 151, 151, 220) render.setFont(fontArial46)
render.setRGBA(200, 51, 51, 256)
render.setFont(fontArial92) local t = 'N'
local str = string.rep("0", 3 - #tostring(CAR_SPEED)) .. CAR_SPEED if GEARBOX_GEAR == -1 then
render.setRGBA(51, 51, 51, 256) t = 'R'
elseif GEARBOX_GEAR == 0 then
for k = 1, 3 do t = 'N'
local num = string.sub(str, k, k) else
if num ~= "0" then t = GEARBOX_GEAR
render.setRGBA(255, 255, 255, 250) end
end render.drawText(linesposx - 35 + 164, resy - 130 - 40, t)
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) end)
net.receive("ENGINE_RPM", function() net.receive('ENGINE_RPM', function()
local rpm = net.readFloat() local rpm = net.readFloat()
if rpm then if rpm then
ENGINE_RPM = rpm ENGINE_RPM = rpm
end end
end) end)
net.receive("CAR_SPEED", function() net.receive('CAR_SPEED', function()
local speed = net.readUInt(12) local speed = net.readUInt(12)
if speed then if speed then
CAR_SPEED = math.clamp(speed, 0, 999) CAR_SPEED = math.clamp(speed, 0, 999)
end end
end)
net.receive('GEARBOX_GEAR', function()
local gear = net.readInt(5)
if gear then
GEARBOX_GEAR = gear
end
end) end)
net.receive("GEARBOX_GEAR", function()
local gear = net.readInt(5)
if gear then
GEARBOX_GEAR = gear
end
end)

View File

@ -5,68 +5,87 @@ local PowertrainComponent = require('./powertrain_component.txt')
require('/koptilnya/libs/constants.txt') require('/koptilnya/libs/constants.txt')
---@class ClutchConfig: PowertrainComponentConfig
---@field SlipTorque? number
---@class Clutch: PowertrainComponent
---@field slipTorque number
local Clutch = class('Clutch', PowertrainComponent) local Clutch = class('Clutch', PowertrainComponent)
---@private
---@param vehicle KPTLVehicle
---@param name string
---@param config ClutchConfig
function Clutch:initialize(vehicle, name, config) 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 = { self.wireInputs = {
Clutch = 'number' Clutch = 'number',
} }
self.wireOutputs = { self.wireOutputs = {
Clutch_Torque = 'number' Clutch_Torque = 'number',
} }
self.inertia = config.Inertia or 0.1 self.inertia = config.Inertia or 0.1
self.slipTorque = config.SlipTorque or 1000 self.slipTorque = config.SlipTorque or 1000
end end
---@return nil
function Clutch:updateWireOutputs() function Clutch:updateWireOutputs()
PowertrainComponent.updateWireOutputs(self) PowertrainComponent.updateWireOutputs(self)
wire.ports.Clutch_Torque = self.torque wire.ports.Clutch_Torque = self.torque
end end
---@return number
function Clutch:getPress() function Clutch:getPress()
return 1 - wire.ports.Clutch return 1 - wire.ports.Clutch
end end
---@return number
function Clutch:queryInertia() function Clutch:queryInertia()
if self.output == nil then if self.output == nil then
return self.inertia return self.inertia
end end
return self.inertia + self.output:queryInertia() * self:getPress() return self.inertia + self.output:queryInertia() * self:getPress()
end end
---@param angularVelocity number
---@return number
function Clutch:queryAngularVelocity(angularVelocity) function Clutch:queryAngularVelocity(angularVelocity)
self.angularVelocity = angularVelocity self.angularVelocity = angularVelocity
if self.output == nil then if self.output == nil then
return angularVelocity return angularVelocity
end end
local outputW = self.output:queryAngularVelocity(angularVelocity) * self:getPress() local outputW = self.output:queryAngularVelocity(angularVelocity) * self:getPress()
local inputW = angularVelocity * (1 - self:getPress()) local inputW = angularVelocity * (1 - self:getPress())
return outputW + inputW return outputW + inputW
end end
---@param torque number
---@param inertia number
---@return number
function Clutch:forwardStep(torque, inertia) function Clutch:forwardStep(torque, inertia)
if self.output == nil then if self.output == nil then
return torque return torque
end end
local press = self:getPress() local press = self:getPress()
self.torque = math.clamp(torque, -self.slipTorque, self.slipTorque) self.torque = math.clamp(torque, -self.slipTorque, self.slipTorque)
self.torque = self.torque * (1 - (1 - math.pow(press, 0.3))) 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 end
return Clutch return Clutch

View File

@ -2,236 +2,295 @@
--@include ./powertrain_component.txt --@include ./powertrain_component.txt
local PowertrainComponent = require('./powertrain_component.txt') local PowertrainComponent = require('./powertrain_component.txt')
local WheelComponent = require('./wheel.txt')
require('/koptilnya/libs/constants.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) local Differential = class('Differential', PowertrainComponent)
function Differential.getSplitStrategy(type) ---@private
return SPLIT_STRATEGIES[type] or SPLIT_STRATEGIES[Differential.TYPES.Open] ---@param vehicle KPTLVehicle
end ---@param name string
---@param config DifferentialConfig
function Differential:initialize(vehicle, name, config) 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 = { self.wireOutputs = {
[string.format('%s_Torque', self.name)] = 'number' [string.format('%s_Torque', self.name)] = 'number',
} [string.format('%s_W', self.name)] = 'number',
[string.format('%s_MzDiff', self.name)] = 'number',
}
self.finalDrive = config.FinalDrive or 4 self.finalDrive = config.FinalDrive or 4
self.inertia = config.Inertia or 0.2 self.inertia = config.Inertia or 0.2
self.biasAB = config.Bias or 0.5 self.biasAB = config.Bias or 0.5
self.coastRamp = config.CoastRamp or 0.5 self.coastRamp = config.CoastRamp or 0.5
self.powerRamp = config.PowerRamp or 1 self.powerRamp = config.PowerRamp or 1
self.preload = config.Preload or 10 self.preload = config.Preload or 10
self.stiffness = config.Stiffness or 0.1 self.stiffness = config.Stiffness or 0.1
self.slipTorque = config.SlipTorque or 1000 self.slipTorque = config.SlipTorque or 1000
self.splitStrategy = Differential.getSplitStrategy(config.Type or Differential.TYPES.Open) self.splitStrategy = Differential.getSplitStrategy(config.Type or DIFFERENTIAL_TYPES.Open)
self.steerLock = config.SteerLock or 0 self.steerLock = config.SteerLock or 0
self.steerAngle = 0 self.toeAngle = config.ToeAngle or 0
self.steerAngle = 0
self.mzDiff = 0
end end
---@param component PowertrainComponent
---@return nil
function Differential:linkComponent(component) function Differential:linkComponent(component)
if not component:isInstanceOf(PowertrainComponent) then ---@diagnostic disable-next-line: undefined-field
return if not component:isInstanceOf(PowertrainComponent) then
end return
end
if self.outputA == nil then if self.outputA == nil then
self.outputA = component self.outputA = component
component.input = self component.input = self
elseif self.outputB == nil then elseif self.outputB == nil then
self.outputB = component self.outputB = component
component.input = self component.input = self
end end
end end
---@return nil
function Differential:updateWireOutputs() function Differential:updateWireOutputs()
PowertrainComponent.updateWireOutputs(self) PowertrainComponent.updateWireOutputs(self)
wire.ports[string.format('%s_Torque', self.name)] = self.torque
if self.outputA ~= nil then wire.ports[string.format('%s_Torque', self.name)] = self.torque
self.outputA:updateWireOutputs() wire.ports[string.format('%s_W', self.name)] = self.angularVelocity
end wire.ports[string.format('%s_MzDiff', self.name)] = self.mzDiff
if self.outputB ~= nil then if self.outputA ~= nil then
self.outputB:updateWireOutputs() self.outputA:updateWireOutputs()
end end
if self.outputB ~= nil then
self.outputB:updateWireOutputs()
end
end end
---@return number
function Differential:queryInertia() function Differential:queryInertia()
if self.outputA == nil or self.outputB == nil then if self.outputA == nil or self.outputB == nil then
return self.inertia return self.inertia
end 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 end
---@return number
function Differential:queryAngularVelocity(angularVelocity) function Differential:queryAngularVelocity(angularVelocity)
self.angularVelocity = angularVelocity self.angularVelocity = angularVelocity
if self.outputA == nil or self.outputB == nil then if self.outputA == nil or self.outputB == nil then
return angularVelocity return angularVelocity
end end
local aW = self.outputA:queryAngularVelocity(angularVelocity) local aW = self.outputA:queryAngularVelocity(angularVelocity)
local bW = self.outputB:queryAngularVelocity(angularVelocity) local bW = self.outputB:queryAngularVelocity(angularVelocity)
return (aW + bW) * self.finalDrive * 0.5 return (aW + bW) * self.finalDrive * 0.5
end end
---@param torque number
---@param inertia number
---@return number
function Differential:forwardStep(torque, inertia) function Differential:forwardStep(torque, inertia)
if self.outputA == nil or self.outputB == nil then if self.outputA == nil or self.outputB == nil then
return torque return torque
end end
local aW = self.outputA:queryAngularVelocity(self.angularVelocity) local aW = self.outputA:queryAngularVelocity(self.angularVelocity)
local bW = self.outputB:queryAngularVelocity(self.angularVelocity) local bW = self.outputB:queryAngularVelocity(self.angularVelocity)
local aI = self.outputA:queryInertia() local aI = self.outputA:queryInertia()
local bI = self.outputB:queryInertia() local bI = self.outputB:queryInertia()
self.torque = torque * self.finalDrive self.torque = torque * self.finalDrive
local tqA, tqB = self:splitStrategy( local tqA, tqB = self.splitStrategy(
self.torque, self.torque,
aW, aW,
bW, bW,
aI, aI,
bI, bI,
self.biasAB, self.biasAB,
self.preload, self.preload,
self.stiffness, self.stiffness,
self.powerRamp, self.powerRamp,
self.coastRamp, self.coastRamp,
self.slipTorque self.slipTorque
) )
tqA = self.outputA:forwardStep(tqA, inertia * 0.5 * math.pow(self.finalDrive, 2) + aI) 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) tqB = self.outputB:forwardStep(tqB, inertia * 0.5 * math.pow(self.finalDrive, 2) + bI)
-- // REFACTOR -- // REFACTOR
if self.steerLock ~= 0 then if self.steerLock ~= 0 then
local steerInertia = (aI + bI) / 2 local outputA = self.outputA --[[@as Wheel]]
local driverInput = 5 local outputB = self.outputB --[[@as Wheel]]
local localVelocityLength = chip():getVelocity():getLength() local steerInertia = (aI + bI) / 2
local MPH = localVelocityLength * (15 / 352) local driverInput = 15
local KPH = MPH * 1.609
local assist = math.clamp(10.0 / math.sqrt(KPH / 3), 2.0, 10.0) local localVelocityLength = chip():getVelocity():getLength()
local inputForce = driverInput * assist local MPH = localVelocityLength * (15 / 352)
local KPH = MPH * 1.609
local maxSteerSpeed = math.rad(1337) local assist = math.clamp(10.0 / math.sqrt(KPH / 3), 2.0, 10.0)
local inputTorque = self.vehicle.steer * inputForce local inputForce = driverInput * assist
local avgSteerAngle = (self.outputA.customWheel.steerAngle + self.outputB.customWheel.steerAngle) / 2 local inputTorque = self.vehicle.steer * inputForce
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 steerTorque = mzDiff / 2 * -1 + inputTorque local mzDiff = outputA.customWheel.mz - outputB.customWheel.mz
local steerAngularAccel = steerTorque / steerInertia self.mzDiff = mzDiff
self.steerAngle = math.clamp( local steerTorque = mzDiff * -1 + inputTorque
self.steerAngle + steerAngularAccel * TICK_INTERVAL,
-self.steerLock,
self.steerLock
)
local wheelbase = 2.05 local steerAngularAccel = steerTorque / steerInertia
local trackWidth = 1.124
local radius = wheelbase / math.tan(math.rad(self.steerAngle))
local innerAngle = math.deg(math.atan(wheelbase / (radius - (trackWidth / 2)))) self.steerAngle = math.clamp(self.steerAngle + steerAngularAccel * TICK_INTERVAL, -self.steerLock, self.steerLock)
local outerAngle = math.deg(math.atan(wheelbase / (radius + (trackWidth / 2))))
self.outputA.customWheel.steerAngle = outerAngle local wheelbase = 2.05
self.outputB.customWheel.steerAngle = innerAngle local trackWidth = 1.124
end local radius = wheelbase / math.tan(math.rad(self.steerAngle))
return tqA + tqB 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
return tqA + tqB
end end
function Differential:_openDiffTorqueSplit(tq, aW, bW, aI, bI, biasAB, preload, stiffness, powerRamp, coastRamp, slipTorque) ---@return number, number
return tq * (1 - biasAB), tq * biasAB; local function _openDiffTorqueSplit(tq, aW, bW, aI, bI, biasAB, preload, stiffness, powerRamp, coastRamp, slipTorque)
return tq * (1 - biasAB), tq * biasAB
end end
function Differential:_lockingDiffTorqueSplit(tq, aW, bW, aI, bI, biasAB, preload, stiffness, powerRamp, coastRamp, slipTorque) ---@return number, number
Ta = tq * (1 - biasAB); local function _lockingDiffTorqueSplit(tq, aW, bW, aI, bI, biasAB, preload, stiffness, powerRamp, coastRamp, slipTorque)
Tb = tq * biasAB; aTq = tq * 0.5
bTq = tq * 0.5
local syncTorque = (aW - bW) * stiffness * (aI + bI) * 0.5 / TICK_INTERVAL; local syncTorque = (aW - bW) * stiffness * (aI + bI) * 0.5 / TICK_INTERVAL
Ta = Ta - syncTorque; aTq = aTq - syncTorque
Tb = Tb + syncTorque; bTq = bTq + syncTorque
return Ta, Tb return aTq, bTq
-- local sumI = aI + bI -- local sumI = aI + bI
-- local w = aI / sumI * aW + bI / sumI * bW -- local w = aI / sumI * aW + bI / sumI * bW
-- local aTqCorr = (w - aW) * aI -- / dt -- local aTqCorr = (w - aW) * aI -- / dt
-- aTqCorr = aTqCorr * stiffness -- aTqCorr = aTqCorr * stiffness
-- local bTqCorr = (w - bW) * bI -- / dt -- local bTqCorr = (w - bW) * bI -- / dt
-- bTqCorr = bTqCorr * stiffness -- bTqCorr = bTqCorr * stiffness
-- local biasA = math.clamp(0.5 + (bW - aW) * 0.1 * stiffness, 0, 1) -- 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 end
function Differential:_VLSDTorqueSplit(tq, aW, bW, aI, bI, biasAB, preload, stiffness, powerRamp, coastRamp, slipTorque) ---@return number, number
if aW < 0 or bW < 0 then local function _VLSDTorqueSplit(tq, aW, bW, aI, bI, biasAB, preload, stiffness, powerRamp, coastRamp, slipTorque)
return tq * (1 - biasAB), tq * biasAB if aW < 0 or bW < 0 then
end return tq * (1 - biasAB), tq * biasAB
end
local c = tq > 0 and powerRamp or coastRamp local c = tq > 0 and powerRamp or coastRamp
local totalW = math.abs(aW) + math.abs(bW) local totalW = math.abs(aW) + math.abs(bW)
local slip = 0 local slip = 0
if aW > 0 or bW > 0 then if aW > 0 or bW > 0 then
slip = (aW - bW) / totalW slip = (aW - bW) / totalW
end end
local dTq = slip * stiffness * c * slipTorque local dTq = slip * stiffness * c * slipTorque
return tq * (1 - biasAB) - dTq, tq * biasAB + dTq return tq * (1 - biasAB) - dTq, tq * biasAB + dTq
end end
function Differential:_HLSDTorqueSplit(tq, aW, bW, aI, bI, biasAB, preload, stiffness, powerRamp, coastRamp, slipTorque) ---@return number, number
if aW < 0 or bW < 0 then local function _HLSDTorqueSplit(tq, aW, bW, aI, bI, biasAB, preload, stiffness, powerRamp, coastRamp, slipTorque)
return tq * (1 - biasAB), tq * biasAB if aW < 0 or bW < 0 then
end return tq * (1 - biasAB), tq * biasAB
end
local c = tq > 0 and powerRamp or coastRamp local c = tq > 0 and powerRamp or coastRamp
local tqFactor = math.clamp(math.abs(tq) / slipTorque, -1, 1) local tqFactor = math.clamp(math.abs(tq) / slipTorque, -1, 1)
local totalW = math.abs(aW) + math.abs(bW) local totalW = math.abs(aW) + math.abs(bW)
local slip = 0 local slip = 0
if aW > 0 or bW > 0 then if aW > 0 or bW > 0 then
slip = (aW - bW) / totalW slip = (aW - bW) / totalW
end end
local dTq = slip * stiffness * c * slipTorque * tqFactor local dTq = slip * stiffness * c * slipTorque * tqFactor
return tq * (1 - biasAB) - dTq, tq * biasAB + dTq return tq * (1 - biasAB) - dTq, tq * biasAB + dTq
end end
Differential.TYPES = { ---@enum DIFFERENTIAL_TYPE
Open = 'Open', DIFFERENTIAL_TYPES = {
Locking = 'Locking', Open = 'Open',
VLSD = 'VLSD', Locking = 'Locking',
HLSD = 'HLSD', VLSD = 'VLSD',
HLSD = 'HLSD',
} }
SPLIT_STRATEGIES = { ---@type { [DIFFERENTIAL_TYPE]: DifferentialSplitStrategyFn }
[Differential.TYPES.Open] = Differential._openDiffTorqueSplit, local SPLIT_STRATEGIES = {
[Differential.TYPES.Locking] = Differential._lockingDiffTorqueSplit, [DIFFERENTIAL_TYPES.Open] = _openDiffTorqueSplit,
[Differential.TYPES.VLSD] = Differential._VLSDTorqueSplit, [DIFFERENTIAL_TYPES.Locking] = _lockingDiffTorqueSplit,
[Differential.TYPES.HLSD] = Differential._HLSDTorqueSplit, [DIFFERENTIAL_TYPES.VLSD] = _VLSDTorqueSplit,
[DIFFERENTIAL_TYPES.HLSD] = _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
return Differential return Differential

View File

@ -3,130 +3,165 @@
local PowertrainComponent = require('./powertrain_component.txt') local PowertrainComponent = require('./powertrain_component.txt')
require('/koptilnya/libs/constants.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) local Engine = class('Engine', PowertrainComponent)
---@private
---@param vehicle KPTLVehicle
---@param name string
---@param config EngineConfig
function Engine:initialize(vehicle, name, config) function Engine:initialize(vehicle, name, config)
PowertrainComponent.initialize(self, vehicle, name, config) PowertrainComponent.initialize(self, vehicle, name, config)
self.wireInputs = { self.wireInputs = {
Throttle = 'number' Throttle = 'number',
} }
self.wireOutputs = { self.wireOutputs = {
Engine_RPM = 'number', Engine_RPM = 'number',
Engine_Torque = 'number', Engine_Torque = 'number',
Engine_MasterThrottle = 'number', Engine_MasterThrottle = 'number',
Engine_ReactTorque = 'number', Engine_ReactTorque = 'number',
Engine_ReturnedTorque = 'number', Engine_ReturnedTorque = 'number',
Engine_FinalTorque = 'number' Engine_FinalTorque = 'number',
} }
self.idleRPM = config.IdleRPM or 900 self.idleRPM = config.IdleRPM or 900
self.maxRPM = config.MaxRPM or 7000 self.maxRPM = config.MaxRPM or 7000
self.inertia = config.Inertia or 0.4 self.inertia = config.Inertia or 0.4
self.startFriction = config.StartFriction or -50 self.startFriction = config.StartFriction or -50
self.frictionCoeff = config.FrictionCoeff or 0.02 self.frictionCoeff = config.FrictionCoeff or 0.02
self.limiterDuration = config.LimiterDuration or 0.05 self.limiterDuration = config.LimiterDuration or 0.05
self.torqueMap = config.TorqueMap or {} self.torqueMap = config.TorqueMap or {}
self.friction = 0 self.friction = 0
self.masterThrottle = 0 self.masterThrottle = 0
self.limiterTimer = 0 self.limiterTimer = 0
self.reactTorque = 0 self.finalTorque = 0
self.returnedTorque = 0 self.reactTorque = 0
self.returnedTorque = 0
if CLIENT then if CLIENT then
--@include /koptilnya/engine_sound_2.txt -- --@include /koptilnya/engine_sound_2.txt
local Sound = require('/koptilnya/engine_sound_2.txt') -- local Sound = require('/koptilnya/engine_sound_2.txt')
Sound(config.MaxRPM or 7000, chip()) -- Sound(self.maxRPM, chip())
end end
end end
---@return nil
function Engine:updateWireOutputs() function Engine:updateWireOutputs()
PowertrainComponent.updateWireOutputs(self) PowertrainComponent.updateWireOutputs(self)
wire.ports.Engine_RPM = self:getRPM() wire.ports.Engine_RPM = self:getRPM()
wire.ports.Engine_Torque = self.torque wire.ports.Engine_Torque = self.torque
wire.ports.Engine_MasterThrottle = self.masterThrottle wire.ports.Engine_MasterThrottle = self.masterThrottle
wire.ports.Engine_ReactTorque = self.reactTorque wire.ports.Engine_ReactTorque = self.reactTorque
wire.ports.Engine_ReturnedTorque = self.returnedTorque wire.ports.Engine_ReturnedTorque = self.returnedTorque
wire.ports.Engine_FinalTorque = self.finalTorque wire.ports.Engine_FinalTorque = self.finalTorque
end end
---@return number
function Engine:getThrottle() function Engine:getThrottle()
return wire.ports.Throttle return wire.ports.Throttle
end end
function Engine:_generateTorque() ---@private
local throttle = self:getThrottle() ---@return number
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) local rpmFrac = math.clamp((rpm - self.idleRPM) / (self.maxRPM - self.idleRPM), 0, 1)
self.friction = self.startFriction - self:getRPM() * self.frictionCoeff self.friction = self.startFriction - self:getRPM() * self.frictionCoeff
local tqIdx = math.clamp(math.floor(rpmFrac * #self.torqueMap), 1, #self.torqueMap) local tqIdx = math.clamp(math.floor(rpmFrac * #self.torqueMap), 1, #self.torqueMap)
local maxInitialTorque = self.torqueMap[tqIdx] - self.friction local maxInitialTorque = self.torqueMap[tqIdx] - self.friction
local idleFadeStart = math.clamp(math.remap(rpm, self.idleRPM - 300, self.idleRPM, 1, 0), 0, 1) 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 idleFadeEnd = math.clamp(math.remap(rpm, self.idleRPM, self.idleRPM + 600, 1, 0), 0, 1)
local additionalEnergySupply = idleFadeEnd * (-self.friction / maxInitialTorque) + idleFadeStart local additionalEnergySupply = idleFadeEnd * (-self.friction / maxInitialTorque) + idleFadeStart
if rpm > self.maxRPM then if rpm > self.maxRPM then
throttle = 0 throttle = 0
self.limiterTimer = timer.systime() self.limiterTimer = timer.systime()
else else
throttle = timer.systime() >= self.limiterTimer + self.limiterDuration and throttle or 0 throttle = timer.systime() >= self.limiterTimer + self.limiterDuration and throttle or 0
end 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 end
---@param torque number
---@param inertia number
---@return number
function Engine:forwardStep(torque, inertia) function Engine:forwardStep(torque, inertia)
if self.output == nil then if self.output == nil then
local generatedTorque = self:_generateTorque() local generatedTorque = self:generateTorque()
self.angularVelocity = self.angularVelocity + generatedTorque / self.inertia * TICK_INTERVAL self.angularVelocity = self.angularVelocity + generatedTorque / self.inertia * TICK_INTERVAL
self.angularVelocity = math.max(self.angularVelocity, 0) self.angularVelocity = math.max(self.angularVelocity, 0)
end
local outputInertia = self.output:queryInertia() return 0
local inertiaSum = self.inertia + outputInertia end
local outputW = self.output:queryAngularVelocity(self.angularVelocity)
local targetW = self.inertia / inertiaSum * self.angularVelocity + outputInertia / inertiaSum * outputW
local generatedTorque = self:_generateTorque() local outputInertia = self.output:queryInertia()
local reactTorque = (targetW - self.angularVelocity) * self.inertia / TICK_INTERVAL local inertiaSum = self.inertia + outputInertia
local returnedTorque = self.output:forwardStep(torque + generatedTorque - reactTorque, inertia + self.inertia) local outputW = self.output:queryAngularVelocity(self.angularVelocity)
local targetW = self.inertia / inertiaSum * self.angularVelocity + outputInertia / inertiaSum * outputW
self.reactTorque = reactTorque local generatedTorque = self:generateTorque()
self.returnedTorque = returnedTorque local reactTorque = (targetW - self.angularVelocity) * self.inertia / TICK_INTERVAL
local returnedTorque = self.output:forwardStep(generatedTorque - reactTorque, 0)
local finalTorque = generatedTorque + reactTorque + returnedTorque self.reactTorque = reactTorque
self.returnedTorque = returnedTorque
self.finalTorque = finalTorque local finalTorque = generatedTorque + reactTorque + returnedTorque
self.angularVelocity = self.angularVelocity + finalTorque / inertiaSum * TICK_INTERVAL self.finalTorque = finalTorque
self.angularVelocity = math.max(self.angularVelocity, 0)
-- net.start("ENGINE_RPM") self.angularVelocity = self.angularVelocity + finalTorque / inertiaSum * TICK_INTERVAL
-- net.writeFloat(self:getRPM() / self.maxRPM) self.angularVelocity = math.max(self.angularVelocity, 0)
-- net.send(self.vehicle.playersConnectedToHUD, true)
-- net.start("ENGINE_FULLRPM") -- net.start('ENGINE_RPM')
-- net.writeUInt(self:getRPM(), 16) -- net.writeFloat(self:getRPM() / self.maxRPM)
-- net.writeFloat(self.masterThrottle) -- net.send(self.vehicle.playersConnectedToHUD, true)
-- net.send(nil, true)
-- net.start('ENGINE_FULLRPM')
-- net.writeUInt(self:getRPM(), 16)
-- net.writeFloat(self.masterThrottle)
-- net.send(nil, true)
return 0
end end
return Engine return Engine

View File

@ -5,66 +5,68 @@ local PowertrainComponent = require('../powertrain_component.txt')
local Gearbox = class('Gearbox', PowertrainComponent) local Gearbox = class('Gearbox', PowertrainComponent)
function Gearbox:initialize(vehicle, name, config) 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 = { self.wireInputs = {
Upshift = 'number', Upshift = 'number',
Downshift = 'number' Downshift = 'number',
} }
self.wireOutputs = { self.wireOutputs = {
[string.format('%s_RPM', self.name)] = 'number', [string.format('%s_RPM', self.name)] = 'number',
[string.format('%s_Torque', self.name)] = 'number', [string.format('%s_Torque', self.name)] = 'number',
[string.format('%s_Ratio', self.name)] = 'number' [string.format('%s_Ratio', self.name)] = 'number',
} }
self.type = config.Type or 'MANUAL' self.type = config.Type or 'MANUAL'
self.inertia = config.Inertia or 1000 self.inertia = config.Inertia or 1000
self.ratio = 0 self.ratio = 0
end end
function Gearbox:updateWireOutputs() function Gearbox:updateWireOutputs()
PowertrainComponent.updateWireOutputs(self) PowertrainComponent.updateWireOutputs(self)
wire.ports[string.format('%s_RPM', self.name)] = self:getRPM() 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_Torque', self.name)] = self.torque
wire.ports[string.format('%s_Ratio', self.name)] = self.ratio wire.ports[string.format('%s_Ratio', self.name)] = self.ratio
end end
function Gearbox:queryInertia() function Gearbox:queryInertia()
if self.output == nil or self.ratio == 0 then if self.output == nil or self.ratio == 0 then
return self.inertia return self.inertia
end end
return self.inertia + self.output:queryInertia() / math.pow(self.ratio, 2) return self.inertia + self.output:queryInertia() / math.pow(self.ratio, 2)
end end
function Gearbox:queryAngularVelocity(angularVelocity) function Gearbox:queryAngularVelocity(angularVelocity)
self.angularVelocity = angularVelocity self.angularVelocity = angularVelocity
if self.output == nil or self.ratio == 0 then if self.output == nil or self.ratio == 0 then
return angularVelocity return angularVelocity
end end
return self.output:queryAngularVelocity(angularVelocity) * self.ratio return self.output:queryAngularVelocity(angularVelocity) * self.ratio
end end
function Gearbox:forwardStep(torque, inertia) function Gearbox:forwardStep(torque, inertia)
self.torque = torque * self.ratio self.torque = torque * self.ratio
if self.output == nil then if self.output == nil then
return torque return torque
end end
if self.ratio == 0 then if self.ratio == 0 then
self.output:forwardStep(0, self.inertia * 0.5) self.output:forwardStep(0, self.inertia * 0.5)
return torque return torque
end 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 end
return Gearbox return Gearbox

View File

@ -10,74 +10,78 @@ require('/koptilnya/libs/utils.txt')
local ManualGearbox = class('ManualGearbox', BaseGearbox) local ManualGearbox = class('ManualGearbox', BaseGearbox)
function ManualGearbox:initialize(vehicle, name, config) 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, { table.merge(self.wireOutputs, {
[string.format('%s_Gear', self.name)] = 'number' [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.ratios = config.Ratios or { 3.6, 2.2, 1.5, 1.2, 1.0, 0.8 }
self.reverse = config.Reverse or 3.4 self.reverse = config.Reverse or 3.4
self.gear = 0 self.gear = 0
function shiftFunc() function shiftFunc()
if wire.ports.Clutch == 0 then return 0 end if wire.ports.Clutch == 0 then
return 0
end
local upshift = wire.ports.Upshift or 0 local upshift = wire.ports.Upshift or 0
local downshift = wire.ports.Downshift or 0 local downshift = wire.ports.Downshift or 0
return upshift - downshift return upshift - downshift
end end
self.shiftWatcher = watcher(shiftFunc, function(val) self.shiftWatcher = watcher(shiftFunc, function(val)
if val ~= 0 then if val ~= 0 then
self:shift(val) self:shift(val)
end end
end) end)
self:recalcRatio() self:recalcRatio()
end end
function ManualGearbox:updateWireOutputs() 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 end
function ManualGearbox:setGear(gear) function ManualGearbox:setGear(gear)
if gear >= -1 and gear <= #self.ratios then if gear >= -1 and gear <= #self.ratios then
self.gear = gear self.gear = gear
self:recalcRatio() self:recalcRatio()
net.start('GEARBOX_GEAR') net.start('GEARBOX_GEAR')
net.writeInt(gear, 5) net.writeInt(gear, 5)
net.send(self.vehicle.playersConnectedToHUD, true) net.send(self.vehicle.playersConnectedToHUD, true)
end end
end end
function ManualGearbox:recalcRatio() function ManualGearbox:recalcRatio()
if self.gear == -1 then if self.gear == -1 then
self.ratio = -self.reverse self.ratio = -self.reverse
elseif self.gear == 0 then elseif self.gear == 0 then
self.ratio = 0 self.ratio = 0
else else
self.ratio = self.ratios[self.gear] self.ratio = self.ratios[self.gear]
end end
end end
function ManualGearbox:shift(dir) function ManualGearbox:shift(dir)
self:setGear(self.gear + dir) self:setGear(self.gear + dir)
end end
function ManualGearbox:forwardStep(torque, inertia) function ManualGearbox:forwardStep(torque, inertia)
self.shiftWatcher() self.shiftWatcher()
local result = BaseGearbox.forwardStep(self, torque, inertia)
return result local result = BaseGearbox.forwardStep(self, torque, inertia)
return result
end end
return ManualGearbox return ManualGearbox

View File

@ -1,104 +1,132 @@
--@include /koptilnya/libs/constants.txt --@include /koptilnya/libs/constants.txt
--@include /koptilnya/libs/utils.txt --@include /koptilnya/libs/utils.txt
--@include ../wire_component.txt --@include ../wire_component.txt
--@include ../vehicle.txt
local WireComponent = require('../wire_component.txt') local WireComponent = require('../wire_component.txt')
require('/koptilnya/libs/constants.txt') require('/koptilnya/libs/constants.txt')
require('/koptilnya/libs/utils.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) local PowertrainComponent = class('PowertrainComponent', WireComponent)
---@protected
---@param vehicle KPTLVehicle
---@param name? string
---@param config? PowertrainComponentConfig
function PowertrainComponent:initialize(vehicle, name, config) function PowertrainComponent:initialize(vehicle, name, config)
config = config or {} config = config or {}
WireComponent.initialize(self) WireComponent.initialize(self)
self.vehicle = vehicle self.vehicle = vehicle
self.name = name or 'PowertrainComponent' self.name = name or 'PowertrainComponent'
self.CONFIG = config self.CONFIG = config
self.DEBUG = config.DEBUG or false self.DEBUG = config.DEBUG or false
self.input = nil self.input = nil
self.output = nil self.output = nil
self.inertia = 0.02
self.angularVelocity = 0
self.torque = 0
self.inertia = 0.02 if self.DEBUG then
self.angularVelocity = 0 if CLIENT then
self.torque = 0 self.DEBUG_DATA = {}
net.receive('DEBUG_' .. self.name, function()
self:deserializeDebugData(self.DEBUG_DATA)
end)
end
if self.DEBUG then self.DEBUG_SEND_DATA_DEBOUNCED = debounce(function()
if CLIENT then net.start('DEBUG_' .. self.name)
self.DEBUG_DATA = {} self:serializeDebugData()
net.send(self.vehicle.playersConnectedToHUD, true)
net.receive('DEBUG_' .. self.name, function() end, 1 / 10)
self:deserializeDebugData(self.DEBUG_DATA) end
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 end
---@param component PowertrainComponent
---@return nil
function PowertrainComponent:linkComponent(component) function PowertrainComponent:linkComponent(component)
if not component:isInstanceOf(PowertrainComponent) then ---@diagnostic disable-next-line: undefined-field
return if not component:isInstanceOf(PowertrainComponent) then
end return
end
if self.output == nil then if self.output == nil then
self.output = component self.output = component
component.input = self component.input = self
end end
end end
---@return number
function PowertrainComponent:getRPM() function PowertrainComponent:getRPM()
return self.angularVelocity * RAD_TO_RPM return self.angularVelocity * RAD_TO_RPM
end end
---@return number
function PowertrainComponent:queryInertia() function PowertrainComponent:queryInertia()
if self.output == nil then if self.output == nil then
return self.inertia return self.inertia
end end
return self.inertia + self.output:queryInertia() return self.inertia + self.output:queryInertia()
end end
---@param angularVelocity number
---@return number
function PowertrainComponent:queryAngularVelocity(angularVelocity) function PowertrainComponent:queryAngularVelocity(angularVelocity)
self.angularVelocity = angularVelocity self.angularVelocity = angularVelocity
if self.output == nil then if self.output == nil then
return 0 return 0
end end
return self.output:queryAngularVelocity(angularVelocity) return self.output:queryAngularVelocity(angularVelocity)
end end
---@param torque number
---@param inertia number
---@return number
function PowertrainComponent:forwardStep(torque, inertia) function PowertrainComponent:forwardStep(torque, inertia)
if self.output == nil then if self.output == nil then
return self.torque return self.torque
end end
return self.output:forwardStep(self.torque, self.inertia + inertia) return self.output:forwardStep(self.torque, self.inertia + inertia)
end end
---@return nil
function PowertrainComponent:updateWireOutputs() function PowertrainComponent:updateWireOutputs()
WireComponent.updateWireOutputs(self) WireComponent.updateWireOutputs(self)
if self.DEBUG then if self.DEBUG then
self:DEBUG_SEND_DATA_DEBOUNCED() self:DEBUG_SEND_DATA_DEBOUNCED()
end end
end end
function PowertrainComponent:serializeDebugData() ---@return nil
end function PowertrainComponent:serializeDebugData() end
function PowertrainComponent:deserializeDebugData(result) ---@param result table
end ---@return nil
function PowertrainComponent:deserializeDebugData(result) end
return PowertrainComponent return PowertrainComponent

View File

@ -9,184 +9,223 @@ local CustomWheel = require('../wheel/wheel.txt')
require('/koptilnya/libs/constants.txt') require('/koptilnya/libs/constants.txt')
require('/koptilnya/libs/entity.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 entity Entity
---@field brakePower number
---@field handbrakePower number
---@field direction integer
---@field private rot number
---@field model string
---@field holo Hologram | Entity
---@field customWheel CustomWheel
local Wheel = class('Wheel', PowertrainComponent) local Wheel = class('Wheel', PowertrainComponent)
local DEBUG = true ---@private
---@param vehicle KPTLVehicle
---@param name string
---@param config WheelConfig
function Wheel:initialize(vehicle, name, config) function Wheel:initialize(vehicle, name, config)
PowertrainComponent.initialize(self, vehicle, name, config) PowertrainComponent.initialize(self, vehicle, name, config)
if CLIENT and self.DEBUG then if CLIENT and self.DEBUG then
local scale = 0.1 local mat = render.createMaterial('models/debug/debugwhite')
local font = render.createFont("Roboto", 256, 400, true)
local mat = render.createMaterial('models/debug/debugwhite')
local lerpLoad = 0 local lerpLoad = 0
local lerpForwardFrictionForce = 0 local lerpForwardFrictionForce = 0
local lerpSideFrictionForce = 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
local pos = self.DEBUG_DATA.entity:getPos() hook.add('PostDrawTranslucentRenderables', 'DEBUG_RENDER_' .. self.name, function()
if next(self.DEBUG_DATA) == nil then
render.setMaterial(mat) return
render.setColor(Color(255, 0, 0, 200)) end
lerpForwardFrictionForce = math.lerp(0.1, lerpForwardFrictionForce, self.DEBUG_DATA.forwardFrictionForce) if not isValid(self.DEBUG_DATA.entity) then
render.draw3DBeam(pos, pos + ((lerpForwardFrictionForce / 500 + 16) * self.DEBUG_DATA.forward), 1, 0, 0) return
end
render.setColor(Color(0, 255, 0, 255)) local pos = self.DEBUG_DATA.entity:localToWorld(Vector(0, 0, -self.DEBUG_DATA.radius * UNITS_PER_METER))
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.setColor(Color(0, 0, 255, 200)) render.setMaterial(mat)
lerpLoad = math.lerp(0.1, lerpLoad, self.DEBUG_DATA.load) render.setColor(Color(255, 0, 0, 200))
render.draw3DBeam(pos, pos + ((lerpLoad * 4 + 16) * self.DEBUG_DATA.up), 1, 0, 0) lerpForwardFrictionForce = math.lerp(0.1, lerpForwardFrictionForce, self.DEBUG_DATA.forwardFrictionForce)
end) render.draw3DBeam(pos, pos + ((lerpForwardFrictionForce / 500 + 16) * self.DEBUG_DATA.forward), 1, 0, 0)
if player() == owner() and not render.isHUDActive() then render.setColor(Color(0, 255, 0, 255))
enableHud(nil, true) lerpSideFrictionForce = math.lerp(0.1, lerpSideFrictionForce, self.DEBUG_DATA.sideFrictionForce)
end render.draw3DBeam(pos, pos + ((lerpSideFrictionForce / 500 + 16) * self.DEBUG_DATA.right), 1, 0, 0)
return render.setColor(Color(0, 0, 255, 200))
end 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)
self.steerLock = config.SteerLock or 0 if player() == owner() and not render.isHUDActive() then
self.brakePower = config.BrakePower or 0 enableHud(owner(), true)
self.handbrakePower = config.HandbrakePower or 0 end
return
end
self.direction = math.sign(math.cos(math.rad(config.Offset or 0))) self.entity = config.Entity or NULL_ENTITY
self.brakePower = config.BrakePower or 0
self.handbrakePower = config.HandbrakePower or 0
self.model = config.Model or ''
self.wireInputs = { self.direction = math.sign(math.cos(math.rad(config.Offset or 0)))
[self.name] = 'entity'
}
self.wireOutputs = {
[string.format('%s_RPM', self.name)] = 'number',
[string.format('%s_W', self.name)] = 'number'
}
self.rot = 0 self.wireInputs = {
[self.name] = 'entity',
}
self.wireOutputs = {
[string.format('%s_RPM', self.name)] = 'number',
[string.format('%s_Fx', self.name)] = 'number',
[string.format('%s_CTq', self.name)] = 'number',
}
self.entity = config.Entity or NULL_ENTITY self.rot = 0
self.holo = self:createHolo(self.entity) self.holo = self:createHolo(self.entity)
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 }))
hook.add('input', 'vehicle_wheel_update' .. self.name, function(name, value) hook.add('Input', 'vehicle_wheel_update' .. self.name, function(name, value)
if name == self.name then if name == self.name then
self.entity = value self.entity = value
self.customWheel:setEntity(value) self.customWheel:setEntity(value)
if isValid(self.holo) then if isValid(self.holo) then
self.holo:remove() self.holo:remove()
end end
self.holo = self:createHolo(self.entity) self.holo = self:createHolo(self.entity)
self.customWheel.radius = self:getEntityRadius()
end
end)
if not config.Radius then self.steerVelocity = 0
self.customWheel.radius = self:getEntityRadius()
end
end
end)
self.steerVelocity = 0
end end
function Wheel:getEntityRadius() function Wheel:getEntityRadius()
if not isValid(self.entity) then if not isValid(self.entity) then
return 0 return 0
end end
return self.entity:getModelRadius() * UNITS_TO_METERS return self.entity:getModelRadius() * UNITS_TO_METERS
end end
---@return Hologram | Entity
function Wheel:createHolo(entity) function Wheel:createHolo(entity)
if not isValid(entity) then if not isValid(entity) then
return NULL_ENTITY return NULL_ENTITY
end end
local holo = holograms.create( local holo = hologram.create(entity:getPos(), entity:getAngles(), self.model or '')
entity:getPos(),
entity:getAngles(),
self.CONFIG.Model or ''
)
holo:setParent(entity)
entity:setColor(Color(0,0,0,0)) if holo == nil or isValid(holo) then
return NULL_ENTITY
end
return holo holo:setParent(entity)
entity:setColor(Color(0, 0, 0, 0))
return holo
end end
---@return nil
function Wheel:updateWireOutputs() 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 wire.ports[string.format('%s_Fx', self.name)] = self.customWheel.forwardFriction.force
wire.ports[string.format('%s_CTq', self.name)] = self.customWheel.counterTorque
end end
---@return number
function Wheel:queryInertia() function Wheel:queryInertia()
return self.customWheel.inertia return self.customWheel.inertia
end end
---@return number
function Wheel:queryAngularVelocity() function Wheel:queryAngularVelocity()
return self.angularVelocity return self.angularVelocity
end end
---@param torque number
---@param inertia number
---@return number
function Wheel:forwardStep(torque, inertia) function Wheel:forwardStep(torque, inertia)
if not isValid(self.customWheel) then if not isValid(self.customWheel) then
return 0 return 0
end end
self.customWheel.motorTorque = torque
self.customWheel.brakeTorque = math.max(self.brakePower * self.vehicle.brake, self.handbrakePower * self.vehicle.handbrake)
self.customWheel:update() self.customWheel.motorTorque = torque
self.customWheel.brakeTorque = math.max(self.brakePower * self.vehicle.brake, self.handbrakePower * self.vehicle.handbrake)
self.angularVelocity = self.customWheel.angularVelocity self.customWheel:update()
if self.customWheel.hasHit and isValid(self.vehicle.basePhysObject) then self.angularVelocity = self.customWheel.angularVelocity
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()))) if self.customWheel.hasHit and isValid(self.vehicle.basePhysObject) then
end local surfaceForceVector = self.customWheel.right * self.customWheel.sideFriction.force
+ self.customWheel.forward * self.customWheel.forwardFriction.force
if isValid(self.holo) then self.vehicle.basePhysObject:applyForceOffset(
local entityAngles = self.entity:getAngles() surfaceForceVector,
self.entity:localToWorld(Vector(0, 0, -self.customWheel.radius * UNITS_PER_METER))
)
end
self.rot = self.rot + self.angularVelocity * TICK_INTERVAL 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)
local steerRotated = entityAngles:rotateAroundAxis(self.customWheel.up, nil, math.rad(-self.customWheel.steerAngle) + self.customWheel.toeAngle) -- Step 2: Apply spin (rolling) to forward and up directions
local camberRotated = steerRotated:rotateAroundAxis(self.customWheel.forward, nil, -self.customWheel.camberAngle * self.direction) local spunFwd = self.customWheel.forward:rotateAroundAxis(self.customWheel.right, nil, self.rot)
local angularVelocityRotated = camberRotated:rotateAroundAxis(self.customWheel.right, nil, self.rot) local spunUp = self.customWheel.up:rotateAroundAxis(self.customWheel.right, nil, self.rot)
self.holo:setAngles(angularVelocityRotated) -- Step 3: Get quaternion from spun directions
end local visualQuat = spunFwd:getQuaternion(spunUp)
return self.customWheel.counterTorque -- Step 4: Apply orientation to holo
self.holo:setAngles(visualQuat:getEulerAngle())
end
return self.customWheel.counterTorque
end end
---@return nil
function Wheel:serializeDebugData() function Wheel:serializeDebugData()
net.writeEntity(self.entity) net.writeEntity(self.entity)
net.writeVector(self.customWheel.forward) net.writeFloat(self.customWheel.radius)
net.writeVector(self.customWheel.right) net.writeVector(self.customWheel.forward)
net.writeVector(self.customWheel.up) net.writeVector(self.customWheel.right)
net.writeFloat(self.customWheel.load) net.writeVector(self.customWheel.up)
net.writeFloat(self.customWheel.forwardFriction.force) net.writeFloat(self.customWheel.load)
net.writeFloat(self.customWheel.sideFriction.force) net.writeFloat(self.customWheel.forwardFriction.force)
net.writeFloat(self.customWheel.sideFriction.force)
end end
---@param result table
---@return nil
function Wheel:deserializeDebugData(result) function Wheel:deserializeDebugData(result)
net.readEntity(function(ent) net.readEntity(function(ent)
result.entity = ent result.entity = ent
end) end)
result.forward = net.readVector() result.radius = net.readFloat()
result.right = net.readVector() result.forward = net.readVector()
result.up = net.readVector() result.right = net.readVector()
result.load = net.readFloat() result.up = net.readVector()
result.forwardFrictionForce = net.readFloat() result.load = net.readFloat()
result.sideFrictionForce = net.readFloat() result.forwardFrictionForce = net.readFloat()
result.sideFrictionForce = net.readFloat()
end end
return Wheel return Wheel

View File

@ -6,155 +6,227 @@
local Task = require('/libs/task.txt') local Task = require('/libs/task.txt')
local PowertrainComponentFactory = require('./factories/powertrain_component.txt') local PowertrainComponentFactory = require('./factories/powertrain_component.txt')
local CustomWheel = require('./wheel/wheel.txt')
local POWERTRAIN_COMPONENT = require('./enums/powertrain_component.txt') local POWERTRAIN_COMPONENT = require('./enums/powertrain_component.txt')
require('/koptilnya/libs/table.txt') require('/koptilnya/libs/table.txt')
require('/koptilnya/libs/constants.txt') require('/koptilnya/libs/constants.txt')
---@class KPTLVehicle
---@field config { [string]: any }
---@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
local Vehicle = class('Vehicle') local Vehicle = class('Vehicle')
---@param config { [string]: any }
function Vehicle:initialize(config) function Vehicle:initialize(config)
if not Vehicle:validateConfig(config) then if not Vehicle:validateConfig(config) then
throw('Please check your vehicle configuration!') throw('Please check your vehicle configuration!')
end end
self.config = config self.initialized = false
self.components = {}
self.headComponents = {}
self:createComponents() self.config = config
self:linkComponents() self.components = {}
self.headComponents = {}
if SERVER then self.steer = 0
self:createIO() self.brake = 0
end self.handbrake = 0
self.playersConnectedToHUD = {}
if SERVER then if SERVER then
self.steer = 0 self.playersConnectedToHUD = find.allPlayers(function(ply)
self.brake = 0 return isValid(ply) and ply:isHUDActive()
self.handbrake = 0 end)
self.playersConnectedToHUD = {}
hook.add('input', 'vehicle_wire_input', function(name, value) hook.add('Input', 'vehicle_wire_input', function(name, value)
self:handleWireInput(name, value) self:handleWireInput(name, value)
end) end)
hook.add('tick', 'vehicle_update', function() ---@diagnostic disable-next-line: param-type-mismatch
self:update() hook.add('Tick', 'vehicle_update', function()
end) self:update()
end)
hook.add('HUDConnected', 'vehicle_hudconnected', function(ent, ply) hook.add('HUDConnected', 'vehicle_hudconnected', function(ent, ply)
table.insert(self.playersConnectedToHUD, ply) table.insert(self.playersConnectedToHUD, ply)
end) end)
hook.add('HUDDisconnected', 'vehicle_huddisconnected', function(ent, ply)
hook.add('HUDDisconnected', 'vehicle_huddisconnected', function(ent, ply) table.removeByValue(self.playersConnectedToHUD, ply)
table.removeByValue(self.playersConnectedToHUD, ply) end)
end)
else hook.add('PlayerDisconnected', 'vehicle_huddisconnected', function(ply)
--@include ./hud.txt table.removeByValue(self.playersConnectedToHUD, ply)
require("./hud.txt") end)
end else
--@include ./hud.txt
require('./hud.txt')
net.receive('VEHICLE_READY', function()
self:start()
end)
end
end end
function Vehicle.static:validateConfig(config) ---@return nil
return type(config) == 'table' function Vehicle:start()
self:createComponents()
self:linkComponents()
if SERVER then
self:createIO()
end
self.initialized = true
print('VEHICLE READY')
if SERVER then
net.start('VEHICLE_READY')
net.send(find.allPlayers(), true)
end
end end
---@param config any
---@return boolean
function Vehicle:validateConfig(config)
return type(config) == 'table'
end
---@param name string
---@return PowertrainComponent
function Vehicle:getComponentByName(name) function Vehicle:getComponentByName(name)
return table.find(self.components, function(component) return table.find(self.components, function(component)
return component.name == name return component.name == name
end) end)
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() function Vehicle:createComponents()
for _, componentConfig in pairs(self.config) do for _, componentConfig in pairs(self.config) do
local component = PowertrainComponentFactory:create(self, componentConfig.Type, componentConfig.Name, componentConfig.Config) local component = PowertrainComponentFactory:create(self, componentConfig.Type, componentConfig.Name, componentConfig.Config)
table.insert(self.components, component) table.insert(self.components, component)
end end
end end
---@return nil
function Vehicle:linkComponents() function Vehicle:linkComponents()
for _, componentConfig in pairs(self.config) do for _, componentConfig in pairs(self.config) do
local component = self:getComponentByName(componentConfig.Name) local component = self:getComponentByName(componentConfig.Name)
if componentConfig.Input == nil then if componentConfig.Input == nil then
table.insert(self.headComponents, component) table.insert(self.headComponents, component)
else else
local inputComponent = self:getComponentByName(componentConfig.Input) local inputComponent = self:getComponentByName(componentConfig.Input)
if inputComponent ~= nil then if inputComponent ~= nil then
inputComponent:linkComponent(component) inputComponent:linkComponent(component)
end end
end end
end end
if SERVER then
for _, component in pairs(self.headComponents) do
self:printComponentTree(component)
end
end
end end
---@return nil
function Vehicle:createIO() function Vehicle:createIO()
local inputs = { local inputs = {
Base = 'entity', Base = 'entity',
Steer = 'number', Steer = 'number',
Brake = 'number', Brake = 'number',
Handbrake = 'number' Handbrake = 'number',
} }
local outputs = {} local outputs = {}
for _, component in ipairs(self.components) do for _, component in ipairs(self.components) do
inputs = table.merge(inputs, component.wireInputs) inputs = table.merge(inputs, component.wireInputs)
outputs = table.merge(outputs, component.wireOutputs) outputs = table.merge(outputs, component.wireOutputs)
end end
wire.adjustPorts(inputs, outputs) wire.adjustPorts(inputs, outputs)
end
function Vehicle:getRootComponent()
return table.find(self.components, function(component)
return component.input == nil
end)
end end
---@return nil
function Vehicle:handleWireInput(name, value) function Vehicle:handleWireInput(name, value)
if name == 'Base' then if name == 'Base' then
self.base = value self.base = value
self.basePhysObject = isValid(value) and value:getPhysicsObject() or nil self.basePhysObject = isValid(value) and value:getPhysicsObject() or nil
elseif name == 'Steer' then elseif name == 'Steer' then
self.steer = value self.steer = value
elseif name == 'Brake' then elseif name == 'Brake' then
self.brake = value self.brake = value
elseif name == 'Handbrake' then elseif name == 'Handbrake' then
self.handbrake = value self.handbrake = value
end end
if not self.initialized and self.base ~= nil and self.basePhysObject ~= nil then
self:start()
end
end end
---@return nil
function Vehicle:update() function Vehicle:update()
if SERVER then if SERVER then
local base = wire.ports.Base local base = wire.ports.Base
for _, component in pairs(self.headComponents) do for _, component in pairs(self.headComponents) do
component:forwardStep(0, 0) component:forwardStep(0, 0)
end end
for _, component in pairs(self.components) do for _, component in pairs(self.components) do
component:updateWireOutputs() component:updateWireOutputs()
end end
-- net.start("CAR_SPEED") -- net.start("CAR_SPEED")
-- net.writeUInt(base:getVelocity():getLength()* 1.905 / 100000 * 3600, 12) -- net.writeUInt(base:getVelocity():getLength()* 1.905 / 100000 * 3600, 12)
-- net.send(self.playersConnectedToHUD, true) -- net.send(self.playersConnectedToHUD, true)
end end
--if isValid(base) and (self.clutch:getPress() == 1 or self.gearbox.ratio == 0) then --if isValid(base) and (self.clutch:getPress() == 1 or self.gearbox.ratio == 0) then
-- local tiltForce = self.engine.torque * (-1 + self.engine.masterThrottle * 2) -- 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)
-- 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
end
---@param component PowertrainComponent
---@param result? string
---@return nil
function Vehicle:printComponentTree(component, result)
-- result = result or component.name
-- if component.output then
-- result = result .. component.name
-- return self:printComponentTree(component.output, result .. ' -> ')
-- else
-- print(result)
-- end
end end
return { return {
Vehicle, Vehicle,
POWERTRAIN_COMPONENT POWERTRAIN_COMPONENT,
} }

View File

@ -1,289 +1,349 @@
--@include ./friction.txt --@include ./friction.txt
--@include ./friction_preset.txt --@include ./friction_preset.txt
--@include /koptilnya/libs/constants.txt --@include /koptilnya/libs/constants.txt
--@include /koptilnya/libs/utils.txt
local Friction = require('./friction.txt') local Friction = require('./friction.txt')
local FrictionPreset = require('./friction_preset.txt') local FrictionPreset = require('./friction_preset.txt')
require('/koptilnya/libs/constants.txt') require('/koptilnya/libs/constants.txt')
require('/koptilnya/libs/utils.txt')
---@class CustomWheelConfig
---@field Name? string
---@field Direction? integer
---@class CustomWheel
local Wheel = class('Wheel') local Wheel = class('Wheel')
function Wheel:initialize(config) function Wheel:initialize(config)
config = config or {} config = config or {}
self.direction = config.Direction or 1 self.name = config.Name
self.mass = config.Mass or 20 self.direction = config.Direction or 1
self.radius = config.Radius or 0.27 self.mass = config.Mass or 20
self.rollingResistance = config.RollingResistance or 20 self.radius = config.Radius or 0.27
self.squat = config.Squat or 0.1 self.rollingResistance = config.RollingResistance or 20
self.slipCircleShape = config.SlipCircleShape or 1.05 self.squat = config.Squat or 0.1
self.casterAngle = math.rad(config.CasterAngle or 0) * self.direction self.slipCircleShape = config.SlipCircleShape or 1.05
self.camberAngle = math.rad(config.CamberAngle or 0) self.casterAngle = math.rad(config.CasterAngle or 0)
self.toeAngle = math.rad(config.ToeAngle or 0) * -self.direction self.camberAngle = math.rad(config.CamberAngle or 0)
self.toeAngle = math.rad(config.ToeAngle or 0)
self.forwardFriction = Friction:new(config.ForwardFriction) self.forwardFriction = Friction:new(config.ForwardFriction)
self.sideFriction = Friction:new(config.SideFriction) self.sideFriction = Friction:new(config.SideFriction)
self.lateralFrictionPreset = FrictionPreset:new(config.LateralFrictionPreset) self.lateralFrictionPreset = FrictionPreset:new(config.LateralFrictionPreset)
self.longitudinalFrictionPreset = FrictionPreset:new(config.LongitudinalFrictionPreset) self.longitudinalFrictionPreset = FrictionPreset:new(config.LongitudinalFrictionPreset)
self.aligningFrictionPreset = FrictionPreset:new(config.AligningFrictionPreset) self.aligningFrictionPreset = FrictionPreset:new(config.AligningFrictionPreset)
self.motorTorque = 0 self.motorTorque = 0
self.brakeTorque = 0 self.brakeTorque = 0
self.steerAngle = 0 self.steerAngle = 0
self.hasHit = false self.hasHit = false
self.counterTorque = 0 self.counterTorque = 0
self.inertia = 0 self.inertia = 0
self.load = 0 self.load = 0
self.angularVelocity = 0 self.angularVelocity = 0
self.mz = 0 self.mz = 0
self.forward = Vector(0) self.forward = Vector(0)
self.right = Vector(0) self.right = Vector(0)
self.up = Vector(0) self.up = Vector(0)
self.entity = NULL_ENTITY self.entity = NULL_ENTITY
self.physObj = nil self.physObj = nil
self.baseInertia = 0.5 * self.mass * math.pow(self.radius, 2) self.baseInertia = 0.5 * self.mass * math.pow(self.radius, 2)
self.inertia = self.baseInertia self.inertia = self.baseInertia
self.printDebounced = debounce(function(...)
print(...)
end, 1)
end end
function Wheel:setEntity(entity) function Wheel:setEntity(entity)
self.entity = entity self.entity = entity
self.entity:setNoDraw(false) 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 if isValid(self.physObj) then
self.physObj:setMaterial('friction_00') self.physObj:setMaterial('friction_00')
self.physObj:setMass(self.mass) self.physObj:setMass(self.mass)
self.physObj:enableDrag(false) self.physObj:enableDrag(false)
self.physObj:setDragCoefficient(0) self.physObj:setDragCoefficient(0)
self.physObj:setAngleDragCoefficient(0) self.physObj:setAngleDragCoefficient(0)
end end
end end
function Wheel:setInertia(inertia) function Wheel:setInertia(inertia)
if isValid(self.physObj) then if isValid(self.physObj) then
print(inertia) print(inertia)
self.physObj:setInertia(Vector(inertia)) self.physObj:setInertia(Vector(inertia))
self.inertia = inertia self.inertia = inertia
end end
end end
function Wheel:isValid() function Wheel:isValid()
return isValid(self.entity) and isValid(self.physObj) return isValid(self.entity) and isValid(self.physObj)
end end
function Wheel:getRPM() function Wheel:getRPM()
return self.angularVelocity * RAD_TO_RPM return self.angularVelocity * RAD_TO_RPM
end end
function Wheel:getLongitudinalLoadCoefficient(load) function Wheel:getLongitudinalLoadCoefficient(load)
return 11000 * (1 - math.exp(-0.00014 * load)); return 11000 * (1 - math.exp(-0.00014 * load))
end end
function Wheel:getLateralLoadCoefficient(load) function Wheel:getLateralLoadCoefficient(load)
return 18000 * (1 - math.exp(-0.0001 * load)); return 18000 * (1 - math.exp(-0.0001 * load))
end end
function Wheel:stepLongitudinal(Tm, Tb, Vx, W, Lc, R, I, kFx, kSx) function Wheel:stepLongitudinal(Tm, Tb, Vx, W, Lc, R, I)
local Winit = W local Winit = W
local VxAbs = math.abs(Vx) local VxAbs = math.abs(Vx)
local Sx = 0 local Sx = 0
if VxAbs >= 0.1 then if Lc / 1000 < 0.01 then
Sx = (Vx - W * R) / VxAbs Sx = 0
else elseif VxAbs >= 0.01 then
Sx = (Vx - W * R) * 0.6 Sx = (W * R - Vx) / VxAbs
end else
Sx = (W * R - Vx) * 0.6
end
Sx = Sx * kSx; Sx = math.clamp(Sx, -1, 1)
Sx = math.clamp(Sx, -1, 1)
W = W + Tm / I * TICK_INTERVAL W = W + Tm / I * TICK_INTERVAL
Tb = Tb * (W > 0 and -1 or 1) Tb = Tb * (W > 0 and -1 or 1)
local TbCap = math.abs(W) * I / TICK_INTERVAL local TbCap = math.abs(W) * I / TICK_INTERVAL
local Tbr = math.abs(Tb) - math.abs(TbCap) local Tbr = math.abs(Tb) - math.abs(TbCap)
Tbr = math.max(Tbr, 0) Tbr = math.max(Tbr, 0)
Tb = math.clamp(Tb, -TbCap, TbCap) Tb = math.clamp(Tb, -TbCap, TbCap)
W = W + Tb / I * TICK_INTERVAL W = W + Tb / I * TICK_INTERVAL
local maxTorque = self.longitudinalFrictionPreset:evaluate(math.abs(Sx)) * Lc * kFx * R local maxTorque = self.longitudinalFrictionPreset:evaluate(math.abs(Sx)) * Lc * R
local errorTorque = (W - Vx / R) * I / TICK_INTERVAL
local surfaceTorque = math.clamp(errorTorque, -maxTorque, maxTorque)
W = W - surfaceTorque / I * TICK_INTERVAL local errorTorque = (W - Vx / R) * I / TICK_INTERVAL
local Fx = surfaceTorque / R local surfaceTorque = math.clamp(errorTorque, -maxTorque, maxTorque)
Tbr = Tbr * (W > 0 and -1 or 1) if self.name == 'WheelFL' or self.name == 'WheelFR' then
local TbCap2 = math.abs(W) * I / TICK_INTERVAL surfaceTorque = surfaceTorque
local Tb2 = math.clamp(Tbr, -TbCap2, TbCap2) end
W = W + Tb2 / I * TICK_INTERVAL
local deltaOmegaTorque = (W - Winit) * I / TICK_INTERVAL W = W - surfaceTorque / I * TICK_INTERVAL
local Tcnt = -surfaceTorque + Tb + Tb2 - deltaOmegaTorque local Fx = surfaceTorque / R
if Lc < 0.001 then Tbr = Tbr * (W > 0 and -1 or 1)
Sx = 0 local TbCap2 = math.abs(W) * I / TICK_INTERVAL
end local Tb2 = math.clamp(Tbr, -TbCap2, TbCap2)
W = W + Tb2 / I * TICK_INTERVAL
return W, Sx, Fx, Tcnt local deltaOmegaTorque = (W - Winit) * I / TICK_INTERVAL
local Tcnt = -surfaceTorque + Tb + Tb2 - deltaOmegaTorque
-- self.printDebounced(
-- Color(255, 255, 0),
-- string.format('[%s] ', self.name),
-- Color(255, 255, 255),
-- string.format('%s | %s | %s', math.round(Fx), math.round(W), math.round(R))
-- )
return W, Sx, Fx, Tcnt
end end
function Wheel:stepLateral(Vx, Vy, Lc, kFy, kSy) function Wheel:stepLongitudinal2(Tm, Tb, Vx, W, Lc, R, I)
local VxAbs = math.abs(Vx) -- Параметры Pacejka (примерные значения, можно калибровать под задачу)
local Sy = 0 local B = 10.0 -- stiffness factor
local C = 1.9 -- shape factor
local D = Lc -- peak factor (максимальное сцепление зависит от нагрузки)
local E = 0.97 -- curvature factor
if VxAbs > 0.1 then -- Угловая скорость колеса и радиус
Sy = math.deg(math.atan(Vy / VxAbs)) local wheelSpeed = W * R
Sy = Sy / 50
else
Sy = Vy * (0.003 / TICK_INTERVAL)
end
Sy = Sy * kSy * 0.95 -- Slip ratio (проверка на деление на 0)
Sy = math.clamp(Sy, -1, 1) local slip
local slipSign = Sy < 0 and -1 or 1 if math.abs(Vx) < 0.1 then
local Fy = -slipSign * self.lateralFrictionPreset:evaluate(math.abs(Sy)) * Lc * kFy slip = 0.0
else
slip = (wheelSpeed - Vx) / math.abs(Vx)
end
if Lc < 0.0001 then -- Формула Pacejka для расчета силы тяги (longitudinal force)
Sy = 0 local Fx = D * math.sin(C * math.atan(B * slip - E * (B * slip - math.atan(B * slip))))
end
return Sy, Fy -- Добавим моторный момент и торможение, сопротивление качению
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 / 1000 < 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
end end
function Wheel:slipCircle(Sx, Sy, Fx, Fy, slipCircleShape) function Wheel:slipCircle(Sx, Sy, Fx, Fy, slipCircleShape)
local SxAbs = math.abs(Sx) local SxAbs = math.abs(Sx)
if SxAbs > 0.01 then if SxAbs > 0.01 then
local SxClamped = math.clamp(Sx, -1, 1) local SxClamped = math.clamp(Sx, -1, 1)
local SyClamped = math.clamp(Sy, -1, 1) local SyClamped = math.clamp(Sy, -1, 1)
local combinedSlip = Vector2( local combinedSlip = Vector2(SxClamped * slipCircleShape, SyClamped)
SxClamped * slipCircleShape, local slipDir = combinedSlip:getNormalized()
SyClamped
)
local slipDir = combinedSlip:getNormalized()
local F = math.sqrt(Fx * Fx + Fy * Fy) local F = math.sqrt(Fx * Fx + Fy * Fy)
local absSlipDirY = math.abs(slipDir.y) local absSlipDirY = math.abs(slipDir.y)
Fy = F * absSlipDirY * (Fy < 0 and -1 or 1) Fy = F * absSlipDirY * (Fy < 0 and -1 or 1)
end end
return Sx, Sy, Fx, Fy return Sx, Sy, Fx, Fy
end end
function Wheel:selfAligningTorque(Sy, load) function Wheel:selfAligningTorque(Sy, load)
if math.abs(Sy) < 0.001 or load < 0.001 then if math.abs(Sy) < 0.001 or load < 0.001 then
return 0 return 0
end end
local B = self.aligningFrictionPreset.B local B = self.aligningFrictionPreset.B
local C = self.aligningFrictionPreset.C local C = self.aligningFrictionPreset.C
local D = self.aligningFrictionPreset.D local D = self.aligningFrictionPreset.D
local E = self.aligningFrictionPreset.E local E = self.aligningFrictionPreset.E
local loadScale = load * 1000 local entityWheelDown = self.entity:localToWorld(Vector(0, 0, -self.radius))
local mechanicalTrail = 0.15 local physWheelDown = self.entity:localToWorld(-self.up * self.radius)
local casterEffect = math.tan(self.casterAngle) local mechanicalTrail = entityWheelDown:getDistance(physWheelDown) * 10
local effectiveTrail = mechanicalTrail + casterEffect * self.radius
local D_scaled = D * loadScale * effectiveTrail
local camberEffect = 1 + 0.5 * math.abs(self.camberAngle) local casterEffect = math.tan(self.casterAngle) * self.direction
local toeEffect = 1 + 0.3 * math.abs(self.toeAngle) local camberTrailEffect = self.camberAngle * 0.005 * self.direction
-- local toeSyOffset = math.tan(self.toeAngle * self.direction) * 0.1
local term = B * Sy - E * (B * Sy - math.atan(B * Sy)) local effectiveTrail = mechanicalTrail * self.radius
local Mz = D_scaled * camberEffect * toeEffect * math.sin(C * math.atan(term))
return Mz local D_scaled = D * load * effectiveTrail
-- Sy = Sy + toeSyOffset
local term = B * Sy - E * (B * Sy - math.atan(B * Sy))
local Mz = D_scaled * math.sin(C * math.atan(term))
return Mz * self.direction -- / TICK_INTERVAL
end end
function Wheel:rotateVector(vector, jopa) function Wheel:rotateAxes()
local ang = self.entity:getAngles() local ang = self.entity:getAngles()
local baseForward = ang:getForward()
local baseUp = ang:getUp()
local baseRight = -ang:getRight()
local steerRotated = vector:rotateAroundAxis(baseUp, nil, math.rad(-self.steerAngle) + self.toeAngle) -- Base directions
local camberRotated = steerRotated:rotateAroundAxis(baseForward, nil, -self.camberAngle) local forward = ang:getForward() * self.direction
local casterRotated = camberRotated:rotateAroundAxis(baseRight, nil, -self.casterAngle) local up = ang:getUp()
local right = -ang:getRight() * self.direction
return casterRotated:getNormalized() -- 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()
end end
function Wheel:update() function Wheel:update()
if not isValid(self.entity) or not isValid(self.physObj) then if not isValid(self.entity) or not isValid(self.physObj) then
return return
end end
local externalStress = self.physObj:getStress() local externalStress = self.physObj:getStress()
local velocity = self.physObj:getVelocity() * UNITS_TO_METERS local velocity = self.physObj:getVelocity() * UNITS_TO_METERS
self.hasHit = externalStress ~= 0 self.hasHit = externalStress ~= 0
self.load = externalStress * KG_TO_KN self.load = externalStress * KG_TO_KN * 1000
self.longitudinalLoadCoefficient = self:getLongitudinalLoadCoefficient(self.load * 1000) self.longitudinalLoadCoefficient = self:getLongitudinalLoadCoefficient(self.load)
self.lateralLoadCoefficient = self:getLateralLoadCoefficient(self.load * 1000) self.lateralLoadCoefficient = self:getLateralLoadCoefficient(self.load)
local ang = self.entity:getAngles() self:rotateAxes()
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)
local forwardSpeed = 0 local forwardSpeed = 0
local sideSpeed = 0 local sideSpeed = 0
if self.hasHit then if self.hasHit then
forwardSpeed = velocity:dot(self.forward) forwardSpeed = velocity:dot(self.forward)
sideSpeed = velocity:dot(self.right) sideSpeed = velocity:dot(self.right)
end end
local W, Sx, Fx, CounterTq = self:stepLongitudinal( local W, Sx, Fx, CounterTq = self:stepLongitudinal(
self.motorTorque, self.motorTorque,
self.brakeTorque + self.rollingResistance, self.brakeTorque + self.rollingResistance,
forwardSpeed, forwardSpeed,
self.angularVelocity, self.angularVelocity,
self.longitudinalLoadCoefficient, self.longitudinalLoadCoefficient,
self.radius, self.radius,
self.inertia, self.inertia
0.95, )
0.9
)
local Sy, Fy = self:stepLateral( local Sy, Fy = self:stepLateral(forwardSpeed, sideSpeed, self.lateralLoadCoefficient)
forwardSpeed,
sideSpeed,
self.lateralLoadCoefficient,
0.95,
0.9
) Sx, Sy, Fx, Fy = self:slipCircle(Sx, Sy, Fx, Fy, 1.05)
Sx, Sy, Fx, Fy = self:slipCircle( self.mz = self:selfAligningTorque(Sy, self.load)
Sx,
Sy,
Fx,
Fy,
1.05
)
self.mz = self:selfAligningTorque(Sy, self.load) self.angularVelocity = W
self.counterTorque = CounterTq * self.direction
self.angularVelocity = W self.forwardFriction.slip = Sx
self.counterTorque = CounterTq self.forwardFriction.force = Fx
self.forwardFriction.slip = Sx self.forwardFriction.speed = forwardSpeed
self.forwardFriction.force = Fx self.sideFriction.slip = Sy
self.forwardFriction.speed = forwardSpeed self.sideFriction.force = Fy
self.sideFriction.slip = Sy self.sideFriction.speed = sideSpeed
self.sideFriction.force = Fy
self.sideFriction.speed = sideSpeed
end end
return Wheel return Wheel

View File

@ -1,13 +1,25 @@
---@alias KPTLWirePortType
---| "number"
---| "normal"
---| "string"
---| "entity"
---@class KPTLWireComponent
---@field wireInputs { [string]: KPTLWirePortType }
---@field wireOutputs { [string]: KPTLWirePortType }
local WireComponent = class('WireComponent') local WireComponent = class('WireComponent')
---@return nil
function WireComponent:initialize() function WireComponent:initialize()
if CLIENT then return end if CLIENT then
return
self.wireInputs = {} end
self.wireOutputs = {}
self.wireInputs = {}
self.wireOutputs = {}
end end
function WireComponent:updateWireOutputs() ---@return nil
end function WireComponent:updateWireOutputs() end
return WireComponent return WireComponent

View File

@ -2,88 +2,87 @@
--@include /libs/task.txt --@include /libs/task.txt
local Task = require('/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) 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 end
local function fade(n, min, mid, max) local function fade(n, min, mid, max)
if n < min or n > max then if n < min or n > max then
return 0 return 0
end end
if n > mid then if n > mid then
min = mid - (max - mid) min = mid - (max - mid)
end end
return math.cos((1 - ((n - min) / (mid - min))) * (math.pi / 2)) return math.cos((1 - ((n - min) / (mid - min))) * (math.pi / 2))
end end
function Sound:initialize(redline, parent, sounds) function Sound:initialize(redline, parent, sounds)
local sounds = sounds or { local sounds = sounds
[900] = "https://raw.githubusercontent.com/koptilnya/gmod-data/main/engine_sounds/bmw_s54/ext_e30s54_idle.ogg", or {
[2500] = "https://raw.githubusercontent.com/koptilnya/gmod-data/main/engine_sounds/bmw_s54/ext_e30s54_on_2500.ogg", [900] = 'https://raw.githubusercontent.com/koptilnya/gmod-data/main/engine_sounds/bmw_s54/ext_e30s54_idle.ogg',
[4000] = "https://raw.githubusercontent.com/koptilnya/gmod-data/main/engine_sounds/bmw_s54/ext_e30s54_on_4000.ogg", [2500] = 'https://raw.githubusercontent.com/koptilnya/gmod-data/main/engine_sounds/bmw_s54/ext_e30s54_on_2500.ogg',
[6750] = "https://raw.githubusercontent.com/koptilnya/gmod-data/main/engine_sounds/bmw_s54/ext_e30s54_on_6750.ogg", [4000] = 'https://raw.githubusercontent.com/koptilnya/gmod-data/main/engine_sounds/bmw_s54/ext_e30s54_on_4000.ogg',
[8500] = "https://raw.githubusercontent.com/koptilnya/gmod-data/main/engine_sounds/bmw_s54/ext_e30s54_on_8500.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 redline = redline or 7000
local soundObjects = {} self.active = false
local soundRpms = {} local soundObjects = {}
local maxValue = 0 local soundRpms = {}
local throttle = 0 local maxValue = 0
local engineRpm = 0 local throttle = 0
local smoothRpm = 0 local engineRpm = 0
local smoothThrottle = 0 local smoothRpm = 0
local smoothThrottle = 0
Task.run(function() Task.run(function()
for soundRpm, soundPath in pairs(sounds) do for soundRpm, soundPath in pairs(sounds) do
local sound = await* soundLoad(soundPath, "3d noblock noplay") local sound = await * soundLoad(soundPath, '3d noblock noplay')
soundObjects[soundRpm] = sound soundObjects[soundRpm] = sound
table.insert(soundRpms,soundRpm) table.insert(soundRpms, soundRpm)
if maxValue < soundRpm then if maxValue < soundRpm then
maxValue = soundRpm maxValue = soundRpm
end end
end end
table.sort(soundRpms) table.sort(soundRpms)
hook.add("think", table.address({}), function()
if not self.active then
return
end
smoothRpm = smoothRpm * (1 - 0.2) + engineRpm * 0.2 hook.add('think', table.address({}), function()
smoothThrottle = smoothThrottle * (1 - 0.1) + throttle * 0.1 if not self.active then
return
for n, rpm in ipairs(soundRpms) do end
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() smoothRpm = smoothRpm * (1 - 0.2) + engineRpm * 0.2
local rpm = net.readUInt(16) smoothThrottle = smoothThrottle * (1 - 0.1) + throttle * 0.1
engineRpm = rpm * (maxValue / redline)
throttle = math.max(net.readFloat(), 0) for n, rpm in ipairs(soundRpms) do
end) 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)
end end
return Sound return Sound

View File

@ -1,13 +1,13 @@
--@name koptilnya/libs/constants --@name koptilnya/libs/constants
NULL_ENTITY = entity(0) _G.NULL_ENTITY = entity(0)
CURRENT_PLAYER = player() _G.CURRENT_PLAYER = player()
OWNER = owner() _G.OWNER = owner()
IS_ME = CURRENT_PLAYER == OWNER _G.IS_ME = CURRENT_PLAYER == OWNER
TICK_INTERVAL = game.getTickInterval() _G.TICK_INTERVAL = game.getTickInterval()
RAD_TO_RPM = 9.5493 _G.RAD_TO_RPM = 9.5493
RPM_TO_RAD = 0.10472 _G.RPM_TO_RAD = 0.10472
UNITS_PER_METER = 39.37 _G.UNITS_PER_METER = 39.37
UNITS_TO_METERS = 0.01905 _G.UNITS_TO_METERS = 0.01905
KG_TO_N = 9.80665 _G.KG_TO_N = 9.80665
KG_TO_KN = 0.00980665 _G.KG_TO_KN = 0.00980665

View File

@ -1,86 +1,86 @@
-- @name koptilnya/libs/utils -- @name koptilnya/libs/utils
function checkVarClass(var, class, message) function checkVarClass(var, class, message)
if type(var) ~= "table" or var["class"] == nil or not var:isInstanceOf(class) then 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) throw(message == nil and 'Wrong variable class.' or message, 1, true)
end end
end end
function accessorFunc(tbl, varName, name, defaultValue) function accessorFunc(tbl, varName, name, defaultValue)
tbl[varName] = defaultValue tbl[varName] = defaultValue
tbl["get" .. name] = function(self) tbl['get' .. name] = function(self)
return self[varName] return self[varName]
end end
tbl["set" .. name] = function(self, value) tbl['set' .. name] = function(self, value)
self[varName] = value self[varName] = value
end end
end end
function rotateAround(entity, pivot, angles) function rotateAround(entity, pivot, angles)
local pos = entity:getPos() local pos = entity:getPos()
local localPivotPos = entity:worldToLocal(pivot) local localPivotPos = entity:worldToLocal(pivot)
entity:setAngles(angles) entity:setAngles(angles)
pos = pos + (pivot - entity:localToWorld(localPivotPos)) pos = pos + (pivot - entity:localToWorld(localPivotPos))
entity:setPos(pos) entity:setPos(pos)
end end
function tobase(number, base) function tobase(number, base)
local ret = "" local ret = ''
if base < 2 or base > 36 or number == 0 then if base < 2 or base > 36 or number == 0 then
return "0" return '0'
end end
if base == 10 then if base == 10 then
return tostring(number) return tostring(number)
end end
local chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" local chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'
local loops = 0 local loops = 0
while number > 0 do while number > 0 do
loops = loops + 1 loops = loops + 1
number, d = math.floor(number / base), (number % base) + 1 number, d = math.floor(number / base), (number % base) + 1
ret = string.sub(chars, d, d) .. ret ret = string.sub(chars, d, d) .. ret
if (loops > 32000) then if loops > 32000 then
break break
end end
end end
return ret return ret
end end
function bytesToHex(bytes) function bytesToHex(bytes)
local hex = "0x" local hex = '0x'
for _, v in pairs(bytes) do for _, v in pairs(bytes) do
hex = hex .. bit.tohex(v, 2) hex = hex .. bit.tohex(v, 2)
end end
return hex return hex
end end
function byteTable(str, start, length) function byteTable(str, start, length)
local result = {} local result = {}
for i = 1, length do for i = 1, length do
result[i] = string.byte(str, i + start - 1) result[i] = string.byte(str, i + start - 1)
end end
return result return result
end end
function isURL(str) 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 end
function debounce(func, delay) function debounce(func, delay)
local lastCall = 0 local lastCall = 0
return function(...) return function(...)
local now = timer.systime() local now = timer.systime()
if now - lastCall >= delay then if now - lastCall >= delay then
lastCall = now lastCall = now
return func(...) return func(...)
end end
end end
end end