Compare commits
No commits in common. "027bb31bffbfc3b35d528f511b4bca38a99c8e24" and "b9d25e18d810b9b9eae83e7c5041a93acd98ea46" have entirely different histories.
027bb31bff
...
b9d25e18d8
|
@ -23,7 +23,7 @@ function Announcer.new()
|
||||||
}, { __index = Announcer })
|
}, { __index = Announcer })
|
||||||
end
|
end
|
||||||
|
|
||||||
local DurationMs <const> = 2000
|
local DurationMs <const> = 3000
|
||||||
|
|
||||||
function Announcer:popIn()
|
function Announcer:popIn()
|
||||||
self.animatorY = AnnouncerAnimatorInY
|
self.animatorY = AnnouncerAnimatorInY
|
||||||
|
|
35
src/ball.lua
35
src/ball.lua
|
@ -1,5 +1,6 @@
|
||||||
-- selene: allow(unscoped_variables)
|
-- selene: allow(unscoped_variables)
|
||||||
---@class Ball
|
---@class Ball
|
||||||
|
---@field private animator pd_animator_lib
|
||||||
---@field x number
|
---@field x number
|
||||||
---@field y number
|
---@field y number
|
||||||
---@field z number
|
---@field z number
|
||||||
|
@ -9,15 +10,13 @@
|
||||||
---@field yAnimator SimpleAnimator
|
---@field yAnimator SimpleAnimator
|
||||||
---@field sizeAnimator SimpleAnimator
|
---@field sizeAnimator SimpleAnimator
|
||||||
---@field floatAnimator SimpleAnimator
|
---@field floatAnimator SimpleAnimator
|
||||||
---@field private animatorLib pd_animator_lib
|
|
||||||
---@field private flyTimeMs number
|
|
||||||
Ball = {}
|
Ball = {}
|
||||||
|
|
||||||
---@param animatorLib pd_animator_lib
|
---@param animator pd_animator_lib
|
||||||
---@return Ball
|
---@return Ball
|
||||||
function Ball.new(animatorLib)
|
function Ball.new(animator)
|
||||||
return setmetatable({
|
return setmetatable({
|
||||||
animatorLib = animatorLib,
|
animator = animator,
|
||||||
x = C.Center.x --[[@as number]],
|
x = C.Center.x --[[@as number]],
|
||||||
y = C.Center.y --[[@as number]],
|
y = C.Center.y --[[@as number]],
|
||||||
z = 0,
|
z = 0,
|
||||||
|
@ -30,26 +29,10 @@ function Ball.new(animatorLib)
|
||||||
-- TODO? Replace these with a ballAnimatorZ?
|
-- TODO? Replace these with a ballAnimatorZ?
|
||||||
-- ...that might lose some of the magic of both. Compromise available? idk
|
-- ...that might lose some of the magic of both. Compromise available? idk
|
||||||
sizeAnimator = utils.staticAnimator(C.SmallestBallRadius),
|
sizeAnimator = utils.staticAnimator(C.SmallestBallRadius),
|
||||||
floatAnimator = animatorLib.new(2000, -60, 0, utils.easingHill),
|
floatAnimator = animator.new(2000, -60, 0, utils.easingHill),
|
||||||
}, { __index = Ball })
|
}, { __index = Ball })
|
||||||
end
|
end
|
||||||
|
|
||||||
function Ball:updatePosition()
|
|
||||||
if self.heldBy then
|
|
||||||
self.x = self.heldBy.x
|
|
||||||
self.y = self.heldBy.y
|
|
||||||
self.z = C.GloveZ
|
|
||||||
self.size = C.SmallestBallRadius
|
|
||||||
else
|
|
||||||
self.x = self.xAnimator:currentValue()
|
|
||||||
local z = self.floatAnimator:currentValue()
|
|
||||||
-- TODO: This `+ z` is more graphics logic than physics logic
|
|
||||||
self.y = self.yAnimator:currentValue() + z
|
|
||||||
self.z = z
|
|
||||||
self.size = self.sizeAnimator:currentValue()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
--- Launches the ball from its current position to the given destination.
|
--- Launches the ball from its current position to the given destination.
|
||||||
---@param destX number
|
---@param destX number
|
||||||
---@param destY number
|
---@param destY number
|
||||||
|
@ -64,16 +47,14 @@ function Ball:launch(destX, destY, easingFunc, flyTimeMs, floaty, customBallScal
|
||||||
flyTimeMs = utils.distanceBetween(self.x, self.y, destX, destY) * C.DefaultLaunchPower
|
flyTimeMs = utils.distanceBetween(self.x, self.y, destX, destY) * C.DefaultLaunchPower
|
||||||
end
|
end
|
||||||
|
|
||||||
self.flyTimeMs = flyTimeMs
|
|
||||||
|
|
||||||
if customBallScaler then
|
if customBallScaler then
|
||||||
self.sizeAnimator = customBallScaler
|
self.sizeAnimator = customBallScaler
|
||||||
else
|
else
|
||||||
-- TODO? Scale based on distance?
|
-- TODO? Scale based on distance?
|
||||||
self.sizeAnimator = self.animatorLib.new(flyTimeMs, 9, C.SmallestBallRadius, utils.easingHill)
|
self.sizeAnimator = self.animator.new(flyTimeMs, 9, C.SmallestBallRadius, utils.easingHill)
|
||||||
end
|
end
|
||||||
self.yAnimator = self.animatorLib.new(flyTimeMs, self.y, destY, easingFunc)
|
self.yAnimator = self.animator.new(flyTimeMs, self.y, destY, easingFunc)
|
||||||
self.xAnimator = self.animatorLib.new(flyTimeMs, self.x, destX, easingFunc)
|
self.xAnimator = self.animator.new(flyTimeMs, self.x, destX, easingFunc)
|
||||||
if floaty then
|
if floaty then
|
||||||
self.floatAnimator:reset(flyTimeMs)
|
self.floatAnimator:reset(flyTimeMs)
|
||||||
end
|
end
|
||||||
|
|
|
@ -17,9 +17,7 @@
|
||||||
---@field onThirdOut fun()
|
---@field onThirdOut fun()
|
||||||
Baserunning = {}
|
Baserunning = {}
|
||||||
|
|
||||||
-- TODO: Implement slides. Would require making fielders' gloves "real objects" whose state is tracked.
|
---@param announcer any
|
||||||
|
|
||||||
---@param announcer Announcer
|
|
||||||
---@return Baserunning
|
---@return Baserunning
|
||||||
function Baserunning.new(announcer, onThirdOut)
|
function Baserunning.new(announcer, onThirdOut)
|
||||||
local o = setmetatable({
|
local o = setmetatable({
|
||||||
|
@ -34,7 +32,7 @@ function Baserunning.new(announcer, onThirdOut)
|
||||||
onThirdOut = onThirdOut,
|
onThirdOut = onThirdOut,
|
||||||
}, { __index = Baserunning })
|
}, { __index = Baserunning })
|
||||||
|
|
||||||
o:pushNewBatter()
|
o.batter = o:newRunner()
|
||||||
|
|
||||||
return o
|
return o
|
||||||
end
|
end
|
||||||
|
@ -79,7 +77,6 @@ end
|
||||||
function Baserunning:outEligibleRunners(fielder)
|
function Baserunning:outEligibleRunners(fielder)
|
||||||
local touchedBase = utils.isTouchingBase(fielder.x, fielder.y)
|
local touchedBase = utils.isTouchingBase(fielder.x, fielder.y)
|
||||||
local didOutRunner = false
|
local didOutRunner = false
|
||||||
|
|
||||||
for i, runner in pairs(self.runners) do
|
for i, runner in pairs(self.runners) do
|
||||||
local runnerOnBase = utils.isTouchingBase(runner.x, runner.y)
|
local runnerOnBase = utils.isTouchingBase(runner.x, runner.y)
|
||||||
if -- Force out
|
if -- Force out
|
||||||
|
@ -117,14 +114,6 @@ function Baserunning:updateForcedRunners()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function Baserunning:convertBatterToRunner()
|
|
||||||
self.batter.nextBase = C.Bases[C.First]
|
|
||||||
self.batter.prevBase = C.Bases[C.Home]
|
|
||||||
self:updateForcedRunners()
|
|
||||||
self.batter.forcedTo = C.Bases[C.First]
|
|
||||||
self.batter = nil -- Demote batter to a mere runner
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param deltaSeconds number
|
---@param deltaSeconds number
|
||||||
function Baserunning:walkAwayOutRunners(deltaSeconds)
|
function Baserunning:walkAwayOutRunners(deltaSeconds)
|
||||||
for i, runner in ipairs(self.outRunners) do
|
for i, runner in ipairs(self.outRunners) do
|
||||||
|
@ -138,7 +127,7 @@ function Baserunning:walkAwayOutRunners(deltaSeconds)
|
||||||
end
|
end
|
||||||
|
|
||||||
---@return Runner
|
---@return Runner
|
||||||
function Baserunning:pushNewBatter()
|
function Baserunning:newRunner()
|
||||||
local new = {
|
local new = {
|
||||||
-- imageSet = math.random() < C.WokeMeter and FemmeSet or MascSet, -- TODO? lol.
|
-- imageSet = math.random() < C.WokeMeter and FemmeSet or MascSet, -- TODO? lol.
|
||||||
x = C.RightHandedBattersBox.x - 60,
|
x = C.RightHandedBattersBox.x - 60,
|
||||||
|
@ -148,7 +137,6 @@ function Baserunning:pushNewBatter()
|
||||||
forcedTo = C.Bases[C.First],
|
forcedTo = C.Bases[C.First],
|
||||||
}
|
}
|
||||||
self.runners[#self.runners + 1] = new
|
self.runners[#self.runners + 1] = new
|
||||||
self.batter = new
|
|
||||||
return new
|
return new
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -191,10 +179,8 @@ function Baserunning:updateRunner(runner, runnerIndex, appliedSpeed, deltaSecond
|
||||||
|
|
||||||
-- TODO: Do a better job drifting runners toward their bases when appliedSpeed is low/zero
|
-- TODO: Do a better job drifting runners toward their bases when appliedSpeed is low/zero
|
||||||
if distance < 2 then
|
if distance < 2 then
|
||||||
if runner.prevBase ~= nearestBase then
|
runner.prevBase = runner.nextBase
|
||||||
runner.prevBase = runner.nextBase
|
runner.nextBase = C.NextBaseMap[runner.nextBase]
|
||||||
runner.nextBase = C.NextBaseMap[runner.nextBase]
|
|
||||||
end
|
|
||||||
runner.forcedTo = nil
|
runner.forcedTo = nil
|
||||||
return false, false
|
return false, false
|
||||||
end
|
end
|
||||||
|
@ -210,9 +196,6 @@ function Baserunning:updateRunner(runner, runnerIndex, appliedSpeed, deltaSecond
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- TODO: Make this less "sticky" for the user.
|
|
||||||
-- Currently it can be a little hard to run *past* a base.
|
|
||||||
|
|
||||||
local autoRun = (nearestBaseDistance > 40 or runner.forcedTo) and mult * autoRunSpeed
|
local autoRun = (nearestBaseDistance > 40 or runner.forcedTo) and mult * autoRunSpeed
|
||||||
or nearestBaseDistance < 5 and 0
|
or nearestBaseDistance < 5 and 0
|
||||||
or (nearestBase == runner.nextBase and autoRunSpeed or -1 * autoRunSpeed)
|
or (nearestBase == runner.nextBase and autoRunSpeed or -1 * autoRunSpeed)
|
||||||
|
@ -226,21 +209,16 @@ end
|
||||||
|
|
||||||
--- Update non-batter runners.
|
--- Update non-batter runners.
|
||||||
--- Returns true only if at least one of the given runners moved during this update
|
--- Returns true only if at least one of the given runners moved during this update
|
||||||
---@param appliedSpeed number | fun(runner: Runner): number
|
---@param appliedSpeed number
|
||||||
---@return boolean someRunnerMoved, number runnersScored
|
---@return boolean someRunnerMoved, number runnersScored
|
||||||
function Baserunning:updateNonBatterRunners(appliedSpeed, forcedOnly, deltaSeconds)
|
function Baserunning:updateNonBatterRunners(appliedSpeed, forcedOnly, deltaSeconds)
|
||||||
local someRunnerMoved = false
|
local someRunnerMoved = false
|
||||||
local runnersScored = 0
|
local runnersScored = 0
|
||||||
|
|
||||||
local speedIsFunction = type(appliedSpeed) == "function"
|
|
||||||
-- TODO: Filter for the runner closest to the currently-held direction button
|
-- TODO: Filter for the runner closest to the currently-held direction button
|
||||||
for runnerIndex, runner in ipairs(self.runners) do
|
for runnerIndex, runner in ipairs(self.runners) do
|
||||||
if runner ~= self.batter and (not forcedOnly or runner.forcedTo) then
|
if runner ~= self.batter and (not forcedOnly or runner.forcedTo) then
|
||||||
local speed = appliedSpeed
|
local thisRunnerMoved, thisRunnerScored = self:updateRunner(runner, runnerIndex, appliedSpeed, deltaSeconds)
|
||||||
if speedIsFunction then
|
|
||||||
speed = appliedSpeed(runner)
|
|
||||||
end
|
|
||||||
local thisRunnerMoved, thisRunnerScored = self:updateRunner(runner, runnerIndex, speed, deltaSeconds)
|
|
||||||
someRunnerMoved = someRunnerMoved or thisRunnerMoved
|
someRunnerMoved = someRunnerMoved or thisRunnerMoved
|
||||||
if thisRunnerScored then
|
if thisRunnerScored then
|
||||||
runnersScored = runnersScored + 1
|
runnersScored = runnersScored + 1
|
||||||
|
@ -254,7 +232,3 @@ function Baserunning:updateNonBatterRunners(appliedSpeed, forcedOnly, deltaSecon
|
||||||
|
|
||||||
return someRunnerMoved, runnersScored
|
return someRunnerMoved, runnersScored
|
||||||
end
|
end
|
||||||
|
|
||||||
if not playdate or playdate.TEST_MODE then
|
|
||||||
return Baserunning
|
|
||||||
end
|
|
||||||
|
|
|
@ -2,8 +2,8 @@
|
||||||
C = {}
|
C = {}
|
||||||
|
|
||||||
C.Screen = {
|
C.Screen = {
|
||||||
W = playdate and playdate.display.getWidth() or 400,
|
W = playdate.display.getWidth(),
|
||||||
H = playdate and playdate.display.getHeight() or 240,
|
H = playdate.display.getHeight(),
|
||||||
}
|
}
|
||||||
|
|
||||||
C.Center = utils.xy(C.Screen.W / 2, C.Screen.H / 2)
|
C.Center = utils.xy(C.Screen.W / 2, C.Screen.H / 2)
|
||||||
|
@ -86,7 +86,6 @@ C.BaseHitbox = 10
|
||||||
|
|
||||||
C.BattingPower = 25
|
C.BattingPower = 25
|
||||||
C.BatterHandPos = utils.xy(25, 15)
|
C.BatterHandPos = utils.xy(25, 15)
|
||||||
C.GloveZ = 0 -- 10
|
|
||||||
|
|
||||||
C.SmallestBallRadius = 6
|
C.SmallestBallRadius = 6
|
||||||
|
|
||||||
|
@ -131,7 +130,3 @@ C.UserThrowPower = 0.3
|
||||||
C.WalkedRunnerSpeed = 10
|
C.WalkedRunnerSpeed = 10
|
||||||
|
|
||||||
C.ResetFieldersAfterSeconds = 2.5
|
C.ResetFieldersAfterSeconds = 2.5
|
||||||
|
|
||||||
if not playdate then
|
|
||||||
return C
|
|
||||||
end
|
|
||||||
|
|
|
@ -21,9 +21,9 @@ 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(br)
|
function dbg.loadTheBases(br)
|
||||||
br:pushNewBatter()
|
br:newRunner()
|
||||||
br:pushNewBatter()
|
br:newRunner()
|
||||||
br:pushNewBatter()
|
br:newRunner()
|
||||||
|
|
||||||
br.runners[2].x = C.Bases[C.First].x
|
br.runners[2].x = C.Bases[C.First].x
|
||||||
br.runners[2].y = C.Bases[C.First].y
|
br.runners[2].y = C.Bases[C.First].y
|
||||||
|
|
|
@ -1,16 +1,14 @@
|
||||||
--- @class Fielder {
|
--- @alias Fielder {
|
||||||
--- @field catchEligible boolean
|
--- x: number,
|
||||||
--- @field x number
|
--- y: number,
|
||||||
--- @field y number
|
--- target: XyPair | nil,
|
||||||
--- @field target XyPair | nil
|
--- speed: number,
|
||||||
--- @field speed number
|
--- }
|
||||||
|
|
||||||
-- TODO: Run down baserunners in a pickle.
|
|
||||||
|
|
||||||
-- selene: allow(unscoped_variables)
|
-- selene: allow(unscoped_variables)
|
||||||
---@class Fielding
|
---@class Fielding
|
||||||
---@field fielders table<string, Fielder>
|
---@field fielders table<string, Fielder>
|
||||||
---@field fielderHoldingBall Fielder | nil
|
---@field fielderTouchingBall Fielder | nil
|
||||||
Fielding = {}
|
Fielding = {}
|
||||||
|
|
||||||
local FielderDanceAnimator <const> = playdate.graphics.animator.new(1, 10, 0, utils.easingHill)
|
local FielderDanceAnimator <const> = playdate.graphics.animator.new(1, 10, 0, utils.easingHill)
|
||||||
|
@ -23,7 +21,6 @@ local function newFielder(name, speed)
|
||||||
return {
|
return {
|
||||||
name = name,
|
name = name,
|
||||||
speed = speed * C.FielderRunMult,
|
speed = speed * C.FielderRunMult,
|
||||||
catchEligible = true,
|
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -41,7 +38,7 @@ function Fielding.new()
|
||||||
right = newFielder("Right", 50),
|
right = newFielder("Right", 50),
|
||||||
},
|
},
|
||||||
---@type Fielder | nil
|
---@type Fielder | nil
|
||||||
fielderHoldingBall = nil,
|
fielderTouchingBall = nil,
|
||||||
}, { __index = Fielding })
|
}, { __index = Fielding })
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -80,7 +77,7 @@ end
|
||||||
---@param deltaSeconds number
|
---@param deltaSeconds number
|
||||||
---@param fielder Fielder
|
---@param fielder Fielder
|
||||||
---@param ballPos XyPair
|
---@param ballPos XyPair
|
||||||
---@return boolean inCatchingRange
|
---@return boolean isTouchingBall
|
||||||
local function updateFielderPosition(deltaSeconds, fielder, ballPos)
|
local function updateFielderPosition(deltaSeconds, fielder, ballPos)
|
||||||
if fielder.target ~= nil then
|
if fielder.target ~= nil then
|
||||||
if not utils.moveAtSpeed(fielder, fielder.speed * deltaSeconds, fielder.target) then
|
if not utils.moveAtSpeed(fielder, fielder.speed * deltaSeconds, fielder.target) then
|
||||||
|
@ -109,62 +106,37 @@ function Fielding:haveSomeoneChase(ballDestX, ballDestY)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param ball Ball
|
---@param ball XyPair
|
||||||
---@param deltaSeconds number
|
---@param deltaSeconds number
|
||||||
---@return Fielder | nil fielderHoldingBall nil if no fielder is currently touching the ball
|
---@return Fielder | nil fielderTouchingBall nil if no fielder is currently touching the ball
|
||||||
function Fielding:updateFielderPositions(ball, deltaSeconds)
|
function Fielding:updateFielderPositions(ball, deltaSeconds)
|
||||||
local fielderHoldingBall = nil
|
local fielderTouchingBall = nil
|
||||||
for _, fielder in pairs(self.fielders) do
|
for _, fielder in pairs(self.fielders) do
|
||||||
local inCatchingRange = updateFielderPosition(deltaSeconds, fielder, ball)
|
local isTouchingBall = updateFielderPosition(deltaSeconds, fielder, ball)
|
||||||
if inCatchingRange and fielder.catchEligible then
|
if isTouchingBall then
|
||||||
-- TODO: Base this catch on fielder skill?
|
fielderTouchingBall = fielder
|
||||||
fielderHoldingBall = fielder
|
|
||||||
ball.heldBy = fielder -- How much havoc will this wreak?
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
-- TODO: The need is growing for a distinction between touching the ball and holding the ball.
|
self.fielderTouchingBall = fielderTouchingBall
|
||||||
-- Or, at least, fielders need to start *stopping* the ball when they make contact with it.
|
return fielderTouchingBall
|
||||||
-- Right now, a line-drive *through* first will be counted as an out.
|
|
||||||
self.fielderHoldingBall = fielderHoldingBall
|
|
||||||
return fielderHoldingBall
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param fielder Fielder
|
|
||||||
function Fielding.markIneligible(fielder)
|
|
||||||
fielder.catchEligible = false
|
|
||||||
playdate.timer.new(500, function()
|
|
||||||
fielder.catchEligible = true
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
function Fielding:markAllIneligible()
|
|
||||||
for _, fielder in pairs(self.fielders) do
|
|
||||||
fielder.catchEligible = false
|
|
||||||
end
|
|
||||||
playdate.timer.new(750, function()
|
|
||||||
for _, fielder in pairs(self.fielders) do
|
|
||||||
fielder.catchEligible = true
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- TODO? Start moving target fielders close sooner?
|
-- TODO? Start moving target fielders close sooner?
|
||||||
---@param field Fielding
|
---@param field table
|
||||||
---@param targetBase Base
|
---@param targetBase Base
|
||||||
---@param launchBall LaunchBall
|
---@param launchBall LaunchBall
|
||||||
---@param throwFlyMs number
|
---@param throwFlyMs number
|
||||||
---@return ActionResult
|
---@return ActionResult
|
||||||
local function userThrowToImpl(field, targetBase, launchBall, throwFlyMs)
|
local function userThrowToImpl(field, targetBase, launchBall, throwFlyMs)
|
||||||
if field.fielderHoldingBall == nil then
|
if field.fielderTouchingBall == nil then
|
||||||
return ActionResult.NeedsMoreTime
|
return ActionResult.NeedsMoreTime
|
||||||
end
|
end
|
||||||
local closestFielder = utils.getNearestOf(field.fielders, targetBase.x, targetBase.y, function(fielder)
|
local closestFielder = utils.getNearestOf(field.fielders, targetBase.x, targetBase.y, function(fielder)
|
||||||
return fielder ~= field.fielderHoldingBall -- presumably, this is who will be doing the throwing
|
return fielder ~= field.fielderTouchingBall -- presumably, this is who will be doing the throwing
|
||||||
end)
|
end)
|
||||||
|
|
||||||
closestFielder.target = targetBase
|
closestFielder.target = targetBase
|
||||||
launchBall(targetBase.x, targetBase.y, playdate.easingFunctions.linear, throwFlyMs)
|
launchBall(targetBase.x, targetBase.y, playdate.easingFunctions.linear, throwFlyMs)
|
||||||
Fielding.markIneligible(field.fielderHoldingBall)
|
|
||||||
return ActionResult.Succeeded
|
return ActionResult.Succeeded
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -195,7 +167,3 @@ function Fielding:drawFielders(fielderSprites, ball)
|
||||||
end
|
end
|
||||||
return ballIsHeld
|
return ballIsHeld
|
||||||
end
|
end
|
||||||
|
|
||||||
if not playdate or playdate.TEST_MODE then
|
|
||||||
return { Fielding, newFielder }
|
|
||||||
end
|
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 31 KiB |
125
src/main.lua
125
src/main.lua
|
@ -42,23 +42,15 @@ local gfx <const>, C <const> = playdate.graphics, C
|
||||||
|
|
||||||
local announcer = Announcer.new()
|
local announcer = Announcer.new()
|
||||||
local fielding = Fielding.new()
|
local fielding = Fielding.new()
|
||||||
-- TODO: Find a way to get baserunning and npc instantiated closer to the top, here.
|
|
||||||
-- Currently difficult because they depend on nextHalfInning/each other.
|
|
||||||
|
|
||||||
------------------
|
---@alias SimpleAnimator { currentValue: fun(self): number; reset: fun(self, durationMs: number | nil) }
|
||||||
-- GLOBAL STATE --
|
|
||||||
------------------
|
|
||||||
|
|
||||||
local deltaSeconds = 0
|
local deltaSeconds = 0
|
||||||
|
|
||||||
local ball = Ball.new(gfx.animator)
|
local ball = Ball.new(gfx.animator)
|
||||||
|
|
||||||
local batBase <const> = utils.xy(C.Center.x - 34, 215)
|
|
||||||
local batTip <const> = utils.xy(0, 0)
|
|
||||||
local batAngleDeg = C.CrankOffsetDeg
|
|
||||||
|
|
||||||
local catcherThrownBall = false
|
|
||||||
|
|
||||||
---@alias Team { score: number, benchPosition: XyPair }
|
---@alias Team { score: number, benchPosition: XyPair }
|
||||||
|
|
||||||
---@type table<string, Team>
|
---@type table<string, Team>
|
||||||
local teams <const> = {
|
local teams <const> = {
|
||||||
home = {
|
home = {
|
||||||
|
@ -71,28 +63,25 @@ local teams <const> = {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
local inning = 1
|
local UserTeam <const> = teams.away
|
||||||
|
|
||||||
local battingTeam = teams.away
|
local battingTeam = teams.away
|
||||||
|
local battingTeamSprites = AwayTeamSprites
|
||||||
|
local fieldingTeamSprites = HomeTeamSprites
|
||||||
|
local runnerBlipper = battingTeam == teams.away and AwayTeamBlipper or HomeTeamBlipper
|
||||||
|
local inning = 1
|
||||||
local offenseState = C.Offense.batting
|
local offenseState = C.Offense.batting
|
||||||
|
|
||||||
-- TODO: Replace with timers, repeatedly reset, instead of constantly setting to 0
|
-- TODO: Replace with timers, repeatedly reset, instead of constantly setting to 0
|
||||||
local secondsSinceLastRunnerMove = 0
|
local secondsSinceLastRunnerMove = 0
|
||||||
local secondsSincePitchAllowed = 0
|
local secondsSincePitchAllowed = -5
|
||||||
|
|
||||||
-- These are only sort-of global state. They are purely graphical,
|
local catcherThrownBall = false
|
||||||
-- but they need to be kept in sync with the rest of the globals.
|
|
||||||
local runnerBlipper = battingTeam == teams.away and AwayTeamBlipper or HomeTeamBlipper
|
|
||||||
local battingTeamSprites = AwayTeamSprites
|
|
||||||
local fieldingTeamSprites = HomeTeamSprites
|
|
||||||
|
|
||||||
-------------------------
|
local batBase <const> = utils.xy(C.Center.x - 34, 215)
|
||||||
-- END OF GLOBAL STATE --
|
local batTip <const> = utils.xy(0, 0)
|
||||||
-------------------------
|
|
||||||
|
|
||||||
local UserTeam <const> = teams.away
|
local batAngleDeg = C.CrankOffsetDeg
|
||||||
|
|
||||||
---@alias SimpleAnimator { currentValue: fun(self): number; reset: fun(self, durationMs: number | nil) }
|
|
||||||
---@alias Pitch { x: SimpleAnimator, y: SimpleAnimator, z: SimpleAnimator | nil }
|
---@alias Pitch { x: SimpleAnimator, y: SimpleAnimator, z: SimpleAnimator | nil }
|
||||||
|
|
||||||
---@type Pitch[]
|
---@type Pitch[]
|
||||||
|
@ -124,12 +113,8 @@ local Pitches <const> = {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
---@return boolean userIsOnSide, boolean userIsOnOtherSide
|
---@return boolean userIsOnSide, boolean playerIsOnOtherSide
|
||||||
local function userIsOn(side)
|
local function userIsOn(side)
|
||||||
if UserTeam == nil then
|
|
||||||
-- Both teams are NPC-driven
|
|
||||||
return false, false
|
|
||||||
end
|
|
||||||
local ret
|
local ret
|
||||||
if UserTeam == battingTeam then
|
if UserTeam == battingTeam then
|
||||||
ret = side == C.Sides.offense
|
ret = side == C.Sides.offense
|
||||||
|
@ -139,7 +124,13 @@ local function userIsOn(side)
|
||||||
return ret, not ret
|
return ret, not ret
|
||||||
end
|
end
|
||||||
|
|
||||||
---@type LaunchBall
|
--- Launches the ball from its current position to the given destination.
|
||||||
|
---@param destX number
|
||||||
|
---@param destY number
|
||||||
|
---@param easingFunc EasingFunc
|
||||||
|
---@param flyTimeMs number | nil
|
||||||
|
---@param floaty boolean | nil
|
||||||
|
---@param customBallScaler pd_animator | nil
|
||||||
local function launchBall(destX, destY, easingFunc, flyTimeMs, floaty, customBallScaler)
|
local function launchBall(destX, destY, easingFunc, flyTimeMs, floaty, customBallScaler)
|
||||||
throwMeter:reset()
|
throwMeter:reset()
|
||||||
ball:launch(destX, destY, easingFunc, flyTimeMs, floaty, customBallScaler)
|
ball:launch(destX, destY, easingFunc, flyTimeMs, floaty, customBallScaler)
|
||||||
|
@ -148,8 +139,6 @@ end
|
||||||
---@param pitchFlyTimeMs number | nil
|
---@param pitchFlyTimeMs number | nil
|
||||||
---@param pitchTypeIndex number | nil
|
---@param pitchTypeIndex number | nil
|
||||||
local function pitch(pitchFlyTimeMs, pitchTypeIndex)
|
local function pitch(pitchFlyTimeMs, pitchTypeIndex)
|
||||||
Fielding.markIneligible(fielding.fielders.pitcher)
|
|
||||||
ball.heldBy = nil
|
|
||||||
catcherThrownBall = false
|
catcherThrownBall = false
|
||||||
offenseState = C.Offense.batting
|
offenseState = C.Offense.batting
|
||||||
|
|
||||||
|
@ -185,26 +174,28 @@ local function nextHalfInning()
|
||||||
announcer:say("SWITCHING SIDES...")
|
announcer:say("SWITCHING SIDES...")
|
||||||
end
|
end
|
||||||
|
|
||||||
if gameOver then
|
-- TODO: Make the overlay handle its own dang delay.
|
||||||
announcer:say("AND THAT'S THE BALL GAME!")
|
-- Delay to keep end-of-inning on the scoreboard for a few seconds
|
||||||
else
|
playdate.timer.new(3000, function()
|
||||||
fielding:resetFielderPositions()
|
|
||||||
if battingTeam == teams.home then
|
|
||||||
inning = inning + 1
|
|
||||||
end
|
|
||||||
battingTeam = currentlyFieldingTeam
|
battingTeam = currentlyFieldingTeam
|
||||||
playdate.timer.new(2000, function()
|
if battingTeam == teams.home then
|
||||||
|
battingTeamSprites = HomeTeamSprites
|
||||||
|
runnerBlipper = HomeTeamBlipper
|
||||||
|
fieldingTeamSprites = AwayTeamSprites
|
||||||
|
else
|
||||||
|
battingTeamSprites = AwayTeamSprites
|
||||||
|
fieldingTeamSprites = HomeTeamSprites
|
||||||
|
runnerBlipper = AwayTeamBlipper
|
||||||
|
end
|
||||||
|
if gameOver then
|
||||||
|
announcer:say("AND THAT'S THE BALL GAME!")
|
||||||
|
else
|
||||||
|
fielding:resetFielderPositions()
|
||||||
if battingTeam == teams.home then
|
if battingTeam == teams.home then
|
||||||
battingTeamSprites = HomeTeamSprites
|
inning = inning + 1
|
||||||
runnerBlipper = HomeTeamBlipper
|
|
||||||
fieldingTeamSprites = AwayTeamSprites
|
|
||||||
else
|
|
||||||
battingTeamSprites = AwayTeamSprites
|
|
||||||
fieldingTeamSprites = HomeTeamSprites
|
|
||||||
runnerBlipper = AwayTeamBlipper
|
|
||||||
end
|
end
|
||||||
end)
|
end
|
||||||
end
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
local baserunning = Baserunning.new(announcer, nextHalfInning)
|
local baserunning = Baserunning.new(announcer, nextHalfInning)
|
||||||
|
@ -248,14 +239,13 @@ local function nextBatter()
|
||||||
playdate.timer.new(2000, function()
|
playdate.timer.new(2000, function()
|
||||||
pitchTracker:reset()
|
pitchTracker:reset()
|
||||||
if not baserunning.batter then
|
if not baserunning.batter then
|
||||||
baserunning:pushNewBatter()
|
baserunning.batter = baserunning:newRunner()
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
local function walk()
|
local function walk()
|
||||||
announcer:say("Walk!")
|
announcer:say("Walk!")
|
||||||
-- TODO? Use baserunning:convertBatterToRunner()
|
|
||||||
baserunning.batter.nextBase = C.Bases[C.First]
|
baserunning.batter.nextBase = C.Bases[C.First]
|
||||||
baserunning.batter.prevBase = C.Bases[C.Home]
|
baserunning.batter.prevBase = C.Bases[C.Home]
|
||||||
offenseState = C.Offense.walking
|
offenseState = C.Offense.walking
|
||||||
|
@ -311,13 +301,17 @@ local function updateBatting(batDeg, batSpeed)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
baserunning:convertBatterToRunner()
|
baserunning.batter.nextBase = C.Bases[C.First]
|
||||||
|
baserunning.batter.prevBase = C.Bases[C.Home]
|
||||||
|
baserunning:updateForcedRunners()
|
||||||
|
baserunning.batter.forcedTo = C.Bases[C.First]
|
||||||
|
baserunning.batter = nil -- Demote batter to a mere runner
|
||||||
|
|
||||||
fielding:haveSomeoneChase(ballDestX, ballDestY)
|
fielding:haveSomeoneChase(ballDestX, ballDestY)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param appliedSpeed number | fun(runner: Runner): number
|
---@param appliedSpeed number
|
||||||
---@return boolean someRunnerMoved
|
---@return boolean someRunnerMoved
|
||||||
local function updateNonBatterRunners(appliedSpeed, forcedOnly)
|
local function updateNonBatterRunners(appliedSpeed, forcedOnly)
|
||||||
local runnerMoved, runnersScored = baserunning:updateNonBatterRunners(appliedSpeed, forcedOnly, deltaSeconds)
|
local runnerMoved, runnersScored = baserunning:updateNonBatterRunners(appliedSpeed, forcedOnly, deltaSeconds)
|
||||||
|
@ -350,7 +344,16 @@ local function updateGameState()
|
||||||
crankLimited = crankLimited * -1
|
crankLimited = crankLimited * -1
|
||||||
end
|
end
|
||||||
|
|
||||||
ball:updatePosition()
|
if ball.heldBy then
|
||||||
|
ball.x = ball.heldBy.x
|
||||||
|
ball.y = ball.heldBy.y
|
||||||
|
ball.size = C.SmallestBallRadius
|
||||||
|
else
|
||||||
|
ball.x = ball.xAnimator:currentValue()
|
||||||
|
ball.z = ball.floatAnimator:currentValue()
|
||||||
|
ball.y = ball.yAnimator:currentValue() + ball.z
|
||||||
|
ball.size = ball.sizeAnimator:currentValue()
|
||||||
|
end
|
||||||
|
|
||||||
local userOnOffense, userOnDefense = userIsOn(C.Sides.offense)
|
local userOnOffense, userOnDefense = userIsOn(C.Sides.offense)
|
||||||
|
|
||||||
|
@ -408,10 +411,7 @@ local function updateGameState()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
elseif offenseState == C.Offense.running then
|
elseif offenseState == C.Offense.running then
|
||||||
local appliedSpeed = userOnOffense and crankLimited
|
local appliedSpeed = userOnOffense and crankLimited or npc:runningSpeed(ball)
|
||||||
or function(runner)
|
|
||||||
return npc:runningSpeed(runner, ball)
|
|
||||||
end
|
|
||||||
if updateNonBatterRunners(appliedSpeed) then
|
if updateNonBatterRunners(appliedSpeed) then
|
||||||
secondsSinceLastRunnerMove = 0
|
secondsSinceLastRunnerMove = 0
|
||||||
else
|
else
|
||||||
|
@ -419,12 +419,11 @@ local function updateGameState()
|
||||||
if secondsSinceLastRunnerMove > C.ResetFieldersAfterSeconds then
|
if secondsSinceLastRunnerMove > C.ResetFieldersAfterSeconds then
|
||||||
-- End of play. Throw the ball back to the pitcher
|
-- End of play. Throw the ball back to the pitcher
|
||||||
launchBall(C.PitchStartX, C.PitchStartY, playdate.easingFunctions.linear, nil, true)
|
launchBall(C.PitchStartX, C.PitchStartY, playdate.easingFunctions.linear, nil, true)
|
||||||
fielding:markAllIneligible() -- This is ugly, and ideally would not be necessary if Fielding handled the return throw directly.
|
|
||||||
fielding:resetFielderPositions()
|
fielding:resetFielderPositions()
|
||||||
offenseState = C.Offense.batting
|
offenseState = C.Offense.batting
|
||||||
-- TODO: Remove, or replace with nextBatter()
|
-- TODO: Remove, or replace with nextBatter()
|
||||||
if not baserunning.batter then
|
if not baserunning.batter then
|
||||||
baserunning:pushNewBatter()
|
baserunning.batter = baserunning:newRunner()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -444,8 +443,8 @@ local function updateGameState()
|
||||||
end
|
end
|
||||||
if fielderHoldingBall then
|
if fielderHoldingBall then
|
||||||
local outedSomeRunner = baserunning:outEligibleRunners(fielderHoldingBall)
|
local outedSomeRunner = baserunning:outEligibleRunners(fielderHoldingBall)
|
||||||
if not userOnDefense and offenseState == C.Offense.running then
|
if userOnOffense then
|
||||||
npc:fielderAction(fielderHoldingBall, outedSomeRunner, ball, launchBall)
|
npc:fielderAction(offenseState, fielderHoldingBall, outedSomeRunner, ball, launchBall)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -453,8 +452,6 @@ local function updateGameState()
|
||||||
actionQueue:runWaiting(deltaSeconds)
|
actionQueue:runWaiting(deltaSeconds)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- TODO: Swappable update() for main menu, etc.
|
|
||||||
|
|
||||||
function playdate.update()
|
function playdate.update()
|
||||||
playdate.timer.updateTimers()
|
playdate.timer.updateTimers()
|
||||||
gfx.animation.blinker.updateAll()
|
gfx.animation.blinker.updateAll()
|
||||||
|
|
30
src/npc.lua
30
src/npc.lua
|
@ -39,25 +39,26 @@ end
|
||||||
local baseRunningSpeed = 25
|
local baseRunningSpeed = 25
|
||||||
|
|
||||||
--- TODO: Individual runner control.
|
--- TODO: Individual runner control.
|
||||||
---@param runner Runner
|
|
||||||
---@param ball Point3d
|
---@param ball Point3d
|
||||||
---@return number
|
---@return number
|
||||||
function Npc:runningSpeed(runner, ball)
|
function Npc:runningSpeed(ball)
|
||||||
if #self.runners == 0 then
|
if #self.runners == 0 then
|
||||||
return 0
|
return 0
|
||||||
end
|
end
|
||||||
|
|
||||||
local distanceFromBall = utils.distanceBetweenZ(ball.x, ball.y, ball.z, runner.x, runner.y, 0)
|
local runner1 = self.runners[1]
|
||||||
|
|
||||||
if distanceFromBall > 400 or runner.forcedTo then
|
local ballIsFar = utils.distanceBetweenZ(ball.x, ball.y, ball.z, runner1.x, runner1.y, 0) > 250
|
||||||
|
|
||||||
|
if ballIsFar or runner1.forcedTo then
|
||||||
return baseRunningSpeed
|
return baseRunningSpeed
|
||||||
end
|
end
|
||||||
|
|
||||||
local touchedBase = utils.isTouchingBase(runner.x, runner.y)
|
local touchedBase = utils.isTouchingBase(runner1.x, runner1.y)
|
||||||
if not touchedBase and runner.nextBase then
|
if not touchedBase and runner1.nextBase then
|
||||||
local distToNext = utils.distanceBetween(runner.x, runner.y, runner.nextBase.x, runner.nextBase.y)
|
local distToNext = utils.distanceBetween(runner1.x, runner1.y, runner1.nextBase.x, runner1.nextBase.y)
|
||||||
local distToPrev = utils.distanceBetween(runner.x, runner.y, runner.prevBase.x, runner.prevBase.y)
|
local distToPrev = utils.distanceBetween(runner1.x, runner1.y, runner1.prevBase.x, runner1.prevBase.y)
|
||||||
if distToNext < distToPrev or distanceFromBall > 350 then
|
if distToNext < distToPrev then
|
||||||
return baseRunningSpeed
|
return baseRunningSpeed
|
||||||
else
|
else
|
||||||
return -1 * baseRunningSpeed
|
return -1 * baseRunningSpeed
|
||||||
|
@ -123,24 +124,25 @@ end
|
||||||
local function tryToMakeAPlay(fielders, fielder, runners, ball, launchBall)
|
local function tryToMakeAPlay(fielders, fielder, runners, ball, launchBall)
|
||||||
local targetX, targetY = getNextOutTarget(runners)
|
local targetX, targetY = getNextOutTarget(runners)
|
||||||
if targetX ~= nil and targetY ~= nil then
|
if targetX ~= nil and targetY ~= nil then
|
||||||
local nearestFielder = utils.getNearestOf(fielders, targetX, targetY, function(grabCandidate)
|
local nearestFielder = utils.getNearestOf(fielders, targetX, targetY)
|
||||||
return grabCandidate.catchEligible
|
|
||||||
end)
|
|
||||||
nearestFielder.target = utils.xy(targetX, targetY)
|
nearestFielder.target = utils.xy(targetX, targetY)
|
||||||
if nearestFielder == fielder then
|
if nearestFielder == fielder then
|
||||||
ball.heldBy = fielder
|
ball.heldBy = fielder
|
||||||
else
|
else
|
||||||
launchBall(targetX, targetY, playdate.easingFunctions.linear, nil, true)
|
launchBall(targetX, targetY, playdate.easingFunctions.linear, nil, true)
|
||||||
Fielding.markIneligible(nearestFielder)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
---@param offenseState OffenseState
|
||||||
---@param fielder Fielder
|
---@param fielder Fielder
|
||||||
---@param outedSomeRunner boolean
|
---@param outedSomeRunner boolean
|
||||||
---@param ball { x: number, y: number, heldBy: Fielder | nil }
|
---@param ball { x: number, y: number, heldBy: Fielder | nil }
|
||||||
---@param launchBall LaunchBall
|
---@param launchBall LaunchBall
|
||||||
function Npc:fielderAction(fielder, outedSomeRunner, ball, launchBall)
|
function Npc:fielderAction(offenseState, fielder, outedSomeRunner, ball, launchBall)
|
||||||
|
if offenseState ~= C.Offense.running then
|
||||||
|
return
|
||||||
|
end
|
||||||
if outedSomeRunner then
|
if outedSomeRunner then
|
||||||
-- Delay a little before the next play
|
-- Delay a little before the next play
|
||||||
playdate.timer.new(750, function()
|
playdate.timer.new(750, function()
|
||||||
|
|
|
@ -1,21 +0,0 @@
|
||||||
utils = require("utils")
|
|
||||||
|
|
||||||
local mockPlaydate = {
|
|
||||||
TEST_MODE = true,
|
|
||||||
graphics = {
|
|
||||||
animator = {
|
|
||||||
new = function()
|
|
||||||
return utils.staticAnimator(0)
|
|
||||||
end,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
---@type Announcer
|
|
||||||
local mockAnnouncer = {
|
|
||||||
say = function(self, message)
|
|
||||||
self.lastMessage = message
|
|
||||||
end,
|
|
||||||
}
|
|
||||||
|
|
||||||
return { mockPlaydate, mockAnnouncer }
|
|
|
@ -1,170 +0,0 @@
|
||||||
import = function() end
|
|
||||||
luaunit = require("luaunit")
|
|
||||||
luaunit.ORDER_ACTUAL_EXPECTED = false
|
|
||||||
|
|
||||||
utils = require("utils")
|
|
||||||
C = require("constants")
|
|
||||||
mocks = require("test/mocks")
|
|
||||||
playdate, announcer = mocks[1], mocks[2]
|
|
||||||
|
|
||||||
Baserunning = require("baserunning")
|
|
||||||
|
|
||||||
local _f = require("fielding")
|
|
||||||
Fielding, newFielder = _f[1], _f[2]
|
|
||||||
|
|
||||||
---@return Baserunning, { called: boolean }
|
|
||||||
function buildBaserunning()
|
|
||||||
local thirdOutCallbackData = { called = false }
|
|
||||||
local baserunning = Baserunning.new(announcer, function()
|
|
||||||
thirdOutCallbackData.called = true
|
|
||||||
end)
|
|
||||||
return baserunning, thirdOutCallbackData
|
|
||||||
end
|
|
||||||
|
|
||||||
---@alias BaseIndexOrXyPair (integer | XyPair)
|
|
||||||
|
|
||||||
--- NOTE: in addition to the given runners, there is implicitly a batter running from first.
|
|
||||||
---@param runnerLocations BaseIndexOrXyPair[]
|
|
||||||
---@return Baserunning
|
|
||||||
function buildRunnersOn(runnerLocations)
|
|
||||||
local baserunning = buildBaserunning()
|
|
||||||
baserunning:convertBatterToRunner()
|
|
||||||
for _, location in ipairs(runnerLocations) do
|
|
||||||
baserunning:pushNewBatter()
|
|
||||||
local runner = baserunning.batter
|
|
||||||
baserunning:convertBatterToRunner()
|
|
||||||
if type(location) == "number" then
|
|
||||||
-- Is a base index
|
|
||||||
-- Push the runner *through* each base.
|
|
||||||
for b = 1, location do
|
|
||||||
runner.x = C.Bases[b].x
|
|
||||||
runner.y = C.Bases[b].y
|
|
||||||
baserunning:updateNonBatterRunners(0.001, false, 0.001)
|
|
||||||
end
|
|
||||||
else
|
|
||||||
-- Is a raw XyPair
|
|
||||||
runner.x = location.x
|
|
||||||
runner.y = location.y
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return baserunning
|
|
||||||
end
|
|
||||||
|
|
||||||
---@alias Condition { fielderWithBallAt: XyPair, outWhen: BaseIndexOrXyPair[][], safeWhen: BaseIndexOrXyPair[][] }
|
|
||||||
|
|
||||||
---@param expected boolean
|
|
||||||
---@param fielderWithBallAt XyPair
|
|
||||||
---@param when integer[][]
|
|
||||||
function assertRunnerOutCondition(expected, when, fielderWithBallAt)
|
|
||||||
local msg = expected and "out" or "safe"
|
|
||||||
for _, runnersOn in ipairs(when) do
|
|
||||||
local baserunning = buildRunnersOn(runnersOn)
|
|
||||||
local outedSomeRunner = baserunning:outEligibleRunners(fielderWithBallAt)
|
|
||||||
luaunit.failIf(outedSomeRunner ~= expected, "Runner should have been " .. msg .. ", but was not!")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param condition Condition
|
|
||||||
function assertRunnerStatuses(condition)
|
|
||||||
assertRunnerOutCondition(true, condition.outWhen, condition.fielderWithBallAt)
|
|
||||||
assertRunnerOutCondition(false, condition.safeWhen, condition.fielderWithBallAt)
|
|
||||||
end
|
|
||||||
|
|
||||||
function testForceOutsAtFirst()
|
|
||||||
assertRunnerStatuses({
|
|
||||||
fielderWithBallAt = C.Bases[C.First],
|
|
||||||
outWhen = {
|
|
||||||
{},
|
|
||||||
{ 1 },
|
|
||||||
{ 2 },
|
|
||||||
{ 3 },
|
|
||||||
{ 1, 2 },
|
|
||||||
{ 1, 3 },
|
|
||||||
{ 2, 3 },
|
|
||||||
{ 1, 2, 3 },
|
|
||||||
},
|
|
||||||
safeWhen = {},
|
|
||||||
})
|
|
||||||
end
|
|
||||||
|
|
||||||
function testForceOutsAtSecond()
|
|
||||||
assertRunnerStatuses({
|
|
||||||
fielderWithBallAt = C.Bases[C.Second],
|
|
||||||
outWhen = {
|
|
||||||
{ 1 },
|
|
||||||
{ 1, 2 },
|
|
||||||
{ 1, 3 },
|
|
||||||
{ 1, 2, 3 },
|
|
||||||
},
|
|
||||||
safeWhen = {
|
|
||||||
{},
|
|
||||||
{ 2 },
|
|
||||||
{ 3 },
|
|
||||||
{ 2, 3 },
|
|
||||||
},
|
|
||||||
})
|
|
||||||
end
|
|
||||||
|
|
||||||
function testForceOutsAtThird()
|
|
||||||
assertRunnerStatuses({
|
|
||||||
fielderWithBallAt = C.Bases[C.Third],
|
|
||||||
outWhen = {
|
|
||||||
{ 1, 2 },
|
|
||||||
{ 1, 2, 3 },
|
|
||||||
},
|
|
||||||
safeWhen = {
|
|
||||||
{ 1 },
|
|
||||||
{ 2 },
|
|
||||||
{ 3 },
|
|
||||||
{ 2, 3 },
|
|
||||||
{ 1, 3 },
|
|
||||||
},
|
|
||||||
})
|
|
||||||
end
|
|
||||||
|
|
||||||
function testForceOutsAtHome()
|
|
||||||
assertRunnerStatuses({
|
|
||||||
fielderWithBallAt = C.Bases[C.Home],
|
|
||||||
outWhen = {
|
|
||||||
{ 1, 2, 3 },
|
|
||||||
},
|
|
||||||
safeWhen = {
|
|
||||||
{},
|
|
||||||
{ 1 },
|
|
||||||
{ 2 },
|
|
||||||
{ 3 },
|
|
||||||
{ 1, 2 },
|
|
||||||
{ 1, 3 },
|
|
||||||
{ 2, 3 },
|
|
||||||
},
|
|
||||||
})
|
|
||||||
end
|
|
||||||
|
|
||||||
function testTagOutsShouldHappenOffBase()
|
|
||||||
local fielderWithBallAt = utils.xy(10, 10) -- Some location not on a base.
|
|
||||||
local farFromFielder = utils.xy(100, 100)
|
|
||||||
assertRunnerStatuses({
|
|
||||||
fielderWithBallAt = fielderWithBallAt,
|
|
||||||
outWhen = {
|
|
||||||
{ fielderWithBallAt },
|
|
||||||
},
|
|
||||||
safeWhen = {
|
|
||||||
{ farFromFielder },
|
|
||||||
},
|
|
||||||
})
|
|
||||||
end
|
|
||||||
|
|
||||||
function testTagOutsShouldNotHappenOnBase()
|
|
||||||
assertRunnerStatuses({
|
|
||||||
fielderWithBallAt = C.Bases[C.Third],
|
|
||||||
outWhen = {},
|
|
||||||
safeWhen = {
|
|
||||||
{ 2 },
|
|
||||||
{ 3 },
|
|
||||||
{ 2, 3 },
|
|
||||||
{ 2, 3, 4 },
|
|
||||||
},
|
|
||||||
})
|
|
||||||
end
|
|
||||||
|
|
||||||
os.exit(luaunit.LuaUnit.run())
|
|
|
@ -1,57 +0,0 @@
|
||||||
import = function() end
|
|
||||||
luaunit = require("luaunit")
|
|
||||||
luaunit.ORDER_ACTUAL_EXPECTED = false
|
|
||||||
|
|
||||||
utils = require("utils")
|
|
||||||
C = require("constants")
|
|
||||||
mocks = require("test/mocks")
|
|
||||||
playdate = mocks[1]
|
|
||||||
|
|
||||||
_f = require("fielding")
|
|
||||||
Fielding = _f[1]
|
|
||||||
|
|
||||||
---@return Fielding, number fielderCount
|
|
||||||
local function fieldersAtDefaultPositions()
|
|
||||||
local fielding = Fielding.new()
|
|
||||||
fielding:resetFielderPositions()
|
|
||||||
|
|
||||||
local fielderCount = 0
|
|
||||||
for _, fielder in pairs(fielding.fielders) do
|
|
||||||
fielder.x = fielder.target.x
|
|
||||||
fielder.y = fielder.target.y
|
|
||||||
fielderCount = fielderCount + 1
|
|
||||||
end
|
|
||||||
|
|
||||||
return fielding, fielderCount
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param x number
|
|
||||||
---@param y number
|
|
||||||
---@param z number | nil
|
|
||||||
function fakeBall(x, y, z)
|
|
||||||
return {
|
|
||||||
x = x,
|
|
||||||
y = y,
|
|
||||||
z = z or 0,
|
|
||||||
heldBy = nil,
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
function testBallPickedUpByNearbyFielders()
|
|
||||||
local fielding, fielderCount = fieldersAtDefaultPositions()
|
|
||||||
luaunit.assertIs("table", type(fielding))
|
|
||||||
luaunit.assertIs("table", type(fielding.fielders))
|
|
||||||
luaunit.assertEquals(9, fielderCount)
|
|
||||||
|
|
||||||
local ball = fakeBall(-100, -100, -100)
|
|
||||||
fielding:updateFielderPositions(ball, 0.01)
|
|
||||||
luaunit.assertIsNil(ball.heldBy, "Ball should not be held by a fielder yet")
|
|
||||||
|
|
||||||
local secondBaseman = fielding.fielders.second
|
|
||||||
ball.x = secondBaseman.x
|
|
||||||
ball.y = secondBaseman.y
|
|
||||||
fielding:updateFielderPositions(ball, 0.01)
|
|
||||||
luaunit.assertIs(secondBaseman, ball.heldBy, "Ball should be held by the nearest fielder")
|
|
||||||
end
|
|
||||||
|
|
||||||
os.exit(luaunit.LuaUnit.run())
|
|
Loading…
Reference in New Issue