Try to cluster global state together.

Start peeling out fielder functions into a new file.
A bit more constant use.
In Makefile, parse main.lua imports for source files.
This commit is contained in:
Sage Vaillancourt 2025-02-08 11:52:39 -05:00
parent 324673ea98
commit 66bd97499a
7 changed files with 190 additions and 178 deletions

View File

@ -1,4 +1,4 @@
SOURCE_FILES := src/utils.lua src/constants.lua src/assets.lua src/draw/* src/dbg.lua src/npc.lua src/announcer.lua src/graphics.lua src/main.lua SOURCE_FILES := $(shell grep "import '" src/main.lua | grep -v CoreLibs | sed "s/.*'\(.*\)'.*/\1/") main.lua
GENERATED_FILES := src/assets.lua GENERATED_FILES := src/assets.lua
all: all:

View File

@ -98,3 +98,5 @@ C.CrankPower = 10
--- How fast baserunners move after a walk --- How fast baserunners move after a walk
C.WalkedRunnerSpeed = 10 C.WalkedRunnerSpeed = 10
C.ResetFieldersAfterSeconds = 3

View File

@ -21,9 +21,9 @@ end
-- Only works if called with the bases empty (i.e. the only runner should be the batter. -- Only works if called with the bases empty (i.e. the only runner should be the batter.
-- selene: allow(unused_variable) -- selene: allow(unused_variable)
function dbg.loadTheBases(runners) function dbg.loadTheBases(runners)
newRunner() utils.newRunner()
newRunner() utils.newRunner()
newRunner() utils.newRunner()
runners[2].x = C.Bases[C.First].x runners[2].x = C.Bases[C.First].x
runners[2].y = C.Bases[C.First].y runners[2].y = C.Bases[C.First].y
runners[2].nextBase = C.Bases[C.Second] runners[2].nextBase = C.Bases[C.Second]

56
src/field.lua Normal file
View File

@ -0,0 +1,56 @@
---@param name string
---@param speed number
---@return Fielder
function newFielder(name, speed)
return {
name = name,
speed = speed,
}
end
-- selene: allow(unscoped_variables)
Field = {
fielders = {
first = newFielder("First", 40),
second = newFielder("Second", 40),
shortstop = newFielder("Shortstop", 40),
third = newFielder("Third", 40),
pitcher = newFielder("Pitcher", 30),
catcher = newFielder("Catcher", 35),
left = newFielder("Left", 40),
center = newFielder("C.Center", 40),
right = newFielder("Right", 40),
},
}
--- Actually only benches the infield, because outfielders are far away!
---@param position XYPair
function Field.benchTo(self, position)
self.fielders.first.target = position
self.fielders.second.target = position
self.fielders.shortstop.target = position
self.fielders.third.target = position
self.fielders.pitcher.target = position
self.fielders.catcher.target = position
end
--- Resets the target positions of all fielders to their defaults (at their field positions).
---@param fromOffTheField XYPair | nil If provided, also sets all runners' current position to one centralized location.
function Field.resetFielderPositions(self, fromOffTheField)
if fromOffTheField then
for _, fielder in pairs(self.fielders) do
fielder.x = fromOffTheField.x
fielder.y = fromOffTheField.y
end
end
self.fielders.first.target = utils.xy(C.Screen.W - 65, C.Screen.H * 0.48)
self.fielders.second.target = utils.xy(C.Screen.W * 0.70, C.Screen.H * 0.30)
self.fielders.shortstop.target = utils.xy(C.Screen.W * 0.30, C.Screen.H * 0.30)
self.fielders.third.target = utils.xy(C.Screen.W * 0.1, C.Screen.H * 0.48)
self.fielders.pitcher.target = utils.xy(C.PitcherStartPos.x, C.PitcherStartPos.y)
self.fielders.catcher.target = utils.xy(C.Screen.W * 0.475, C.Screen.H * 0.92)
self.fielders.left.target = utils.xy(C.Screen.W * -1, C.Screen.H * -0.2)
self.fielders.center.target = utils.xy(C.Center.x, C.Screen.H * -0.4)
self.fielders.right.target = utils.xy(C.Screen.W * 2, self.fielders.left.target.y)
end

View File

@ -2,19 +2,19 @@
--- XXX --- XXX
--- XOX --- XOX
--- Where each character is the size of the screen, and 'O' is the default view. --- Where each character is the size of the screen, and 'O' is the default view.
function getDrawOffset(screenW, screenH, ballX, ballY) function getDrawOffset(ballX, ballY)
local offsetX, offsetY local offsetX, offsetY
if ballY > screenH then if ballY > C.Screen.H or ballX >= C.BallOffscreen then
return 0, 0 return 0, 0
end end
offsetY = math.max(0, -1 * ballY) offsetY = math.max(0, -1 * ballY)
if ballX > 0 and ballX < screenW then if ballX > 0 and ballX < C.Screen.W then
offsetX = 0 offsetX = 0
elseif ballX < 0 then elseif ballX < 0 then
offsetX = math.max(-1 * screenW, ballX * -1) offsetX = math.max(-1 * C.Screen.W, ballX * -1)
elseif ballX > screenW then elseif ballX > C.Screen.W then
offsetX = math.min(screenW * 2, (ballX * -1) + screenW) offsetX = math.min(C.Screen.W * 2, (ballX * -1) + C.Screen.W)
end end
return offsetX * 1.3, offsetY * 1.5 return offsetX * 1.3, offsetY * 1.5

View File

@ -30,10 +30,11 @@ import 'assets.lua'
import 'announcer.lua' import 'announcer.lua'
import 'dbg.lua' import 'dbg.lua'
import 'field.lua'
import 'graphics.lua' import 'graphics.lua'
import 'npc.lua' import 'npc.lua'
import 'draw/overlay' import 'draw/overlay.lua'
import 'draw/fielder' import 'draw/fielder.lua'
-- stylua: ignore end -- stylua: ignore end
-- selene: allow(shadowing) -- selene: allow(shadowing)
@ -44,16 +45,72 @@ local PlayerImageBlipper <const> = blipper.new(100, Player, PlayerLowHat)
local FielderDanceAnimator <const> = gfx.animator.new(1, 10, 0, utils.easingHill) local FielderDanceAnimator <const> = gfx.animator.new(1, 10, 0, utils.easingHill)
FielderDanceAnimator.repeatCount = C.DanceBounceCount - 1 FielderDanceAnimator.repeatCount = C.DanceBounceCount - 1
function fieldersDance()
FielderDanceAnimator:reset(C.DanceBounceMs)
end
local ballAnimatorY = gfx.animator.new(0, C.BallOffscreen, C.BallOffscreen, playdate.easingFunctions.linear)
local ballAnimatorX = gfx.animator.new(0, C.BallOffscreen, C.BallOffscreen, playdate.easingFunctions.linear)
---@alias PseudoAnimator { currentValue: fun(self): number; reset: fun(self, durationMs: number | nil) } ---@alias PseudoAnimator { currentValue: fun(self): number; reset: fun(self, durationMs: number | nil) }
---@alias Pitch { x: PseudoAnimator, y: PseudoAnimator, z: PseudoAnimator | nil } ---@alias Pitch { x: PseudoAnimator, y: PseudoAnimator, z: PseudoAnimator | nil }
local ballAnimatorX = utils.staticAnimator(C.BallOffscreen)
local ballAnimatorY = utils.staticAnimator(C.BallOffscreen)
local ballSizeAnimator = utils.staticAnimator(C.SmallestBallRadius)
-- TODO? Replace this AND ballSizeAnimator with a ballAnimatorZ?
-- ...that might lose some of the magic of both. Compromise available? idk
local ballFloatAnimator = gfx.animator.new(2000, -60, 0, utils.easingHill)
local deltaSeconds = 0
local BatterHandPos <const> = utils.xy(10, 25)
local batBase <const> = utils.xy(C.Center.x - 34, 215)
local batTip <const> = utils.xy(0, 0)
local ball <const> = {
x = C.Center.x --[[@as number]],
y = C.Center.y --[[@as number]],
z = 0,
size = C.SmallestBallRadius,
heldBy = nil --[[@type Runner | nil]],
}
---@alias Team { score: number, benchPosition: XYPair }
---@type table<string, Team>
local teams <const> = {
home = {
score = 0,
benchPosition = utils.xy(C.Screen.W + 10, C.Center.y),
},
away = {
score = 0,
benchPosition = utils.xy(-10, C.Center.y),
},
}
local PlayerTeam <const> = teams.away
local battingTeam = teams.away
local outs = 0
local inning = 1
local offenseMode = C.Offense.batting
--- @type Runner[]
local runners <const> = {}
--- @type Runner[]
local outRunners <const> = {}
---@type Runner | nil
local batter = utils.newRunner(runners)
local throwMeter = 0
-- TODO: Replace with a timer, repeatedly reset, instead of setting to 0
local secondsSinceLastRunnerMove = 0
-- TODO: Replace with a timer, repeatedly reset instead of setting to 0
local secondsSincePitchAllowed = -5
local catcherThrownBall = false
local batAngleDeg = C.CrankOffsetDeg
---@type Pitch[] ---@type Pitch[]
local Pitches <const> = { local Pitches <const> = {
-- Fastball -- Fastball
@ -83,39 +140,6 @@ local Pitches <const> = {
}, },
} }
local BatterHandPos <const> = utils.xy(10, 25)
local batBase <const> = utils.xy(C.Center.x - 34, 215)
local batTip <const> = utils.xy(0, 0)
local ball <const> = {
x = C.Center.x --[[@as number]],
y = C.Center.y --[[@as number]],
z = 0,
size = C.SmallestBallRadius,
heldBy = nil --[[@type Runner | nil]],
}
---@alias Team { score: number, benchPosition: XYPair }
---@type table<string, Team>
local teams <const> = {
home = {
score = 0,
benchPosition = utils.xy(C.Screen.W + 10, C.Center.y),
},
away = {
score = 0,
benchPosition = utils.xy(-10, C.Center.y),
},
}
local PlayerTeam <const> = teams.home
local battingTeam = teams.away
local outs = 0
local inning = 1
local offenseMode = C.Offense.batting
---@return boolean playerIsOnSide, boolean playerIsOnOtherSide ---@return boolean playerIsOnSide, boolean playerIsOnOtherSide
function playerIsOn(side) function playerIsOn(side)
local ret local ret
@ -127,93 +151,6 @@ function playerIsOn(side)
return ret, not ret return ret, not ret
end end
-- TODO? Replace this AND ballSizeAnimator with a ballHeightAnimator
-- ...that might lose some of the magic of both. Compromise available? idk
local ballFloatAnimator = gfx.animator.new(2000, -60, 0, utils.easingHill)
local BallSizeMs = 2000
local ballSizeAnimator = gfx.animator.new(BallSizeMs, 9, C.SmallestBallRadius, utils.easingHill)
local deltaSeconds = 0
---@param name string
---@param speed number
---@return Fielder
function newFielder(name, speed)
return {
name = name,
speed = speed,
}
end
---@type table<string, Fielder>
local fielders <const> = {
first = newFielder("First", 40),
second = newFielder("Second", 40),
shortstop = newFielder("Shortstop", 40),
third = newFielder("Third", 40),
pitcher = newFielder("Pitcher", 30),
catcher = newFielder("Catcher", 35),
left = newFielder("Left", 40),
center = newFielder("C.Center", 40),
right = newFielder("Right", 40),
}
--- Actually only benches the infield, because outfielders are far away!
---@param position XYPair
function benchAllFielders(position)
fielders.first.target = position
fielders.second.target = position
fielders.shortstop.target = position
fielders.third.target = position
fielders.pitcher.target = position
fielders.catcher.target = position
end
--- Resets the target positions of all fielders to their defaults (at their field positions).
---@param fromOffTheField XYPair | nil If provided, also sets all runners' current position to one centralized location.
function resetFielderPositions(fromOffTheField)
if fromOffTheField then
for _, fielder in pairs(fielders) do
fielder.x = fromOffTheField.x
fielder.y = fromOffTheField.y
end
end
fielders.first.target = utils.xy(C.Screen.W - 65, C.Screen.H * 0.48)
fielders.second.target = utils.xy(C.Screen.W * 0.70, C.Screen.H * 0.30)
fielders.shortstop.target = utils.xy(C.Screen.W * 0.30, C.Screen.H * 0.30)
fielders.third.target = utils.xy(C.Screen.W * 0.1, C.Screen.H * 0.48)
fielders.pitcher.target = utils.xy(C.PitcherStartPos.x, C.PitcherStartPos.y)
fielders.catcher.target = utils.xy(C.Screen.W * 0.475, C.Screen.H * 0.92)
fielders.left.target = utils.xy(C.Screen.W * -1, C.Screen.H * -0.2)
fielders.center.target = utils.xy(C.Center.x, C.Screen.H * -0.4)
fielders.right.target = utils.xy(C.Screen.W * 2, fielders.left.target.y)
end
--- @type Runner[]
local runners <const> = {}
--- @type Runner[]
local outRunners <const> = {}
---@return Runner
function newRunner()
local new = {
x = C.RightHandedBattersBox.x - 60,
y = C.RightHandedBattersBox.y + 60,
nextBase = C.RightHandedBattersBox,
prevBase = nil,
forcedTo = C.Bases[C.First],
}
runners[#runners + 1] = new
return new
end
---@type Runner | nil
local batter = newRunner()
local throwMeter = 0
--- "Throws" the ball from its current position to the given destination. --- "Throws" the ball from its current position to the given destination.
---@param destX number ---@param destX number
---@param destY number ---@param destY number
@ -242,10 +179,6 @@ function throwBall(destX, destY, easingFunc, flyTimeMs, floaty, customBallScaler
end end
end end
-- TODO: Replace with a timer, repeatedly reset instead of setting to 0
local secondsSincePitchAllowed = -5
local catcherThrownBall = false
---@param pitchFlyTimeMs number | nil ---@param pitchFlyTimeMs number | nil
---@param pitchTypeIndex number | nil ---@param pitchTypeIndex number | nil
function pitch(pitchFlyTimeMs, pitchTypeIndex) function pitch(pitchFlyTimeMs, pitchTypeIndex)
@ -316,10 +249,6 @@ function updateForcedRunners()
end end
end end
local ResetFieldersAfterSeconds <const> = 5
-- TODO: Replace with a timer, repeatedly reset, instead of setting to 0
local secondsSinceLastRunnerMove = 0
---@param runner integer | Runner ---@param runner integer | Runner
function outRunner(runner, message) function outRunner(runner, message)
if type(runner) ~= "number" then if type(runner) ~= "number" then
@ -346,9 +275,9 @@ function outRunner(runner, message)
local currentlyFieldingTeam = battingTeam == teams.home and teams.away or teams.home local currentlyFieldingTeam = battingTeam == teams.home and teams.away or teams.home
local gameOver = inning == 9 and teams.away.score ~= teams.home.score local gameOver = inning == 9 and teams.away.score ~= teams.home.score
if not gameOver then if not gameOver then
fieldersDance() FielderDanceAnimator:reset(C.DanceBounceMs)
secondsSinceLastRunnerMove = -7 secondsSinceLastRunnerMove = -7
benchAllFielders(currentlyFieldingTeam.benchPosition) Field:benchTo(currentlyFieldingTeam.benchPosition)
announcer:say("SWITCHING SIDES...") announcer:say("SWITCHING SIDES...")
end end
while #runners > 0 do while #runners > 0 do
@ -361,7 +290,7 @@ function outRunner(runner, message)
if gameOver then if gameOver then
announcer:say("AND THAT'S THE BALL GAME!") announcer:say("AND THAT'S THE BALL GAME!")
else else
resetFielderPositions() Field:resetFielderPositions()
if battingTeam == teams.home then if battingTeam == teams.home then
inning = inning + 1 inning = inning + 1
end end
@ -427,7 +356,7 @@ end
function tryToMakeAnOut(fielder) function tryToMakeAnOut(fielder)
local targetX, targetY = getNextOutTarget() local targetX, targetY = getNextOutTarget()
if targetX ~= nil and targetY ~= nil then if targetX ~= nil and targetY ~= nil then
local nearestFielder = utils.getNearestOf(fielders, targetX, targetY) local nearestFielder = utils.getNearestOf(Field.fielders, targetX, targetY)
nearestFielder.target = utils.xy(targetX, targetY) nearestFielder.target = utils.xy(targetX, targetY)
if nearestFielder == fielder then if nearestFielder == fielder then
ball.heldBy = fielder ball.heldBy = fielder
@ -461,7 +390,7 @@ function buttonControlledThrow(thrower, throwFlyMs, forbidThrowHome)
return false return false
end end
local closestFielder = utils.getNearestOf(fielders, targetBase.x, targetBase.y, function(fielder) local closestFielder = utils.getNearestOf(Field.fielders, targetBase.x, targetBase.y, function(fielder)
return fielder ~= thrower return fielder ~= thrower
end) end)
@ -524,7 +453,7 @@ function updateFielder(fielder)
if playerIsOn(C.Sides.defense) then if playerIsOn(C.Sides.defense) then
local throwFly = readThrow() local throwFly = readThrow()
if throwFly then if throwFly then
buttonControlledThrow(fielders.pitcher, throwFly) buttonControlledThrow(Field.fielders.pitcher, throwFly)
end end
else else
updateNpcFielder(fielder, outedSomeRunner) updateNpcFielder(fielder, outedSomeRunner)
@ -586,22 +515,19 @@ function updateRunner(runner, runnerIndex, appliedSpeed)
return prevX ~= runner.x or prevY ~= runner.y return prevX ~= runner.x or prevY ~= runner.y
end end
---@type number
local batAngleDeg
function nextBatter() function nextBatter()
batter = nil batter = nil
playdate.timer.new(2000, function() playdate.timer.new(2000, function()
pitchTracker:reset() pitchTracker:reset()
if not batter then if not batter then
batter = newRunner() batter = utils.newRunner(runners)
end end
end) end)
end end
function walk() function walk()
announcer:say("Walk!") announcer:say("Walk!")
fielders.first.target = C.Bases[C.First] Field.fielders.first.target = C.Bases[C.First]
batter.nextBase = C.Bases[C.First] batter.nextBase = C.Bases[C.First]
batter.prevBase = C.Bases[C.Home] batter.prevBase = C.Bases[C.Home]
offenseMode = C.Offense.walking offenseMode = C.Offense.walking
@ -653,14 +579,14 @@ function updateBatting(batDeg, batSpeed)
local hitBallScaler = gfx.animator.new(2000, 9 + (mult * mult * 0.5), C.SmallestBallRadius, utils.easingHill) local hitBallScaler = gfx.animator.new(2000, 9 + (mult * mult * 0.5), C.SmallestBallRadius, utils.easingHill)
throwBall(ballDestX, ballDestY, playdate.easingFunctions.outQuint, 2000, nil, hitBallScaler) throwBall(ballDestX, ballDestY, playdate.easingFunctions.outQuint, 2000, nil, hitBallScaler)
fielders.first.target = C.Bases[C.First] Field.fielders.first.target = C.Bases[C.First]
batter.nextBase = C.Bases[C.First] batter.nextBase = C.Bases[C.First]
batter.prevBase = C.Bases[C.Home] batter.prevBase = C.Bases[C.Home]
updateForcedRunners() updateForcedRunners()
batter.forcedTo = C.Bases[C.First] batter.forcedTo = C.Bases[C.First]
batter = nil -- Demote batter to a mere runner batter = nil -- Demote batter to a mere runner
local chasingFielder = utils.getNearestOf(fielders, ballDestX, ballDestY) local chasingFielder = utils.getNearestOf(Field.fielders, ballDestX, ballDestY)
chasingFielder.target = { x = ballDestX, y = ballDestY } chasingFielder.target = { x = ballDestX, y = ballDestY }
end end
end end
@ -721,10 +647,12 @@ function updateGameState()
if ball.heldBy then if ball.heldBy then
ball.x = ball.heldBy.x ball.x = ball.heldBy.x
ball.y = ball.heldBy.y ball.y = ball.heldBy.y
ball.size = C.SmallestBallRadius
else else
ball.x = ballAnimatorX:currentValue() ball.x = ballAnimatorX:currentValue()
ball.z = ballFloatAnimator:currentValue() ball.z = ballFloatAnimator:currentValue()
ball.y = ballAnimatorY:currentValue() + ball.z ball.y = ballAnimatorY:currentValue() + ball.z
ball.size = ballSizeAnimator:currentValue()
end end
local playerOnOffense, playerOnDefense = playerIsOn(C.Sides.offense) local playerOnOffense, playerOnDefense = playerIsOn(C.Sides.offense)
@ -741,7 +669,7 @@ function updateGameState()
pitchTracker.recordedPitchX = ball.x pitchTracker.recordedPitchX = ball.x
end end
local pitcher = fielders.pitcher local pitcher = Field.fielders.pitcher
if utils.distanceBetween(pitcher.x, pitcher.y, C.PitchStartX, C.PitchStartY) < C.BaseHitbox then if utils.distanceBetween(pitcher.x, pitcher.y, C.PitchStartX, C.PitchStartY) < C.BaseHitbox then
secondsSincePitchAllowed = secondsSincePitchAllowed + deltaSeconds secondsSincePitchAllowed = secondsSincePitchAllowed + deltaSeconds
end end
@ -787,12 +715,12 @@ function updateGameState()
secondsSinceLastRunnerMove = 0 secondsSinceLastRunnerMove = 0
else else
secondsSinceLastRunnerMove = secondsSinceLastRunnerMove + deltaSeconds secondsSinceLastRunnerMove = secondsSinceLastRunnerMove + deltaSeconds
if secondsSinceLastRunnerMove > ResetFieldersAfterSeconds then if secondsSinceLastRunnerMove > C.ResetFieldersAfterSeconds then
throwBall(C.PitchStartX, C.PitchStartY, playdate.easingFunctions.linear, nil, true) throwBall(C.PitchStartX, C.PitchStartY, playdate.easingFunctions.linear, nil, true)
resetFielderPositions() Field:resetFielderPositions()
offenseMode = C.Offense.batting offenseMode = C.Offense.batting
if not batter then if not batter then
batter = newRunner() batter = utils.newRunner(runners)
end end
end end
end end
@ -803,7 +731,7 @@ function updateGameState()
end end
end end
for _, fielder in pairs(fielders) do for _, fielder in pairs(Field.fielders) do
updateFielder(fielder) updateFielder(fielder)
end end
walkAwayOutRunners() walkAwayOutRunners()
@ -817,17 +745,14 @@ function playdate.update()
gfx.clear() gfx.clear()
gfx.setColor(gfx.kColorBlack) gfx.setColor(gfx.kColorBlack)
local offsetX, offsetY = 0, 0 local offsetX, offsetY = getDrawOffset(ball.x, ball.y)
if ball.x < C.BallOffscreen then
offsetX, offsetY = getDrawOffset(C.Screen.W, C.Screen.H, ball.x, ball.y)
gfx.setDrawOffset(offsetX, offsetY) gfx.setDrawOffset(offsetX, offsetY)
end
GrassBackground:draw(-400, -240) GrassBackground:draw(-400, -240)
local fielderDanceHeight = FielderDanceAnimator:currentValue() local fielderDanceHeight = FielderDanceAnimator:currentValue()
local ballIsHeld = false local ballIsHeld = false
for _, fielder in pairs(fielders) do for _, fielder in pairs(Field.fielders) do
ballIsHeld = drawFielder(ball, fielder.x, fielder.y + fielderDanceHeight) or ballIsHeld ballIsHeld = drawFielder(ball, fielder.x, fielder.y + fielderDanceHeight) or ballIsHeld
end end
@ -853,6 +778,7 @@ function playdate.update()
PlayerImageBlipper:draw(false, runner.x, runner.y) PlayerImageBlipper:draw(false, runner.x, runner.y)
end end
end end
for _, runner in pairs(outRunners) do for _, runner in pairs(outRunners) do
PlayerFrown:draw(runner.x, runner.y) PlayerFrown:draw(runner.x, runner.y)
end end
@ -880,7 +806,7 @@ function init()
playdate.display.setRefreshRate(50) playdate.display.setRefreshRate(50)
gfx.setBackgroundColor(gfx.kColorWhite) gfx.setBackgroundColor(gfx.kColorWhite)
playdate.setMenuImage(gfx.image.new("images/game/menu-image.png")) playdate.setMenuImage(gfx.image.new("images/game/menu-image.png"))
resetFielderPositions(teams.home.benchPosition) Field:resetFielderPositions(teams.home.benchPosition)
playdate.getSystemMenu():addMenuItem("Restart game", function() end) -- TODO? playdate.getSystemMenu():addMenuItem("Restart game", function() end) -- TODO?
playdate.timer.new(2000, function() playdate.timer.new(2000, function()

View File

@ -1,6 +1,13 @@
-- selene: allow(unscoped_variables) -- selene: allow(unscoped_variables)
utils = {} utils = {}
--- @alias XYPair {
--- x: number,
--- y: number,
--- }
local sqrt <const> = math.sqrt
function utils.easingHill(t, b, c, d) function utils.easingHill(t, b, c, d)
c = c + 0.0 -- convert to float to prevent integer overflow c = c + 0.0 -- convert to float to prevent integer overflow
t = t / d t = t / d
@ -9,10 +16,18 @@ function utils.easingHill(t, b, c, d)
return (c * t) + b return (c * t) + b
end end
--- @alias XYPair { --- Build an "animator" whose `:currentValue()` always returns the given value.
--- x: number, --- Essentially an "empty object" pattern for initial object positions.
--- y: number, ---@param value number
--- } ---@return PseudoAnimator
function utils.staticAnimator(value)
return {
currentValue = function(_)
return value
end,
reset = function(_) end
}
end
---@param x number ---@param x number
---@param y number ---@param y number
@ -84,7 +99,7 @@ end
function utils.distanceBetween(x1, y1, x2, y2) function utils.distanceBetween(x1, y1, x2, y2)
local x = x1 - x2 local x = x1 - x2
local y = y1 - y2 local y = y1 - y2
return math.sqrt((x * x) + (y * y)), x, y return sqrt((x * x) + (y * y)), x, y
end end
---@return number distance, number x, number y, number z ---@return number distance, number x, number y, number z
@ -92,7 +107,20 @@ function utils.distanceBetweenZ(x1, y1, z1, x2, y2, z2)
local x = x1 - x2 local x = x1 - x2
local y = y1 - y2 local y = y1 - y2
local z = z1 - z2 local z = z1 - z2
return math.sqrt((x * x) + (y * y) + (z * z)), x, y, z return sqrt((x * x) + (y * y) + (z * z)), x, y, z
end
---@return Runner
function utils.newRunner(runners)
local new = {
x = C.RightHandedBattersBox.x - 60,
y = C.RightHandedBattersBox.y + 60,
nextBase = C.RightHandedBattersBox,
prevBase = nil,
forcedTo = C.Bases[C.First],
}
runners[#runners + 1] = new
return new
end end
--- Returns true only if the point is below the given line, within the x bounds of said line, and above the bottomBound --- Returns true only if the point is below the given line, within the x bounds of said line, and above the bottomBound