From 1a68521bd47ddf68e8fba02f36cc0fca6040d058 Mon Sep 17 00:00:00 2001 From: Sage Vaillancourt Date: Sun, 9 Feb 2025 10:06:57 -0500 Subject: [PATCH] Extract baserunning.lua field.lua -> fielding.lua npcFielderAction() -> npc.fielderAction() Generally, a pinch of additional or stricter typing --- src/baserunning.lua | 209 +++++++++++++++++++++++++++ src/constants.lua | 6 + src/dbg.lua | 27 ++-- src/{field.lua => fielding.lua} | 14 ++ src/main.lua | 243 ++++++-------------------------- src/npc.lua | 30 ++++ src/utils.lua | 13 -- 7 files changed, 317 insertions(+), 225 deletions(-) create mode 100644 src/baserunning.lua rename src/{field.lua => fielding.lua} (92%) diff --git a/src/baserunning.lua b/src/baserunning.lua new file mode 100644 index 0000000..6e5c00e --- /dev/null +++ b/src/baserunning.lua @@ -0,0 +1,209 @@ +--- @alias Runner { +--- x: number, +--- y: number, +--- nextBase: Base, +--- prevBase: Base | nil, +--- forcedTo: Base | nil, +--- } + +-- selene: allow(unscoped_variables) +baserunning = { + ---@type Runner[] + 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 +} + +---@param runner integer | Runner +---@param message string | nil +function baserunning.outRunner(self, runner, message) + self.outs = self.outs + 1 + if type(runner) ~= "number" then + for i, maybe in ipairs(self.runners) do + if runner == maybe then + runner = i + end + end + end + if type(runner) ~= "number" then + error("Expected runner to have type 'number', but was: " .. type(runner)) + end + self.outRunners[#self.outRunners + 1] = self.runners[runner] + table.remove(self.runners, runner) + + self:updateForcedRunners() + + announcer:say(message or "YOU'RE OUT!") + if self.outs < 3 then + return + end + + -- TODO: outRunners/scoredRunners split + while #self.runners > 0 do + self.outRunners[#self.outRunners + 1] = table.remove(self.runners, #self.runners) + end +end + +function baserunning.outEligibleRunners(self, fielder) + local touchedBase = utils.isTouchingBase(fielder.x, fielder.y) + local didOutRunner = false + for i, runner in pairs(self.runners) do + local runnerOnBase = utils.isTouchingBase(runner.x, runner.y) + if -- Force out + touchedBase + and runner.prevBase -- Make sure the runner is not standing at home + and runner.forcedTo == touchedBase + and touchedBase ~= runnerOnBase + -- Tag out + or not runnerOnBase and utils.distanceBetween(runner.x, runner.y, fielder.x, fielder.y) < C.TagDistance + then + self:outRunner(i) + didOutRunner = true + end + end + + return didOutRunner +end + +function baserunning.updateForcedRunners(self) + local stillForced = true + for _, base in ipairs(C.Bases) do + local runnerTargetingBase = utils.getRunnerWithNextBase(self.runners, base) + if runnerTargetingBase then + if stillForced then + runnerTargetingBase.forcedTo = base + else + runnerTargetingBase.forcedTo = nil + end + else + stillForced = false + end + end +end + +---@param deltaSeconds number +function baserunning.walkAwayOutRunners(self, 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) + runner.y = runner.y + (deltaSeconds * 25) + else + table.remove(self.outRunners, i) + end + end +end + +---@return Runner +function baserunning.newRunner(self) + local new = { + x = C.RightHandedBattersBox.x - 60, + y = C.RightHandedBattersBox.y + 60, + nextBase = C.RightHandedBattersBox, + prevBase = nil, + forcedTo = C.Bases[C.First], + } + self.runners[#self.runners + 1] = new + return new +end + +---@param self table +---@param runnerIndex integer +function baserunning.runnerScored(self, runnerIndex) + -- TODO: outRunners/scoredRunners split + self.outRunners[#self.outRunners + 1] = self.runners[runnerIndex] + table.remove(self.runners, runnerIndex) +end + +--- Returns true only if the given runner moved during this update. +---@param runner Runner | nil +---@param runnerIndex integer | nil May only be nil if runner == batter +---@param appliedSpeed number +---@param deltaSeconds number +---@return boolean runnerMoved, boolean runnerScored +function baserunning.updateRunner(self, runner, runnerIndex, appliedSpeed, deltaSeconds) + local autoRunSpeed = 20 * deltaSeconds + + if not runner or not runner.nextBase then + return false, false + end + + local nearestBase, nearestBaseDistance = utils.getNearestOf(C.Bases, runner.x, runner.y) + + if + nearestBaseDistance < 5 + and runnerIndex ~= nil + and runner ~= self.batter --runner.prevBase + and runner.nextBase == C.Bases[C.Home] + and nearestBase == C.Bases[C.Home] + then + self:runnerScored(runnerIndex) + return true, true + end + + local nb = runner.nextBase + local x, y, distance = utils.normalizeVector(runner.x, runner.y, nb.x, nb.y) + + if distance < 2 then + runner.nextBase = C.NextBaseMap[runner.nextBase] + runner.forcedTo = nil + return false, false + end + + local prevX, prevY = runner.x, runner.y + local mult = 1 + if appliedSpeed < 0 then + if runner.prevBase then + mult = -1 + else + -- Don't allow running backwards when approaching the plate + appliedSpeed = 0 + end + end + + local autoRun = (nearestBaseDistance > 40 or runner.forcedTo) and mult * autoRunSpeed + or nearestBaseDistance < 5 and 0 + or (nearestBase == runner.nextBase and autoRunSpeed or -1 * autoRunSpeed) + + mult = autoRun + (appliedSpeed / 20) + runner.x = runner.x - (x * mult) + runner.y = runner.y - (y * mult) + + return prevX ~= runner.x or prevY ~= runner.y, false +end + +--- Update non-batter runners. +--- 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(self, appliedSpeed, forcedOnly, deltaSeconds) + local someRunnerMoved = false + local runnersScored = 0 + + -- TODO: Filter for the runner closest to the currently-held direction button + for runnerIndex, runner in ipairs(self.runners) do + if runner ~= self.batter and (not forcedOnly or runner.forcedTo) then + local thisRunnerMoved, thisRunnerScored = self:updateRunner(runner, runnerIndex, appliedSpeed, deltaSeconds) + someRunnerMoved = someRunnerMoved or thisRunnerMoved + if thisRunnerScored then + runnersScored = runnersScored + 1 + end + end + end + + if someRunnerMoved then + self:updateForcedRunners() + end + + return someRunnerMoved, runnersScored +end diff --git a/src/constants.lua b/src/constants.lua index 703fe69..ceb0257 100644 --- a/src/constants.lua +++ b/src/constants.lua @@ -71,13 +71,19 @@ C.SmallestBallRadius = 6 C.BatLength = 50 +---@alias OffenseState table + --- An enum for what state the offense is in +---@type table C.Offense = { batting = {}, running = {}, walking = {}, } +---@alias Side table + +---@type table --- An enum for which side (offense or defense) a team is on. C.Sides = { offense = {}, diff --git a/src/dbg.lua b/src/dbg.lua index 37b754e..4a4a79d 100644 --- a/src/dbg.lua +++ b/src/dbg.lua @@ -20,21 +20,22 @@ end -- Only works if called with the bases empty (i.e. the only runner should be the batter. -- selene: allow(unused_variable) -function dbg.loadTheBases(runners) - utils.newRunner() - utils.newRunner() - utils.newRunner() - runners[2].x = C.Bases[C.First].x - runners[2].y = C.Bases[C.First].y - runners[2].nextBase = C.Bases[C.Second] +function dbg.loadTheBases(br) + br:newRunner() + br:newRunner() + br:newRunner() - runners[3].x = C.Bases[C.Second].x - runners[3].y = C.Bases[C.Second].y - runners[3].nextBase = C.Bases[C.Third] + br.runners[2].x = C.Bases[C.First].x + br.runners[2].y = C.Bases[C.First].y + br.runners[2].nextBase = C.Bases[C.Second] - runners[4].x = C.Bases[C.Third].x - runners[4].y = C.Bases[C.Third].y - runners[4].nextBase = C.Bases[C.Home] + br.runners[3].x = C.Bases[C.Second].x + br.runners[3].y = C.Bases[C.Second].y + br.runners[3].nextBase = C.Bases[C.Third] + + br.runners[4].x = C.Bases[C.Third].x + br.runners[4].y = C.Bases[C.Third].y + br.runners[4].nextBase = C.Bases[C.Home] end if not playdate then diff --git a/src/field.lua b/src/fielding.lua similarity index 92% rename from src/field.lua rename to src/fielding.lua index 3e7cb8d..fc34662 100644 --- a/src/field.lua +++ b/src/fielding.lua @@ -57,6 +57,7 @@ function Field.resetFielderPositions(self, fromOffTheField) self.fielders.right.target = utils.xy(C.Screen.W * 2, self.fielders.left.target.y) end +---@param deltaSeconds number ---@param fielder Fielder ---@param ballPos XYPair ---@return boolean isTouchingBall @@ -72,6 +73,9 @@ end --- Selects the nearest fielder to move toward the given coordinates. --- Other fielders should attempt to cover their bases +---@param self table +---@param ballDestX number +---@param ballDestY number function Field.haveSomeoneChase(self, ballDestX, ballDestY) local chasingFielder = utils.getNearestOf(self.fielders, ballDestX, ballDestY) chasingFielder.target = { x = ballDestX, y = ballDestY } @@ -86,6 +90,7 @@ function Field.haveSomeoneChase(self, ballDestX, ballDestY) end ---@param ball XYPair +---@param deltaSeconds number ---@return Fielder | nil fielderTouchingBall nil if no fielder is currently touching the ball function Field.updateFielderPositions(self, ball, deltaSeconds) local fielderTouchingBall = nil @@ -100,6 +105,11 @@ function Field.updateFielderPositions(self, ball, deltaSeconds) end -- TODO? Start moving target fielders close sooner? +---@param field table +---@param targetBase Base +---@param throwBall ThrowBall +---@param throwFlyMs number +---@return ActionResult local function playerThrowToImpl(field, targetBase, throwBall, throwFlyMs) if field.fielderTouchingBall == nil then return ActionResult.NeedsMoreTime @@ -113,6 +123,10 @@ local function playerThrowToImpl(field, targetBase, throwBall, throwFlyMs) end --- Buffer in a fielder throw action. +---@param self table +---@param targetBase Base +---@param throwBall ThrowBall +---@param throwFlyMs number function Field.playerThrowTo(self, targetBase, throwBall, throwFlyMs) local maxTryTimeMs = 5000 actionQueue:upsert('playerThrowTo', maxTryTimeMs, function() diff --git a/src/main.lua b/src/main.lua index a5e0b2d..f9e75e4 100644 --- a/src/main.lua +++ b/src/main.lua @@ -7,14 +7,6 @@ import 'CoreLibs/object.lua' import 'CoreLibs/timer.lua' import 'CoreLibs/ui.lua' ---- @alias Runner { ---- x: number, ---- y: number, ---- nextBase: Base, ---- prevBase: Base | nil, ---- forcedTo: Base | nil, ---- } - --- @alias Fielder { --- x: number, --- y: number, @@ -24,14 +16,17 @@ 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) + import 'utils.lua' import 'constants.lua' import 'assets.lua' import 'action-queue.lua' import 'announcer.lua' +import 'baserunning.lua' import 'dbg.lua' -import 'field.lua' +import 'fielding.lua' import 'graphics.lua' import 'npc.lua' import 'draw/overlay.lua' @@ -86,20 +81,14 @@ local teams = { }, } -local PlayerTeam = teams.home +local PlayerTeam = teams.away local battingTeam = teams.away local outs = 0 local inning = 1 -local offenseMode = C.Offense.batting - ---- @type Runner[] -local runners = {} - ---- @type Runner[] -local outRunners = {} +local offenseState = C.Offense.batting ---@type Runner | nil -local batter = utils.newRunner(runners) +local batter = baserunning:newRunner() local throwMeter = 0 @@ -184,7 +173,7 @@ end ---@param pitchTypeIndex number | nil local function pitch(pitchFlyTimeMs, pitchTypeIndex) catcherThrownBall = false - offenseMode = C.Offense.batting + offenseState = C.Offense.batting local current = Pitches[pitchTypeIndex] ballAnimatorX = current.x @@ -207,42 +196,11 @@ local function pitch(pitchFlyTimeMs, pitchTypeIndex) secondsSincePitchAllowed = 0 end -local function updateForcedRunners() - local stillForced = true - for _, base in ipairs(C.Bases) do - local runnerTargetingBase = utils.getRunnerWithNextBase(runners, base) - if runnerTargetingBase then - if stillForced then - runnerTargetingBase.forcedTo = base - else - runnerTargetingBase.forcedTo = nil - end - else - stillForced = false - end - end -end - ---@param runner integer | Runner +---@param message string | nil local function outRunner(runner, message) - if type(runner) ~= "number" then - for i, maybe in ipairs(runners) do - if runner == maybe then - runner = i - end - end - end - if type(runner) ~= "number" then - error("Expected runner to have type 'number', but was: " .. type(runner)) - end - outRunners[#outRunners + 1] = runners[runner] - table.remove(runners, runner) - - outs = outs + 1 - updateForcedRunners() - - announcer:say(message or "YOU'RE OUT!") - if outs < 3 then + baserunning:outRunner(runner, message) + if baserunning.outs < 3 then return end @@ -254,9 +212,6 @@ local function outRunner(runner, message) Field:benchTo(currentlyFieldingTeam.benchPosition) announcer:say("SWITCHING SIDES...") end - while #runners > 0 do - outRunners[#outRunners + 1] = table.remove(runners, #runners) - end -- Delay to keep end-of-inning on the scoreboard for a few seconds playdate.timer.new(3000, function() outs = 0 @@ -270,13 +225,12 @@ local function outRunner(runner, message) end end end) + end ----@param runnerIndex number -local function score(runnerIndex) - outRunners[#outRunners + 1] = runners[runnerIndex] - table.remove(runners, runnerIndex) - battingTeam.score = battingTeam.score + 1 +---@param scoredRunCount number +local function score(scoredRunCount) + battingTeam.score = battingTeam.score + scoredRunCount announcer:say("SCORE!") end @@ -308,108 +262,17 @@ local function buttonControlledThrow(throwFlyMs, forbidThrowHome) Field:playerThrowTo(targetBase, throwBall, throwFlyMs) secondsSinceLastRunnerMove = 0 - offenseMode = C.Offense.running + offenseState = C.Offense.running return true end -local function outEligibleRunners(fielder) - local touchedBase = utils.isTouchingBase(fielder.x, fielder.y) - local didOutRunner = false - for i, runner in pairs(runners) do - local runnerOnBase = utils.isTouchingBase(runner.x, runner.y) - if -- Force out - touchedBase - and runner.prevBase -- Make sure the runner is not standing at home - and runner.forcedTo == touchedBase - and touchedBase ~= runnerOnBase - -- Tag out - or not runnerOnBase and utils.distanceBetween(runner.x, runner.y, fielder.x, fielder.y) < C.TagDistance - then - outRunner(i) - didOutRunner = true - end - end - - return didOutRunner -end - -local function npcFielderAction(fielder, outedSomeRunner) - if offenseMode ~= C.Offense.running then - return - end - if outedSomeRunner then - -- Delay a little before the next play - playdate.timer.new(750, function() - npc.tryToMakeAPlay(fielder, runners, ball, throwBall) - end) - else - npc.tryToMakeAPlay(fielder, runners, ball, throwBall) - end -end - ---- Returns true only if the given runner moved during this update. ----@param runner Runner | nil ----@param runnerIndex integer | nil May only be nil if runner == batter ----@param appliedSpeed number ----@return boolean -local function updateRunner(runner, runnerIndex, appliedSpeed) - local autoRunSpeed = 20 * deltaSeconds - --autoRunSpeed = 140 - - if not runner or not runner.nextBase then - return false - end - - local nearestBase, nearestBaseDistance = utils.getNearestOf(C.Bases, runner.x, runner.y) - - if - nearestBaseDistance < 5 - and runnerIndex ~= nil - and runner ~= batter --runner.prevBase - and runner.nextBase == C.Bases[C.Home] - and nearestBase == C.Bases[C.Home] - then - score(runnerIndex) - end - - local nb = runner.nextBase - local x, y, distance = utils.normalizeVector(runner.x, runner.y, nb.x, nb.y) - - if distance < 2 then - runner.nextBase = C.NextBaseMap[runner.nextBase] - runner.forcedTo = nil - return false - end - - local prevX, prevY = runner.x, runner.y - local mult = 1 - if appliedSpeed < 0 then - if runner.prevBase then - mult = -1 - else - -- Don't allow running backwards when approaching the plate - appliedSpeed = 0 - end - end - - local autoRun = (nearestBaseDistance > 40 or runner.forcedTo) and mult * autoRunSpeed - or nearestBaseDistance < 5 and 0 - or (nearestBase == runner.nextBase and autoRunSpeed or -1 * autoRunSpeed) - - mult = autoRun + (appliedSpeed / 20) - runner.x = runner.x - (x * mult) - runner.y = runner.y - (y * mult) - - return prevX ~= runner.x or prevY ~= runner.y -end - local function nextBatter() batter = nil playdate.timer.new(2000, function() pitchTracker:reset() if not batter then - batter = utils.newRunner(runners) + batter = baserunning:newRunner() end end) end @@ -418,9 +281,9 @@ local function walk() announcer:say("Walk!") batter.nextBase = C.Bases[C.First] batter.prevBase = C.Bases[C.Home] - offenseMode = C.Offense.walking + offenseState = C.Offense.walking batter = nil - updateForcedRunners() + baserunning:updateForcedRunners() nextBatter() end @@ -446,7 +309,7 @@ local function updateBatting(batDeg, batSpeed) and ball.y < 232 then BatCrackReverb:play() - offenseMode = C.Offense.running + offenseState = C.Offense.running local ballAngle = batAngle + math.rad(90) local mult = math.abs(batSpeed / 15) @@ -464,7 +327,7 @@ local function updateBatting(batDeg, batSpeed) batter.nextBase = C.Bases[C.First] batter.prevBase = C.Bases[C.Home] - updateForcedRunners() + baserunning:updateForcedRunners() batter.forcedTo = C.Bases[C.First] batter = nil -- Demote batter to a mere runner @@ -472,34 +335,16 @@ local function updateBatting(batDeg, batSpeed) end end ---- Update non-batter runners. ---- Returns true only if at least one of the given runners moved during this update ---@param appliedSpeed number ----@return boolean -local function updateRunning(appliedSpeed, forcedOnly) - local runnerMoved = false - - -- TODO: Filter for the runner closest to the currently-held direction button - for runnerIndex, runner in ipairs(runners) do - if runner ~= batter and (not forcedOnly or runner.forcedTo) then - runnerMoved = updateRunner(runner, runnerIndex, appliedSpeed) or runnerMoved - end +---@return boolean someRunnerMoved +local function updateNonBatterRunners(appliedSpeed, forcedOnly) + local runnerMoved, runnersScored = baserunning:updateRunning(appliedSpeed, forcedOnly, deltaSeconds) + if runnersScored ~= 0 then + score(runnersScored) end - return runnerMoved end -local function walkAwayOutRunners() - for i, runner in ipairs(outRunners) do - if runner.x < C.Screen.W + 50 and runner.y < C.Screen.H + 50 then - runner.x = runner.x + (deltaSeconds * 25) - runner.y = runner.y + (deltaSeconds * 25) - else - table.remove(outRunners, i) - end - end -end - local function playerPitch(throwFly) local aButton = playdate.buttonIsPressed(playdate.kButtonA) local bButton = playdate.buttonIsPressed(playdate.kButtonB) @@ -541,7 +386,7 @@ local function updateGameState() throwMeter = throwMeter + math.abs(crankLimited * C.PitchPower) end - if offenseMode == C.Offense.batting then + if offenseState == C.Offense.batting then if ball.y < C.StrikeZoneStartY then pitchTracker.recordedPitchX = nil elseif not pitchTracker.recordedPitchX then @@ -575,8 +420,9 @@ local function updateGameState() updateBatting(batAngleDeg, batSpeed) + -- Walk batter to the plate -- TODO: Ensure batter can't be nil, here - updateRunner(batter, nil, crankLimited) + baserunning:updateRunner(batter, nil, crankLimited, deltaSeconds) if secondsSincePitchAllowed > C.PitchAfterSeconds then if playerOnDefense then @@ -588,25 +434,24 @@ local function updateGameState() pitch(C.PitchFlyMs, math.random(#Pitches)) end end - elseif offenseMode == C.Offense.running then - local appliedSpeed = playerOnOffense and crankLimited or npc.runningSpeed(runners) - if updateRunning(appliedSpeed) then + elseif offenseState == C.Offense.running then + local appliedSpeed = playerOnOffense and crankLimited or npc.runningSpeed(baserunning.runners) + if updateNonBatterRunners(appliedSpeed) then secondsSinceLastRunnerMove = 0 else secondsSinceLastRunnerMove = secondsSinceLastRunnerMove + deltaSeconds if secondsSinceLastRunnerMove > C.ResetFieldersAfterSeconds then throwBall(C.PitchStartX, C.PitchStartY, playdate.easingFunctions.linear, nil, true) Field:resetFielderPositions() - offenseMode = C.Offense.batting + offenseState = C.Offense.batting if not batter then - batter = utils.newRunner(runners) + batter = baserunning:newRunner() end end end - elseif offenseMode == C.Offense.walking then - updateForcedRunners() - if not updateRunning(C.WalkedRunnerSpeed, true) then - offenseMode = C.Offense.batting + elseif offenseState == C.Offense.walking then + if not updateNonBatterRunners(C.WalkedRunnerSpeed, true) then + offenseState = C.Offense.batting end end @@ -619,13 +464,13 @@ local function updateGameState() end end if fielderHoldingBall then - local outedSomeRunner = outEligibleRunners(fielderHoldingBall) + local outedSomeRunner = baserunning:outEligibleRunners(fielderHoldingBall) if playerOnOffense then - npcFielderAction(fielderHoldingBall, outedSomeRunner) + npc.fielderAction(offenseState, fielderHoldingBall, outedSomeRunner, ball, throwBall) end end - walkAwayOutRunners() + baserunning:walkAwayOutRunners(deltaSeconds) actionQueue:runWaiting(deltaSeconds) end @@ -648,7 +493,7 @@ function playdate.update() ballIsHeld = drawFielder(ball, fielder.x, fielder.y + fielderDanceHeight) or ballIsHeld end - if offenseMode == C.Offense.batting then + if offenseState == C.Offense.batting then gfx.setLineWidth(5) gfx.drawLine(batBase.x, batBase.y, batTip.x, batTip.y) end @@ -658,7 +503,7 @@ function playdate.update() end -- TODO? Scale sprites down as y increases - for _, runner in pairs(runners) do + for _, runner in pairs(baserunning.runners) do if runner == batter then if batAngleDeg > 50 and batAngleDeg < 200 then PlayerBack:draw(runner.x, runner.y) @@ -671,7 +516,7 @@ function playdate.update() end end - for _, runner in pairs(outRunners) do + for _, runner in pairs(baserunning.outRunners) do PlayerFrown:draw(runner.x, runner.y) end @@ -687,7 +532,7 @@ function playdate.update() gfx.setDrawOffset(0, 0) if math.abs(offsetX) > 10 or math.abs(offsetY) > 10 then - drawMinimap(runners, Field.fielders) + drawMinimap(baserunning.runners, Field.fielders) end drawScoreboard(0, C.Screen.H * 0.77, teams, outs, battingTeam, inning) drawBallsAndStrikes(290, C.Screen.H - 20, pitchTracker.balls, pitchTracker.strikes) diff --git a/src/npc.lua b/src/npc.lua index 33cfca3..9bef3f5 100644 --- a/src/npc.lua +++ b/src/npc.lua @@ -5,6 +5,10 @@ local npcBatSpeed = 1500 -- selene: allow(unscoped_variables) npc = {} +---@param ball XYPair +---@param catcherThrownBall boolean +---@param deltaSec number +---@return number function npc.updateBatAngle(ball, catcherThrownBall, deltaSec) if not catcherThrownBall and ball.y > 200 and ball.y < 230 and (ball.x < C.Center.x + 15) then npcBatDeg = npcBatDeg + (deltaSec * npcBatSpeed) @@ -19,6 +23,8 @@ function npc.batSpeed() return npcBatSpeed end +---@param runners Runner[] +---@return number function npc.runningSpeed(runners) if #runners == 0 then return 0 @@ -30,6 +36,7 @@ function npc.runningSpeed(runners) return 0 end +---@param runners Runner[] ---@return Base[] local function getForcedOutTargets(runners) local targets = {} @@ -45,6 +52,7 @@ local function getForcedOutTargets(runners) end --- Returns the position,distance of the base closest to the runner who is *furthest* from a base +---@param runners Runner[] ---@return Base | nil, number | nil local function getBaseOfStrandedRunner(runners) local farRunnersBase, farDistance @@ -62,6 +70,7 @@ local function getBaseOfStrandedRunner(runners) end --- Returns x,y of the out target +---@param runners Runner[] ---@return number|nil, number|nil function npc.getNextOutTarget(runners) -- TODO: Handle missed throws, check for fielders at target, etc. @@ -77,6 +86,8 @@ function npc.getNextOutTarget(runners) end ---@param fielder Fielder +---@param runners Runner[] +---@param throwBall ThrowBall function npc.tryToMakeAPlay(fielder, runners, ball, throwBall) local targetX, targetY = npc.getNextOutTarget(runners) if targetX ~= nil and targetY ~= nil then @@ -90,6 +101,25 @@ function npc.tryToMakeAPlay(fielder, runners, ball, throwBall) end end +---@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) + 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) + end) + else + npc.tryToMakeAPlay(fielder, baserunning.runners, ball, throwBall) + end +end + if not playdate then return npc end diff --git a/src/utils.lua b/src/utils.lua index e68b68b..763fdeb 100644 --- a/src/utils.lua +++ b/src/utils.lua @@ -119,19 +119,6 @@ function utils.distanceBetweenZ(x1, y1, z1, x2, y2, z2) return sqrt((x * x) + (y * y) + (z * z)), x, y, z end ----@return Runner -function utils.newRunner(runners) - local new = { - x = C.RightHandedBattersBox.x - 60, - y = C.RightHandedBattersBox.y + 60, - nextBase = C.RightHandedBattersBox, - prevBase = nil, - forcedTo = C.Bases[C.First], - } - runners[#runners + 1] = new - return new -end - --- Returns the base being touched by the player at (x,y), or nil, if no base is being touched ---@param x number ---@param y number