--@include ./powertrain_component.txt --@include ../wheel/wheel.txt --@include /koptilnya/libs/constants.txt --@include /koptilnya/libs/entity.txt ---@type PowertrainComponent local PowertrainComponent = require('./powertrain_component.txt') ---@type CustomWheel local CustomWheel = require('../wheel/wheel.txt') require('/koptilnya/libs/constants.txt') 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 brakePower number ---@field handbrakePower number ---@field direction integer ---@field private rot number ---@field model string ---@field customWheel CustomWheel local Wheel = class('Wheel', PowertrainComponent) ---@private ---@param vehicle KPTLVehicle ---@param name string ---@param config WheelConfig function Wheel:initialize(vehicle, name, config) PowertrainComponent.initialize(self, vehicle, name, config) 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 pos = self.DEBUG_DATA.entity:localToWorld(Vector(0, 0, -self.DEBUG_DATA.radius * UNITS_PER_METER)) 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 + 0) * self.DEBUG_DATA.forward), 1, 0, 0) render.setColor(Color(0, 255, 0, 255)) lerpSideFrictionForce = math.lerp(0.1, lerpSideFrictionForce, self.DEBUG_DATA.sideFrictionForce) render.draw3DBeam(pos, pos + ((lerpSideFrictionForce / 500 + 0) * self.DEBUG_DATA.right), 1, 0, 0) render.setColor(Color(0, 0, 255, 200)) lerpLoad = math.lerp(0.1, lerpLoad, self.DEBUG_DATA.load) render.draw3DBeam(pos, pos + ((lerpLoad / 1000 * 4 + 16) * self.DEBUG_DATA.up), 1, 0, 0) end) if player() == owner() and not render.isHUDActive() then enableHud(owner(), true) end return end self.brakePower = config.BrakePower or 0 self.handbrakePower = config.HandbrakePower or 0 self.model = config.Model or '' self.direction = math.sign(math.cos(math.rad(config.Offset or 0))) self.wireInputs = { [self.name] = 'entity', } self.wireOutputs = { [string.format('%s_RPM', self.name)] = 'number', } self.rot = 0 ---@type Entity? self.entity = nil ---@type Hologram? self.holo = nil self.customWheel = CustomWheel:new( table.merge( table.copy(config.CustomWheel), { Direction = self.direction, Name = self.name, ParentPhysObj = vehicle.basePhysObject } ) ) hook.add('Input', 'vehicle_wheel_update' .. self.name, function(name, value) if name == self.name then self:setEntity(value) end end) end ---@override ---@return nil function Wheel:onWirePortsReady() self:setEntity(self.CONFIG.Entity or wire.ports[self.name]) end ---@private ---@param entity Entity ---@return nil function Wheel:setEntity(entity) self.entity = entity self.customWheel:setEntity(entity) self.customWheel.radius = self:getEntityRadius() if isValid(self.holo) then self.holo:remove() end self.holo = self:createHolo(self.entity) end ---@return number function Wheel:getEntityRadius() if not isValid(self.entity) then return 0 end return self.entity:getModelRadius() * UNITS_TO_METERS end ---@return Hologram? function Wheel:createHolo(entity) if not isValid(entity) then return nil end local holo = hologram.create(entity:getPos(), entity:getAngles(), self.model or '') if holo == nil or not isValid(holo) then return nil end holo:setParent(entity) entity:setColor(Color(0, 0, 0, 0)) return holo end ---@return nil function Wheel:updateWireOutputs() PowertrainComponent.updateWireOutputs(self) wire.ports[string.format('%s_RPM', self.name)] = self.customWheel:getRPM() end ---@return number function Wheel:queryInertia() return self.customWheel.inertia end ---@return number function Wheel:queryAngularVelocity(angularVelocity) self.customWheel.angularVelocity = angularVelocity return self.customWheel.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) self.customWheel:update() self.angularVelocity = self.customWheel.angularVelocity 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.vehicle.basePhysObject:applyForceOffset( surfaceForceVector, self.entity:localToWorld(Vector(0, 0, -self.customWheel.radius * UNITS_PER_METER)) ) end 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) -- 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) -- Step 3: Get quaternion from spun directions local visualQuat = spunFwd:getQuaternion(spunUp) -- 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.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.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 ---@type fun(vehicle: KPTLVehicle, name?: string, config?: WheelConfig): Wheel Wheel.new = Wheel.new return Wheel