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

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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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