BatterUp/src/baserunning.lua

212 lines
6.3 KiB
Lua

--- @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(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(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()
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(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()
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
baserunning.batter = baserunning:newRunner()
---@param self table
---@param runnerIndex integer
function baserunning:runnerScored(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(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(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