diff --git a/src/assets.lua b/src/assets.lua index 83176d3..57d659f 100644 --- a/src/assets.lua +++ b/src/assets.lua @@ -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 diff --git a/src/baserunning.lua b/src/baserunning.lua index 88d30a8..7e91680 100644 --- a/src/baserunning.lua +++ b/src/baserunning.lua @@ -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 diff --git a/src/fielding.lua b/src/fielding.lua index 47c4e02..716b172 100644 --- a/src/fielding.lua +++ b/src/fielding.lua @@ -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 diff --git a/src/main.lua b/src/main.lua index 0ec8bfb..e63349e 100644 --- a/src/main.lua +++ b/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 , C = 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 diff --git a/src/npc.lua b/src/npc.lua index 921c4a4..4bb1af9 100644 --- a/src/npc.lua +++ b/src/npc.lua @@ -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 diff --git a/src/test/mocks.lua b/src/test/mocks.lua index 08d69bd..30b5ee1 100644 --- a/src/test/mocks.lua +++ b/src/test/mocks.lua @@ -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() diff --git a/src/test/setup.lua b/src/test/setup.lua index 70de319..a66116b 100644 --- a/src/test/setup.lua +++ b/src/test/setup.lua @@ -9,3 +9,5 @@ playdate, announcer = mocks[1], mocks[2] local _f = require("fielding") Fielding, newFielder = _f[1], _f[2] + +HomeTeamSpriteGroup = {} diff --git a/src/test/testBaserunning.lua b/src/test/testBaserunning.lua index d94fc02..d2d2692 100644 --- a/src/test/testBaserunning.lua +++ b/src/test/testBaserunning.lua @@ -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 diff --git a/src/test/testFielding.lua b/src/test/testFielding.lua index 73c719e..1a94f32 100644 --- a/src/test/testFielding.lua +++ b/src/test/testFielding.lua @@ -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())