figura-skin/scripts/libs/SquAPI_modules/smoothHead.lua
2025-11-23 01:10:42 -08:00

196 lines
No EOL
7.5 KiB
Lua

---SMOOTH HEAD
---Mimics a vanilla player head, but smoother and with some extra life. Can also do smooth Torsos and Smooth Necks!
---@meta _
local squassets
local assetPath = "./SquAssets"
if pcall(require, assetPath) then squassets = require(assetPath) end
assert(squassets, "§4The smoothHead module requires SquAssets, which was not found!§c")
---@class smoothHead
local smoothHead = {}
smoothHead.all = {}
---*CONSTRUCTOR
---@param element ModelPart|table<ModelPart> The head element that you wish to effect. If you want a smooth neck or torso, instead of a single element, input a table of head elements(imagine it like {element1, element2, etc.}). this will apply the head rotations to each of these.
---@param strength? number|table<number> Defaults to `1`, the target rotation is multiplied by this factor. If you want a smooth neck or torso, instead of an single number, you can put in a table(imagine it like {strength1, strength2, etc.}). this will apply each strength to each respective element.(make sure it is the same length as your element table)
---@param tilt? number Defaults to `0.1`, for context the smooth head applies a slight tilt to the head as it's rotated toward the side, this controls the strength of that tilt.
---@param speed? number Defaults to `1`, how fast the head will rotate toward the target rotation.
---@param keepOriginalHeadPos? boolean|number Defaults to `true`, when true the heads position will follow the vanilla head position. For example when crouching the head will shift down to follow. If set to a number, changes which modelpart gets moved when doing actions such as crouching. this should normally be set to the neck modelpart.
---@param fixPortrait? boolean Defaults to `true`, sets whether or not the portrait should be applied if a group named "head" is found in the elements list
---@param animStraightenList? table<Animation> Defaults to `nil`, you can put in a list of animations that when played, will cause the head to straighten.
---@param straightenMultiplier? number Defaults to `0.5`, how strong the head will straighten during above
---@param straightenSpeed? number Defaults to `0.5`, how fast the head will straighten during above
---@param blendToConsiderStopped? number Defaults to `0.1`, for above, the animation will be considered stopped when it's blend is below this value.
---@return SquAPI.SmoothHead
function smoothHead:new(element, strength, tilt, speed, keepOriginalHeadPos, fixPortrait, animStraightenList, straightenMultiplier, straightenSpeed, blendToConsiderStopped)
---@class SquAPI.SmoothHead
local self = setmetatable({}, {__index = smoothHead})
--if it's a single element then converts to table
if type(element) == "ModelPart" then
assert(element, "§4Your model path for smoothHead is incorrect.§c")
element = { element }
end
--checks each element amd updates parent type to none
for i = 1, #element do
assert(element[i]:getType() == "GROUP",
"§4The head element at position " ..
i ..
" of the table is not a group. The head elements need to be groups that are nested inside one another to function properly.§c")
assert(element[i], "§4The head segment at position " .. i .. " is incorrect.§c")
element[i]:setParentType("NONE")
end
self.element = element
--if strength is a single number then converts to table by evenly dividing strength for each element
self.strength = strength or 1
if type(self.strength) == "number" then
local strengthDiv = self.strength / #element
self.strength = {}
for i = 1, #element do
self.strength[i] = strengthDiv
end
end
self.tilt = tilt or 0.1
self.speed = (speed or 1)
if keepOriginalHeadPos == nil then keepOriginalHeadPos = true end
self.keepOriginalHeadPos = keepOriginalHeadPos
self.animStraightenList = animStraightenList
self.straightenMultiplier = straightenMultiplier or 0.5
self.straightenSpeed = straightenSpeed or 0.5
self.blendToConsiderStopped = blendToConsiderStopped or 0.1
self.currentStraightenMultiplier = 1
--portrait fix by FOX
if fixPortrait == nil then fixPortrait = true end
if fixPortrait then
if type(element) == "table" then
for _, part in ipairs(element) do
if squassets.caseInsensitiveFind(part, "head") then
part:copy("_squapi-portrait"):moveTo(models):setParentType("Portrait")
:setPos(-part:getPivot())
break
end
end
elseif type(element) == "ModelPart" and element:getType() == "GROUP" then
if squassets.caseInsensitiveFind(element, "head") then
element:copy("_squapi-portrait"):moveTo(models):setParentType("Portrait")
:setPos(-element:getPivot())
end
end
end
self.rot = {}
self.rotOld = {}
for i in ipairs(self.element) do
self.rot[i] = vec(0,0,0)
self.rotOld[i] = vec(0,0,0)
end
self.enabled = true
table.insert(smoothHead.all, self)
return self
end
--*CONTROL FUNCTIONS
---Applies an offset to the heads rotation to more easily modify it. Applies as a vector.(for multisegments it will modify the target rotation)
---@param xRot number X rotation
---@param yRot number Y rotation
---@param zRot number Z rotation
function smoothHead:setOffset(xRot, yRot, zRot)
self.offset = vec(xRot, yRot, zRot)
return self
end
---smoothHead enable handling
function smoothHead:disable() self.enabled = false return self end
function smoothHead:enable() self.enabled = true return self end
function smoothHead:toggle() self.enabled = not self.enabled return self end
---@param bool boolean
function smoothHead:setEnabled(bool)
self.enabled = bool or false
return self
end
---Resets this smooth head's position and rotation to their initial values
function smoothHead:zero()
for _, v in ipairs(self.element) do
v:setPos(0, 0, 0)
v:setOffsetRot(0, 0, 0)
self.headRot = vec(0, 0, 0)
end
return self
end
--*UPDATE FUNCTIONS
function smoothHead:tick()
if self.animStraightenList then
local strengthMultiplier = 1
for _, v in ipairs(self.animStraightenList) do
if v:isPlaying() and v:getBlend() > self.blendToConsiderStopped then
strengthMultiplier = self.straightenMultiplier
break
end
end
self.currentStraightenMultiplier = math.lerp(self.currentStraightenMultiplier, strengthMultiplier, self.straightenSpeed)
end
for i in ipairs(self.element) do
self.rotOld[i] = self.rot[i]
end
if self.enabled then
local vanillaHeadRot = squassets.getHeadRot():add(vanilla_model.HEAD:getOffsetRot())
for i in ipairs(self.element) do
self.rot[i] = math.lerp(self.rot[i], (vanillaHeadRot + vanillaHeadRot.__y * self.tilt) * self.strength[i] * self.currentStraightenMultiplier , self.speed)
end
end
end
---@param dt number Tick delta
---@param context Event.Render.context
function smoothHead:render(dt, context)
if self.enabled then
for i, element in ipairs(self.element) do
element:setOffsetRot(
math.lerp(self.rotOld[i], self.rot[i], dt)
)
-- Better Combat SquAPI Compatibility created by @jimmyhelp and @foxy2526 on Discord
if renderer:isFirstPerson() and context == "RENDER" then
self.element[i]:setVisible(false)
else
self.element[i]:setVisible(true)
end
end
if self.keepOriginalHeadPos then
self.element
[type(self.keepOriginalHeadPos) == "number" and self.keepOriginalHeadPos or #self.element]
:setPos(-vanilla_model.HEAD:getOriginPos())
end
end
end
return smoothHead