diff --git a/koptilnya/engine_remastered/configs/new_sx240.txt b/koptilnya/engine_remastered/configs/new_sx240.txt index e6b9422..ebfcecd 100644 --- a/koptilnya/engine_remastered/configs/new_sx240.txt +++ b/koptilnya/engine_remastered/configs/new_sx240.txt @@ -8,11 +8,10 @@ local Differential = require('/koptilnya/engine_remastered/powertrain/differenti -- Michelin Pilot Sport 4S (High-Performance Road Tire) local WheelConfig = { - DEBUG = true, + DEBUG = false, BrakePower = 1200, CustomWheel = { Mass = 30, - LateralFrictionPreset = { B = 12.0, C = 1.3, @@ -49,16 +48,24 @@ local FrontWheelsConfig = table.merge(table.copy(WheelConfig), { }) local RearWheelsConfig = table.merge(table.copy(WheelConfig), { - BrakePower = 600, + BrakePower = 0, HandbrakePower = 2200, CustomWheel = { CamberAngle = -2, + LongitudinalFrictionPreset = nil, + LateralFrictionPreset = nil, -- LongitudinalFrictionPreset = { -- B = 0.1, -- C = 1, -- D = 0.9, -- E = 0.9, -- }, + -- LateralFrictionPreset = { + -- B = 0.1, + -- C = 1, + -- D = 0.9, + -- E = 0.9, + -- }, }, }) @@ -201,7 +208,7 @@ Vehicle:new({ }, { Name = 'AxleFront', - -- Input = 'Axle', + Input = 'Axle', Type = POWERTRAIN_COMPONENT.Differential, Config = { Type = Differential.TYPES.Open, @@ -230,7 +237,7 @@ Vehicle:new({ }, { Name = 'AxleBack', - Input = 'Gearbox', + Input = 'Axle', Type = POWERTRAIN_COMPONENT.Differential, Config = { Type = Differential.TYPES.Open, @@ -239,17 +246,17 @@ Vehicle:new({ BiasAB = 0.5, CoastRamp = 1, PowerRamp = 1, - Stiffness = 0.9, + Stiffness = 1, SlipTorque = 1000, ToeAngle = 0.5, }, }, { Name = 'Axle', - -- Input = 'Gearbox', + Input = 'Gearbox', Type = POWERTRAIN_COMPONENT.Differential, Config = { - Type = Differential.TYPES.Open, + Type = Differential.TYPES.Locking, FinalDrive = 1, Inertia = 0.01, BiasAB = 0.5, diff --git a/koptilnya/engine_remastered/powertrain/clutch.txt b/koptilnya/engine_remastered/powertrain/clutch.txt index f902ed5..0ae6244 100644 --- a/koptilnya/engine_remastered/powertrain/clutch.txt +++ b/koptilnya/engine_remastered/powertrain/clutch.txt @@ -1,6 +1,7 @@ --@include ./powertrain_component.txt --@include /koptilnya/libs/constants.txt +---@type PowertrainComponent local PowertrainComponent = require('./powertrain_component.txt') require('/koptilnya/libs/constants.txt') @@ -88,4 +89,7 @@ function Clutch:forwardStep(torque, inertia) return math.clamp(returnTorque, -self.slipTorque, self.slipTorque) end +---@type fun(vehicle: KPTLVehicle, name?: string, config?: ClutchConfig): Clutch +Clutch.new = Clutch.new + return Clutch diff --git a/koptilnya/engine_remastered/powertrain/differential.txt b/koptilnya/engine_remastered/powertrain/differential.txt index d59c3a6..a2e52f2 100644 --- a/koptilnya/engine_remastered/powertrain/differential.txt +++ b/koptilnya/engine_remastered/powertrain/differential.txt @@ -1,6 +1,7 @@ --@include /koptilnya/libs/constants.txt --@include ./powertrain_component.txt +---@type PowertrainComponent local PowertrainComponent = require('./powertrain_component.txt') require('/koptilnya/libs/constants.txt') @@ -47,8 +48,7 @@ function Differential:initialize(vehicle, name, config) self.wireOutputs = { [string.format('%s_Torque', self.name)] = 'number', - [string.format('%s_W', self.name)] = 'number', - [string.format('%s_MzDiff', self.name)] = 'number', + [string.format('%s_RPMDiff', self.name)] = 'number', } self.finalDrive = config.FinalDrive or 4 @@ -66,6 +66,7 @@ function Differential:initialize(vehicle, name, config) self.steerAngle = 0 self.mzDiff = 0 + self.rpmDiff = 0 end ---@param component PowertrainComponent @@ -90,8 +91,7 @@ function Differential:updateWireOutputs() PowertrainComponent.updateWireOutputs(self) 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 + wire.ports[string.format('%s_RPMDiff', self.name)] = self.rpmDiff if self.outputA ~= nil then self.outputA:updateWireOutputs() @@ -157,49 +157,60 @@ function Differential:forwardStep(torque, inertia) tqA = self.outputA:forwardStep(tqA, inertia * 0.5 * math.pow(self.finalDrive, 2) + aI) tqB = self.outputB:forwardStep(tqB, inertia * 0.5 * math.pow(self.finalDrive, 2) + bI) + if self.CONFIG.Type == DIFFERENTIAL_TYPES.Locking then + local avg = (self.outputA.angularVelocity + self.outputB.angularVelocity) / 2 + + self.outputA:queryAngularVelocity(avg) + self.outputB:queryAngularVelocity(avg) + end + + self.rpmDiff = self.outputA:getRPM() - self.outputB:getRPM() + -- // REFACTOR - if self.steerLock ~= 0 then - local outputA = self.outputA --[[@as Wheel]] - local outputB = self.outputB --[[@as Wheel]] + if self.outputA.customWheel ~= nil and self.outputB.customWheel ~= nil then + if self.steerLock ~= 0 then + local outputA = self.outputA --[[@as Wheel]] + local outputB = self.outputB --[[@as Wheel]] - local steerInertia = (aI + bI) / 2 - local driverInput = 15 + local steerInertia = (aI + bI) + local driverInput = 3 - local localVelocityLength = chip():getVelocity():getLength() - local MPH = localVelocityLength * (15 / 352) - local KPH = MPH * 1.609 + local localVelocityLength = chip():getVelocity():getLength() + local MPH = localVelocityLength * (15 / 352) + local KPH = MPH * 1.609 - local assist = math.clamp(10.0 / math.sqrt(KPH / 3), 2.0, 10.0) + local assist = math.clamp(10.0 / math.sqrt(KPH / 3), 2.0, 10.0) / 2 - local inputForce = driverInput * assist + local inputForce = driverInput * assist - local inputTorque = self.vehicle.steer * inputForce + local inputTorque = self.vehicle.steer * inputForce - local mzDiff = outputA.customWheel.mz - outputB.customWheel.mz + local mzDiff = (outputB.customWheel.mz - outputA.customWheel.mz) * TICK_INTERVAL / 2 - self.mzDiff = mzDiff + self.mzDiff = math.lerp(0.1, self.mzDiff, mzDiff) - local steerTorque = mzDiff * -1 + inputTorque + local steerTorque = self.mzDiff * 1 + inputTorque - local steerAngularAccel = steerTorque / steerInertia + local steerAngularAccel = steerTorque / steerInertia - self.steerAngle = math.clamp(self.steerAngle + steerAngularAccel * TICK_INTERVAL, -self.steerLock, self.steerLock) + self.steerAngle = math.clamp(self.steerAngle + math.deg(steerAngularAccel) * TICK_INTERVAL, -self.steerLock, self.steerLock) - local wheelbase = 2.05 - local trackWidth = 1.124 - local radius = wheelbase / math.tan(math.rad(self.steerAngle)) + local wheelbase = 2.05 + local trackWidth = 1.124 + local radius = wheelbase / math.tan(math.rad(self.steerAngle)) - local innerAngle = math.deg(math.atan(wheelbase / (radius - (trackWidth / 2)))) - local outerAngle = math.deg(math.atan(wheelbase / (radius + (trackWidth / 2)))) + 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 = 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 + outputA.customWheel.steerAngle = self.toeAngle * outputA.customWheel.direction + outputB.customWheel.steerAngle = self.toeAngle * outputB.customWheel.direction + end end return tqA + tqB @@ -212,21 +223,63 @@ end ---@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 + return tq * 0.5, tq * 0.5 + -- -- Разность скоростей между выходами + -- local deltaW = aW - bW - local syncTorque = (aW - bW) * stiffness * (aI + bI) * 0.5 / TICK_INTERVAL + -- -- Расчет "проскальзывающего" момента между сторонами + -- local slip = math.abs(deltaW) - aTq = aTq - syncTorque - bTq = bTq + syncTorque + -- -- Применяем преднагрузку и жесткость, чтобы определить базовый момент блокировки + -- local lockingTorque = preload + stiffness * deltaW + + -- -- Определение направления нагрузки: ускорение или торможение + -- local ramp = tq >= 0 and powerRamp or coastRamp + + -- -- Применяем ramp к разности скоростей, усиливая/ослабляя блокировку + -- local rampTorque = slip * ramp + + -- -- Итоговый момент передачи между сторонами + -- local transferTorque = lockingTorque + rampTorque + + -- -- Ограничиваем передачу момента порогом проскальзывания + -- if transferTorque > slipTorque then + -- transferTorque = slipTorque + -- end + + -- -- Учитываем биас (смещение баланса) + -- local maxBias = biasAB + -- local biasLimit = tq * (maxBias - 1) / (maxBias + 1) + + -- -- Финальный момент, передаваемый между сторонами, с учетом направления + -- local direction = deltaW > 0 and -1 or 1 + -- local finalTorque = direction * transferTorque + + -- -- Применяем финальный момент к каждой стороне, ограничивая его bias'ом + -- local aTorque = tq * 0.5 + math.min(finalTorque, biasLimit) + -- local bTorque = tq * 0.5 - math.min(finalTorque, biasLimit) + + -- -- Возвращаем обновленные значения угловых скоростей + -- return aTorque, bTorque + + -- local aTq = tq * 0.5 + -- local bTq = tq * 0.5 + + -- local syncTorque = (aW - bW) * 1e6 * (aI + bI) / TICK_INTERVAL + + -- aTq = aTq - syncTorque + -- bTq = bTq + syncTorque + + -- -- print(aTq, bTq) + + -- return aTq, bTq - return aTq, bTq -- local sumI = aI + bI -- local w = aI / sumI * aW + bI / sumI * bW - -- local aTqCorr = (w - aW) * aI -- / dt + -- local aTqCorr = (w - aW) * aI / TICK_INTERVAL -- aTqCorr = aTqCorr * stiffness - -- local bTqCorr = (w - bW) * bI -- / dt + -- local bTqCorr = (w - bW) * bI / TICK_INTERVAL -- bTqCorr = bTqCorr * stiffness -- local biasA = math.clamp(0.5 + (bW - aW) * 0.1 * stiffness, 0, 1) @@ -293,4 +346,7 @@ end Differential.TYPES = DIFFERENTIAL_TYPES +---@type fun(vehicle: KPTLVehicle, name?: string, config?: DifferentialConfig): Differential +Differential.new = Differential.new + return Differential diff --git a/koptilnya/engine_remastered/powertrain/engine.txt b/koptilnya/engine_remastered/powertrain/engine.txt index 306b5b6..04e2610 100644 --- a/koptilnya/engine_remastered/powertrain/engine.txt +++ b/koptilnya/engine_remastered/powertrain/engine.txt @@ -1,6 +1,7 @@ --@include ./powertrain_component.txt --@include /koptilnya/libs/constants.txt +---@type PowertrainComponent local PowertrainComponent = require('./powertrain_component.txt') require('/koptilnya/libs/constants.txt') @@ -65,10 +66,10 @@ function Engine:initialize(vehicle, name, config) self.returnedTorque = 0 if CLIENT then - -- --@include /koptilnya/engine_sound_2.txt - -- local Sound = require('/koptilnya/engine_sound_2.txt') + --@include /koptilnya/engine_sound_2.txt + local Sound = require('/koptilnya/engine_sound_2.txt') - -- Sound(self.maxRPM, chip()) + Sound(self.maxRPM, chip()) end end @@ -152,16 +153,19 @@ function Engine:forwardStep(torque, inertia) self.angularVelocity = self.angularVelocity + finalTorque / inertiaSum * TICK_INTERVAL self.angularVelocity = math.max(self.angularVelocity, 0) - -- net.start('ENGINE_RPM') - -- net.writeFloat(self:getRPM() / self.maxRPM) - -- net.send(self.vehicle.playersConnectedToHUD, true) + net.start('ENGINE_RPM') + net.writeFloat(self:getRPM() / self.maxRPM) + net.send(self.vehicle.playersConnectedToHUD, true) - -- net.start('ENGINE_FULLRPM') - -- net.writeUInt(self:getRPM(), 16) - -- net.writeFloat(self.masterThrottle) - -- net.send(nil, true) + net.start('ENGINE_FULLRPM') + net.writeUInt(self:getRPM(), 16) + net.writeFloat(self.masterThrottle) + net.send(nil, true) return 0 end +---@type fun(vehicle: KPTLVehicle, name?: string, config?: EngineConfig): Engine +Engine.new = Engine.new + return Engine diff --git a/koptilnya/engine_remastered/powertrain/powertrain_component.txt b/koptilnya/engine_remastered/powertrain/powertrain_component.txt index 986acc0..3e1ac73 100644 --- a/koptilnya/engine_remastered/powertrain/powertrain_component.txt +++ b/koptilnya/engine_remastered/powertrain/powertrain_component.txt @@ -129,4 +129,7 @@ function PowertrainComponent:serializeDebugData() end ---@return nil function PowertrainComponent:deserializeDebugData(result) end +---@type fun(vehicle: KPTLVehicle, name?: string, config?: PowertrainComponentConfig): PowertrainComponent +PowertrainComponent.new = PowertrainComponent.new + return PowertrainComponent diff --git a/koptilnya/engine_remastered/powertrain/wheel.txt b/koptilnya/engine_remastered/powertrain/wheel.txt index 5756957..e5d2ce2 100644 --- a/koptilnya/engine_remastered/powertrain/wheel.txt +++ b/koptilnya/engine_remastered/powertrain/wheel.txt @@ -3,7 +3,10 @@ --@include /koptilnya/libs/constants.txt --@include /koptilnya/libs/entity.txt +---@type PowertrainComponent local PowertrainComponent = require('./powertrain_component.txt') + +---@type CustomWheel local CustomWheel = require('../wheel/wheel.txt') require('/koptilnya/libs/constants.txt') @@ -18,13 +21,11 @@ require('/koptilnya/libs/entity.txt') ---@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) @@ -55,11 +56,11 @@ function Wheel:initialize(vehicle, name, config) 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) + render.draw3DBeam(pos, pos + ((lerpForwardFrictionForce / 500 + 0) * self.DEBUG_DATA.forward), 1, 0, 0) render.setColor(Color(0, 255, 0, 255)) lerpSideFrictionForce = math.lerp(0.1, lerpSideFrictionForce, self.DEBUG_DATA.sideFrictionForce) - render.draw3DBeam(pos, pos + ((lerpSideFrictionForce / 500 + 16) * self.DEBUG_DATA.right), 1, 0, 0) + render.draw3DBeam(pos, pos + ((lerpSideFrictionForce / 500 + 0) * self.DEBUG_DATA.right), 1, 0, 0) render.setColor(Color(0, 0, 255, 200)) lerpLoad = math.lerp(0.1, lerpLoad, self.DEBUG_DATA.load) @@ -72,7 +73,6 @@ function Wheel:initialize(vehicle, name, config) return end - self.entity = config.Entity or NULL_ENTITY self.brakePower = config.BrakePower or 0 self.handbrakePower = config.HandbrakePower or 0 self.model = config.Model or '' @@ -84,33 +84,52 @@ function Wheel:initialize(vehicle, name, config) } self.wireOutputs = { [string.format('%s_RPM', self.name)] = 'number', - [string.format('%s_Fx', self.name)] = 'number', - [string.format('%s_CTq', self.name)] = 'number', } self.rot = 0 - self.holo = self:createHolo(self.entity) + ---@type Entity? + self.entity = nil - self.customWheel = CustomWheel:new(table.merge(table.copy(config.CustomWheel), { Direction = self.direction, Name = self.name })) + ---@type Hologram? + self.holo = nil + + self.customWheel = CustomWheel:new( + table.merge( + table.copy(config.CustomWheel), + { Direction = self.direction, Name = self.name, ParentPhysObj = vehicle.basePhysObject } + ) + ) hook.add('Input', 'vehicle_wheel_update' .. self.name, function(name, value) if name == self.name then - self.entity = value - self.customWheel:setEntity(value) - - if isValid(self.holo) then - self.holo:remove() - end - - self.holo = self:createHolo(self.entity) - self.customWheel.radius = self:getEntityRadius() + self:setEntity(value) end end) - - self.steerVelocity = 0 end +---@override +---@return nil +function Wheel:onWirePortsReady() + self:setEntity(self.CONFIG.Entity or wire.ports[self.name]) +end + +---@private +---@param entity Entity +---@return nil +function Wheel:setEntity(entity) + self.entity = entity + self.customWheel:setEntity(entity) + self.customWheel.radius = self:getEntityRadius() + + if isValid(self.holo) then + self.holo:remove() + end + + self.holo = self:createHolo(self.entity) +end + +---@return number function Wheel:getEntityRadius() if not isValid(self.entity) then return 0 @@ -119,16 +138,16 @@ function Wheel:getEntityRadius() return self.entity:getModelRadius() * UNITS_TO_METERS end ----@return Hologram | Entity +---@return Hologram? function Wheel:createHolo(entity) if not isValid(entity) then - return NULL_ENTITY + return nil end local holo = hologram.create(entity:getPos(), entity:getAngles(), self.model or '') - if holo == nil or isValid(holo) then - return NULL_ENTITY + if holo == nil or not isValid(holo) then + return nil end holo:setParent(entity) @@ -143,8 +162,6 @@ function Wheel:updateWireOutputs() PowertrainComponent.updateWireOutputs(self) 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 @@ -153,8 +170,10 @@ function Wheel:queryInertia() end ---@return number -function Wheel:queryAngularVelocity() - return self.angularVelocity +function Wheel:queryAngularVelocity(angularVelocity) + self.customWheel.angularVelocity = angularVelocity + + return self.customWheel.angularVelocity end ---@param torque number @@ -228,4 +247,7 @@ function Wheel:deserializeDebugData(result) result.sideFrictionForce = net.readFloat() end +---@type fun(vehicle: KPTLVehicle, name?: string, config?: WheelConfig): Wheel +Wheel.new = Wheel.new + return Wheel diff --git a/koptilnya/engine_remastered/vehicle.txt b/koptilnya/engine_remastered/vehicle.txt index 40adcc8..c85e465 100644 --- a/koptilnya/engine_remastered/vehicle.txt +++ b/koptilnya/engine_remastered/vehicle.txt @@ -13,7 +13,7 @@ require('/koptilnya/libs/table.txt') require('/koptilnya/libs/constants.txt') ---@class KPTLVehicle ----@field config { [string]: any } +---@field config { Name: string, Input?: string, Config?: table }[] ---@field components PowertrainComponent[] ---@field headComponents PowertrainComponent[] ---@field steer number @@ -23,6 +23,7 @@ require('/koptilnya/libs/constants.txt') ---@field base? Entity ---@field basePhysObject? PhysObj ---@field initialized boolean +---@field initializedPlayers Player[] local Vehicle = class('Vehicle') ---@param config { [string]: any } @@ -40,6 +41,7 @@ function Vehicle:initialize(config) self.steer = 0 self.brake = 0 self.handbrake = 0 + self.initializedPlayers = {} self.playersConnectedToHUD = {} if SERVER then @@ -67,6 +69,15 @@ function Vehicle:initialize(config) hook.add('PlayerDisconnected', 'vehicle_huddisconnected', function(ply) table.removeByValue(self.playersConnectedToHUD, ply) end) + + hook.add('ClientInitialized', 'vehicle_clientinitialized', function(ply) + table.insert(self.initializedPlayers, ply) + + if self.initialized then + net.start('VEHICLE_READY') + net.send(ply, true) + end + end) else --@include ./hud.txt require('./hud.txt') @@ -88,11 +99,9 @@ function Vehicle:start() self.initialized = true - print('VEHICLE READY') - if SERVER then net.start('VEHICLE_READY') - net.send(find.allPlayers(), true) + net.send(self.initializedPlayers, true) end end @@ -144,8 +153,10 @@ function Vehicle:linkComponents() end if SERVER then + print(Color(0, 255, 0), 'Powertrain tree:') for _, component in pairs(self.headComponents) do - self:printComponentTree(component) + PrintTree(component) + print(' ') end end end @@ -166,6 +177,10 @@ function Vehicle:createIO() end wire.adjustPorts(inputs, outputs) + + for _, component in ipairs(self.components) do + component:onWirePortsReady() + end end ---@return nil @@ -212,18 +227,67 @@ function Vehicle:update() --end end ----@param component PowertrainComponent ----@param result? string ---@return nil -function Vehicle:printComponentTree(component, result) - -- result = result or component.name +function PrintTree(root) + if root == nil then + return + end - -- if component.output then - -- result = result .. component.name - -- return self:printComponentTree(component.output, result .. ' -> ') - -- else - -- print(result) - -- end + local lines = { { Color(255, 255, 255), root.name } } + local subtreeLines = PrintSubtree(root, '') + for _, line in ipairs(subtreeLines) do + table.insert(lines, line) + end + + for _, line in ipairs(lines) do + print(unpack(line)) + end +end + +---@return (string | Color)[] +function PrintSubtree(root, prefix) + if root == nil then + return {} + end + + ---@type (string | Color)[] + local lines = {} + + local left = root.outputB + local right = root.output or root.outputA + + local hasLeft = left ~= nil + local hasRight = right ~= nil + + if not hasLeft and not hasRight then + return lines + end + + if hasRight then + local printStrand = hasLeft and (right.output ~= nil or right.outputA ~= nil or right.outputB ~= nil) + local newPrefix = prefix .. (printStrand and '│ \t' or '\t\t') + + table.insert( + lines, + { Color(80, 80, 80), prefix .. (hasLeft and '├── ' or '└── '), Color(255, 255, 255), tostring(right.name) } + ) + + local rightLines = PrintSubtree(right, newPrefix) + for _, line in ipairs(rightLines) do + table.insert(lines, line) + end + end + + if hasLeft then + table.insert(lines, { Color(80, 80, 80), (hasRight and prefix or '') .. '└── ', Color(255, 255, 255), tostring(left.name) }) + + local leftLines = PrintSubtree(left, prefix .. '\t\t') + for _, line in ipairs(leftLines) do + table.insert(lines, line) + end + end + + return lines end return { diff --git a/koptilnya/engine_remastered/wheel/friction.txt b/koptilnya/engine_remastered/wheel/friction.txt index 0319c47..18fe643 100644 --- a/koptilnya/engine_remastered/wheel/friction.txt +++ b/koptilnya/engine_remastered/wheel/friction.txt @@ -1,14 +1,16 @@ +---@class Friction +---@field force number +---@field slip number +---@field speed number local Friction = class('Friction') -function Friction:initialize(config) - config = config or {} - - self.slipCoefficient = config.SlipCoefficient or 0.8 - self.forceCoefficient = config.ForceCoefficient or 1.2 - - self.force = 0 - self.slip = 0 - self.speed = 0 +function Friction:initialize() + self.force = 0 + self.slip = 0 + self.speed = 0 end +---@type fun(): Friction +Friction.new = Friction.new + return Friction diff --git a/koptilnya/engine_remastered/wheel/friction_preset.txt b/koptilnya/engine_remastered/wheel/friction_preset.txt index 0d77870..62125de 100644 --- a/koptilnya/engine_remastered/wheel/friction_preset.txt +++ b/koptilnya/engine_remastered/wheel/friction_preset.txt @@ -1,18 +1,35 @@ +---@class FrictionPresetConfig +---@field B? number +---@field C? number +---@field D? number +---@field E? number + +---@class FrictionPreset +---@field B number +---@field C number +---@field D number +---@field E number local FrictionPreset = class('FrictionPreset') +---@param config FrictionPresetConfig function FrictionPreset:initialize(config) - config = config or {} + config = config or {} - self.B = config.B or 10.86 - self.C = config.C or 2.15 - self.D = config.D or 0.933 - self.E = config.E or 0.992 + self.B = config.B or 10.86 + self.C = config.C or 2.15 + self.D = config.D or 0.933 + self.E = config.E or 0.992 end +---@param slip number +---@return number function FrictionPreset:evaluate(slip) - local t = math.abs(slip) + local t = math.abs(slip) - return self.D * math.sin(self.C * math.atan(self.B * t - self.E * (self.B * t - math.atan(self.B * t)))) + return self.D * math.sin(self.C * math.atan(self.B * t - self.E * (self.B * t - math.atan(self.B * t)))) end +---@type fun(config?: FrictionPresetConfig): FrictionPreset +FrictionPreset.new = FrictionPreset.new + return FrictionPreset diff --git a/koptilnya/engine_remastered/wheel/wheel.txt b/koptilnya/engine_remastered/wheel/wheel.txt index d22a84c..8911798 100644 --- a/koptilnya/engine_remastered/wheel/wheel.txt +++ b/koptilnya/engine_remastered/wheel/wheel.txt @@ -3,39 +3,51 @@ --@include /koptilnya/libs/constants.txt --@include /koptilnya/libs/utils.txt +---@type Friction local Friction = require('./friction.txt') + +---@type FrictionPreset local FrictionPreset = require('./friction_preset.txt') require('/koptilnya/libs/constants.txt') require('/koptilnya/libs/utils.txt') ---@class CustomWheelConfig +---@field ParentPhysObj PhysObj ---@field Name? string ---@field Direction? integer +---@field LateralFrictionPreset? FrictionPresetConfig +---@field LongitudinalFrictionPreset? FrictionPresetConfig +---@field AligningFrictionPreset? FrictionPresetConfig ---@class CustomWheel local Wheel = class('Wheel') +---@param config CustomWheelConfig function Wheel:initialize(config) config = config or {} + ---@type string self.name = config.Name + self.direction = config.Direction or 1 self.mass = config.Mass or 20 self.radius = config.Radius or 0.27 self.rollingResistance = config.RollingResistance or 20 - self.squat = config.Squat or 0.1 - self.slipCircleShape = config.SlipCircleShape or 1.05 + self.casterAngle = math.rad(config.CasterAngle or 0) self.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() + self.sideFriction = Friction:new() self.lateralFrictionPreset = FrictionPreset:new(config.LateralFrictionPreset) + self.longitudinalFrictionPreset = FrictionPreset:new(config.LongitudinalFrictionPreset) + self.aligningFrictionPreset = FrictionPreset:new(config.AligningFrictionPreset) + self.slipCircleShape = config.SlipCircleShape or 1.05 self.motorTorque = 0 self.brakeTorque = 0 @@ -51,15 +63,20 @@ function Wheel:initialize(config) self.right = Vector(0) self.up = Vector(0) self.entity = NULL_ENTITY + + ---@type PhysObj? self.physObj = nil - self.baseInertia = 0.5 * self.mass * math.pow(self.radius, 2) + self.baseInertia = 0.6 * self.mass * math.pow(self.radius, 2) self.inertia = self.baseInertia self.printDebounced = debounce(function(...) print(...) end, 1) + + self.parentPhysObj = config.ParentPhysObj end +---@param entity Entity function Wheel:setEntity(entity) self.entity = entity self.entity:setNoDraw(false) @@ -72,12 +89,12 @@ function Wheel:setEntity(entity) self.physObj:enableDrag(false) self.physObj:setDragCoefficient(0) self.physObj:setAngleDragCoefficient(0) + self:setInertia(0.6 * self.mass * math.pow(self.radius, 2)) end end function Wheel:setInertia(inertia) if isValid(self.physObj) then - print(inertia) self.physObj:setInertia(Vector(inertia)) self.inertia = inertia end @@ -104,7 +121,7 @@ function Wheel:stepLongitudinal(Tm, Tb, Vx, W, Lc, R, I) local VxAbs = math.abs(Vx) local Sx = 0 - if Lc / 1000 < 0.01 then + if Lc < 0.01 then Sx = 0 elseif VxAbs >= 0.01 then Sx = (W * R - Vx) / VxAbs @@ -116,6 +133,10 @@ function Wheel:stepLongitudinal(Tm, Tb, Vx, W, Lc, R, I) W = W + Tm / I * TICK_INTERVAL + if self.name == 'WheelRL' or self.name == 'WheelRR' then + self.printDebounced(Color(255, 255, 0), string.format('[%s] ', self.name), Color(255, 255, 255), string.format('W: %.2f', W)) + end + Tb = Tb * (W > 0 and -1 or 1) local TbCap = math.abs(W) * I / TICK_INTERVAL local Tbr = math.abs(Tb) - math.abs(TbCap) @@ -125,13 +146,11 @@ function Wheel:stepLongitudinal(Tm, Tb, Vx, W, Lc, R, I) local maxTorque = self.longitudinalFrictionPreset:evaluate(math.abs(Sx)) * Lc * R + -- local inertiaSum = (I + self.vehicle.basePhysObject:getInertia():getLength()) + local errorTorque = (W - Vx / R) * I / TICK_INTERVAL local surfaceTorque = math.clamp(errorTorque, -maxTorque, maxTorque) - if self.name == 'WheelFL' or self.name == 'WheelFR' then - surfaceTorque = surfaceTorque - end - W = W - surfaceTorque / I * TICK_INTERVAL local Fx = surfaceTorque / R @@ -143,13 +162,6 @@ function Wheel:stepLongitudinal(Tm, Tb, Vx, W, Lc, R, I) 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 @@ -202,7 +214,7 @@ function Wheel:stepLateral(Vx, Vy, Lc) local VxAbs = math.abs(Vx) local Sy = 0 - if Lc / 1000 < 0.01 then + if Lc < 0.01 then Sy = 0 elseif VxAbs > 0.1 then Sy = math.deg(math.atan(Vy / VxAbs)) @@ -237,7 +249,7 @@ function Wheel:slipCircle(Sx, Sy, Fx, Fy, slipCircleShape) return Sx, Sy, Fx, Fy end -function Wheel:selfAligningTorque(Sy, load) +function Wheel:selfAligningTorque(Sy, Fy, load) if math.abs(Sy) < 0.001 or load < 0.001 then return 0 end @@ -247,32 +259,34 @@ function Wheel:selfAligningTorque(Sy, load) local D = self.aligningFrictionPreset.D local E = self.aligningFrictionPreset.E - 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 entityWheelDown = self.entity:localToWorld(Vector(0, 0, -self.radius * UNITS_PER_METER)) + local physWheelDown = self.entity:localToWorld(-self.up * self.radius * UNITS_PER_METER) - local 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 mechanicalTrail = entityWheelDown:getDistance(physWheelDown) * UNITS_TO_METERS + local pneumaticTrail = 0.2 * math.exp(-5 * math.abs(Sy)) - local effectiveTrail = mechanicalTrail * self.radius + local Mz = -Fy * (mechanicalTrail + pneumaticTrail) - local D_scaled = D * load * effectiveTrail + if self.name == 'WheelFL' or self.name == 'WheelFR' then + -- self.printDebounced( + -- Color(255, 255, 0), + -- string.format('[%s] ', self.name), + -- Color(255, 255, 255), + -- string.format('Mz: %.2f | Sy: %.2f | Fy: %.2f | mechanicalTrail: %.3f', Mz, Sy, Fy, mechanicalTrail) + -- ) + end - -- 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 + return Mz -- TICK_INTERVAL end function Wheel:rotateAxes() - local ang = self.entity:getAngles() + local ang = self.physObj:getAngles() -- Base directions + ---@type Vector local forward = ang:getForward() * self.direction local up = ang:getUp() + ---@type Vector local right = -ang:getRight() * self.direction -- Step 1: Steer + Toe (rotate forward/right around up) @@ -301,15 +315,44 @@ function Wheel:update() return end - local externalStress = self.physObj:getStress() - local velocity = self.physObj:getVelocity() * UNITS_TO_METERS + local frictionSnapshot = self.physObj:getFrictionSnapshot() + local externalStress, internalStress = self.physObj:getStress() - self.hasHit = externalStress ~= 0 - self.load = externalStress * KG_TO_KN * 1000 + self.hasHit = #frictionSnapshot > 0 + self.load = self.hasHit and externalStress * KG_TO_N or 0 + + ---@type PhysObj? + local frictionSnapshotItem = self.hasHit and frictionSnapshot[1] or nil + + local surfaceVelocity = Vector(0, 0, 0) + + if frictionSnapshotItem ~= nil then + surfaceVelocity = frictionSnapshotItem.Other:getVelocityAtPoint(frictionSnapshotItem.ContactPoint) + end + + local velocity = (self.parentPhysObj:getVelocityAtPoint(self.entity:getPos()) - surfaceVelocity) * UNITS_TO_METERS + -- local velocity = self.physObj:getVelocity() * UNITS_TO_METERS + + -- self.load = frictionSnapshotItem and frictionSnapshotItem.NormalForce / KG_TO_N or 0 self.longitudinalLoadCoefficient = self:getLongitudinalLoadCoefficient(self.load) self.lateralLoadCoefficient = self:getLateralLoadCoefficient(self.load) + -- if self.name == 'WheelRL' or self.name == 'WheelRR' then + -- self.printDebounced( + -- Color(255, 255, 0), + -- string.format('[%s] ', self.name), + -- Color(255, 255, 255), + -- string.format( + -- 'Load: %.2f | LonLc: %.2f | LaLc: %.2f | externalStress: %.2f', + -- self.load, + -- self.longitudinalLoadCoefficient, + -- self.lateralLoadCoefficient, + -- externalStress + -- ) + -- ) + -- end + self:rotateAxes() local forwardSpeed = 0 @@ -332,9 +375,19 @@ function Wheel:update() 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, self.slipCircleShape) - self.mz = self:selfAligningTorque(Sy, self.load) + -- Traction circle: ограничение по максимальной силе сцепления + -- local mu = 1.0 -- коэффициент трения (может быть в пресете) + -- local F_max = self.load * mu + -- local F_total = math.sqrt(Fx * Fx + Fy * Fy) + -- if F_total > F_max and F_total > 0 then + -- local scale = F_max / F_total + -- Fx = Fx * scale + -- Fy = Fy * scale + -- end + + self.mz = self:selfAligningTorque(Sy, Fy, self.load) self.angularVelocity = W self.counterTorque = CounterTq * self.direction @@ -346,4 +399,7 @@ function Wheel:update() self.sideFriction.speed = sideSpeed end +---@type fun(config: CustomWheelConfig): CustomWheel +Wheel.new = Wheel.new + return Wheel diff --git a/koptilnya/engine_remastered/wire_component.txt b/koptilnya/engine_remastered/wire_component.txt index 1be7fd8..32a1249 100644 --- a/koptilnya/engine_remastered/wire_component.txt +++ b/koptilnya/engine_remastered/wire_component.txt @@ -19,6 +19,9 @@ function WireComponent:initialize() self.wireOutputs = {} end +---@return nil +function WireComponent:onWirePortsReady() end + ---@return nil function WireComponent:updateWireOutputs() end diff --git a/koptilnya/engine_sound_2.txt b/koptilnya/engine_sound_2.txt index 2df8f09..3e66a6d 100644 --- a/koptilnya/engine_sound_2.txt +++ b/koptilnya/engine_sound_2.txt @@ -30,7 +30,7 @@ function Sound:initialize(redline, parent, sounds) [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 + self.active = true local soundObjects = {} local soundRpms = {} local maxValue = 0 diff --git a/koptilnya/libs/constants.txt b/koptilnya/libs/constants.txt index 2bd20b6..590b6cd 100644 --- a/koptilnya/libs/constants.txt +++ b/koptilnya/libs/constants.txt @@ -1,13 +1,13 @@ --@name koptilnya/libs/constants -_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 +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