diff --git a/src/action-queue.lua b/src/action-queue.lua index 2571249..efe6956 100644 --- a/src/action-queue.lua +++ b/src/action-queue.lua @@ -25,6 +25,16 @@ function actionQueue:upsert(id, maxTimeMs, action) } end +function actionQueue:newOnly(id, maxTimeMs, action) + if self.queue[id] then + return + end + self.queue[id] = { + coroutine = coroutine.create(action), + expireTimeMs = maxTimeMs + playdate.getCurrentTimeMilliseconds(), + } +end + --- Must be called on every playdate.update() to check for (and run) any waiting tasks. --- Actions that return NeedsMoreTime will not be removed from the queue unless they have expired. function actionQueue:runWaiting(deltaSeconds) diff --git a/src/baserunning.lua b/src/baserunning.lua index 7a91df5..88d30a8 100644 --- a/src/baserunning.lua +++ b/src/baserunning.lua @@ -179,7 +179,7 @@ end ---@param appliedSpeed number ---@param deltaSeconds number ---@return boolean runnerMoved, boolean runnerScored -function Baserunning:updateRunner(runner, runnerIndex, appliedSpeed, deltaSeconds) +function Baserunning:updateRunner(runner, runnerIndex, appliedSpeed, isAutoRun, deltaSeconds) local autoRunSpeed = 20 * deltaSeconds if not runner or not runner.nextBase then @@ -226,9 +226,12 @@ function Baserunning:updateRunner(runner, runnerIndex, appliedSpeed, deltaSecond -- 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 - or nearestBaseDistance < 5 and 0 - or (nearestBase == runner.nextBase and autoRunSpeed or -1 * autoRunSpeed) + 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) + end mult = autoRun + (appliedSpeed / 20) runner.x = runner.x - (x * mult) @@ -240,8 +243,9 @@ end --- Update non-batter runners. --- Returns true only if at least one of the given runners moved during this update ---@param appliedSpeed number | fun(runner: Runner): number +---@param isAutoRun boolean If true, does not attempt to hug the bases ---@return boolean runnersStillMoving, number runnersScored, number secondsSinceLastMove -function Baserunning:updateNonBatterRunners(appliedSpeed, forcedOnly, deltaSeconds) +function Baserunning:updateNonBatterRunners(appliedSpeed, forcedOnly, isAutoRun, deltaSeconds) local runnersStillMoving = false local runnersScored = 0 @@ -253,7 +257,7 @@ function Baserunning:updateNonBatterRunners(appliedSpeed, forcedOnly, deltaSecon if speedIsFunction then speed = appliedSpeed(runner) end - local thisRunnerMoved, thisRunnerScored = self:updateRunner(runner, runnerIndex, speed, 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/draw/fans.lua b/src/draw/fans.lua index 5e6d9df..523458c 100644 --- a/src/draw/fans.lua +++ b/src/draw/fans.lua @@ -1,4 +1,4 @@ -local gfx , C = playdate.graphics, C +local gfx = playdate.graphics fans = {} diff --git a/src/draw/throw-meter.lua b/src/draw/throw-meter.lua index cd0973e..44502cc 100644 --- a/src/draw/throw-meter.lua +++ b/src/draw/throw-meter.lua @@ -15,7 +15,7 @@ function throwMeter:draw(x, y) if self.lastReadThrow then -- TODO: If meter has moved to a new fielder, empty it. local ratio = 1 - if not wasPerfect then + if not self.wasPerfect then ratio = (self.lastReadThrow - throwMeter.MinCharge) / (self.idealPower - throwMeter.MinCharge) end local height = ThrowMeterHeight * ratio diff --git a/src/fielding.lua b/src/fielding.lua index a99a67f..47c4e02 100644 --- a/src/fielding.lua +++ b/src/fielding.lua @@ -31,7 +31,7 @@ local function newFielder(name, speed) return { name = name, speed = speed * C.FielderRunMult, - spriteIndex = math.random(#HomeTeamSpriteGroup) + spriteIndex = math.random(#HomeTeamSpriteGroup), } end diff --git a/src/graphics.lua b/src/graphics.lua index 7c82300..c9e741e 100644 --- a/src/graphics.lua +++ b/src/graphics.lua @@ -32,12 +32,10 @@ function blipper.new(msInterval, spriteCollection) blinker:start() return { blinker = blinker, - smiling = smiling, - lowHat = lowHat, draw = function(self, disableBlipping, x, y, hasSpriteIndex) local spriteBundle = spriteCollection[hasSpriteIndex.spriteIndex] local currentImage = (disableBlipping or self.blinker.on) and spriteBundle.lowHat or spriteBundle.smiling - local offsetY = currentImage == lowHat and -1 or 0 + local offsetY = currentImage == spriteBundle.lowHat and -1 or 0 currentImage:draw(x, y + offsetY) end, } diff --git a/src/images/game/GameLogo.png b/src/images/game/GameLogo.png index 3c231b8..1d3907f 100644 Binary files a/src/images/game/GameLogo.png and b/src/images/game/GameLogo.png differ diff --git a/src/main.lua b/src/main.lua index 687e074..4cfb797 100644 --- a/src/main.lua +++ b/src/main.lua @@ -6,6 +6,7 @@ import 'CoreLibs/graphics.lua' import 'CoreLibs/object.lua' import 'CoreLibs/timer.lua' import 'CoreLibs/ui.lua' +import 'CoreLibs/utilities/where.lua' -- stylua: ignore end --- @alias Scene { update: fun(self: self) } @@ -112,7 +113,7 @@ Game = {} function Game.new(settings, announcer, fielding, baserunning, npc, state) announcer = announcer or Announcer.new() fielding = fielding or Fielding.new() - settings.userTeam = "away" + settings.userTeam = nil -- "away" local homeTeamBlipper = blipper.new(100, settings.homeTeamSpriteGroup) local awayTeamBlipper = blipper.new(100, settings.awayTeamSpriteGroup) @@ -388,25 +389,31 @@ function Game:updateBatting(batDeg, batSpeed) ballVelX = ballVelX * -1 ballVelY = ballVelY * -1 end - local ballDestX = self.state.ball.x + (ballVelX * C.BattingPower) - local ballDestY = self.state.ball.y + (ballVelY * C.BattingPower) + + local ballDest = utils.xy( + self.state.ball.x + (ballVelX * C.BattingPower), + -250 + self.state.ball.y + (ballVelY * C.BattingPower) + ) + pitchTracker:reset() local flyTimeMs = 2000 -- TODO? A dramatic eye-level view on a home-run could be sick. local battingTeamStats = self:battingTeamCurrentInning() - battingTeamStats.hits[#battingTeamStats.hits + 1] = utils.xy(ballDestX, ballDestY) + battingTeamStats.hits[#battingTeamStats.hits + 1] = ballDest - if utils.isFoulBall(ballDestX, ballDestY) then + if utils.isFoulBall(ballDest.x, ballDest.y) then self.announcer:say("Foul ball!") pitchTracker.strikes = math.min(pitchTracker.strikes + 1, 2) -- TODO: Have a fielder chase for the fly-out return end - if utils.pointIsSquarelyAboveLine(utils.xy(ballDestX, ballDestY), C.OutfieldWall) then + if utils.pointIsSquarelyAboveLine(utils.xy(ballDest.x, ballDest.y), C.OutfieldWall) then playdate.timer.new(flyTimeMs, function() -- Verify that the home run wasn't intercepted - if utils.within(1, self.state.ball.x, ballDestX) and utils.within(1, self.state.ball.y, ballDestY) then + if + utils.within(1, self.state.ball.x, ballDest.x) and utils.within(1, self.state.ball.y, ballDest.y) + then self.announcer:say("HOME RUN!") self.state.offenseState = C.Offense.homeRun -- Linger on the home-run ball for a moment, before panning to the bases. @@ -420,18 +427,19 @@ function Game:updateBatting(batDeg, batSpeed) end local hitBallScaler = gfx.animator.new(2000, 9 + (mult * mult * 0.5), C.SmallestBallRadius, utils.easingHill) - self.state.ball:launch(ballDestX, ballDestY, playdate.easingFunctions.outQuint, flyTimeMs, nil, hitBallScaler) + self.state.ball:launch(ballDest.x, ballDest.y, playdate.easingFunctions.outQuint, flyTimeMs, nil, hitBallScaler) self.baserunning:convertBatterToRunner() - self.fielding:haveSomeoneChase(ballDestX, ballDestY) + self.fielding:haveSomeoneChase(ballDest.x, ballDest.y) end end ---@param appliedSpeed number | fun(runner: Runner): number +--- ---@return boolean runnersStillMoving, number secondsSinceLastRunnerMove -function Game:updateNonBatterRunners(appliedSpeed, forcedOnly) +function Game:updateNonBatterRunners(appliedSpeed, forcedOnly, isAutoRun) local runnersStillMoving, runnersScored, secondsSinceLastRunnerMove = - self.baserunning:updateNonBatterRunners(appliedSpeed, forcedOnly, self.state.deltaSeconds) + self.baserunning:updateNonBatterRunners(appliedSpeed, forcedOnly, isAutoRun, self.state.deltaSeconds) if runnersScored ~= 0 then self:score(runnersScored) end @@ -440,7 +448,12 @@ end function Game:returnToPitcher() self.fielding:resetFielderPositions() - actionQueue:upsert("returnToPitcher", 60 * 1000, function() + + if self:pitcherIsReady() then + return -- Don't then! + end + + actionQueue:newOnly("returnToPitcher", 60 * 1000, function() while not self:pitcherIsOnTheMound() do coroutine.yield() end @@ -521,11 +534,17 @@ function Game:updateGameState() self:updateBatting(self.state.batAngleDeg, batSpeed) -- Walk batter to the plate - self.baserunning:updateRunner(self.baserunning.batter, nil, userOnOffense and crankLimited or 0, self.state.deltaSeconds) + self.baserunning:updateRunner( + self.baserunning.batter, + nil, + userOnOffense and crankLimited or 0, + false, + self.state.deltaSeconds + ) if pitchTracker.secondsSinceLastPitch > C.PitchAfterSeconds then if userOnDefense then - local powerRatio, accuracy, isPerfect = throwMeter:readThrow(crankChange) + local powerRatio, accuracy = throwMeter:readThrow(crankChange) if powerRatio then local throwFly = C.PitchFlyMs / powerRatio if throwFly and not self:buttonControlledThrow(throwFly, true) then @@ -541,7 +560,7 @@ function Game:updateGameState() or function(runner) return self.npc:runningSpeed(runner, self.state.ball) end - local _, secondsSinceLastRunnerMove = self:updateNonBatterRunners(appliedSpeed, false) + local _, secondsSinceLastRunnerMove = self:updateNonBatterRunners(appliedSpeed, false, false) if secondsSinceLastRunnerMove > C.ResetFieldersAfterSeconds then -- End of play. Throw the ball back to the pitcher self.state.offenseState = C.Offense.batting @@ -549,7 +568,7 @@ function Game:updateGameState() end if userOnDefense then - local powerRatio, accuracy, isPerfect = throwMeter:readThrow(crankChange) + local powerRatio = throwMeter:readThrow(crankChange) if powerRatio then local throwFly = C.PitchFlyMs / powerRatio if throwFly then @@ -558,11 +577,11 @@ function Game:updateGameState() end end elseif self.state.offenseState == C.Offense.walking then - if not self:updateNonBatterRunners(C.WalkedRunnerSpeed, true) then + if not self:updateNonBatterRunners(C.WalkedRunnerSpeed, true, true) then self.state.offenseState = C.Offense.batting end elseif self.state.offenseState == C.Offense.homeRun then - self:updateNonBatterRunners(C.WalkedRunnerSpeed * 2, false) + self:updateNonBatterRunners(C.WalkedRunnerSpeed * 2, false, true) if #self.baserunning.runners == 0 then -- Give the player a moment to enjoy their home run. playdate.timer.new(1500, function() @@ -607,8 +626,12 @@ function Game:update() local ballHeldBy for _, fielder in pairs(self.fielding.fielders) do addDraw(fielder.y + danceOffset, function() - local ballHeldByThisFielder = - drawFielder(self.state.fieldingTeamSprites[fielder.spriteIndex], self.state.ball, fielder.x, fielder.y + danceOffset) + local ballHeldByThisFielder = drawFielder( + self.state.fieldingTeamSprites[fielder.spriteIndex], + self.state.ball, + fielder.x, + fielder.y + danceOffset + ) if ballHeldByThisFielder then ballHeldBy = fielder end @@ -622,7 +645,10 @@ function Game:update() if self.state.batAngleDeg > 50 and self.state.batAngleDeg < 200 then self.state.battingTeamSprites[runner.spriteIndex].back:draw(runner.x, runner.y - playerHeightOffset) else - self.state.battingTeamSprites[runner.spriteIndex].smiling:draw(runner.x, runner.y - playerHeightOffset) + self.state.battingTeamSprites[runner.spriteIndex].smiling:draw( + runner.x, + runner.y - playerHeightOffset + ) end else -- TODO? Change blip speed depending on runner speed? @@ -650,7 +676,7 @@ function Game:update() self.state.battingTeamSprites[runner.spriteIndex].smiling:draw(runner.x, runner.y - playerHeightOffset) end - if self:userIsOn('defense') then + if self:userIsOn("defense") then throwMeter:drawNearFielder(ballHeldBy) end diff --git a/src/pitching.lua b/src/pitching.lua index 5983465..42381a3 100644 --- a/src/pitching.lua +++ b/src/pitching.lua @@ -21,21 +21,36 @@ Pitches = { -- Fastball function(accuracy) return { - x = gfx.animator.new(0, C.PitchStart.x, getPitchMissBy(accuracy) + C.PitchStart.x, playdate.easingFunctions.linear), + x = gfx.animator.new( + 0, + C.PitchStart.x, + getPitchMissBy(accuracy) + C.PitchStart.x, + playdate.easingFunctions.linear + ), y = gfx.animator.new(C.PitchFlyMs / 1.3, C.PitchStart.y, C.PitchEndY, playdate.easingFunctions.linear), } end, -- Curve ball function(accuracy) return { - x = gfx.animator.new(C.PitchFlyMs, getPitchMissBy(accuracy) + C.PitchStart.x + 20, C.PitchStart.x, utils.easingHill), + x = gfx.animator.new( + C.PitchFlyMs, + getPitchMissBy(accuracy) + C.PitchStart.x + 20, + C.PitchStart.x, + utils.easingHill + ), y = gfx.animator.new(C.PitchFlyMs, C.PitchStart.y, C.PitchEndY, playdate.easingFunctions.linear), } end, -- Slider function(accuracy) return { - x = gfx.animator.new(C.PitchFlyMs, getPitchMissBy(accuracy) + C.PitchStart.x - 20, C.PitchStart.x, utils.easingHill), + x = gfx.animator.new( + C.PitchFlyMs, + getPitchMissBy(accuracy) + C.PitchStart.x - 20, + C.PitchStart.x, + utils.easingHill + ), y = gfx.animator.new(C.PitchFlyMs, C.PitchStart.y, C.PitchEndY, playdate.easingFunctions.linear), } end, @@ -44,10 +59,11 @@ Pitches = { return { x = { currentValue = function() - return getPitchMissBy(accuracy) + C.PitchStart.x + (10 * math.sin((ball.yAnimator:currentValue() - C.PitchStart.y) / 10)) - end, - reset = function() + return getPitchMissBy(accuracy) + + C.PitchStart.x + + (10 * math.sin((ball.yAnimator:currentValue() - C.PitchStart.y) / 10)) end, + reset = function() end, }, y = gfx.animator.new(C.PitchFlyMs * 1.3, C.PitchStart.y, C.PitchEndY, playdate.easingFunctions.linear), }