Extract baserunning.lua
field.lua -> fielding.lua npcFielderAction() -> npc.fielderAction() Generally, a pinch of additional or stricter typing
This commit is contained in:
parent
50ddd67730
commit
1a68521bd4
|
@ -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
|
|
@ -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 = {},
|
||||||
|
|
27
src/dbg.lua
27
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.
|
-- 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
|
||||||
|
|
|
@ -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()
|
243
src/main.lua
243
src/main.lua
|
@ -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)
|
||||||
|
|
30
src/npc.lua
30
src/npc.lua
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue