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 = {
textQueue = {},
animatorY = AnnouncerAnimatorInY,
}
---@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[]
runners = {},
---@class Baserunning
---@field runners Runner[]
---@field outRunners Runner[]
---@field scoredRunners Runner[]
---@field batter Runner | nil
---@field outs number
---@field announcer Announcer
Baserunning = {}
---@type Runner[]
outRunners = {},
---@param announcer any
---@return Baserunning
function Baserunning.new(announcer)
local o = setmetatable({
runners = {},
outRunners = {},
scoredRunners = {},
batter = nil,
--- Since this object is what ultimately *mutates* the out count,
--- it seems sensible to store the value here.
outs = 0,
announcer = announcer
}, { __index = Baserunning })
---@type Runner[]
scoredRunners = {},
o.batter = o:newRunner()
---@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
}
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,33 +1,40 @@
-- 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 = {
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),
},
---@type Fielder | nil
fielderTouchingBall = nil,
}
function Fielding.new()
return setmetatable({
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),
},
---@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

@ -17,7 +17,7 @@ import 'CoreLibs/ui.lua'
--- @alias EasingFunc fun(number, number, number, number): number
---@alias ThrowBall fun(destX: number, destY: number, easingFunc: EasingFunc, flyTimeMs: number | nil, floaty: boolean | nil, customBallScaler: pd_animator | nil)
--- @alias ThrowBall fun(destX: number, destY: number, easingFunc: EasingFunc, flyTimeMs: number | nil, floaty: boolean | nil, customBallScaler: pd_animator | nil)
-- stylua: ignore start
import 'utils.lua'
@ -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

View File

@ -138,7 +138,7 @@ 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
--- @return boolean
---@return boolean
function utils.pointDirectlyUnderLine(pointX, pointY, lineX1, lineY1, lineX2, lineY2, bottomBound)
-- This check currently assumes right-handedness.
-- I.e. it assumes the ball is to the right of batBaseX