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