More type hints.

Also, move pointDirectlyUnderLine() to XyPair-based.
This commit is contained in:
Sage Vaillancourt 2025-02-24 23:55:03 -05:00
parent ddfdc8947a
commit 55a3a7b0ee
12 changed files with 81 additions and 22 deletions

View File

@ -26,6 +26,10 @@ function actionQueue:upsert(id, maxTimeMs, action)
} }
end end
--- The new action will not be added if an entry with the current id already exists in the queue.
---@param id any
---@param maxTimeMs number
---@param action Action
function actionQueue:newOnly(id, maxTimeMs, action) function actionQueue:newOnly(id, maxTimeMs, action)
if self.queue[id] then if self.queue[id] then
return return
@ -38,6 +42,7 @@ end
--- Must be called on every playdate.update() to check for (and run) any waiting tasks. --- Must be called on every playdate.update() to check for (and run) any waiting tasks.
--- Actions that return NeedsMoreTime will not be removed from the queue unless they have expired. --- Actions that return NeedsMoreTime will not be removed from the queue unless they have expired.
---@param deltaSeconds number
function actionQueue:runWaiting(deltaSeconds) function actionQueue:runWaiting(deltaSeconds)
local currentTimeMs = playdate.getCurrentTimeMilliseconds() local currentTimeMs = playdate.getCurrentTimeMilliseconds()

View File

@ -52,6 +52,8 @@ function Announcer:say(text)
end end
end end
---@param x number
---@param y number
function Announcer:draw(x, y) function Announcer:draw(x, y)
if #self.textQueue == 0 then if #self.textQueue == 0 then
return return

View File

