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
--- 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)
if self.queue[id] then
return
@ -38,6 +42,7 @@ end
--- 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.
---@param deltaSeconds number
function actionQueue:runWaiting(deltaSeconds)
local currentTimeMs = playdate.getCurrentTimeMilliseconds()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -21,11 +21,11 @@ end
function Npc.update() end
-- TODO: FAR more nuanced NPC batting.
-- luacheck: no unused
---@param ball XyPair
---@param pitchIsOver boolean
---@param deltaSec number
---@return number batAngleDeg, number batSpeed
-- luacheck: no unused
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
npcBatDeg = npcBatDeg + (deltaSec * npcBatSpeed)
@ -36,10 +36,12 @@ function Npc:updateBat(ball, pitchIsOver, deltaSec)
return npcBatDeg, (self:batSpeed() * deltaSec)
end
---@return number
function Npc:batSpeed()
return npcBatSpeed / 1.5
end
---@return number flyTimeMs, number pitchId, number accuracy
function Npc:pitch()
return C.PitchFlyMs / self:pitchSpeed(), math.random(#Pitches), 0.9
end

View File

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

View File

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

View File

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

View File

@ -90,8 +90,12 @@ function utils.moveAtSpeed(mover, speed, target, tau)
return true
end
function utils.within(within, n1, n2)
return math.abs(n1 - n2) < within
---@param acceptableGap number
---@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
---@generic T
@ -121,6 +125,10 @@ function utils.first(array, condition)
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
@ -137,6 +145,12 @@ function utils.distanceBetweenPoints(point1, point2)
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
@ -164,15 +178,19 @@ function utils.getRunnerWithNextBase(runners, base)
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(pointX, pointY, lineX1, lineY1, lineX2, lineY2, bottomBound)
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 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
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
--- 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
--- 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)
@ -222,6 +246,7 @@ end
---@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