Move fielder targeting to array-based system
Allows for *light* path-finding, but is currently liable to totally lose track of weirdly-hit balls. BUT this may be more of an issue of not correctly parsing the ball's state (home run, foul ball, etc. Bat is now white with a black outline. Some linting. Run tests with `-v` playdate.timer.new in mocks.lua Add test for ball-catchability.
This commit is contained in:
parent
7525daccb6
commit
3715361718
|
@ -39,11 +39,11 @@ GameLogo = playdate.graphics.image.new("images/game/GameLogo.png")
|
|||
|
||||
-- luacheck: ignore
|
||||
---@type pd_image
|
||||
Glove = playdate.graphics.image.new("images/game/Glove.png")
|
||||
GloveHoldingBall = playdate.graphics.image.new("images/game/GloveHoldingBall.png")
|
||||
|
||||
-- luacheck: ignore
|
||||
---@type pd_image
|
||||
GloveHoldingBall = playdate.graphics.image.new("images/game/GloveHoldingBall.png")
|
||||
Glove = playdate.graphics.image.new("images/game/Glove.png")
|
||||
|
||||
-- luacheck: ignore
|
||||
---@type pd_image
|
||||
|
@ -113,11 +113,11 @@ BatCrackReverb = playdate.sound.sampleplayer.new("sounds/BatCrackReverb.wav")
|
|||
|
||||
-- luacheck: ignore
|
||||
---@type pd_sampleplayer
|
||||
BootTune = playdate.sound.sampleplayer.new("music/BootTune.wav")
|
||||
BootTuneOrgany = playdate.sound.sampleplayer.new("music/BootTuneOrgany.wav")
|
||||
|
||||
-- luacheck: ignore
|
||||
---@type pd_sampleplayer
|
||||
BootTuneOrgany = playdate.sound.sampleplayer.new("music/BootTuneOrgany.wav")
|
||||
BootTune = playdate.sound.sampleplayer.new("music/BootTune.wav")
|
||||
|
||||
-- luacheck: ignore
|
||||
---@type pd_sampleplayer
|
||||
|
|
|
@ -53,9 +53,8 @@ function Baserunning:outRunner(runner, message)
|
|||
end
|
||||
end
|
||||
end
|
||||
if type(runner) ~= "number" then
|
||||
error("Expected runner to have type 'number', but was: " .. type(runner))
|
||||
end
|
||||
local runnerType = type(runner)
|
||||
assert(runnerType == "number", "Expected runner to have type 'number', but was: " .. runnerType)
|
||||
self.outRunners[#self.outRunners + 1] = self.runners[runner]
|
||||
table.remove(self.runners, runner)
|
||||
|
||||
|
@ -229,8 +228,8 @@ function Baserunning:updateRunner(runner, runnerIndex, appliedSpeed, isAutoRun,
|
|||
local autoRun = 0
|
||||
if not isAutoRun then
|
||||
autoRun = (nearestBaseDistance > 40 or runner.forcedTo) and mult * autoRunSpeed
|
||||
or nearestBaseDistance < 5 and 0
|
||||
or (nearestBase == runner.nextBase and autoRunSpeed or -1 * autoRunSpeed)
|
||||
or nearestBaseDistance < 5 and 0
|
||||
or (nearestBase == runner.nextBase and autoRunSpeed or -1 * autoRunSpeed)
|
||||
end
|
||||
|
||||
mult = autoRun + (appliedSpeed / 20)
|
||||
|
@ -257,7 +256,8 @@ function Baserunning:updateNonBatterRunners(appliedSpeed, forcedOnly, isAutoRun,
|
|||
if speedIsFunction then
|
||||
speed = appliedSpeed(runner)
|
||||
end
|
||||
local thisRunnerMoved, thisRunnerScored = self:updateRunner(runner, runnerIndex, speed, isAutoRun, deltaSeconds)
|
||||
local thisRunnerMoved, thisRunnerScored =
|
||||
self:updateRunner(runner, runnerIndex, speed, isAutoRun, deltaSeconds)
|
||||
runnersStillMoving = runnersStillMoving or thisRunnerMoved
|
||||
if thisRunnerScored then
|
||||
runnersScored = runnersScored + 1
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
--- @class Fielder {
|
||||
--- @field name string
|
||||
--- @field x number
|
||||
--- @field y number
|
||||
--- @field target XyPair | nil
|
||||
--- @field targets XyPair[]
|
||||
--- @field speed number
|
||||
--- @field spriteIndex number
|
||||
|
||||
|
@ -56,12 +57,12 @@ end
|
|||
--- Actually only benches the infield, because outfielders are far away!
|
||||
---@param position XyPair
|
||||
function Fielding:benchTo(position)
|
||||
self.fielders.first.target = position
|
||||
self.fielders.second.target = position
|
||||
self.fielders.shortstop.target = position
|
||||
self.fielders.third.target = position
|
||||
self.fielders.pitcher.target = position
|
||||
self.fielders.catcher.target = position
|
||||
self.fielders.first.targets = { position }
|
||||
self.fielders.second.targets = { position }
|
||||
self.fielders.shortstop.targets = { position }
|
||||
self.fielders.third.targets = { position }
|
||||
self.fielders.pitcher.targets = { position }
|
||||
self.fielders.catcher.targets = { position }
|
||||
end
|
||||
|
||||
--- Resets the target positions of all fielders to their defaults (at their field positions).
|
||||
|
@ -74,15 +75,15 @@ function Fielding:resetFielderPositions(fromOffTheField)
|
|||
end
|
||||
end
|
||||
|
||||
self.fielders.first.target = utils.xy(C.Screen.W - 65, C.Screen.H * 0.48)
|
||||
self.fielders.second.target = utils.xy(C.Screen.W * 0.70, C.Screen.H * 0.30)
|
||||
self.fielders.shortstop.target = utils.xy(C.Screen.W * 0.30, C.Screen.H * 0.30)
|
||||
self.fielders.third.target = utils.xy(C.Screen.W * 0.1, C.Screen.H * 0.48)
|
||||
self.fielders.pitcher.target = utils.xy(C.PitcherStartPos.x, C.PitcherStartPos.y)
|
||||
self.fielders.catcher.target = utils.xy(C.Screen.W * 0.475, C.Screen.H * 0.92)
|
||||
self.fielders.left.target = utils.xy(C.Screen.W * -0.6, C.Screen.H * -0.1)
|
||||
self.fielders.center.target = utils.xy(C.Center.x, C.Screen.H * -0.4)
|
||||
self.fielders.right.target = utils.xy(C.Screen.W * 1.6, self.fielders.left.target.y)
|
||||
self.fielders.first.targets = { utils.xy(C.Screen.W - 65, C.Screen.H * 0.48) }
|
||||
self.fielders.second.targets = { utils.xy(C.Screen.W * 0.70, C.Screen.H * 0.30) }
|
||||
self.fielders.shortstop.targets = { utils.xy(C.Screen.W * 0.30, C.Screen.H * 0.30) }
|
||||
self.fielders.third.targets = { utils.xy(C.Screen.W * 0.1, C.Screen.H * 0.48) }
|
||||
self.fielders.pitcher.targets = { utils.xy(C.PitcherStartPos.x, C.PitcherStartPos.y) }
|
||||
self.fielders.catcher.targets = { utils.xy(C.Screen.W * 0.475, C.Screen.H * 0.92) }
|
||||
self.fielders.left.targets = { utils.xy(C.Screen.W * -0.6, C.Screen.H * -0.1) }
|
||||
self.fielders.center.targets = { utils.xy(C.Center.x, C.Screen.H * -0.4) }
|
||||
self.fielders.right.targets = { utils.xy(C.Screen.W * 1.6, self.fielders.left.targets[1].y) }
|
||||
end
|
||||
|
||||
---@param deltaSeconds number
|
||||
|
@ -90,14 +91,31 @@ end
|
|||
---@param ball Ball
|
||||
---@return boolean canCatch
|
||||
local function updateFielderPosition(deltaSeconds, fielder, ball)
|
||||
if fielder.target ~= nil then
|
||||
if
|
||||
utils.pointIsSquarelyAboveLine(fielder, C.BottomOfOutfieldWall)
|
||||
or not utils.moveAtSpeed(fielder, fielder.speed * deltaSeconds, fielder.target)
|
||||
then
|
||||
fielder.target = nil
|
||||
if #fielder.targets > 0 then
|
||||
local nextFielderPos = utils.xy(fielder.x, fielder.y)
|
||||
local currentTarget = fielder.targets[#fielder.targets]
|
||||
local willMove = utils.moveAtSpeed(nextFielderPos, fielder.speed * deltaSeconds, currentTarget)
|
||||
|
||||
if willMove and utils.pointIsSquarelyAboveLine(nextFielderPos, C.BottomOfOutfieldWall) then
|
||||
local targetCount = #fielder.targets
|
||||
-- Back up a little
|
||||
fielder.targets[targetCount + 2] = utils.xy(fielder.x, fielder.y + 5)
|
||||
-- Try to come at it from below
|
||||
fielder.targets[targetCount + 1] = utils.xy(currentTarget.x, fielder.y + 10)
|
||||
end
|
||||
|
||||
if not utils.moveAtSpeed(fielder, fielder.speed * deltaSeconds, fielder.targets[#fielder.targets]) then
|
||||
table.remove(fielder.targets, #fielder.targets)
|
||||
end
|
||||
end
|
||||
-- TODO: Clean this up, like, a lot.
|
||||
-- I'd love to avoid any "real" pathfinding implementation, but these huge target queues are liable to be an issue.
|
||||
-- The worst case came when a ball was hit far, but not in a way that the game classed as a home run.
|
||||
-- Maybe this queueing would be fine if that issue was resolved
|
||||
if #fielder.targets >= 10 then
|
||||
fielder.targets = { utils.xy(fielder.x, fielder.y + 100) }
|
||||
end
|
||||
assert(#fielder.targets < 10, "Fielder " .. fielder.name .. " is accruing too many target positions!")
|
||||
|
||||
return ball.catchable and utils.distanceBetweenPoints(fielder, ball) < C.BallCatchHitbox
|
||||
end
|
||||
|
@ -111,7 +129,7 @@ end
|
|||
---@param ballDestY number
|
||||
function Fielding:haveSomeoneChase(ballDestX, ballDestY)
|
||||
local chasingFielder = utils.getNearestOf(self.fielders, ballDestX, ballDestY)
|
||||
chasingFielder.target = utils.xy(ballDestX, ballDestY)
|
||||
chasingFielder.targets = { utils.xy(ballDestX, ballDestY) }
|
||||
|
||||
for _, base in ipairs(C.Bases) do
|
||||
local nearest = utils.getNearestOf(self.fielders, base.x, base.y, function(fielder)
|
||||
|
@ -121,7 +139,7 @@ function Fielding:haveSomeoneChase(ballDestX, ballDestY)
|
|||
end
|
||||
return fielder ~= chasingFielder
|
||||
end)
|
||||
nearest.target = base
|
||||
nearest.targets = { base }
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -160,7 +178,7 @@ local function userThrowToCoroutine(field, targetBase, ball, throwFlyMs)
|
|||
return fielder ~= field.fielderHoldingBall -- presumably, this is who will be doing the throwing
|
||||
end)
|
||||
|
||||
closestFielder.target = targetBase
|
||||
closestFielder.targets = { targetBase }
|
||||
ball:launch(targetBase.x, targetBase.y, playdate.easingFunctions.linear, throwFlyMs)
|
||||
|
||||
return
|
||||
|
|
17
src/main.lua
17
src/main.lua
|
@ -51,6 +51,7 @@ import 'draw/transitions.lua'
|
|||
|
||||
-- TODO: Customizable field structure. E.g. stands and ads etc.
|
||||
|
||||
---@type pd_graphics_lib
|
||||
local gfx <const>, C <const> = playdate.graphics, C
|
||||
|
||||
---@alias Team { benchPosition: XyPair }
|
||||
|
@ -357,6 +358,9 @@ function Game:updateBatting(batDeg, batSpeed)
|
|||
if not self.state.pitchIsOver and batDeg > SwingBackDeg and batDeg < SwingForwardDeg then
|
||||
self.state.didSwing = true
|
||||
end
|
||||
-- TODO? Make the bat angle work more like the throw meter.
|
||||
-- Would instead constantly drift toward a default value, giving us a little more control,
|
||||
-- and letting the user find a crank position and direction that works for them
|
||||
local batAngle = math.rad(batDeg)
|
||||
-- TODO: animate bat-flip or something
|
||||
self.state.batBase.x = self.baserunning.batter and (self.baserunning.batter.x + C.BatterHandPos.x) or 0
|
||||
|
@ -390,10 +394,8 @@ function Game:updateBatting(batDeg, batSpeed)
|
|||
ballVelY = ballVelY * -1
|
||||
end
|
||||
|
||||
local ballDest = utils.xy(
|
||||
self.state.ball.x + (ballVelX * C.BattingPower),
|
||||
self.state.ball.y + (ballVelY * C.BattingPower)
|
||||
)
|
||||
local ballDest =
|
||||
utils.xy(self.state.ball.x + (ballVelX * C.BattingPower), self.state.ball.y + (ballVelY * C.BattingPower))
|
||||
|
||||
pitchTracker:reset()
|
||||
local flyTimeMs = 2000
|
||||
|
@ -665,8 +667,13 @@ function Game:update()
|
|||
end
|
||||
|
||||
if self.state.offenseState == C.Offense.batting then
|
||||
gfx.setLineWidth(5)
|
||||
gfx.setLineWidth(7)
|
||||
gfx.drawLine(self.state.batBase.x, self.state.batBase.y, self.state.batTip.x, self.state.batTip.y)
|
||||
gfx.setColor(gfx.kColorWhite)
|
||||
gfx.setLineCapStyle(gfx.kLineCapStyleRound)
|
||||
gfx.setLineWidth(3)
|
||||
gfx.drawLine(self.state.batBase.x, self.state.batBase.y, self.state.batTip.x, self.state.batTip.y)
|
||||
gfx.setColor(gfx.kColorBlack)
|
||||
end
|
||||
|
||||
for _, runner in pairs(self.baserunning.outRunners) do
|
||||
|
|
|
@ -126,7 +126,7 @@ local function tryToMakeAPlay(fielders, fielder, runners, ball)
|
|||
local targetX, targetY = getNextOutTarget(runners)
|
||||
if targetX ~= nil and targetY ~= nil then
|
||||
local nearestFielder = utils.getNearestOf(fielders, targetX, targetY)
|
||||
nearestFielder.target = utils.xy(targetX, targetY)
|
||||
nearestFielder.targets = { utils.xy(targetX, targetY) }
|
||||
if nearestFielder == fielder then
|
||||
ball.heldBy = fielder
|
||||
else
|
||||
|
|
|
@ -2,6 +2,15 @@ utils = require("utils")
|
|||
|
||||
local mockPlaydate = {
|
||||
TEST_MODE = true,
|
||||
timer = {
|
||||
new = function(_, callback)
|
||||
return {
|
||||
mockCompletion = function()
|
||||
callback()
|
||||
end,
|
||||
}
|
||||
end,
|
||||
},
|
||||
graphics = {
|
||||
animator = {
|
||||
new = function()
|
||||
|
|
|
@ -9,3 +9,5 @@ playdate, announcer = mocks[1], mocks[2]
|
|||
|
||||
local _f = require("fielding")
|
||||
Fielding, newFielder = _f[1], _f[2]
|
||||
|
||||
HomeTeamSpriteGroup = {}
|
||||
|
|
|
@ -29,7 +29,7 @@ function buildRunnersOn(runnerLocations)
|
|||
for b = 1, location do
|
||||
runner.x = C.Bases[b].x
|
||||
runner.y = C.Bases[b].y
|
||||
baserunning:updateNonBatterRunners(0.001, false, 0.001)
|
||||
baserunning:updateNonBatterRunners(0.001, false, false, 0.001)
|
||||
end
|
||||
else
|
||||
-- Is a raw XyPair
|
||||
|
|
|
@ -1,47 +1,54 @@
|
|||
require("test/setup")
|
||||
require("ball")
|
||||
|
||||
---@return Fielding, number fielderCount
|
||||
---@return Fielding, Fielder someBaseman
|
||||
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
|
||||
fielder.x = fielder.targets[#fielder.targets].x
|
||||
fielder.y = fielder.targets[#fielder.targets].y
|
||||
fielderCount = fielderCount + 1
|
||||
end
|
||||
|
||||
return fielding, fielderCount
|
||||
return fielding, fielding.fielders.second
|
||||
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,
|
||||
}
|
||||
local function ballAt(x, y, z)
|
||||
local ball = Ball.new(playdate.graphics.animator)
|
||||
ball.x = x
|
||||
ball.y = y
|
||||
ball.z = z
|
||||
return ball
|
||||
end
|
||||
|
||||
function testBallPickedUpByNearbyFielders()
|
||||
local fielding, fielderCount = fieldersAtDefaultPositions()
|
||||
luaunit.assertIs("table", type(fielding))
|
||||
luaunit.assertIs("table", type(fielding.fielders))
|
||||
luaunit.assertEquals(9, fielderCount)
|
||||
local fielding, baseman = fieldersAtDefaultPositions()
|
||||
local ball = ballAt(baseman.x, baseman.y, baseman.z)
|
||||
|
||||
local ball = fakeBall(-100, -100, -100)
|
||||
fielding:updateFielderPositions(ball, 0.01)
|
||||
luaunit.assertIsNil(ball.heldBy, "Ball should not be held by a fielder yet")
|
||||
luaunit.assertIs(baseman, ball.heldBy, "Ball should be held by the nearest fielder")
|
||||
end
|
||||
|
||||
local secondBaseman = fielding.fielders.second
|
||||
ball.x = secondBaseman.x
|
||||
ball.y = secondBaseman.y
|
||||
function testBallNotPickedUpByDistantFielders()
|
||||
local fielding = fieldersAtDefaultPositions()
|
||||
local ball = ballAt(-100, -100, -100)
|
||||
fielding:updateFielderPositions(ball, 0.01)
|
||||
luaunit.assertIs(secondBaseman, ball.heldBy, "Ball should be held by the nearest fielder")
|
||||
luaunit.assertIsNil(ball.heldBy, "Ball should be too far for any fielders to pick up")
|
||||
end
|
||||
|
||||
function testBallNotPickedUpWhenNotCatchable()
|
||||
local fielding, baseman = fieldersAtDefaultPositions()
|
||||
local ball = ballAt(baseman.x, baseman.y, baseman.z)
|
||||
ball:markUncatchable()
|
||||
|
||||
fielding:updateFielderPositions(ball, 0.01)
|
||||
luaunit.assertIsNil(ball.heldBy, "Ball should be held by the nearest fielder")
|
||||
end
|
||||
|
||||
os.exit(luaunit.LuaUnit.run())
|
||||
|
|
Loading…
Reference in New Issue