From 9c0d263a29741148f6621294a37458a9856fea0a Mon Sep 17 00:00:00 2001 From: Sage Vaillancourt Date: Sun, 9 Feb 2025 11:19:11 -0500 Subject: [PATCH] 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 --- src/announcer.lua | 21 ++++++++++------ src/baserunning.lua | 58 ++++++++++++++++++++++++++------------------- src/fielding.lua | 52 +++++++++++++++++++++++----------------- src/main.lua | 28 ++++++++++++---------- src/npc.lua | 13 ++++++---- src/utils.lua | 2 +- 6 files changed, 102 insertions(+), 72 deletions(-) diff --git a/src/announcer.lua b/src/announcer.lua index f1a9a08..71ab997 100644 --- a/src/announcer.lua +++ b/src/announcer.lua @@ -11,14 +11,21 @@ local AnnouncerAnimatorOutY = 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 = 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 diff --git a/src/baserunning.lua b/src/baserunning.lua index b3fde81..705e58f 100644 --- a/src/baserunning.lua +++ b/src/baserunning.lua @@ -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 diff --git a/src/fielding.lua b/src/fielding.lua index 87e7094..bd2641b 100644 --- a/src/fielding.lua +++ b/src/fielding.lua @@ -1,33 +1,40 @@ +-- selene: allow(unscoped_variables) +---@class Fielding +---@field fielders table +---@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) diff --git a/src/main.lua b/src/main.lua index 7b13a9e..364704b 100644 --- a/src/main.lua +++ b/src/main.lua @@ -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 , C = playdate.graphics, C +local announcer = Announcer:new() +local baserunning = Baserunning.new(announcer) +local fielding = Fielding.new() + local PlayerImageBlipper = blipper.new(100, Player, PlayerLowHat) local FielderDanceAnimator = 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() diff --git a/src/npc.lua b/src/npc.lua index 9bef3f5..7431055 100644 --- a/src/npc.lua +++ b/src/npc.lua @@ -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 diff --git a/src/utils.lua b/src/utils.lua index 763fdeb..4651a92 100644 --- a/src/utils.lua +++ b/src/utils.lua @@ -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