Extract baserunning.lua

field.lua -> fielding.lua
npcFielderAction() -> npc.fielderAction()
Generally, a pinch of additional or stricter typing
This commit is contained in:
Sage Vaillancourt 2025-02-09 10:06:57 -05:00
parent 50ddd67730
commit 1a68521bd4
7 changed files with 317 additions and 225 deletions

209
src/baserunning.lua Normal file
View File

@ -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

View File

@ -71,13 +71,19 @@ C.SmallestBallRadius = 6
C.BatLength = 50 C.BatLength = 50
---@alias OffenseState table
--- An enum for what state the offense is in --- An enum for what state the offense is in
---@type table<string, OffenseState>
C.Offense = { C.Offense = {
batting = {}, batting = {},
running = {}, running = {},
walking = {}, walking = {},
} }
---@alias Side table
---@type table<string, Side>
--- An enum for which side (offense or defense) a team is on. --- An enum for which side (offense or defense) a team is on.
C.Sides = { C.Sides = {
offense = {}, offense = {},

View File

@ -20,21 +20,22 @@ 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.
-- selene: allow(unused_variable) -- selene: allow(unused_variable)
function dbg.loadTheBases(runners) function dbg.loadTheBases(br)
utils.newRunner() br:newRunner()
utils.newRunner() br:newRunner()
utils.newRunner() br:newRunner()
runners[2].x = C.Bases[C.First].x
runners[2].y = C.Bases[C.First].y
runners[2].nextBase = C.Bases[C.Second]
runners[3].x = C.Bases[C.Second].x br.runners[2].x = C.Bases[C.First].x
runners[3].y = C.Bases[C.Second].y br.runners[2].y = C.Bases[C.First].y
runners[3].nextBase = C.Bases[C.Third] br.runners[2].nextBase = C.Bases[C.Second]
runners[4].x = C.Bases[C.Third].x br.runners[3].x = C.Bases[C.Second].x
runners[4].y = C.Bases[C.Third].y br.runners[3].y = C.Bases[C.Second].y
runners[4].nextBase = C.Bases[C.Home] 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 end
if not playdate then if not playdate then

View File

@ -57,6 +57,7 @@ function Field.resetFielderPositions(self, fromOffTheField)
self.fielders.right.target = utils.xy(C.Screen.W * 2, self.fielders.left.target.y) self.fielders.right.target = utils.xy(C.Screen.W * 2, self.fielders.left.target.y)
end end
---@param deltaSeconds number
---@param fielder Fielder ---@param fielder Fielder
---@param ballPos XYPair ---@param ballPos XYPair
---@return boolean isTouchingBall ---@return boolean isTouchingBall
@ -72,6 +73,9 @@ end
--- Selects the nearest fielder to move toward the given coordinates. --- Selects the nearest fielder to move toward the given coordinates.
--- Other fielders should attempt to cover their bases --- Other fielders should attempt to cover their bases
---@param self table
---@param ballDestX number
---@param ballDestY number
function Field.haveSomeoneChase(self, ballDestX, ballDestY) function Field.haveSomeoneChase(self, ballDestX, ballDestY)
local chasingFielder = utils.getNearestOf(self.fielders, ballDestX, ballDestY) local chasingFielder = utils.getNearestOf(self.fielders, ballDestX, ballDestY)
chasingFielder.target = { x = ballDestX, y = ballDestY } chasingFielder.target = { x = ballDestX, y = ballDestY }
@ -86,6 +90,7 @@ function Field.haveSomeoneChase(self, ballDestX, ballDestY)
end end
---@param ball XYPair ---@param ball XYPair
---@param deltaSeconds number
---@return Fielder | nil fielderTouchingBall nil if no fielder is currently touching the ball ---@return Fielder | nil fielderTouchingBall nil if no fielder is currently touching the ball
function Field.updateFielderPositions(self, ball, deltaSeconds) function Field.updateFielderPositions(self, ball, deltaSeconds)
local fielderTouchingBall = nil local fielderTouchingBall = nil
@ -100,6 +105,11 @@ function Field.updateFielderPositions(self, ball, deltaSeconds)
end end
-- TODO? Start moving target fielders close sooner? -- 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) local function playerThrowToImpl(field, targetBase, throwBall, throwFlyMs)
if field.fielderTouchingBall == nil then if field.fielderTouchingBall == nil then
return ActionResult.NeedsMoreTime return ActionResult.NeedsMoreTime
@ -113,6 +123,10 @@ local function playerThrowToImpl(field, targetBase, throwBall, throwFlyMs)
end end
--- Buffer in a fielder throw action. --- 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) function Field.playerThrowTo(self, targetBase, throwBall, throwFlyMs)
local maxTryTimeMs = 5000 local maxTryTimeMs = 5000
actionQueue:upsert('playerThrowTo', maxTryTimeMs, function() actionQueue:upsert('playerThrowTo', maxTryTimeMs, function()

View File

@ -7,14 +7,6 @@ import 'CoreLibs/object.lua'
import 'CoreLibs/timer.lua' import 'CoreLibs/timer.lua'
import 'CoreLibs/ui.lua' import 'CoreLibs/ui.lua'
--- @alias Runner {
--- x: number,
--- y: number,
--- nextBase: Base,
--- prevBase: Base | nil,
--- forcedTo: Base | nil,
--- }
--- @alias Fielder { --- @alias Fielder {
--- x: number, --- x: number,
--- y: number, --- y: number,
@ -24,14 +16,17 @@ import 'CoreLibs/ui.lua'
--- @alias EasingFunc fun(number, number, number, number): number --- @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 'utils.lua'
import 'constants.lua' import 'constants.lua'
import 'assets.lua' import 'assets.lua'
import 'action-queue.lua' import 'action-queue.lua'
import 'announcer.lua' import 'announcer.lua'
import 'baserunning.lua'
import 'dbg.lua' import 'dbg.lua'
import 'field.lua' import 'fielding.lua'
import 'graphics.lua' import 'graphics.lua'
import 'npc.lua' import 'npc.lua'
import 'draw/overlay.lua' import 'draw/overlay.lua'
@ -86,20 +81,14 @@ local teams <const> = {
}, },
} }
local PlayerTeam <const> = teams.home local PlayerTeam <const> = teams.away
local battingTeam = teams.away local battingTeam = teams.away
local outs = 0 local outs = 0
local inning = 1 local inning = 1
local offenseMode = C.Offense.batting local offenseState = C.Offense.batting
--- @type Runner[]
local runners <const> = {}
--- @type Runner[]
local outRunners <const> = {}
---@type Runner | nil ---@type Runner | nil
local batter = utils.newRunner(runners) local batter = baserunning:newRunner()
local throwMeter = 0 local throwMeter = 0
@ -184,7 +173,7 @@ end
---@param pitchTypeIndex number | nil ---@param pitchTypeIndex number | nil
local function pitch(pitchFlyTimeMs, pitchTypeIndex) local function pitch(pitchFlyTimeMs, pitchTypeIndex)
catcherThrownBall = false catcherThrownBall = false
offenseMode = C.Offense.batting offenseState = C.Offense.batting
local current = Pitches[pitchTypeIndex] local current = Pitches[pitchTypeIndex]
ballAnimatorX = current.x ballAnimatorX = current.x
@ -207,42 +196,11 @@ local function pitch(pitchFlyTimeMs, pitchTypeIndex)
secondsSincePitchAllowed = 0 secondsSincePitchAllowed = 0
end 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 runner integer | Runner
---@param message string | nil
local function outRunner(runner, message) local function outRunner(runner, message)
if type(runner) ~= "number" then baserunning:outRunner(runner, message)
for i, maybe in ipairs(runners) do if baserunning.outs < 3 then
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
return return
end end
@ -254,9 +212,6 @@ local function outRunner(runner, message)
Field:benchTo(currentlyFieldingTeam.benchPosition) Field:benchTo(currentlyFieldingTeam.benchPosition)
announcer:say("SWITCHING SIDES...") announcer:say("SWITCHING SIDES...")
end 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 -- Delay to keep end-of-inning on the scoreboard for a few seconds
playdate.timer.new(3000, function() playdate.timer.new(3000, function()
outs = 0 outs = 0
@ -270,13 +225,12 @@ local function outRunner(runner, message)
end end
end end
end) end)
end end
---@param runnerIndex number ---@param scoredRunCount number
local function score(runnerIndex) local function score(scoredRunCount)
outRunners[#outRunners + 1] = runners[runnerIndex] battingTeam.score = battingTeam.score + scoredRunCount
table.remove(runners, runnerIndex)
battingTeam.score = battingTeam.score + 1
announcer:say("SCORE!") announcer:say("SCORE!")
end end
@ -308,108 +262,17 @@ local function buttonControlledThrow(throwFlyMs, forbidThrowHome)
Field:playerThrowTo(targetBase, throwBall, throwFlyMs) Field:playerThrowTo(targetBase, throwBall, throwFlyMs)
secondsSinceLastRunnerMove = 0 secondsSinceLastRunnerMove = 0
offenseMode = C.Offense.running offenseState = C.Offense.running
return true return true
end 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() local function nextBatter()
batter = nil batter = nil
playdate.timer.new(2000, function() playdate.timer.new(2000, function()
pitchTracker:reset() pitchTracker:reset()
if not batter then if not batter then
batter = utils.newRunner(runners) batter = baserunning:newRunner()
end end
end) end)
end end
@ -418,9 +281,9 @@ local function walk()
announcer:say("Walk!") announcer:say("Walk!")
batter.nextBase = C.Bases[C.First] batter.nextBase = C.Bases[C.First]
batter.prevBase = C.Bases[C.Home] batter.prevBase = C.Bases[C.Home]
offenseMode = C.Offense.walking offenseState = C.Offense.walking
batter = nil batter = nil
updateForcedRunners() baserunning:updateForcedRunners()
nextBatter() nextBatter()
end end
@ -446,7 +309,7 @@ local function updateBatting(batDeg, batSpeed)
and ball.y < 232 and ball.y < 232
then then
BatCrackReverb:play() BatCrackReverb:play()
offenseMode = C.Offense.running offenseState = C.Offense.running
local ballAngle = batAngle + math.rad(90) local ballAngle = batAngle + math.rad(90)
local mult = math.abs(batSpeed / 15) local mult = math.abs(batSpeed / 15)
@ -464,7 +327,7 @@ local function updateBatting(batDeg, batSpeed)
batter.nextBase = C.Bases[C.First] batter.nextBase = C.Bases[C.First]
batter.prevBase = C.Bases[C.Home] batter.prevBase = C.Bases[C.Home]
updateForcedRunners() baserunning:updateForcedRunners()
batter.forcedTo = C.Bases[C.First] batter.forcedTo = C.Bases[C.First]
batter = nil -- Demote batter to a mere runner batter = nil -- Demote batter to a mere runner
@ -472,34 +335,16 @@ local function updateBatting(batDeg, batSpeed)
end end
end end
--- Update non-batter runners.
--- Returns true only if at least one of the given runners moved during this update
---@param appliedSpeed number ---@param appliedSpeed number
---@return boolean ---@return boolean someRunnerMoved
local function updateRunning(appliedSpeed, forcedOnly) local function updateNonBatterRunners(appliedSpeed, forcedOnly)
local runnerMoved = false local runnerMoved, runnersScored = baserunning:updateRunning(appliedSpeed, forcedOnly, deltaSeconds)
if runnersScored ~= 0 then
-- TODO: Filter for the runner closest to the currently-held direction button score(runnersScored)
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 end
end
return runnerMoved return runnerMoved
end 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 function playerPitch(throwFly)
local aButton = playdate.buttonIsPressed(playdate.kButtonA) local aButton = playdate.buttonIsPressed(playdate.kButtonA)
local bButton = playdate.buttonIsPressed(playdate.kButtonB) local bButton = playdate.buttonIsPressed(playdate.kButtonB)
@ -541,7 +386,7 @@ local function updateGameState()
throwMeter = throwMeter + math.abs(crankLimited * C.PitchPower) throwMeter = throwMeter + math.abs(crankLimited * C.PitchPower)
end end
if offenseMode == C.Offense.batting then if offenseState == C.Offense.batting then
if ball.y < C.StrikeZoneStartY then if ball.y < C.StrikeZoneStartY then
pitchTracker.recordedPitchX = nil pitchTracker.recordedPitchX = nil
elseif not pitchTracker.recordedPitchX then elseif not pitchTracker.recordedPitchX then
@ -575,8 +420,9 @@ local function updateGameState()
updateBatting(batAngleDeg, batSpeed) updateBatting(batAngleDeg, batSpeed)
-- Walk batter to the plate
-- TODO: Ensure batter can't be nil, here -- TODO: Ensure batter can't be nil, here
updateRunner(batter, nil, crankLimited) baserunning:updateRunner(batter, nil, crankLimited, deltaSeconds)
if secondsSincePitchAllowed > C.PitchAfterSeconds then if secondsSincePitchAllowed > C.PitchAfterSeconds then
if playerOnDefense then if playerOnDefense then
@ -588,25 +434,24 @@ local function updateGameState()
pitch(C.PitchFlyMs, math.random(#Pitches)) pitch(C.PitchFlyMs, math.random(#Pitches))
end end
end end
elseif offenseMode == C.Offense.running then elseif offenseState == C.Offense.running then
local appliedSpeed = playerOnOffense and crankLimited or npc.runningSpeed(runners) local appliedSpeed = playerOnOffense and crankLimited or npc.runningSpeed(baserunning.runners)
if updateRunning(appliedSpeed) then if updateNonBatterRunners(appliedSpeed) then
secondsSinceLastRunnerMove = 0 secondsSinceLastRunnerMove = 0
else else
secondsSinceLastRunnerMove = secondsSinceLastRunnerMove + deltaSeconds secondsSinceLastRunnerMove = secondsSinceLastRunnerMove + deltaSeconds
if secondsSinceLastRunnerMove > C.ResetFieldersAfterSeconds then if secondsSinceLastRunnerMove > C.ResetFieldersAfterSeconds then
throwBall(C.PitchStartX, C.PitchStartY, playdate.easingFunctions.linear, nil, true) throwBall(C.PitchStartX, C.PitchStartY, playdate.easingFunctions.linear, nil, true)
Field:resetFielderPositions() Field:resetFielderPositions()
offenseMode = C.Offense.batting offenseState = C.Offense.batting
if not batter then if not batter then
batter = utils.newRunner(runners) batter = baserunning:newRunner()
end end
end end
end end
elseif offenseMode == C.Offense.walking then elseif offenseState == C.Offense.walking then
updateForcedRunners() if not updateNonBatterRunners(C.WalkedRunnerSpeed, true) then
if not updateRunning(C.WalkedRunnerSpeed, true) then offenseState = C.Offense.batting
offenseMode = C.Offense.batting
end end
end end
@ -619,13 +464,13 @@ local function updateGameState()
end end
end end
if fielderHoldingBall then if fielderHoldingBall then
local outedSomeRunner = outEligibleRunners(fielderHoldingBall) local outedSomeRunner = baserunning:outEligibleRunners(fielderHoldingBall)
if playerOnOffense then if playerOnOffense then
npcFielderAction(fielderHoldingBall, outedSomeRunner) npc.fielderAction(offenseState, fielderHoldingBall, outedSomeRunner, ball, throwBall)
end end
end end
walkAwayOutRunners() baserunning:walkAwayOutRunners(deltaSeconds)
actionQueue:runWaiting(deltaSeconds) actionQueue:runWaiting(deltaSeconds)
end end
@ -648,7 +493,7 @@ function playdate.update()
ballIsHeld = drawFielder(ball, fielder.x, fielder.y + fielderDanceHeight) or ballIsHeld ballIsHeld = drawFielder(ball, fielder.x, fielder.y + fielderDanceHeight) or ballIsHeld
end end
if offenseMode == C.Offense.batting then if offenseState == C.Offense.batting then
gfx.setLineWidth(5) gfx.setLineWidth(5)
gfx.drawLine(batBase.x, batBase.y, batTip.x, batTip.y) gfx.drawLine(batBase.x, batBase.y, batTip.x, batTip.y)
end end
@ -658,7 +503,7 @@ function playdate.update()
end end
-- TODO? Scale sprites down as y increases -- 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 runner == batter then
if batAngleDeg > 50 and batAngleDeg < 200 then if batAngleDeg > 50 and batAngleDeg < 200 then
PlayerBack:draw(runner.x, runner.y) PlayerBack:draw(runner.x, runner.y)
@ -671,7 +516,7 @@ function playdate.update()
end end
end end
for _, runner in pairs(outRunners) do for _, runner in pairs(baserunning.outRunners) do
PlayerFrown:draw(runner.x, runner.y) PlayerFrown:draw(runner.x, runner.y)
end end
@ -687,7 +532,7 @@ function playdate.update()
gfx.setDrawOffset(0, 0) gfx.setDrawOffset(0, 0)
if math.abs(offsetX) > 10 or math.abs(offsetY) > 10 then if math.abs(offsetX) > 10 or math.abs(offsetY) > 10 then
drawMinimap(runners, Field.fielders) drawMinimap(baserunning.runners, Field.fielders)
end end
drawScoreboard(0, C.Screen.H * 0.77, teams, outs, battingTeam, inning) drawScoreboard(0, C.Screen.H * 0.77, teams, outs, battingTeam, inning)
drawBallsAndStrikes(290, C.Screen.H - 20, pitchTracker.balls, pitchTracker.strikes) drawBallsAndStrikes(290, C.Screen.H - 20, pitchTracker.balls, pitchTracker.strikes)

View File

@ -5,6 +5,10 @@ local npcBatSpeed = 1500
-- selene: allow(unscoped_variables) -- selene: allow(unscoped_variables)
npc = {} npc = {}
---@param ball XYPair
---@param catcherThrownBall boolean
---@param deltaSec number
---@return number
function npc.updateBatAngle(ball, catcherThrownBall, deltaSec) 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 if not catcherThrownBall and ball.y > 200 and ball.y < 230 and (ball.x < C.Center.x + 15) then
npcBatDeg = npcBatDeg + (deltaSec * npcBatSpeed) npcBatDeg = npcBatDeg + (deltaSec * npcBatSpeed)
@ -19,6 +23,8 @@ function npc.batSpeed()
return npcBatSpeed return npcBatSpeed
end end
---@param runners Runner[]
---@return number
function npc.runningSpeed(runners) function npc.runningSpeed(runners)
if #runners == 0 then if #runners == 0 then
return 0 return 0
@ -30,6 +36,7 @@ function npc.runningSpeed(runners)
return 0 return 0
end end
---@param runners Runner[]
---@return Base[] ---@return Base[]
local function getForcedOutTargets(runners) local function getForcedOutTargets(runners)
local targets = {} local targets = {}
@ -45,6 +52,7 @@ local function getForcedOutTargets(runners)
end end
--- Returns the position,distance of the base closest to the runner who is *furthest* from a base --- 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 ---@return Base | nil, number | nil
local function getBaseOfStrandedRunner(runners) local function getBaseOfStrandedRunner(runners)
local farRunnersBase, farDistance local farRunnersBase, farDistance
@ -62,6 +70,7 @@ local function getBaseOfStrandedRunner(runners)
end end
--- Returns x,y of the out target --- Returns x,y of the out target
---@param runners Runner[]
---@return number|nil, number|nil ---@return number|nil, number|nil
function npc.getNextOutTarget(runners) function npc.getNextOutTarget(runners)
-- TODO: Handle missed throws, check for fielders at target, etc. -- TODO: Handle missed throws, check for fielders at target, etc.
@ -77,6 +86,8 @@ function npc.getNextOutTarget(runners)
end end
---@param fielder Fielder ---@param fielder Fielder
---@param runners Runner[]
---@param throwBall ThrowBall
function npc.tryToMakeAPlay(fielder, runners, ball, throwBall) function npc.tryToMakeAPlay(fielder, runners, ball, throwBall)
local targetX, targetY = npc.getNextOutTarget(runners) local targetX, targetY = npc.getNextOutTarget(runners)
if targetX ~= nil and targetY ~= nil then if targetX ~= nil and targetY ~= nil then
@ -90,6 +101,25 @@ function npc.tryToMakeAPlay(fielder, runners, ball, throwBall)
end end
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 if not playdate then
return npc return npc
end end

View File

@ -119,19 +119,6 @@ function utils.distanceBetweenZ(x1, y1, z1, x2, y2, z2)
return sqrt((x * x) + (y * y) + (z * z)), x, y, z return sqrt((x * x) + (y * y) + (z * z)), x, y, z
end 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 --- Returns the base being touched by the player at (x,y), or nil, if no base is being touched
---@param x number ---@param x number
---@param y number ---@param y number