From 6dd8469409532c02069cc11bd68d0b39625c6c0a Mon Sep 17 00:00:00 2001 From: Sage Vaillancourt Date: Mon, 3 Feb 2025 20:26:08 -0500 Subject: [PATCH] Implement multi-out fielding. Extract announcer.lua Fielders now only dance at the end of the half-inning. Ball is now drawn over everything but the scoreboard and announcer. --- Makefile | 2 +- src/announcer.lua | 62 +++++++++++++++++++++++++++++++++++++++++++++++ src/main.lua | 61 ++++++++++++++++++++++++++++++++++------------ src/utils.lua | 25 ------------------- 4 files changed, 109 insertions(+), 41 deletions(-) create mode 100644 src/announcer.lua diff --git a/Makefile b/Makefile index 09674bf..0af9a72 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -SOURCE_FILES := src/utils.lua src/graphics.lua src/scoreboard.lua src/main.lua +SOURCE_FILES := src/utils.lua src/announcer.lua src/graphics.lua src/scoreboard.lua src/main.lua all: pdc src BatterUp.pdx diff --git a/src/announcer.lua b/src/announcer.lua new file mode 100644 index 0000000..e1ae328 --- /dev/null +++ b/src/announcer.lua @@ -0,0 +1,62 @@ +local AnnouncementFont = playdate.graphics.font.new("fonts/Roobert-20-Medium.pft") +local AnnouncementTransitionMs = 300 +local AnnouncerMarginX = 26 + +local AnnouncerAnimatorInY = + playdate.graphics.animator.new(AnnouncementTransitionMs, -70, 0, playdate.easingFunctions.outBounce) +local AnnouncerAnimatorOutY = + playdate.graphics.animator.new(AnnouncementTransitionMs, 0, -70, playdate.easingFunctions.outQuint) + +-- selene: allow(unscoped_variables) +announcer = { + textQueue = {}, + animatorY = AnnouncerAnimatorInY, +} + +local DurationMs = 3000 + +function announcer.popIn(self) + self.animatorY = AnnouncerAnimatorInY + self.animatorY:reset() + + playdate.timer.new(DurationMs, function() + self.animatorY = AnnouncerAnimatorOutY + self.animatorY:reset() + -- If this popIn() call was inside a timer, successive messages would be + -- allowed to transition out. However, the Out animation, shortly followed by + -- a new message popping in, is actually *more* jarring than the interrupt. + if #self.textQueue ~= 1 then + self:popIn() + table.remove(self.textQueue, 1) + else + playdate.timer.new(AnnouncementTransitionMs, function() + table.remove(self.textQueue, 1) + end) + end + end) +end + +function announcer.say(self, text) + self.textQueue[#self.textQueue + 1] = text + if #self.textQueue == 1 then + self:popIn() + end +end + +function announcer.draw(self, x, y) + if #self.textQueue == 0 then + return + end + x = x - 5 -- Infield center is slightly offset from screen center + + local gfx = playdate.graphics + local originalDrawMode = gfx.getImageDrawMode() + local width = math.max(150, (AnnouncerMarginX * 2) + AnnouncementFont:getTextWidth(self.textQueue[1])) + local animY = self.animatorY:currentValue() + + gfx.setColor(gfx.kColorBlack) + gfx.fillRect(x - (width / 2), y + animY, width, 50) + gfx.setImageDrawMode(gfx.kDrawModeInverted) + AnnouncementFont:drawTextAligned(self.textQueue[1], x, y + 10 + animY, kTextAlignment.center) + gfx.setImageDrawMode(originalDrawMode) +end diff --git a/src/main.lua b/src/main.lua index 0282181..156d979 100644 --- a/src/main.lua +++ b/src/main.lua @@ -33,6 +33,7 @@ import 'CoreLibs/ui.lua' --- speed: number, --- } +import 'announcer.lua' import 'graphics.lua' import 'scoreboard.lua' import 'utils.lua' @@ -58,15 +59,16 @@ local DanceBounceCount = 4 local FielderDanceAnimator = gfx.animator.new(1, 10, 0, easingHill) FielderDanceAnimator.repeatCount = DanceBounceCount - 1 +-- selene: allow(unused_variable) function fieldersDance() FielderDanceAnimator:reset(DanceBounceMs) end local BallOffscreen = 999 -local PitchFlyMs = 1000 -local PitchStartX = 200 -local PitchStartY , PitchEndY = 90, 240 +local PitchFlyMs = 1050 +local PitchStartX = 195 +local PitchStartY , PitchEndY = 105, 240 local PitchesX = { -- Fastball @@ -207,12 +209,17 @@ end local batter = newRunner() --- "Throws" the ball from its current position to the given destination. -function throwBall(destX, destY, easingFunc, flyTimeMs, floaty) +function throwBall(destX, destY, easingFunc, flyTimeMs, floaty, customBallScaler) if not flyTimeMs then flyTimeMs = distanceBetween(ball.x, ball.y, destX, destY) * 5 end ball.heldBy = nil - ballSizeAnimator:reset(flyTimeMs) + if customBallScaler then + ballSizeAnimator = customBallScaler + else + -- TODO? Scale based on distance? + ballSizeAnimator = gfx.animator.new(flyTimeMs, 9, 6, easingHill) + end ballAnimatorY = gfx.animator.new(flyTimeMs, ball.y, destY, easingFunc) ballAnimatorX = gfx.animator.new(flyTimeMs, ball.x, destX, easingFunc) if floaty then @@ -305,12 +312,13 @@ function outRunner(runnerIndex) outs = outs + 1 outRunners[#outRunners + 1] = runners[runnerIndex] table.remove(runners, runnerIndex) - fieldersDance() updateForcedRunners() + announcer:say("YOU'RE OUT!") if outs == 3 then local gameOver = inning == 9 and teams.away.score ~= teams.home.score if not gameOver then + fieldersDance() announcer:say("SWITCHING SIDES...") end while #runners > 0 do @@ -373,9 +381,15 @@ function updateFielders() and touchedBase ~= touchingBaseCache.get(runner) then outRunner(i) + playdate.timer.new(750, function() + tryToThrowOut(fielder) + end) elseif not touchingBaseCache.get(runner) then if distanceBetween(runner.x, runner.y, fielder.x, fielder.y) < TagDistance then outRunner(i) + playdate.timer.new(750, function() + tryToThrowOut(fielder) + end) end end end @@ -546,7 +560,14 @@ function updateBatting() local ballDestX = ball.x + (ballVelX * HitMult) local ballDestY = ball.y + (ballVelY * HitMult) -- Hit! - throwBall(ballDestX, ballDestY, playdate.easingFunctions.outQuint, 2000) + throwBall( + ballDestX, + ballDestY, + playdate.easingFunctions.outQuint, + 2000, + nil, + gfx.animator.new(2000, 9 + (mult * mult * 0.5), 6, easingHill) + ) fielders.first.target = Bases[First] batter.nextBase = Bases[First] @@ -623,6 +644,9 @@ function updateGameState() updateOutRunners() end +-- TODO +function drawMinimap() end + function playdate.update() playdate.timer.updateTimers() @@ -630,20 +654,16 @@ function playdate.update() gfx.animation.blinker.updateAll() gfx.clear() + gfx.setColor(gfx.kColorBlack) + local offsetX, offsetY = 0, 0 if ball.x < BallOffscreen then - -- TODO: Show baserunning minimap when panning? - local offsetX, offsetY = getDrawOffset(Screen.W, Screen.H, ball.x, ball.y) + offsetX, offsetY = getDrawOffset(Screen.W, Screen.H, ball.x, ball.y) gfx.setDrawOffset(offsetX, offsetY) end GrassBackground:draw(-400, -240) - gfx.setColor(gfx.kColorBlack) - gfx.setLineWidth(2) - - gfx.drawCircleAtPoint(ball.x, ball.y, ball.size) - local fielderDanceHeight = FielderDanceAnimator:currentValue() for _, fielder in pairs(fielders) do gfx.fillRect(fielder.x, fielder.y - fielderDanceHeight, 14, 25) @@ -654,7 +674,7 @@ function playdate.update() gfx.drawLine(batBase.x, batBase.y, batTip.x, batTip.y) end - if playdate.isCrankDocked() then -- or (crankChange < 2 and currentMode == Modes.running) then + if playdate.isCrankDocked() then playdate.ui.crankIndicator:draw() end @@ -667,7 +687,18 @@ function playdate.update() PlayerFrown:draw(runner.x, runner.y) end + gfx.setLineWidth(2) + + gfx.setColor(gfx.kColorWhite) + gfx.fillCircleAtPoint(ball.x, ball.y, ball.size) + + gfx.setColor(gfx.kColorBlack) + gfx.drawCircleAtPoint(ball.x, ball.y, ball.size) + gfx.setDrawOffset(0, 0) + if offsetX > 0 or offsetY > 0 then + drawMinimap() + end drawScoreboard(0, Screen.H * 0.77, teams, outs, battingTeam, inning) announcer:draw(Center.x, 10) end diff --git a/src/utils.lua b/src/utils.lua index bfb7ee1..bce070d 100644 --- a/src/utils.lua +++ b/src/utils.lua @@ -137,28 +137,3 @@ function buildCache(fetcher) end, } end - -local AnnouncementFont = playdate.graphics.font.new("fonts/Roobert-20-Medium.pft") - --- selene: allow(unscoped_variables) -announcer = { - textQueue = {}, -} - -function announcer.say(self, text, durationMs) - self.textQueue[#self.textQueue + 1] = text - -- Could cause some timing funk if messages are queued up asyncronously. - -- I.e. `:say("hello")` <1.5 seconds pass> `:say("hello")` would result - -- in the second message being displayed for 4.5 seconds instead of just 3. - durationMs = durationMs and durationMs or (3000 * #self.textQueue) - playdate.timer.new(durationMs, function() - table.remove(self.textQueue, 1) - end) -end - -function announcer.draw(self, x, y) - if #self.textQueue == 0 then - return - end - AnnouncementFont:drawTextAligned(self.textQueue[1], x, y, kTextAlignment.center) -end