Насрал типами, но еще не до конца
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user