Class-ify Announcer, Baserunning, and Fielding

Largely to enable dependency injection.
I am pushing AWAY the paranoia that metatable lookups will slow things down. (You've got like 20 entities, bud, chill.)
Field -> Fielding
This commit is contained in:
Sage Vaillancourt 2025-02-09 11:19:11 -05:00
parent aadaa6e0d6
commit 9c0d263a29
6 changed files with 102 additions and 72 deletions

View File

@ -11,14 +11,21 @@ local AnnouncerAnimatorOutY <const> =
playdate.graphics.animator.new(AnnouncementTransitionMs, 0, -70, playdate.easingFunctions.outQuint)
-- selene: allow(unscoped_variables)
announcer = {
---@class Announcer
---@field textQueue string[]
---@field animatorY pd_animator
Announcer = {}
function Announcer.new()
return setmetatable({
textQueue = {},
animatorY = AnnouncerAnimatorInY,
}
}, { __index = Announcer })
end
local DurationMs <const> = 3000
function announcer:popIn()
function Announcer:popIn()
self.animatorY = AnnouncerAnimatorInY
self.animatorY:reset()
@ -39,14 +46,14 @@ function announcer:popIn()
end)
end
function announcer:say(text)
function Announcer:say(text)
self.textQueue[#self.textQueue + 1] = text
if #self.textQueue == 1 then
self:popIn()
end
end
function announcer:draw(x, y)
function Announcer:draw(x, y)
if #self.textQueue == 0 then
return
end

View File

@ -7,27 +7,37 @@
--- }
-- selene: allow(unscoped_variables)
baserunning = {
---@type Runner[]
---@class Baserunning
---@field runners Runner[]
---@field outRunners Runner[]
---@field scoredRunners Runner[]
---@field batter Runner | nil
---@field outs number
---@field announcer Announcer
Baserunning = {}
---@param announcer any
---@return Baserunning
function Baserunning.new(announcer)
local o = setmetatable({
runners = {},
---@type Runner[]
outRunners = {},
---@type Runner[]
scoredRunners = {},
---@type Runner | nil
batter = nil,
--- Since this object is what ultimately *mutates* the out count,
--- it seems sensible to store the value here.
outs = 0
}
outs = 0,
announcer = announcer
}, { __index = Baserunning })
o.batter = o:newRunner()
return o
end
---@param runner integer | Runner
---@param message string | nil
function baserunning:outRunner(runner, message)
function Baserunning:outRunner(runner, message)
self.outs = self.outs + 1
if type(runner) ~= "number" then
for i, maybe in ipairs(self.runners) do
@ -44,7 +54,7 @@ function baserunning:outRunner(runner, message)
self:updateForcedRunners()
announcer:say(message or "YOU'RE OUT!")
self.announcer:say(message or "YOU'RE OUT!")
if self.outs < 3 then
return
end
@ -55,7 +65,7 @@ function baserunning:outRunner(runner, message)
end
end
function baserunning:outEligibleRunners(fielder)
function Baserunning:outEligibleRunners(fielder)
local touchedBase = utils.isTouchingBase(fielder.x, fielder.y)
local didOutRunner = false
for i, runner in pairs(self.runners) do
@ -76,7 +86,7 @@ function baserunning:outEligibleRunners(fielder)
return didOutRunner
end
function baserunning:updateForcedRunners()
function Baserunning:updateForcedRunners()
local stillForced = true
for _, base in ipairs(C.Bases) do
local runnerTargetingBase = utils.getRunnerWithNextBase(self.runners, base)
@ -93,7 +103,7 @@ function baserunning:updateForcedRunners()
end
---@param deltaSeconds number
function baserunning:walkAwayOutRunners(deltaSeconds)
function Baserunning:walkAwayOutRunners(deltaSeconds)
for i, runner in ipairs(self.outRunners) do
if runner.x < C.Screen.W + 50 and runner.y < C.Screen.H + 50 then
runner.x = runner.x + (deltaSeconds * 25)
@ -105,7 +115,7 @@ function baserunning:walkAwayOutRunners(deltaSeconds)
end
---@return Runner
function baserunning:newRunner()
function Baserunning:newRunner()
local new = {
x = C.RightHandedBattersBox.x - 60,
y = C.RightHandedBattersBox.y + 60,
@ -117,11 +127,9 @@ function baserunning:newRunner()
return new
end
baserunning.batter = baserunning:newRunner()
---@param self table
---@param runnerIndex integer
function baserunning:runnerScored(runnerIndex)
function Baserunning:runnerScored(runnerIndex)
-- TODO: outRunners/scoredRunners split
self.outRunners[#self.outRunners + 1] = self.runners[runnerIndex]
table.remove(self.runners, runnerIndex)
@ -133,7 +141,7 @@ end
---@param appliedSpeed number
---@param deltaSeconds number
---@return boolean runnerMoved, boolean runnerScored
function baserunning:updateRunner(runner, runnerIndex, appliedSpeed, deltaSeconds)
function Baserunning:updateRunner(runner, runnerIndex, appliedSpeed, deltaSeconds)
local autoRunSpeed = 20 * deltaSeconds
if not runner or not runner.nextBase then
@ -188,7 +196,7 @@ end
--- Returns true only if at least one of the given runners moved during this update
---@param appliedSpeed number
---@return boolean someRunnerMoved, number runnersScored
function baserunning:updateRunning(appliedSpeed, forcedOnly, deltaSeconds)
function Baserunning:updateRunning(appliedSpeed, forcedOnly, deltaSeconds)
local someRunnerMoved = false
local runnersScored = 0

View File

@ -1,15 +1,21 @@
-- selene: allow(unscoped_variables)
---@class Fielding
---@field fielders table<string, Fielder>
---@field fielderTouchingBall Fielder | nil
Fielding = {}
---@param name string
---@param speed number
---@return Fielder
function newFielder(name, speed)
local function newFielder(name, speed)
return {
name = name,
speed = speed,
}
end
-- selene: allow(unscoped_variables)
Field = {
function Fielding.new()
return setmetatable({
fielders = {
first = newFielder("First", 40),
second = newFielder("Second", 40),
@ -23,11 +29,12 @@ Field = {
},
---@type Fielder | nil
fielderTouchingBall = nil,
}
}, { __index = Fielding })
end
--- Actually only benches the infield, because outfielders are far away!
---@param position XYPair
function Field:benchTo(position)
function Fielding:benchTo(position)
self.fielders.first.target = position
self.fielders.second.target = position
self.fielders.shortstop.target = position
@ -38,7 +45,7 @@ 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(fromOffTheField)
function Fielding:resetFielderPositions(fromOffTheField)
if fromOffTheField then
for _, fielder in pairs(self.fielders) do
fielder.x = fromOffTheField.x
@ -76,7 +83,7 @@ end
---@param self table
---@param ballDestX number
---@param ballDestY number
function Field:haveSomeoneChase(ballDestX, ballDestY)
function Fielding:haveSomeoneChase(ballDestX, ballDestY)
local chasingFielder = utils.getNearestOf(self.fielders, ballDestX, ballDestY)
chasingFielder.target = { x = ballDestX, y = ballDestY }
@ -92,7 +99,7 @@ end
---@param ball XYPair
---@param deltaSeconds number
---@return Fielder | nil fielderTouchingBall nil if no fielder is currently touching the ball
function Field:updateFielderPositions(ball, deltaSeconds)
function Fielding:updateFielderPositions(ball, deltaSeconds)
local fielderTouchingBall = nil
for _, fielder in pairs(self.fielders) do
local isTouchingBall = updateFielderPosition(deltaSeconds, fielder, ball)
@ -120,6 +127,7 @@ local function playerThrowToImpl(field, targetBase, throwBall, throwFlyMs)
closestFielder.target = targetBase
throwBall(targetBase.x, targetBase.y, playdate.easingFunctions.linear, throwFlyMs)
return ActionResult.Succeeded
end
--- Buffer in a fielder throw action.
@ -127,7 +135,7 @@ end
---@param targetBase Base
---@param throwBall ThrowBall
---@param throwFlyMs number
function Field:playerThrowTo(targetBase, throwBall, throwFlyMs)
function Fielding:playerThrowTo(targetBase, throwBall, throwFlyMs)
local maxTryTimeMs = 5000
actionQueue:upsert('playerThrowTo', maxTryTimeMs, function()
return playerThrowToImpl(self, targetBase, throwBall, throwFlyMs)

View File

@ -38,6 +38,10 @@ import 'draw/fielder.lua'
-- selene: allow(shadowing)
local gfx <const>, C <const> = playdate.graphics, C
local announcer = Announcer:new()
local baserunning = Baserunning.new(announcer)
local fielding = Fielding.new()
local PlayerImageBlipper <const> = blipper.new(100, Player, PlayerLowHat)
local FielderDanceAnimator <const> = gfx.animator.new(1, 10, 0, utils.easingHill)
@ -208,7 +212,7 @@ local function outRunner(runner, message)
if not gameOver then
FielderDanceAnimator:reset(C.DanceBounceMs)
secondsSinceLastRunnerMove = -7
Field:benchTo(currentlyFieldingTeam.benchPosition)
fielding:benchTo(currentlyFieldingTeam.benchPosition)
announcer:say("SWITCHING SIDES...")
end
-- Delay to keep end-of-inning on the scoreboard for a few seconds
@ -218,7 +222,7 @@ local function outRunner(runner, message)
if gameOver then
announcer:say("AND THAT'S THE BALL GAME!")
else
Field:resetFielderPositions()
fielding:resetFielderPositions()
if battingTeam == teams.home then
inning = inning + 1
end
@ -259,7 +263,7 @@ local function buttonControlledThrow(throwFlyMs, forbidThrowHome)
-- Power for this throw has already been determined
throwMeter = 0
Field:playerThrowTo(targetBase, throwBall, throwFlyMs)
fielding:playerThrowTo(targetBase, throwBall, throwFlyMs)
secondsSinceLastRunnerMove = 0
offenseState = C.Offense.running
@ -330,7 +334,7 @@ local function updateBatting(batDeg, batSpeed)
baserunning.batter.forcedTo = C.Bases[C.First]
baserunning.batter = nil -- Demote batter to a mere runner
Field:haveSomeoneChase(ballDestX, ballDestY)
fielding:haveSomeoneChase(ballDestX, ballDestY)
end
end
@ -392,7 +396,7 @@ local function updateGameState()
pitchTracker.recordedPitchX = ball.x
end
local pitcher = Field.fielders.pitcher
local pitcher = fielding.fielders.pitcher
if utils.distanceBetween(pitcher.x, pitcher.y, C.PitchStartX, C.PitchStartY) < C.BaseHitbox then
secondsSincePitchAllowed = secondsSincePitchAllowed + deltaSeconds
end
@ -441,7 +445,7 @@ local function updateGameState()
secondsSinceLastRunnerMove = secondsSinceLastRunnerMove + deltaSeconds
if secondsSinceLastRunnerMove > C.ResetFieldersAfterSeconds then
throwBall(C.PitchStartX, C.PitchStartY, playdate.easingFunctions.linear, nil, true)
Field:resetFielderPositions()
fielding:resetFielderPositions()
offenseState = C.Offense.batting
if not baserunning.batter then
baserunning.batter = baserunning:newRunner()
@ -454,7 +458,7 @@ local function updateGameState()
end
end
local fielderHoldingBall = Field:updateFielderPositions(ball, deltaSeconds)
local fielderHoldingBall = fielding:updateFielderPositions(ball, deltaSeconds)
if playerOnDefense then
local throwFly = readThrow()
@ -465,7 +469,7 @@ local function updateGameState()
if fielderHoldingBall then
local outedSomeRunner = baserunning:outEligibleRunners(fielderHoldingBall)
if playerOnOffense then
npc.fielderAction(offenseState, fielderHoldingBall, outedSomeRunner, ball, throwBall)
npc.fielderAction(fielding, baserunning, offenseState, fielderHoldingBall, outedSomeRunner, ball, throwBall)
end
end
@ -488,7 +492,7 @@ function playdate.update()
local fielderDanceHeight = FielderDanceAnimator:currentValue()
local ballIsHeld = false
for _, fielder in pairs(Field.fielders) do
for _, fielder in pairs(fielding.fielders) do
ballIsHeld = drawFielder(ball, fielder.x, fielder.y + fielderDanceHeight) or ballIsHeld
end
@ -531,7 +535,7 @@ function playdate.update()
gfx.setDrawOffset(0, 0)
if math.abs(offsetX) > 10 or math.abs(offsetY) > 10 then
drawMinimap(baserunning.runners, Field.fielders)
drawMinimap(baserunning.runners, fielding.fielders)
end
drawScoreboard(0, C.Screen.H * 0.77, teams, outs, battingTeam, inning)
drawBallsAndStrikes(290, C.Screen.H - 20, pitchTracker.balls, pitchTracker.strikes)
@ -542,7 +546,7 @@ local function init()
playdate.display.setRefreshRate(50)
gfx.setBackgroundColor(gfx.kColorWhite)
playdate.setMenuImage(gfx.image.new("images/game/menu-image.png"))
Field:resetFielderPositions(teams.home.benchPosition)
fielding:resetFielderPositions(teams.home.benchPosition)
playdate.getSystemMenu():addMenuItem("Restart game", function() end) -- TODO?
playdate.timer.new(2000, function()

View File

@ -85,13 +85,14 @@ function npc.getNextOutTarget(runners)
end
end
---@param fielding Fielding
---@param fielder Fielder
---@param runners Runner[]
---@param throwBall ThrowBall
function npc.tryToMakeAPlay(fielder, runners, ball, throwBall)
function npc.tryToMakeAPlay(fielding, fielder, runners, ball, throwBall)
local targetX, targetY = npc.getNextOutTarget(runners)
if targetX ~= nil and targetY ~= nil then
local nearestFielder = utils.getNearestOf(Field.fielders, targetX, targetY)
local nearestFielder = utils.getNearestOf(fielding.fielders, targetX, targetY)
nearestFielder.target = utils.xy(targetX, targetY)
if nearestFielder == fielder then
ball.heldBy = fielder
@ -101,22 +102,24 @@ function npc.tryToMakeAPlay(fielder, runners, ball, throwBall)
end
end
---@param fielding Fielding
---@param baserunning Baserunning
---@param offenseState OffenseState
---@param fielder Fielder
---@param outedSomeRunner boolean
---@param ball { x: number, y: number, heldBy: Fielder | nil }
---@param throwBall ThrowBall
function npc.fielderAction(offenseState, fielder, outedSomeRunner, ball, throwBall)
function npc.fielderAction(fielding, baserunning, offenseState, fielder, outedSomeRunner, ball, throwBall)
if offenseState ~= C.Offense.running then
return
end
if outedSomeRunner then
-- Delay a little before the next play
playdate.timer.new(750, function()
npc.tryToMakeAPlay(fielder, baserunning.runners, ball, throwBall)
npc.tryToMakeAPlay(fielding, fielder, baserunning.runners, ball, throwBall)
end)
else
npc.tryToMakeAPlay(fielder, baserunning.runners, ball, throwBall)
npc.tryToMakeAPlay(fielding, fielder, baserunning.runners, ball, throwBall)
end
end