diff --git a/src/ball.lua b/src/ball.lua index 3fef096..9b7016c 100644 --- a/src/ball.lua +++ b/src/ball.lua @@ -1,6 +1,5 @@ -- selene: allow(unscoped_variables) ---@class Ball ----@field private animator pd_animator_lib ---@field x number ---@field y number ---@field z number @@ -10,13 +9,15 @@ ---@field yAnimator SimpleAnimator ---@field sizeAnimator SimpleAnimator ---@field floatAnimator SimpleAnimator +---@field private animatorLib pd_animator_lib +---@field private flyTimeMs number Ball = {} ----@param animator pd_animator_lib +---@param animatorLib pd_animator_lib ---@return Ball -function Ball.new(animator) +function Ball.new(animatorLib) return setmetatable({ - animator = animator, + animatorLib = animatorLib, x = C.Center.x --[[@as number]], y = C.Center.y --[[@as number]], z = 0, @@ -29,10 +30,26 @@ function Ball.new(animator) -- TODO? Replace these with a ballAnimatorZ? -- ...that might lose some of the magic of both. Compromise available? idk sizeAnimator = utils.staticAnimator(C.SmallestBallRadius), - floatAnimator = animator.new(2000, -60, 0, utils.easingHill), + floatAnimator = animatorLib.new(2000, -60, 0, utils.easingHill), }, { __index = Ball }) 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. ---@param destX number ---@param destY number @@ -47,14 +64,16 @@ function Ball:launch(destX, destY, easingFunc, flyTimeMs, floaty, customBallScal flyTimeMs = utils.distanceBetween(self.x, self.y, destX, destY) * C.DefaultLaunchPower end + self.flyTimeMs = flyTimeMs + if customBallScaler then self.sizeAnimator = customBallScaler else -- TODO? Scale based on distance? - self.sizeAnimator = self.animator.new(flyTimeMs, 9, C.SmallestBallRadius, utils.easingHill) + self.sizeAnimator = self.animatorLib.new(flyTimeMs, 9, C.SmallestBallRadius, utils.easingHill) end - self.yAnimator = self.animator.new(flyTimeMs, self.y, destY, easingFunc) - self.xAnimator = self.animator.new(flyTimeMs, self.x, destX, easingFunc) + self.yAnimator = self.animatorLib.new(flyTimeMs, self.y, destY, easingFunc) + self.xAnimator = self.animatorLib.new(flyTimeMs, self.x, destX, easingFunc) if floaty then self.floatAnimator:reset(flyTimeMs) end diff --git a/src/baserunning.lua b/src/baserunning.lua index 18f95a5..d36e019 100644 --- a/src/baserunning.lua +++ b/src/baserunning.lua @@ -181,8 +181,10 @@ function Baserunning:updateRunner(runner, runnerIndex, appliedSpeed, deltaSecond -- TODO: Do a better job drifting runners toward their bases when appliedSpeed is low/zero if distance < 2 then - runner.prevBase = runner.nextBase - runner.nextBase = C.NextBaseMap[runner.nextBase] + if runner.prevBase ~= nearestBase then + runner.prevBase = runner.nextBase + runner.nextBase = C.NextBaseMap[runner.nextBase] + end runner.forcedTo = nil return false, false end @@ -214,16 +216,21 @@ 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 | fun(runner: Runner): number ---@return boolean someRunnerMoved, number runnersScored function Baserunning:updateNonBatterRunners(appliedSpeed, forcedOnly, deltaSeconds) local someRunnerMoved = false local runnersScored = 0 + local speedIsFunction = type(appliedSpeed) == "function" -- 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) + local speed = appliedSpeed + if speedIsFunction then + speed = appliedSpeed(runner) + end + local thisRunnerMoved, thisRunnerScored = self:updateRunner(runner, runnerIndex, speed, deltaSeconds) someRunnerMoved = someRunnerMoved or thisRunnerMoved if thisRunnerScored then runnersScored = runnersScored + 1 diff --git a/src/constants.lua b/src/constants.lua index 1981be9..d658e7c 100644 --- a/src/constants.lua +++ b/src/constants.lua @@ -86,6 +86,7 @@ C.BaseHitbox = 10 C.BattingPower = 25 C.BatterHandPos = utils.xy(25, 15) +C.GloveZ = 0 -- 10 C.SmallestBallRadius = 6 diff --git a/src/fielding.lua b/src/fielding.lua index 353e5ac..9746b97 100644 --- a/src/fielding.lua +++ b/src/fielding.lua @@ -1,16 +1,16 @@ ---- @alias Fielder { ---- x: number, ---- y: number, ---- target: XyPair | nil, ---- speed: number, ---- } +--- @class Fielder { +--- @field catchEligible boolean +--- @field x number +--- @field y number +--- @field target XyPair | nil +--- @field speed number -- TODO: Run down baserunners in a pickle. -- selene: allow(unscoped_variables) ---@class Fielding ---@field fielders table ----@field fielderTouchingBall Fielder | nil +---@field fielderHoldingBall Fielder | nil Fielding = {} local FielderDanceAnimator = playdate.graphics.animator.new(1, 10, 0, utils.easingHill) @@ -23,6 +23,7 @@ local function newFielder(name, speed) return { name = name, speed = speed * C.FielderRunMult, + catchEligible = true, } end @@ -40,7 +41,7 @@ function Fielding.new() right = newFielder("Right", 50), }, ---@type Fielder | nil - fielderTouchingBall = nil, + fielderHoldingBall = nil, }, { __index = Fielding }) end @@ -79,7 +80,7 @@ end ---@param deltaSeconds number ---@param fielder Fielder ---@param ballPos XyPair ----@return boolean isTouchingBall +---@return boolean inCatchingRange local function updateFielderPosition(deltaSeconds, fielder, ballPos) if fielder.target ~= nil then if not utils.moveAtSpeed(fielder, fielder.speed * deltaSeconds, fielder.target) then @@ -108,40 +109,62 @@ function Fielding:haveSomeoneChase(ballDestX, ballDestY) end end ----@param ball XyPair +---@param ball Ball ---@param deltaSeconds number ----@return Fielder | nil fielderTouchingBall nil if no fielder is currently touching the ball +---@return Fielder | nil fielderHoldingBall nil if no fielder is currently touching the ball function Fielding:updateFielderPositions(ball, deltaSeconds) - local fielderTouchingBall = nil + local fielderHoldingBall = nil for _, fielder in pairs(self.fielders) do - local isTouchingBall = updateFielderPosition(deltaSeconds, fielder, ball) - if isTouchingBall then - fielderTouchingBall = fielder + local inCatchingRange = updateFielderPosition(deltaSeconds, fielder, ball) + if inCatchingRange and fielder.catchEligible then + -- TODO: Base this catch on fielder skill? + fielderHoldingBall = fielder + ball.heldBy = fielder -- How much havoc will this wreak? end end -- TODO: The need is growing for a distinction between touching the ball and holding the ball. -- Or, at least, fielders need to start *stopping* the ball when they make contact with it. -- Right now, a line-drive *through* first will be counted as an out. - self.fielderTouchingBall = fielderTouchingBall - return fielderTouchingBall + 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 -- TODO? Start moving target fielders close sooner? ----@param field table +---@param field Fielding ---@param targetBase Base ---@param launchBall LaunchBall ---@param throwFlyMs number ---@return ActionResult local function userThrowToImpl(field, targetBase, launchBall, throwFlyMs) - if field.fielderTouchingBall == nil then + if field.fielderHoldingBall == nil then return ActionResult.NeedsMoreTime end local closestFielder = utils.getNearestOf(field.fielders, targetBase.x, targetBase.y, function(fielder) - return fielder ~= field.fielderTouchingBall -- presumably, this is who will be doing the throwing + return fielder ~= field.fielderHoldingBall -- presumably, this is who will be doing the throwing end) closestFielder.target = targetBase launchBall(targetBase.x, targetBase.y, playdate.easingFunctions.linear, throwFlyMs) + Fielding.markIneligible(field.fielderHoldingBall) return ActionResult.Succeeded end diff --git a/src/images/game/GrassBackground.png b/src/images/game/GrassBackground.png index 7b45da2..a33353f 100644 Binary files a/src/images/game/GrassBackground.png and b/src/images/game/GrassBackground.png differ diff --git a/src/main.lua b/src/main.lua index 7f90495..4c3a2e2 100644 --- a/src/main.lua +++ b/src/main.lua @@ -148,6 +148,8 @@ end ---@param pitchFlyTimeMs number | nil ---@param pitchTypeIndex number | nil local function pitch(pitchFlyTimeMs, pitchTypeIndex) + Fielding.markIneligible(fielding.fielders.pitcher) + ball.heldBy = nil catcherThrownBall = false offenseState = C.Offense.batting @@ -318,7 +320,7 @@ local function updateBatting(batDeg, batSpeed) end end ----@param appliedSpeed number +---@param appliedSpeed number | fun(runner: Runner): number ---@return boolean someRunnerMoved local function updateNonBatterRunners(appliedSpeed, forcedOnly) local runnerMoved, runnersScored = baserunning:updateNonBatterRunners(appliedSpeed, forcedOnly, deltaSeconds) @@ -351,16 +353,7 @@ local function updateGameState() crankLimited = crankLimited * -1 end - 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 + ball:updatePosition() local userOnOffense, userOnDefense = userIsOn(C.Sides.offense) @@ -418,7 +411,10 @@ local function updateGameState() end end elseif offenseState == C.Offense.running then - local appliedSpeed = userOnOffense and crankLimited or npc:runningSpeed(ball) + local appliedSpeed = userOnOffense and crankLimited + or function(runner) + return npc:runningSpeed(runner, ball) + end if updateNonBatterRunners(appliedSpeed) then secondsSinceLastRunnerMove = 0 else @@ -426,6 +422,7 @@ local function updateGameState() if secondsSinceLastRunnerMove > C.ResetFieldersAfterSeconds then -- End of play. Throw the ball back to the pitcher 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() offenseState = C.Offense.batting -- TODO: Remove, or replace with nextBatter() diff --git a/src/npc.lua b/src/npc.lua index 6694379..f888566 100644 --- a/src/npc.lua +++ b/src/npc.lua @@ -39,26 +39,25 @@ end local baseRunningSpeed = 25 --- TODO: Individual runner control. +---@param runner Runner ---@param ball Point3d ---@return number -function Npc:runningSpeed(ball) +function Npc:runningSpeed(runner, ball) if #self.runners == 0 then return 0 end - local runner1 = self.runners[1] + local distanceFromBall = utils.distanceBetweenZ(ball.x, ball.y, ball.z, runner.x, runner.y, 0) - local ballIsFar = utils.distanceBetweenZ(ball.x, ball.y, ball.z, runner1.x, runner1.y, 0) > 300 - - if ballIsFar or runner1.forcedTo then + if distanceFromBall > 400 or runner.forcedTo then return baseRunningSpeed end - local touchedBase = utils.isTouchingBase(runner1.x, runner1.y) - if not touchedBase and runner1.nextBase then - local distToNext = utils.distanceBetween(runner1.x, runner1.y, runner1.nextBase.x, runner1.nextBase.y) - local distToPrev = utils.distanceBetween(runner1.x, runner1.y, runner1.prevBase.x, runner1.prevBase.y) - if distToNext < distToPrev then + local touchedBase = utils.isTouchingBase(runner.x, runner.y) + if not touchedBase and runner.nextBase then + local distToNext = utils.distanceBetween(runner.x, runner.y, runner.nextBase.x, runner.nextBase.y) + local distToPrev = utils.distanceBetween(runner.x, runner.y, runner.prevBase.x, runner.prevBase.y) + if distToNext < distToPrev or distanceFromBall > 350 then return baseRunningSpeed else return -1 * baseRunningSpeed @@ -124,12 +123,15 @@ end local function tryToMakeAPlay(fielders, fielder, runners, ball, launchBall) local targetX, targetY = getNextOutTarget(runners) if targetX ~= nil and targetY ~= nil then - local nearestFielder = utils.getNearestOf(fielders, targetX, targetY) + local nearestFielder = utils.getNearestOf(fielders, targetX, targetY, function(grabCandidate) + return grabCandidate.catchEligible + end) nearestFielder.target = utils.xy(targetX, targetY) if nearestFielder == fielder then ball.heldBy = fielder else launchBall(targetX, targetY, playdate.easingFunctions.linear, nil, true) + Fielding.markIneligible(nearestFielder) end end end