diff --git a/koptilnya/grip_steering/main.txt b/koptilnya/grip_steering/main.txt new file mode 100644 index 0000000..e2bddf2 --- /dev/null +++ b/koptilnya/grip_steering/main.txt @@ -0,0 +1,188 @@ +-- @name Grip Steering +-- @author DarkSupah +-- @server +-- @include ./slave.txt +Steering = class('Steering') + +require('./slave.txt') + +function inrange(val, min, max) + return val >= min and val <= max +end + +function isNaN(val) + return val ~= val +end + +function vectorIsNaN(vec) + return isNaN(vec[1]) or isNaN(vec[2]) or isNaN(vec[3]) +end + +function getLocalVelocity(ent) + return ent:worldToLocal((ent:getVelocity() + ent:getPos())) +end + +local options = { + Camber = -2, + Caster = 5, + Ackermann = 15, + Lock = 45, + + ZeroThreshold = 2, + + StartSpeedThreshold = 40, + EndSpeedThreshold = 150, + SpeedCoeff = 0.2, + + StartCorrectionThreshold = 5, + EndCorrectionThreshold = 40, + CorrectionOnCoeff = 0.6, + CorrectionOffCoeff = 1, + CorrectionOnLerp = 0.3, + CorrectionOffLerp = 0.6 +} + +function Steering:_adjustPorts() + local inputs = { + Axles = 'table', + Base = 'entity', + LeftSlave = 'entity', + RightSlave = 'entity' + } + + local outputs = { + SteerAngle = 'number', + TargetAngle = 'number', + CorrectionAngle = 'number' + } + + wire.adjustPorts(inputs, outputs) +end + +function Steering:_createSlaves() + local entities = {wire.ports.LeftSlave, wire.ports.RightSlave} + local slaves = {} + + for k, v in pairs(entities) do + local slave = Slave:new({ + Entity = v, + IsLeft = k % 2 == 1, + Offset = 180, + Base = wire.ports.Base, + Camber = self.slavesConfig.Camber, + Caster = self.slavesConfig.Caster, + Ackermann = self.slavesConfig.Ackermann + }) + + table.insert(self.slaves, slave) + end +end + +function Steering:_updateOutputs() + wire.ports.TargetAngle = self.targetAngle + wire.ports.SteerAngle = self.angle + wire.ports.CorrectionAngle = self.correction +end + +function Steering:_getCorrectionGradient(speed) + return math.clamp(math.remap(speed, self.startCorrectionThreshold, self.endCorrectionThreshold, 0, 1), 0, 1) +end + +function Steering:getBaseSpeed() + local MPH = self.base:getVelocity():getLength() * 3600 / 63360 + local KPH = MPH * 1.609 + + return KPH +end + +function Steering:_getSpeedCorrectionGradient(speed) + return math.clamp(math.remap(speed, self.startSpeedThreshold, self.endSpeedThreshold, 0, 1), 0, 1) +end + +function Steering:_getCorrectionAngle() + local localVel = getLocalVelocity(self.base) + local forwardVel = (localVel * Vector(1, 0, 0)):getNormalized() + local planarVec = (localVel * Vector(1, 1, 0)):getNormalized() + + forwardVel = vectorIsNaN(forwardVel) and Vector(0, 0, 0) or forwardVel + planarVec = vectorIsNaN(planarVec) and Vector(0, 0, 0) or planarVec + + local cross = forwardVel:cross(planarVec) + + return math.deg(math.acos(forwardVel:dot(planarVec))) * math.sign(localVel[2]) +end + +function Steering:initialize(options) + self:_adjustPorts() + + options = options or {} + + self.base = wire.ports.Base + + self.slavesConfig = { + Camber = options.Camber, + Caster = options.Caster, + Ackermann = options.Ackermann + } + + self.lock = options.Lock + + self.targetAngle = 0 + self.angle = 0 + + self.zeroThreshold = options.ZeroThreshold + + self.speedCoeff = options.SpeedCoeff + self.startSpeedThreshold = options.StartSpeedThreshold + self.endSpeedThreshold = options.EndSpeedThreshold + + self.startCorrectionThreshold = options.StartCorrectionThreshold + self.endCorrectionThreshold = options.EndCorrectionThreshold + self.correctionOnCoeff = options.CorrectionOnCoeff + self.correctionOffCoeff = options.CorrectionOffCoeff + + self.correctionOnLerp = options.CorrectionOnLerp + self.correctionOffLerp = options.CorrectionOffLerp + + self.correction = 0 + + self.slaves = {} + + self:_createSlaves() +end + +function Steering:update() + local horizontal = wire.ports.Axles.Horizontal.Value + local horizontalTarget = wire.ports.Axles.Horizontal.Target + + local baseSpeedKMH = self:getBaseSpeed() + local angleCorrection = self:_getCorrectionGradient(baseSpeedKMH) + local speedCorrection = self.angle * self:_getSpeedCorrectionGradient(baseSpeedKMH) * self.speedCoeff + + self.targetAngle = horizontal * self.lock + + local correctionCoeff = horizontalTarget ~= 0 and self.correctionOnCoeff or self.correctionOffCoeff + local correctionLerpCoeff = horizontalTarget ~= 0 and self.correctionOnLerp or self.correctionOffLerp + local correction = self:_getCorrectionAngle() * angleCorrection * correctionCoeff + self.correction = math.lerp(correctionLerpCoeff, self.correction, isNaN(correction) and 0 or correction) + + local possibleAngle = math.clamp(horizontal * self.lock + speedCorrection + self.correction, -self.lock, self.lock) + + if inrange(possibleAngle, -self.zeroThreshold, self.zeroThreshold) then + self.angle = 0 + else + self.angle = possibleAngle + end + + for k, v in pairs(self.slaves) do + v:rotate(self.angle) + end + + self:_updateOutputs() +end + +local steering = Steering:new(options) + +hook.add('think', 'SteeringUpdate', function() + steering:update() +end) diff --git a/koptilnya/grip_steering/slave.txt b/koptilnya/grip_steering/slave.txt new file mode 100644 index 0000000..6497fff --- /dev/null +++ b/koptilnya/grip_steering/slave.txt @@ -0,0 +1,29 @@ +Slave = class('Slave') + +function Slave:initialize(options) + options = options or {} + + self.entity = options.Entity + self.base = options.Base + + self.camber = options.Camber + self.caster = options.Caster + self.ackermann = options.Ackermann + self.isLeft = options.IsLeft + self.offset = options.Offset +end + +function Slave:rotate(ang) + if self.entity:isFrozen() == false then + self.entity:setFrozen(1) + end + + local ackermann = math.sin(math.rad(ang)) ^ 2 * self.ackermann * (self.isLeft and -1 or 1) + local yaw = self.offset + ang + ackermann + local roll = (self.isLeft and 1 or -1) * self.camber - ang / 270 * self.caster + + local angle = Angle(0, yaw, roll) + local transformedAngle = self.base:localToWorldAngles(angle) + + self.entity:setAngles(transformedAngle) +end diff --git a/koptilnya/input_system/cl_input.txt b/koptilnya/input_system/cl_input.txt new file mode 100644 index 0000000..3d48cd0 --- /dev/null +++ b/koptilnya/input_system/cl_input.txt @@ -0,0 +1,80 @@ +-- @include ../libs/constants.txt +require('../libs/constants.txt') + +Input = class('Input') +-- Private here + +function Input:_setupHooks() + hook.add('inputPressed', 'KeyPress', function(key) + if self.driver == CURRENT_PLAYER then + self:_trySetTargetValue(key) + end + end) + + hook.add('inputReleased', 'KeyRelease', function(key) + if self.driver == CURRENT_PLAYER then + self:_trySetTargetValue(key) + end + end) +end + +function Input:bothKeysHolding(axle) + return (input.isKeyDown(axle.Positive) == true) and (input.isKeyDown(axle.Negative) == true) +end + +function Input:noKeysHolding(axle) + return (input.isKeyDown(axle.Positive) == false) and (input.isKeyDown(axle.Negative) == false) +end + +function Input:getAxleValue(axle) + return (input.isKeyDown(axle.Positive) and 1 or 0) - (input.isKeyDown(axle.Negative) and 1 or 0) +end + +function Input:_trySetTargetValue(key) + local triggeredKey = self.keys[key] + + if triggeredKey ~= nil then + local triggeredAxle = self.axles[triggeredKey.Axle] + local targetValue = self:getAxleValue(triggeredAxle) + + net.start('SetTargetAxle') + net.writeString(triggeredKey.Axle) + net.writeInt(targetValue, 4) + net.send() + end +end + +function Input:_setupNetListeners() + net.receive('SyncDriver', function() + self.driver = net.readEntity() + end) +end + +function Input:_mapKeys(axles) + for k, v in pairs(axles) do + self.keys[v.Negative] = { + Axle = k + } + + self.keys[v.Positive] = { + Axle = k + } + end +end + +-- Public here + +function Input:initialize(options) + options = options or {} + self.axles = options.Axles + self.keys = {} + self.driver = NULL_ENTITY + + self:_setupHooks(); + self:_setupNetListeners(); + self:_mapKeys(options.Axles) +end + +function Input:update() + +end diff --git a/koptilnya/input_system/main.txt b/koptilnya/input_system/main.txt new file mode 100644 index 0000000..6bd7350 --- /dev/null +++ b/koptilnya/input_system/main.txt @@ -0,0 +1,29 @@ +-- @name Input System +-- @author DarkSupah +-- @shared +-- @include ./cl_input.txt +-- @include ./sv_input.txt +local Axles = { + Horizontal = { + Negative = KEY.D, + Positive = KEY.A, + Gravity = 0.1, + Sensitivity = 0.2 + } +} + +local options = { + Axles = Axles +} + +if SERVER then + require('./sv_input.txt') +else + require('./cl_input.txt') +end + +local input = Input:new(options) + +hook.add('tick', 'InputUpdate', function() + input:update() +end) diff --git a/koptilnya/input_system/sv_input.txt b/koptilnya/input_system/sv_input.txt new file mode 100644 index 0000000..8629dca --- /dev/null +++ b/koptilnya/input_system/sv_input.txt @@ -0,0 +1,131 @@ +-- @include ../libs/constants.txt +require('../libs/constants.txt') + +Input = class('Input') + +-- Private here + +function Input:_adjustPorts() + local inputs = { + Seat = 'entity' + } + local outputs = { + Driver = 'entity', + Axles = 'table' + } + + wire.adjustPorts(inputs, outputs) +end + +function Input:_syncDriver(ply) + net.start('SyncDriver') + net.writeEntity(ply) + net.send() +end + +function Input:_setupHooks() + hook.add('PlayerEnteredVehicle', 'VehicleEnter', function(ply, vehicle) + if vehicle == self.seat then + self.driver = ply + self:_syncDriver(self.driver) + end + + end) + + hook.add('PlayerLeaveVehicle', 'VehicleLeave', function(_, vehicle) + if vehicle == self.seat then + self.driver = NULL_ENTITY + self:_syncDriver(self.driver) + end + end) +end + +function Input:_setTargetValue(target) + local targetAxle = self.axles[target.Axle] + targetAxle.Target = target.Target +end + +function Input:_setupNetListeners() + net.receive('SetTargetAxle', function() + local target = { + Axle = net.readString(), + Target = net.readInt(4) + } + + self:_setTargetValue(target) + end) +end + +function Input:_updateOutputs() + local driver = self.driver + + if self.driver:isValid() == false then + driver = NULL_ENTITY + end + + wire.ports.Driver = driver + wire.ports.Axles = self.axles +end + +function Input:_setupAxles(axles) + for k, v in pairs(axles) do + self.axles[k] = { + Value = 0, + Target = 0, + Gravity = v.Gravity, + Sensitivity = v.Sensitivity + } + end +end + +function Input:_getAxleValue(axle) + local usedLerpCoeff = axle.Target ~= 0 and axle.Sensitivity or axle.Gravity + + return math.lerp(usedLerpCoeff, axle.Value, axle.Target) +end + +function Input:_updateAxles() + for k, v in pairs(self.axles) do + v.Value = self:_getAxleValue(v) + end +end + +-- Public here + +function Input:initialize(options) + options = options or {} + local axles = options.Axles or {} + + self.axles = {} + + self:_adjustPorts() + self:_setupHooks() + self:_setupNetListeners() + + self:_setupAxles(axles) + + self.seat = wire.ports.Seat + self.driver = self:getDriver() + + -- in case chip was reset + self:_syncDriver(self.driver) +end + +function Input:getDriver() + if self.seat == nil or self.seat:isValid() == false then + return NULL_ENTITY + end + + local driver = self.seat:getDriver() + + if driver:isValid() == true then + return driver + else + return NULL_ENTITY + end +end + +function Input:update() + self:_updateOutputs() + self:_updateAxles() +end