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