---@meta _ local squassets local assetPath = "./SquAssets" if pcall(require, assetPath) then squassets = require(assetPath) end assert(squassets, "§4The tail module requires SquAssets, which was not found!§c") ---@class tail local tail = {} tail.all = {} ---*CONSTRUCTOR ---@param tailSegmentList table The list of each individual tail segment of your tail. ---@param idleXMovement? number Defaults to `15`, how much the tail should sway side to side. ---@param idleYMovement? number Defaults to `5`, how much the tail should sway up and down. ---@param idleXSpeed? number Defaults to `1.2`, how fast the tail should sway side to side. ---@param idleYSpeed? number Defaults to `2`, how fast the tail should sway up and down. ---@param bendStrength? number Defaults to `2`, how strongly the tail moves when you move. ---@param velocityPush? number Defaults to `0`, this will cause the tail to bend when you move forward/backward, good if your tail is bent downward or upward. ---@param initialMovementOffset? number Defaults to `0`, this will offset the tails initial sway, this is good for when you have multiple tails and you want to desync them. ---@param offsetBetweenSegments? number Defaults to `1`, how much each tail segment should be offset from the previous one. ---@param stiffness? number Defaults to `0.005`, how stiff the tail should be. ---@param bounce? number Defaults to `0.9`, how bouncy the tail should be. ---@param flyingOffset? number Defaults to `90`, when flying, riptiding, or swimming, it may look strange to have the tail stick out, so instead it will rotate to this value(so use this to flatten your tail during these movements). ---@param downLimit? number Defaults to `-90`, the lowest each tail segment can rotate. ---@param upLimit? number Defaults to `45`, the highest each tail segment can rotate. ---@return SquAPI.Tail function tail:new(tailSegmentList, idleXMovement, idleYMovement, idleXSpeed, idleYSpeed, bendStrength, velocityPush, initialMovementOffset, offsetBetweenSegments, stiffness, bounce, flyingOffset, downLimit, upLimit) ---@class SquAPI.tail local self = setmetatable({}, {__index = tail}) --error checker self.tailSegmentList = tailSegmentList if type(self.tailSegmentList) == "ModelPart" then self.tailSegmentList = {self.tailSegmentList} end assert(type(self.tailSegmentList) == "table", "your tailSegmentList table seems to to be incorrect") self.berps = {} self.targets = {} self.stiffness = stiffness or .005 self.bounce = bounce or .9 if type(self.tailSegmentList[2]) == "number" then --ah I see you stumbled across my custom tail list creator, if you curious ask me. tail must be >= 3 segments. Naming: tail, tailseg, tailseg2, tailseg3..., tailtip local range = self.tailSegmentList[2] local str = "" if self.tailSegmentList[3] then str = self.tailSegmentList[3] end self.tailSegmentList[2] = self.tailSegmentList[1][str .. "tailseg"] for i = 2, range - 2 do self.tailSegmentList[i + 1] = self.tailSegmentList[i][str .. "tailseg" .. i] end self.tailSegmentList[range] = self.tailSegmentList[range - 1][str .. "tailtip"] end for i = 1, #self.tailSegmentList do assert(self.tailSegmentList[i]:getType() == "GROUP", "§4The tail segment at position "..i.." of the table is not a group. The tail segments need to be groups that are nested inside the previous segment.§c") self.berps[i] = {squassets.BERP:new(self.stiffness, self.bounce), squassets.BERP:new(self.stiffness, self.bounce, self.downLimit, self.upLimit)} self.targets[i] = {0, 0} end self.downLimit = downLimit or -90 self.upLimit = upLimit or 45 self.tailSegmentList = tailSegmentList self.idleXMovement = idleXMovement or 15 self.idleYMovement = idleYMovement or 5 self.idleXSpeed = idleXSpeed or 1.2 self.idleYSpeed = idleYSpeed or 2 self.bendStrength = bendStrength or 2 self.velocityPush = velocityPush or 0 self.initialMovementOffset = initialMovementOffset or 0 self.flyingOffset = flyingOffset or 90 self.offsetBetweenSegments = offsetBetweenSegments or 1 self.currentBodyRot = 0 self.oldBodyRot = 0 self.bodyRotSpeed = 0 self.enabled = true table.insert(tail.all, self) return self end --*CONTROL FUNCTIONS ---tail enable handling function tail:disable() self.enabled = false return self end function tail:enable() self.enabled = true return self end function tail:toggle() self.enabled = not self.enabled return self end ---@param bool boolean function tail:setEnabled(bool) self.enabled = bool or false return self end function tail:zero() for _, v in pairs(self.tailSegmentList) do v:setOffsetRot(0, 0, 0) end return self end --*UPDATE FUNCTIONS function tail:tick() if self.enabled then self.oldBodyRot = self.currentBodyRot self.currentBodyRot = player:getBodyYaw() self.bodyRotSpeed = math.max(math.min(self.currentBodyRot-self.oldBodyRot, 20), -20) local time = world.getTime() local vel = squassets.forwardVel() local yvel = squassets.verticalVel() local svel = squassets.sideVel() local bendStrength = self.bendStrength/(math.abs((yvel*30))+vel*30 + 1) local pose = player:getPose() for i = 1, #self.tailSegmentList do self.targets[i][1] = math.sin((time * self.idleXSpeed)/10 - (i * self.offsetBetweenSegments)) * self.idleXMovement self.targets[i][2] = math.sin((time * self.idleYSpeed)/10 - (i * self.offsetBetweenSegments) + self.initialMovementOffset) * self.idleYMovement self.targets[i][1] = self.targets[i][1] + self.bodyRotSpeed*self.bendStrength + svel*self.bendStrength*40 self.targets[i][2] = self.targets[i][2] + yvel * 15 * self.bendStrength - vel*self.bendStrength*15*self.velocityPush if i == 1 then if pose == "FALL_FLYING" or pose == "SWIMMING" or player:riptideSpinning() then self.targets[i][2] = self.flyingOffset end end end end end ---@param dt number Tick delta function tail:render(dt, _) if self.enabled then local pose = player:getPose() if pose ~= "SLEEPING" then for i, tail in ipairs(self.tailSegmentList) do tail:setOffsetRot( self.berps[i][2]:berp(self.targets[i][2], dt), self.berps[i][1]:berp(self.targets[i][1], dt), 0 ) end end end end return tail