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: all:
pdc src BatterUp.pdx 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, --- speed: number,
--- } --- }
import 'announcer.lua'
import 'graphics.lua' import 'graphics.lua'
import 'scoreboard.lua' import 'scoreboard.lua'
import 'utils.lua' import 'utils.lua'
@ -58,15 +59,16 @@ local DanceBounceCount <const> = 4
local FielderDanceAnimator <const> = gfx.animator.new(1, 10, 0, easingHill) local FielderDanceAnimator <const> = gfx.animator.new(1, 10, 0, easingHill)
FielderDanceAnimator.repeatCount = DanceBounceCount - 1 FielderDanceAnimator.repeatCount = DanceBounceCount - 1
-- selene: allow(unused_variable)
function fieldersDance() function fieldersDance()
FielderDanceAnimator:reset(DanceBounceMs) FielderDanceAnimator:reset(DanceBounceMs)
end end
local BallOffscreen <const> = 999 local BallOffscreen <const> = 999
local PitchFlyMs <const> = 1000 local PitchFlyMs <const> = 1050
local PitchStartX <const> = 200 local PitchStartX <const> = 195
local PitchStartY <const>, PitchEndY <const> = 90, 240 local PitchStartY <const>, PitchEndY <const> = 105, 240
local PitchesX <const> = { local PitchesX <const> = {
-- Fastball -- Fastball
@ -207,12 +209,17 @@ end
local batter = newRunner() local batter = newRunner()
--- "Throws" the ball from its current position to the given destination. --- "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 if not flyTimeMs then
flyTimeMs = distanceBetween(ball.x, ball.y, destX, destY) * 5 flyTimeMs = distanceBetween(ball.x, ball.y, destX, destY) * 5
end end
ball.heldBy = nil 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) ballAnimatorY = gfx.animator.new(flyTimeMs, ball.y, destY, easingFunc)
ballAnimatorX = gfx.animator.new(flyTimeMs, ball.x, destX, easingFunc) ballAnimatorX = gfx.animator.new(flyTimeMs, ball.x, destX, easingFunc)
if floaty then if floaty then
@ -305,12 +312,13 @@ function outRunner(runnerIndex)
outs = outs + 1 outs = outs + 1
outRunners[#outRunners + 1] = runners[runnerIndex] outRunners[#outRunners + 1] = runners[runnerIndex]
table.remove(runners, runnerIndex) table.remove(runners, runnerIndex)
fieldersDance()
updateForcedRunners() updateForcedRunners()
announcer:say("YOU'RE OUT!") announcer:say("YOU'RE OUT!")
if outs == 3 then if outs == 3 then
local gameOver = inning == 9 and teams.away.score ~= teams.home.score local gameOver = inning == 9 and teams.away.score ~= teams.home.score
if not gameOver then if not gameOver then
fieldersDance()
announcer:say("SWITCHING SIDES...") announcer:say("SWITCHING SIDES...")
end end
while #runners > 0 do while #runners > 0 do
@ -373,9 +381,15 @@ function updateFielders()
and touchedBase ~= touchingBaseCache.get(runner) and touchedBase ~= touchingBaseCache.get(runner)
then then
outRunner(i) outRunner(i)
playdate.timer.new(750, function()
tryToThrowOut(fielder)
end)
elseif not touchingBaseCache.get(runner) then elseif not touchingBaseCache.get(runner) then
if distanceBetween(runner.x, runner.y, fielder.x, fielder.y) < TagDistance then if distanceBetween(runner.x, runner.y, fielder.x, fielder.y) < TagDistance then
outRunner(i) outRunner(i)
playdate.timer.new(750, function()
tryToThrowOut(fielder)
end)
end end
end end
end end
@ -546,7 +560,14 @@ function updateBatting()
local ballDestX = ball.x + (ballVelX * HitMult) local ballDestX = ball.x + (ballVelX * HitMult)
local ballDestY = ball.y + (ballVelY * HitMult) local ballDestY = ball.y + (ballVelY * HitMult)
-- Hit! -- 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] fielders.first.target = Bases[First]
batter.nextBase = Bases[First] batter.nextBase = Bases[First]
@ -623,6 +644,9 @@ function updateGameState()
updateOutRunners() updateOutRunners()
end end
-- TODO
function drawMinimap() end
function playdate.update() function playdate.update()
playdate.timer.updateTimers() playdate.timer.updateTimers()
@ -630,20 +654,16 @@ function playdate.update()
gfx.animation.blinker.updateAll() gfx.animation.blinker.updateAll()
gfx.clear() gfx.clear()
gfx.setColor(gfx.kColorBlack)
local offsetX, offsetY = 0, 0
if ball.x < BallOffscreen then if ball.x < BallOffscreen then
-- TODO: Show baserunning minimap when panning? offsetX, offsetY = getDrawOffset(Screen.W, Screen.H, ball.x, ball.y)
local offsetX, offsetY = getDrawOffset(Screen.W, Screen.H, ball.x, ball.y)
gfx.setDrawOffset(offsetX, offsetY) gfx.setDrawOffset(offsetX, offsetY)
end end
GrassBackground:draw(-400, -240) GrassBackground:draw(-400, -240)
gfx.setColor(gfx.kColorBlack)
gfx.setLineWidth(2)
gfx.drawCircleAtPoint(ball.x, ball.y, ball.size)
local fielderDanceHeight = FielderDanceAnimator:currentValue() local fielderDanceHeight = FielderDanceAnimator:currentValue()
for _, fielder in pairs(fielders) do for _, fielder in pairs(fielders) do
gfx.fillRect(fielder.x, fielder.y - fielderDanceHeight, 14, 25) 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) gfx.drawLine(batBase.x, batBase.y, batTip.x, batTip.y)
end end
if playdate.isCrankDocked() then -- or (crankChange < 2 and currentMode == Modes.running) then if playdate.isCrankDocked() then
playdate.ui.crankIndicator:draw() playdate.ui.crankIndicator:draw()
end end
@ -667,7 +687,18 @@ function playdate.update()
PlayerFrown:draw(runner.x, runner.y) PlayerFrown:draw(runner.x, runner.y)
end 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) gfx.setDrawOffset(0, 0)
if offsetX > 0 or offsetY > 0 then
drawMinimap()
end
drawScoreboard(0, Screen.H * 0.77, teams, outs, battingTeam, inning) drawScoreboard(0, Screen.H * 0.77, teams, outs, battingTeam, inning)
announcer:draw(Center.x, 10) announcer:draw(Center.x, 10)
end end

View File

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