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.
This commit is contained in:
Sage Vaillancourt 2025-02-03 20:26:08 -05:00
parent 5d01769eb1
commit 6dd8469409
4 changed files with 109 additions and 41 deletions

View File

@ -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

62
src/announcer.lua Normal file
View File

@ -0,0 +1,62 @@
local AnnouncementFont <const> = playdate.graphics.font.new("fonts/Roobert-20-Medium.pft")
local AnnouncementTransitionMs <const> = 300
local AnnouncerMarginX <const> = 26
local AnnouncerAnimatorInY <const> =
playdate.graphics.animator.new(AnnouncementTransitionMs, -70, 0, playdate.easingFunctions.outBounce)
local AnnouncerAnimatorOutY <const> =
playdate.graphics.animator.new(AnnouncementTransitionMs, 0, -70, playdate.easingFunctions.outQuint)
-- selene: allow(unscoped_variables)
announcer = {
textQueue = {},
animatorY = AnnouncerAnimatorInY,
}
local DurationMs <const> = 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

View File

@ -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 <const> = 4
local FielderDanceAnimator <const> = gfx.animator.new(1, 10, 0, easingHill)
FielderDanceAnimator.repeatCount = DanceBounceCount - 1
-- selene: allow(unused_variable)
function fieldersDance()
FielderDanceAnimator:reset(DanceBounceMs)
end
local BallOffscreen <const> = 999
local PitchFlyMs <const> = 1000
local PitchStartX <const> = 200
local PitchStartY <const>, PitchEndY <const> = 90, 240
local PitchFlyMs <const> = 1050
local PitchStartX <const> = 195
local PitchStartY <const>, PitchEndY <const> = 105, 240
local PitchesX <const> = {
-- 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

View File

@ -137,28 +137,3 @@ function buildCache(fetcher)
end,
}
end
local AnnouncementFont <const> = 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