@ -21,8 +21,9 @@ Baserunning = {}
-- TODO: Implement slides? Would require making fielders' gloves "real objects" whose state is tracked. -- TODO: Implement slides? Would require making fielders' gloves "real objects" whose state is tracked.
---@param announcer Announcer ---@param announcer Announcer
---@param onThirdOutCallback fun()
---@return Baserunning ---@return Baserunning
function Baserunning.new(announcer, onThirdOut) function Baserunning.new(announcer, onThirdOutCallback)
local o = setmetatable({ local o = setmetatable({
runners = {}, runners = {},
outRunners = {}, outRunners = {},
@ -32,7 +33,7 @@ function Baserunning.new(announcer, onThirdOut)
--- it seems sensible to store the value here. --- it seems sensible to store the value here.
outs = 0, outs = 0,
announcer = announcer, announcer = announcer,
onThirdOut = onThirdOut, onThirdOut = onThirdOutCallback,
}, { __index = Baserunning }) }, { __index = Baserunning })
o:pushNewBatter() o:pushNewBatter()
@ -133,6 +134,9 @@ function Baserunning:convertBatterToRunner()
self.batter = nil -- Demote batter to a mere runner self.batter = nil -- Demote batter to a mere runner
end end
---@param deltaSeconds number
---@param runner Runner
---@return boolean isStillWalking
local function walkWayOutRunner(deltaSeconds, runner) local function walkWayOutRunner(deltaSeconds, runner)
if runner.x < C.Screen.W + 50 and runner.y < C.Screen.H + 50 then if runner.x < C.Screen.W + 50 and runner.y < C.Screen.H + 50 then
runner.x = runner.x + (deltaSeconds * 25) runner.x = runner.x + (deltaSeconds * 25)
@ -156,7 +160,7 @@ function Baserunning:walkAwayOutRunners(deltaSeconds)
end end
end end
---@return Runner ---@return Runner theBatterPushed
function Baserunning:pushNewBatter() function Baserunning:pushNewBatter()
local new = { local new = {
-- imageSet = math.random() < C.WokeMeter and FemmeSet or MascSet, -- TODO? lol. -- imageSet = math.random() < C.WokeMeter and FemmeSet or MascSet, -- TODO? lol.
@ -182,6 +186,7 @@ end
---@param runner Runner | nil ---@param runner Runner | nil
---@param runnerIndex number | nil May only be nil if runner == batter ---@param runnerIndex number | nil May only be nil if runner == batter
---@param appliedSpeed number ---@param appliedSpeed number
---@param isAutoRun boolean
---@param deltaSeconds number ---@param deltaSeconds number
---@return boolean runnerMoved, boolean runnerScored ---@return boolean runnerMoved, boolean runnerScored
function Baserunning:updateRunner(runner, runnerIndex, appliedSpeed, isAutoRun, deltaSeconds) function Baserunning:updateRunner(runner, runnerIndex, appliedSpeed, isAutoRun, deltaSeconds)
@ -248,7 +253,9 @@ end
--- Update non-batter runners. --- Update non-batter runners.
--- Returns true only if at least one of the given runners moved during this update --- Returns true only if at least one of the given runners moved during this update
---@param appliedSpeed number | fun(runner: Runner): number ---@param appliedSpeed number | fun(runner: Runner): number
---@param forcedOnly boolean If true, only move forced runners (e.g. for a walk)
---@param isAutoRun boolean If true, does not attempt to hug the bases ---@param isAutoRun boolean If true, does not attempt to hug the bases
---@param deltaSeconds number
---@return boolean runnersStillMoving, number runnersScored, number secondsSinceLastMove ---@return boolean runnersStillMoving, number runnersScored, number secondsSinceLastMove
function Baserunning:updateNonBatterRunners(appliedSpeed, forcedOnly, isAutoRun, deltaSeconds) function Baserunning:updateNonBatterRunners(appliedSpeed, forcedOnly, isAutoRun, deltaSeconds)
local runnersStillMoving = false local runnersStillMoving = false

View File

@ -52,7 +52,7 @@ local function detail(text)
return { text = text, font = DetailFont } return { text = text, font = DetailFont }
end end
---@class ---@class ControlScreen
---@field sceneToReturnTo Scene ---@field sceneToReturnTo Scene
---@field private renderedImage pd_image Static image doesn't need to be constantly re-rendered. ---@field private renderedImage pd_image Static image doesn't need to be constantly re-rendered.
ControlScreen = {} ControlScreen = {}
@ -81,6 +81,7 @@ local function draw()
end end
---@param sceneToReturnTo Scene ---@param sceneToReturnTo Scene
---@return ControlScreen
function ControlScreen.new(sceneToReturnTo) function ControlScreen.new(sceneToReturnTo)
return setmetatable({ return setmetatable({
sceneToReturnTo = sceneToReturnTo, sceneToReturnTo = sceneToReturnTo,

View File

@ -16,7 +16,8 @@ function dbg.label(value, name)
return value return value
end 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.
---@param br Baserunning
function dbg.loadTheBases(br) function dbg.loadTheBases(br)
br:pushNewBatter() br:pushNewBatter()
br:pushNewBatter() br:pushNewBatter()
@ -73,6 +74,7 @@ local hitSamples = {
}, },
} }
---@param inningCount number Number of innings to mock
---@return Statistics ---@return Statistics
function dbg.mockStatistics(inningCount) function dbg.mockStatistics(inningCount)
inningCount = inningCount or 9 inningCount = inningCount or 9

View File

@ -41,6 +41,8 @@ local function startGame()
MenuMusic:setPaused(true) MenuMusic:setPaused(true)
end end
---@param baseEaser EasingFunc
---@return EasingFunc
local function pausingEaser(baseEaser) local function pausingEaser(baseEaser)
--- t: elapsedTime --- t: elapsedTime
--- d: duration --- d: duration
@ -65,6 +67,7 @@ local animatorY = gfx.animator.new(2000, 60, 200, pausingEaser(utils.easingHill)
animatorY.repeatCount = -1 animatorY.repeatCount = -1
animatorY.reverses = true animatorY.reverses = true
---@type number
local crankStartPos local crankStartPos
---@generic T ---@generic T
@ -76,6 +79,7 @@ local function arrayElementFromCrank(array, crankPosition)
return array[i] return array[i]
end end
---@type pd_image
local currentLogo local currentLogo
--luacheck: ignore --luacheck: ignore

View File

@ -406,15 +406,7 @@ function Game:updateBatting(offenseHandler)
local ballWasHit = batSpeed > 0 local ballWasHit = batSpeed > 0
and ball.y < 232 and ball.y < 232
and utils.pointDirectlyUnderLine( and utils.pointDirectlyUnderLine(ball, self.state.batBase, self.state.batTip, C.Screen.H)
ball.x,
ball.y,
self.state.batBase.x,
self.state.batBase.y,
self.state.batTip.x,
self.state.batTip.y,
C.Screen.H
)
if not ballWasHit then if not ballWasHit then
return return
@ -472,6 +464,8 @@ function Game:updateBatting(offenseHandler)
end end
---@param appliedSpeed number | fun(runner: Runner): number ---@param appliedSpeed number | fun(runner: Runner): number
---@param forcedOnly boolean
---@param isAutoRun boolean
---@return boolean runnersStillMoving, number secondsSinceLastRunnerMove ---@return boolean runnersStillMoving, number secondsSinceLastRunnerMove
function Game:updateNonBatterRunners(appliedSpeed, forcedOnly, isAutoRun) function Game:updateNonBatterRunners(appliedSpeed, forcedOnly, isAutoRun)
local runnersStillMoving, runnersScored, secondsSinceLastRunnerMove = local runnersStillMoving, runnersScored, secondsSinceLastRunnerMove =
@ -649,10 +643,12 @@ function Game:update()
if self.state.offenseState == C.Offense.batting then if self.state.offenseState == C.Offense.batting then
gfx.setLineWidth(7) gfx.setLineWidth(7)
gfx.drawLine(self.state.batBase.x, self.state.batBase.y, self.state.batTip.x, self.state.batTip.y) gfx.drawLine(self.state.batBase.x, self.state.batBase.y, self.state.batTip.x, self.state.batTip.y)
gfx.setColor(gfx.kColorWhite) gfx.setColor(gfx.kColorWhite)
gfx.setLineCapStyle(gfx.kLineCapStyleRound) gfx.setLineCapStyle(gfx.kLineCapStyleRound)
gfx.setLineWidth(3) gfx.setLineWidth(3)
gfx.drawLine(self.state.batBase.x, self.state.batBase.y, self.state.batTip.x, self.state.batTip.y) gfx.drawLine(self.state.batBase.x, self.state.batBase.y, self.state.batTip.x, self.state.batTip.y)
gfx.setColor(gfx.kColorBlack) gfx.setColor(gfx.kColorBlack)
end end
@ -681,6 +677,7 @@ function Game:update()
if math.abs(offsetX) > 10 or math.abs(offsetY) > 10 then if math.abs(offsetX) > 10 or math.abs(offsetY) > 10 then
drawMinimap(self.baserunning.runners, self.fielding.fielders) drawMinimap(self.baserunning.runners, self.fielding.fielders)
end end
local homeScore, awayScore = utils.totalScores(self.state.stats) local homeScore, awayScore = utils.totalScores(self.state.stats)
drawScoreboard( drawScoreboard(
0, 0,

View File

@ -21,11 +21,11 @@ end
function Npc.update() end function Npc.update() end
-- TODO: FAR more nuanced NPC batting. -- TODO: FAR more nuanced NPC batting.
-- luacheck: no unused
---@param ball XyPair ---@param ball XyPair
---@param pitchIsOver boolean ---@param pitchIsOver boolean
---@param deltaSec number ---@param deltaSec number
---@return number batAngleDeg, number batSpeed ---@return number batAngleDeg, number batSpeed
-- luacheck: no unused
function Npc:updateBat(ball, pitchIsOver, deltaSec) function Npc:updateBat(ball, pitchIsOver, deltaSec)
if not pitchIsOver and ball.y > 200 and ball.y < 230 and (ball.x < C.Center.x + 15) then if not pitchIsOver and ball.y > 200 and ball.y < 230 and (ball.x < C.Center.x + 15) then
npcBatDeg = npcBatDeg + (deltaSec * npcBatSpeed) npcBatDeg = npcBatDeg + (deltaSec * npcBatSpeed)
@ -36,10 +36,12 @@ function Npc:updateBat(ball, pitchIsOver, deltaSec)
return npcBatDeg, (self:batSpeed() * deltaSec) return npcBatDeg, (self:batSpeed() * deltaSec)
end end
---@return number
function Npc:batSpeed() function Npc:batSpeed()
return npcBatSpeed / 1.5 return npcBatSpeed / 1.5
end end
---@return number flyTimeMs, number pitchId, number accuracy
function Npc:pitch() function Npc:pitch()
return C.PitchFlyMs / self:pitchSpeed(), math.random(#Pitches), 0.9 return C.PitchFlyMs / self:pitchSpeed(), math.random(#Pitches), 0.9
end end

View File

@ -7,6 +7,8 @@ local gfx <const> = playdate.graphics
local StrikeZoneWidth <const> = C.StrikeZoneEndX - C.StrikeZoneStartX local StrikeZoneWidth <const> = C.StrikeZoneEndX - C.StrikeZoneStartX
-- TODO? Also degrade speed -- TODO? Also degrade speed
---@param accuracy number
---@return number xValueToMissBy
function getPitchMissBy(accuracy) function getPitchMissBy(accuracy)
accuracy = accuracy or 1.0 accuracy = accuracy or 1.0
local missBy = (1 - accuracy) * StrikeZoneWidth * 3 local missBy = (1 - accuracy) * StrikeZoneWidth * 3
@ -71,6 +73,9 @@ Pitches = {
end, end,
} }
---@alias PitchOutcome "StrikeOut" | "Walk"
---@type table<string, PitchOutcome>
PitchOutcomes = { PitchOutcomes = {
StrikeOut = "StrikeOut", StrikeOut = "StrikeOut",
Walk = "Walk", Walk = "Walk",
@ -93,6 +98,7 @@ function pitchTracker:reset()
self.balls = 0 self.balls = 0
end end
---@param ball XyPair
function pitchTracker:recordIfPassed(ball) function pitchTracker:recordIfPassed(ball)
if ball.y < C.StrikeZoneStartY then if ball.y < C.StrikeZoneStartY then
self.recordedPitchX = nil self.recordedPitchX = nil
@ -103,6 +109,7 @@ end
---@param didSwing boolean ---@param didSwing boolean
---@param fieldingTeamInningData TeamInningData ---@param fieldingTeamInningData TeamInningData
---@return PitchOutcome | nil
function pitchTracker:updatePitchCounts(didSwing, fieldingTeamInningData) function pitchTracker:updatePitchCounts(didSwing, fieldingTeamInningData)
if not self.recordedPitchX then if not self.recordedPitchX then
return return
@ -149,8 +156,6 @@ throwMeter = {
wasPerfect = false, wasPerfect = false,
} }
local crankQueue = {}
local MaxPowerRatio <const> = 1.5 local MaxPowerRatio <const> = 1.5
--- Returns nil when a throw is NOT requested. --- Returns nil when a throw is NOT requested.
@ -176,6 +181,11 @@ end
local CrankRecordSec <const> = 0.33 local CrankRecordSec <const> = 0.33
---@alias CrankQueueEntry { time: number, chargeAmount: number }
---@type CrankQueueEntry[]
local crankQueue = {}
--- If (within approx. a third of a second) the crank has moved more than 45 degrees, call that a throw. --- If (within approx. a third of a second) the crank has moved more than 45 degrees, call that a throw.
---@param chargeAmount number ---@param chargeAmount number
---@return number | nil ---@return number | nil

View File

@ -5,6 +5,7 @@
-- + Batting average -- + Batting average
-- + Farthest hit ball -- + Farthest hit ball
---@return TeamInningData
local function newTeamInning() local function newTeamInning()
return { return {
score = 0, score = 0,

View File

@ -2,6 +2,7 @@
---@field buttonControlledThrow: fun(throwFlyMs: number, forbidThrowHome: boolean): boolean didThrow ---@field buttonControlledThrow: fun(throwFlyMs: number, forbidThrowHome: boolean): boolean didThrow
UserInput = {} UserInput = {}
---@return UserInput
function UserInput.new(buttonControlledThrow) function UserInput.new(buttonControlledThrow)
return setmetatable({ return setmetatable({
buttonControlledThrow = buttonControlledThrow, buttonControlledThrow = buttonControlledThrow,
@ -14,6 +15,7 @@ function UserInput:update()
self.crankLimited = math.abs(crankLimited) self.crankLimited = math.abs(crankLimited)
end end
---@return number batAngleDeg, number batSpeed
function UserInput:updateBat() function UserInput:updateBat()
local batAngleDeg = (playdate.getCrankPosition() + C.CrankOffsetDeg) % 360 local batAngleDeg = (playdate.getCrankPosition() + C.CrankOffsetDeg) % 360
local batSpeed = self.crankLimited local batSpeed = self.crankLimited
@ -43,6 +45,7 @@ local function userPitch(throwFlyMs, accuracy)
return nil, nil, nil return nil, nil, nil
end end
---@return number | nil pitchFlyTimeMs, number | nil pitchTypeIndex, number | nil accuracy
function UserInput:pitch() function UserInput:pitch()
local powerRatio, accuracy = throwMeter:readThrow(self.crankChange) local powerRatio, accuracy = throwMeter:readThrow(self.crankChange)
if powerRatio then if powerRatio then

View File

@ -90,8 +90,12 @@ function utils.moveAtSpeed(mover, speed, target, tau)
return true return true
end end
function utils.within(within, n1, n2) ---@param acceptableGap number
return math.abs(n1 - n2) < within ---@param n1 number
---@param n2 number
---@return boolean n1 is within acceptableGap of n2
function utils.within(acceptableGap, n1, n2)
return math.abs(n1 - n2) < acceptableGap
end end
---@generic T ---@generic T
@ -121,6 +125,10 @@ function utils.first(array, condition)
return nil return nil
end end
---@param x1 number
---@param y1 number
---@param x2 number
---@param y2 number
---@return number distance, number x, number y ---@return number distance, number x, number y
function utils.distanceBetween(x1, y1, x2, y2) function utils.distanceBetween(x1, y1, x2, y2)
local x = x1 - x2 local x = x1 - x2
@ -137,6 +145,12 @@ function utils.distanceBetweenPoints(point1, point2)
return sqrt((x * x) + (y * y)), x, y return sqrt((x * x) + (y * y)), x, y
end 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 ---@return number distance, number x, number y, number z
function utils.distanceBetweenZ(x1, y1, z1, x2, y2, z2) function utils.distanceBetweenZ(x1, y1, z1, x2, y2, z2)
local x = x1 - x2 local x = x1 - x2
@ -164,15 +178,19 @@ function utils.getRunnerWithNextBase(runners, 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. --- 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 ---@return boolean
function utils.pointDirectlyUnderLine(pointX, pointY, lineX1, lineY1, lineX2, lineY2, bottomBound) function utils.pointDirectlyUnderLine(point, line1, line2, bottomBound)
-- This check currently assumes right-handedness. -- This check currently assumes right-handedness.
-- I.e. it assumes the ball is to the right of batBaseX -- I.e. it assumes the ball is to the right of batBaseX
if pointX < lineX1 or pointX > lineX2 or pointY > bottomBound then if point.x < line1.x or point.x > line2.x or point.y > bottomBound then
return false return false
end end
return utils.pointUnderLine(pointX, pointY, lineX1, lineY1, lineX2, lineY2) return utils.pointUnderLine(point.x, point.y, line1.x, line1.y, line2.x, line2.y)
end end
--- Returns true if the given point is anywhere above the given line, with no upper bound. --- Returns true if the given point is anywhere above the given line, with no upper bound.
@ -192,6 +210,12 @@ function utils.pointIsSquarelyAboveLine(point, linePoints)
end end
--- Returns true only if the point is below the given line. --- 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 ---@return boolean
function utils.pointUnderLine(pointX, pointY, lineX1, lineY1, lineX2, lineY2) function utils.pointUnderLine(pointX, pointY, lineX1, lineY1, lineX2, lineY2)
local m = (lineY2 - lineY1) / (lineX2 - lineX1) local m = (lineY2 - lineY1) / (lineX2 - lineX1)
@ -222,6 +246,7 @@ end
---@param array T[] ---@param array T[]
---@param x number ---@param x number
---@param y number ---@param y number
---@param extraCondition fun(t: T): boolean
---@return T nearest,number |nil distance ---@return T nearest,number |nil distance
function utils.getNearestOf(array, x, y, extraCondition) function utils.getNearestOf(array, x, y, extraCondition)
local nearest, nearestDistance = nil, nil local nearest, nearestDistance = nil, nil