BatterUp/src/utils.lua

286 lines
8.0 KiB
Lua

-- luacheck no new globals
utils = {}
--- @alias XyPair {
--- x: number,
--- y: number,
--- }
--- @alias Point3d {
--- x: number,
--- y: number,
--- z: number,
--- }
local sqrt <const> = 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