-- luacheck no new globals utils = {} --- @alias XyPair { --- x: number, --- y: number, --- } --- @alias Point3d { --- x: number, --- y: number, --- z: number, --- } local sqrt = math.sqrt function utils.easingHill(t, b, c, d) c = c + 0.0 -- convert to float to prevent integer overflow t = t / d t = ((t * 2) - 1) t = t * t return (c * t) + b end --- @alias StaticAnimator { --- currentValue: fun(self): number; --- reset: fun(self, durationMs: number | nil); --- ended: fun(self): boolean; --- } --- Build an "animator" whose `:currentValue()` always returns the given value. --- Essentially an "empty object" pattern for initial object positions. ---@param value number ---@return StaticAnimator function utils.staticAnimator(value) return { currentValue = function(_) return value end, reset = function(_) end, ended = function(_) return true end, } end ---@param x number ---@param y number ---@return XyPair function utils.xy(x, y) return { x = x, y = y, } end --- Returns the normalized vector as two values, plus the distance between the given points. ---@param x1 number ---@param y1 number ---@param x2 number ---@param y2 number ---@return number x, number y, number distance function utils.normalizeVector(x1, y1, x2, y2) local distance, x, y = utils.distanceBetween(x1, y1, x2, y2) return x / distance, y / distance, distance end --- Push the given obect at the given speed toward a target. Speed should be pre-multiplied by the frame's delta time. --- Stops when within 1. Returns true only if the object did actually move. ---@param mover { x: number, y: number } ---@param speed number ---@param target { x: number, y: number } ---@param tau number | nil ---@return boolean isStillMoving function utils.moveAtSpeed(mover, speed, target, tau) local x, y, distance = utils.normalizeVector(mover.x, mover.y, target.x, target.y) if distance == 0 then return false end if distance > (tau or 1) then mover.x = mover.x - (x * speed) mover.y = mover.y - (y * speed) else mover.x = target.x mover.y = target.y end return true end ---@generic T ---@param array T[] ---@param condition fun(T): boolean ---@return T[] function utils.filter(array, condition) local newArray = {} for _, element in pairs(array) do if condition(element) then newArray[#newArray + 1] = element end end return newArray end ---@generic T ---@param array T[] ---@param condition fun(T): boolean ---@return T | nil function utils.first(array, condition) for _, element in ipairs(array) do if condition(element) then return element end end return nil end ---@param x1 number ---@param y1 number ---@param x2 number ---@param y2 number ---@return number distance, number x, number y function utils.distanceBetween(x1, y1, x2, y2) local x = x1 - x2 local y = y1 - y2 return sqrt((x * x) + (y * y)), x, y end ---@param point1 XyPair ---@param point2 XyPair ---@return number distance, number x, number y function utils.distanceBetweenPoints(point1, point2) local x = point1.x - point2.x local y = point1.y - point2.y return sqrt((x * x) + (y * y)), x, y end ---@param x1 number ---@param y1 number ---@param z1 number ---@param x2 number ---@param y2 number ---@param z2 number ---@return number distance, number x, number y, number z function utils.distanceBetweenZ(x1, y1, z1, x2, y2, z2) local x = x1 - x2 local y = y1 - y2 local z = z1 - z2 return sqrt((x * x) + (y * y) + (z * z)), x, y, z end --- Returns the base being touched by the player at (x,y), or nil, if no base is being touched ---@param x number ---@param y number ---@return Base | nil function utils.isTouchingBase(x, y) return utils.first(C.Bases, function(base) return utils.distanceBetween(x, y, base.x, base.y) < C.BaseHitbox end) end ---@param base Base ---@return Runner | nil runner The runner whose next base matches the given base. function utils.getRunnerWithNextBase(runners, base) return utils.first(runners, function(runner) return runner.nextBase == base end) end --- Returns true only if the point is below the given line, within the x bounds of said line, and above the bottomBound. ---@param point XyPair ---@param line1 XyPair ---@param line2 XyPair ---@param bottomBound number ---@return boolean function utils.pointDirectlyUnderLine(point, line1, line2, bottomBound) -- This check currently assumes right-handedness. -- I.e. it assumes the ball is to the right of batBaseX if point.x < line1.x or point.x > line2.x or point.y > bottomBound then return false end return utils.pointUnderLine(point.x, point.y, line1.x, line1.y, line2.x, line2.y) end --- Returns true if the given point is anywhere above the given line, with no upper bound. --- This, used for home run calculations, does not *precesely* take into account balls that curve around the foul poles. --- If left of first linePoint and above it, returns true. Similarly if right of the last linePoint. ---@param point XyPair ---@param linePoints XyPair[] ---@return boolean function utils.pointIsAboveLine(point, linePoints) if point.x < linePoints[1].x and point.y < linePoints[1].y then return true end for i = 2, #linePoints do local prev = linePoints[i - 1] local next = linePoints[i] if point.x >= prev.x and point.x <= next.x then return not utils.pointUnderLine(point.x, point.y, prev.x, prev.y, next.x, next.y) end end if point.x > linePoints[#linePoints].x and point.y < linePoints[#linePoints].y then return true end return false end --- Returns true only if the point is below the given line. ---@param pointX number ---@param pointY number ---@param lineX1 number ---@param lineY1 number ---@param lineX2 number ---@param lineY2 number ---@return boolean function utils.pointUnderLine(pointX, pointY, lineX1, lineY1, lineX2, lineY2) local m = (lineY2 - lineY1) / (lineX2 - lineX1) -- y = mx + b -- b = y1 - (m * x1) local b = lineY1 - (m * lineX1) local yOnLine = (m * pointX) + b local yP = pointY local yDelta = yOnLine - yP return yDelta <= 0 end --- Returns true if a ball landing at destX,destY will be foul. ---@param destX number ---@param destY number function utils.isFoulBall(destX, destY) local leftLine = C.LeftFoulLine local rightLine = C.RightFoulLine return utils.pointUnderLine(destX, destY, leftLine.x1, leftLine.y1, leftLine.x2, leftLine.y2) or utils.pointUnderLine(destX, destY, rightLine.x1, rightLine.y1, rightLine.x2, rightLine.y2) end --- Returns the nearest position object from the given point, as well as its distance from that point ---@generic T : { x: number, y: number | nil } ---@param array T[] ---@param x number ---@param y number ---@param extraCondition fun(t: T): boolean ---@return T nearest,number |nil distance function utils.getNearestOf(array, x, y, extraCondition) local nearest, nearestDistance = nil, nil for _, element in pairs(array) do if not extraCondition or extraCondition(element) then if nearest == nil then nearest = element nearestDistance = utils.distanceBetween(element.x, element.y, x, y) else local distance = utils.distanceBetween(element.x, element.y, x, y) if distance < nearestDistance then nearest = element nearestDistance = distance end end end end return nearest, nearestDistance end ---@param stats Statistics ---@return number homeScore, number awayScore function utils.totalScores(stats) local homeScore = 0 local awayScore = 0 for _, inning in pairs(stats.innings) do homeScore = homeScore + inning.home.score awayScore = awayScore + inning.away.score end return homeScore, awayScore end if not playdate then return utils end