Implementing walks and strike-outs

* Look at limiting batting/throw power with math.log()
* Extract draw/fielder.lua
* Extract some values into constants.lua
* Extract npc.lua for computer batting (and eventually probably more CPU behavior)
* pitchTracker in utils
This commit is contained in:
Sage Vaillancourt 2025-02-07 20:29:40 -05:00
parent 969de111fe
commit 881ff0e734
8 changed files with 226 additions and 145 deletions

View File

@ -1,4 +1,4 @@
SOURCE_FILES := src/utils.lua src/dbg.lua src/announcer.lua src/graphics.lua src/scoreboard.lua src/main.lua SOURCE_FILES := src/constants.lua src/draw/* src/utils.lua src/dbg.lua src/npc.lua src/announcer.lua src/graphics.lua src/scoreboard.lua src/main.lua
all: all:
pdc src BatterUp.pdx pdc src BatterUp.pdx

View File

@ -1,3 +1,6 @@
-- selene: allow(shadowing)
local gfx = playdate.graphics
local AnnouncementFont <const> = playdate.graphics.font.new("fonts/Roobert-20-Medium.pft") local AnnouncementFont <const> = playdate.graphics.font.new("fonts/Roobert-20-Medium.pft")
local AnnouncementTransitionMs <const> = 300 local AnnouncementTransitionMs <const> = 300
local AnnouncerMarginX <const> = 26 local AnnouncerMarginX <const> = 26
@ -49,7 +52,6 @@ function announcer.draw(self, x, y)
end end
x = x - 5 -- Infield center is slightly offset from screen center x = x - 5 -- Infield center is slightly offset from screen center
local gfx = playdate.graphics
local originalDrawMode = gfx.getImageDrawMode() local originalDrawMode = gfx.getImageDrawMode()
local width = math.max(150, (AnnouncerMarginX * 2) + AnnouncementFont:getTextWidth(self.textQueue[1])) local width = math.max(150, (AnnouncerMarginX * 2) + AnnouncementFont:getTextWidth(self.textQueue[1]))
local animY = self.animatorY:currentValue() local animY = self.animatorY:currentValue()

13
src/constants.lua Normal file
View File

@ -0,0 +1,13 @@
-- selene: allow(unscoped_variables)
C = {}
C.Screen = {
W = playdate.display.getWidth(),
H = playdate.display.getHeight(),
}
C.Center = utils.xy(C.Screen.W / 2, C.Screen.H / 2)
C.StrikeZoneStartX = C.Center.x - 16
C.StrikeZoneEndX = C.StrikeZoneStartX + 24
C.StrikeZoneStartY = C.Screen.H - 35

29
src/draw/fielder.lua Normal file
View File

@ -0,0 +1,29 @@
-- selene: allow(shadowing)
local gfx = playdate.graphics
local Glove <const> = playdate.graphics.image.new("images/game/glove.png") --[[@as pd_image]]
local GloveSizeX, GloveSizeY <const> = Glove:getSize()
local GloveHoldingBall <const> = playdate.graphics.image.new("images/game/glove-holding-ball.png") --[[@as pd_image]]
local GloveOffX, GloveOffY <const> = GloveSizeX / 2, GloveSizeY / 2
---@param fielderX number
---@param fielderY number
---@return boolean isHoldingBall
local function drawFielderGlove(ball, fielderX, fielderY)
local distanceFromBall = utils.distanceBetweenZ(fielderX, fielderY, 0, ball.x, ball.y, ball.z)
local shoulderX, shoulderY = fielderX + 10, fielderY + 5
if distanceFromBall > 20 then
Glove:draw(shoulderX, shoulderY)
return false
else
GloveHoldingBall:draw(ball.x - GloveOffX, ball.y - GloveOffY)
return true
end
end
---@return boolean isHoldingBall
function drawFielder(ball, x, y)
gfx.fillRect(x, y, 14, 25)
return drawFielderGlove(ball, x, y)
end

View File

@ -34,22 +34,20 @@ import 'CoreLibs/ui.lua'
--- @alias EasingFunc fun(number, number, number, number): number --- @alias EasingFunc fun(number, number, number, number): number
import 'utils.lua'
import 'constants.lua'
import 'announcer.lua' import 'announcer.lua'
import 'dbg.lua' import 'dbg.lua'
import 'graphics.lua' import 'graphics.lua'
import 'npc.lua'
import 'scoreboard.lua' import 'scoreboard.lua'
import 'utils.lua' import 'draw/fielder'
-- stylua: ignore end -- stylua: ignore end
-- selene: allow(shadowing)
local gfx <const> = playdate.graphics local gfx <const> = playdate.graphics
local Screen <const> = {
W = playdate.display.getWidth(),
H = playdate.display.getHeight(),
}
local Center <const> = utils.xy(Screen.W / 2, Screen.H / 2)
local BootTune <const> = playdate.sound.sampleplayer.new("sounds/boot-tune.wav") local BootTune <const> = playdate.sound.sampleplayer.new("sounds/boot-tune.wav")
-- local BootTune <const> = playdate.sound.sampleplayer.new("sounds/boot-tune-organy.wav") -- local BootTune <const> = playdate.sound.sampleplayer.new("sounds/boot-tune-organy.wav")
local TinnyBackground <const> = playdate.sound.sampleplayer.new("sounds/tinny-background.wav") local TinnyBackground <const> = playdate.sound.sampleplayer.new("sounds/tinny-background.wav")
@ -60,11 +58,6 @@ local PlayerFrown <const> = gfx.image.new("images/game/player-frown.png") --[[@a
local PlayerSmile <const> = gfx.image.new("images/game/player.png") --[[@as pd_image]] local PlayerSmile <const> = gfx.image.new("images/game/player.png") --[[@as pd_image]]
local PlayerBack <const> = gfx.image.new("images/game/player-back.png") --[[@as pd_image]] local PlayerBack <const> = gfx.image.new("images/game/player-back.png") --[[@as pd_image]]
local Glove <const> = gfx.image.new("images/game/glove.png") --[[@as pd_image]]
local GloveHoldingBall <const> = gfx.image.new("images/game/glove-holding-ball.png") --[[@as pd_image]]
local GloveSizeX, GloveSizeY <const> = Glove:getSize()
local GloveOffX, GloveOffY <const> = GloveSizeX / 2, GloveSizeY / 2
local PlayerImageBlipper <const> = blipper.new(100, "images/game/player.png", "images/game/player-lowhat.png") local PlayerImageBlipper <const> = blipper.new(100, "images/game/player.png", "images/game/player-lowhat.png")
local DanceBounceMs <const> = 500 local DanceBounceMs <const> = 500
@ -122,7 +115,7 @@ local Pitches <const> = {
local CrankOffsetDeg <const> = 90 local CrankOffsetDeg <const> = 90
local BatOffset <const> = utils.xy(10, 25) local BatOffset <const> = utils.xy(10, 25)
local batBase <const> = utils.xy(Center.x - 34, 215) local batBase <const> = utils.xy(C.Center.x - 34, 215)
local batTip <const> = utils.xy(0, 0) local batTip <const> = utils.xy(0, 0)
local TagDistance <const> = 15 local TagDistance <const> = 15
@ -130,8 +123,9 @@ local TagDistance <const> = 15
local SmallestBallRadius <const> = 6 local SmallestBallRadius <const> = 6
local ball <const> = { local ball <const> = {
x = Center.x --[[@as number]], x = C.Center.x --[[@as number]],
y = Center.y --[[@as number]], y = C.Center.y --[[@as number]],
z = 0,
size = SmallestBallRadius, size = SmallestBallRadius,
heldBy = nil --[[@type Runner | nil]], heldBy = nil --[[@type Runner | nil]],
} }
@ -141,6 +135,7 @@ local BatLength <const> = 50
local Offense <const> = { local Offense <const> = {
batting = {}, batting = {},
running = {}, running = {},
walking = {},
} }
local Sides <const> = { local Sides <const> = {
@ -156,11 +151,11 @@ local offenseMode = Offense.batting
local teams <const> = { local teams <const> = {
home = { home = {
score = 0, score = 0,
benchPosition = utils.xy(Screen.W + 10, Center.y), benchPosition = utils.xy(C.Screen.W + 10, C.Center.y),
}, },
away = { away = {
score = 0, score = 0,
benchPosition = utils.xy(-10, Center.y), benchPosition = utils.xy(-10, C.Center.y),
}, },
} }
@ -194,10 +189,10 @@ local First <const>, Second <const>, Third <const>, Home <const> = 1, 2, 3, 4
---@type Base[] ---@type Base[]
local Bases = { local Bases = {
utils.xy(Screen.W * 0.93, Screen.H * 0.52), utils.xy(C.Screen.W * 0.93, C.Screen.H * 0.52),
utils.xy(Screen.W * 0.47, Screen.H * 0.19), utils.xy(C.Screen.W * 0.47, C.Screen.H * 0.19),
utils.xy(Screen.W * 0.03, Screen.H * 0.52), utils.xy(C.Screen.W * 0.03, C.Screen.H * 0.52),
utils.xy(Screen.W * 0.474, Screen.H * 0.79), utils.xy(C.Screen.W * 0.474, C.Screen.H * 0.79),
} }
-- Pseudo-base for batter to target -- Pseudo-base for batter to target
@ -230,13 +225,13 @@ local fielders <const> = {
pitcher = newFielder("Pitcher", 30), pitcher = newFielder("Pitcher", 30),
catcher = newFielder("Catcher", 35), catcher = newFielder("Catcher", 35),
left = newFielder("Left", 40), left = newFielder("Left", 40),
center = newFielder("Center", 40), center = newFielder("C.Center", 40),
right = newFielder("Right", 40), right = newFielder("Right", 40),
} }
local PitcherStartPos <const> = { local PitcherStartPos <const> = {
x = Screen.W * 0.48, x = C.Screen.W * 0.48,
y = Screen.H * 0.40, y = C.Screen.H * 0.40,
} }
--- Actually only benches the infield, because outfielders are far away! --- Actually only benches the infield, because outfielders are far away!
@ -260,15 +255,15 @@ function resetFielderPositions(fromOffTheField)
end end
end end
fielders.first.target = utils.xy(Screen.W - 65, Screen.H * 0.48) fielders.first.target = utils.xy(C.Screen.W - 65, C.Screen.H * 0.48)
fielders.second.target = utils.xy(Screen.W * 0.70, Screen.H * 0.30) fielders.second.target = utils.xy(C.Screen.W * 0.70, C.Screen.H * 0.30)
fielders.shortstop.target = utils.xy(Screen.W * 0.30, Screen.H * 0.30) fielders.shortstop.target = utils.xy(C.Screen.W * 0.30, C.Screen.H * 0.30)
fielders.third.target = utils.xy(Screen.W * 0.1, Screen.H * 0.48) fielders.third.target = utils.xy(C.Screen.W * 0.1, C.Screen.H * 0.48)
fielders.pitcher.target = utils.xy(PitcherStartPos.x, PitcherStartPos.y) fielders.pitcher.target = utils.xy(PitcherStartPos.x, PitcherStartPos.y)
fielders.catcher.target = utils.xy(Screen.W * 0.475, Screen.H * 0.92) fielders.catcher.target = utils.xy(C.Screen.W * 0.475, C.Screen.H * 0.92)
fielders.left.target = utils.xy(Screen.W * -1, Screen.H * -0.2) fielders.left.target = utils.xy(C.Screen.W * -1, C.Screen.H * -0.2)
fielders.center.target = utils.xy(Center.x, Screen.H * -0.4) fielders.center.target = utils.xy(C.Center.x, C.Screen.H * -0.4)
fielders.right.target = utils.xy(Screen.W * 2, fielders.left.target.y) fielders.right.target = utils.xy(C.Screen.W * 2, fielders.left.target.y)
end end
local BatterStartingX <const> = Bases[Home].x - 40 local BatterStartingX <const> = Bases[Home].x - 40
@ -409,15 +404,22 @@ local ResetFieldersAfterSeconds <const> = 5
-- TODO: Replace with a timer, repeatedly reset, instead of setting to 0 -- TODO: Replace with a timer, repeatedly reset, instead of setting to 0
local secondsSinceLastRunnerMove = 0 local secondsSinceLastRunnerMove = 0
---@param runnerIndex integer ---@param runner integer | Runner
function outRunner(runnerIndex) function outRunner(runner, message)
outRunners[#outRunners + 1] = runners[runnerIndex] if type(runner) ~= "number" then
table.remove(runners, runnerIndex) for i, maybe in ipairs(runners) do
if runner == maybe then
runner = i
end
end
end
outRunners[#outRunners + 1] = runners[runner]
table.remove(runners, runner)
outs = outs + 1 outs = outs + 1
updateForcedRunners() updateForcedRunners()
announcer:say("YOU'RE OUT!") announcer:say(message or "YOU'RE OUT!")
if outs == 3 then if outs == 3 then
local currentlyFieldingTeam = battingTeam == teams.home and teams.away or teams.home local currentlyFieldingTeam = battingTeam == teams.home and teams.away or teams.home
local gameOver = inning == 9 and teams.away.score ~= teams.home.score local gameOver = inning == 9 and teams.away.score ~= teams.home.score
@ -437,6 +439,7 @@ function outRunner(runnerIndex)
if gameOver then if gameOver then
announcer:say("AND THAT'S THE BALL GAME!") announcer:say("AND THAT'S THE BALL GAME!")
else else
resetFielderPositions()
if battingTeam == teams.home then if battingTeam == teams.home then
inning = inning + 1 inning = inning + 1
end end
@ -665,53 +668,34 @@ end
---@type number ---@type number
local batAngleDeg local batAngleDeg
-- Used for tracking whether or not a pitch was a strike
local recordedPitchX = nil
local balls = 0
local strikes = 0
local StrikeZoneStartX <const> = Center.x - 16
local StrikeZoneEndX <const> = StrikeZoneStartX + 24
local StrikeZoneStartY <const> = Screen.H - 35
function recordStrikePosition()
if not recordedPitchX and ball.y > StrikeZoneStartY then
recordedPitchX = ball.x
end
end
function nextBatter() function nextBatter()
batter = nil
playdate.timer.new(2000, function() playdate.timer.new(2000, function()
balls = 0 pitchTracker:reset()
strikes = 0 if not batter then
batter = newRunner() batter = newRunner()
end
end) end)
end end
function walk() function walk()
-- TODO announcer:say("Walk!")
fielders.first.target = Bases[First]
batter.nextBase = Bases[First]
batter.prevBase = Bases[Home]
offenseMode = Offense.walking
batter = nil
updateForcedRunners()
nextBatter() nextBatter()
end end
function strikeOut() function strikeOut()
-- TODO local outBatter = batter
batter = nil
outRunner(outBatter --[[@as Runner]], "Strike out!")
nextBatter() nextBatter()
end end
function recordPitch()
if recordedPitchX > StrikeZoneStartX and recordedPitchX < StrikeZoneEndX then
strikes = strikes + 1
if strikes >= 3 then
strikeOut()
end
else
balls = balls + 1
if balls >= 4 then
walk()
end
end
end
---@param batDeg number ---@param batDeg number
function updateBatting(batDeg, batSpeed) function updateBatting(batDeg, batSpeed)
if ball.y < BallOffscreen then if ball.y < BallOffscreen then
@ -728,7 +712,7 @@ function updateBatting(batDeg, batSpeed)
if if
batSpeed > 0 batSpeed > 0
and utils.pointDirectlyUnderLine(ball.x, ball.y, batBase.x, batBase.y, batTip.x, batTip.y, Screen.H) and utils.pointDirectlyUnderLine(ball.x, ball.y, batBase.x, batBase.y, batTip.x, batTip.y, C.Screen.H)
and ball.y < 232 --not isTouchingBall(fielders.catcher.x, fielders.catcher.y) and ball.y < 232 --not isTouchingBall(fielders.catcher.x, fielders.catcher.y)
then then
BatCrackSound:play() BatCrackSound:play()
@ -770,14 +754,14 @@ end
--- Returns true only if at least one of the given runners moved during this update --- Returns true only if at least one of the given runners moved during this update
---@param appliedSpeed number ---@param appliedSpeed number
---@return boolean ---@return boolean
function updateRunning(appliedSpeed) function updateRunning(appliedSpeed, forcedOnly)
ball.size = ballSizeAnimator:currentValue() ball.size = ballSizeAnimator:currentValue()
local runnerMoved = false local runnerMoved = false
-- TODO: Filter for the runner closest to the currently-held direction button -- TODO: Filter for the runner closest to the currently-held direction button
for runnerIndex, runner in ipairs(runners) do for runnerIndex, runner in ipairs(runners) do
if runner ~= batter then if runner ~= batter and (not forcedOnly or runner.forcedTo) then
runnerMoved = updateRunner(runner, runnerIndex, appliedSpeed) or runnerMoved runnerMoved = updateRunner(runner, runnerIndex, appliedSpeed) or runnerMoved
end end
end end
@ -787,7 +771,7 @@ end
function walkAwayOutRunners() function walkAwayOutRunners()
for i, runner in ipairs(outRunners) do for i, runner in ipairs(outRunners) do
if runner.x < Screen.W + 50 and runner.y < Screen.H + 50 then if runner.x < C.Screen.W + 50 and runner.y < C.Screen.H + 50 then
runner.x = runner.x + (deltaSeconds * 25) runner.x = runner.x + (deltaSeconds * 25)
runner.y = runner.y + (deltaSeconds * 25) runner.y = runner.y + (deltaSeconds * 25)
else else
@ -810,76 +794,66 @@ function playerPitch(throwFly)
end end
end end
local npcBatDeg = 0
local BaseNpcBatSpeed <const> = 1500
local npcBatSpeed = 1500
function npcBatAngle()
if not catcherThrownBall and ball.y > 200 and ball.y < 230 and (ball.x < Center.x + 15) then
npcBatDeg = npcBatDeg + (deltaSeconds * npcBatSpeed)
else
npcBatSpeed = (1 + math.random()) * BaseNpcBatSpeed
npcBatDeg = 200
end
return npcBatDeg
end
function npcBatChange()
return deltaSeconds * npcBatSpeed
end
function npcRunningSpeed()
if #runners == 0 then
return 0
end
local touchedBase = isTouchingBase(runners[1].x, runners[1].y)
if not touchedBase or touchedBase == Bases[Home] then
return 10
end
return 0
end
function updateGameState() function updateGameState()
deltaSeconds = playdate.getElapsedTime() or 0 deltaSeconds = playdate.getElapsedTime() or 0
playdate.resetElapsedTime() playdate.resetElapsedTime()
local crankChange = playdate.getCrankChange() --[[@as number, number]] local crankChange = playdate.getCrankChange() --[[@as number]]
local crankLimited = crankChange == 0 and 0 or (math.log(math.abs(crankChange)) * 10)
if crankChange < 0 then
crankLimited = crankLimited * -1
end
if ball.heldBy then if ball.heldBy then
ball.x = ball.heldBy.x ball.x = ball.heldBy.x
ball.y = ball.heldBy.y ball.y = ball.heldBy.y
else else
ball.x = ballAnimatorX:currentValue() ball.x = ballAnimatorX:currentValue()
ball.y = ballAnimatorY:currentValue() + ballFloatAnimator:currentValue() ball.z = ballFloatAnimator:currentValue()
ball.y = ballAnimatorY:currentValue() + ball.z
end end
local playerOnOffense, playerOnDefense = playerIsOn(Sides.offense) local playerOnOffense, playerOnDefense = playerIsOn(Sides.offense)
if playerOnDefense then if playerOnDefense then
throwMeter = math.max(0, throwMeter - (deltaSeconds * 150)) throwMeter = math.max(0, throwMeter - (deltaSeconds * 150))
throwMeter = throwMeter + math.abs(crankChange) throwMeter = throwMeter + math.abs(crankLimited)
end end
if offenseMode == Offense.batting then if offenseMode == Offense.batting then
recordStrikePosition() if ball.y < C.StrikeZoneStartY then
pitchTracker.recordedPitchX = nil
elseif not pitchTracker.recordedPitchX then
pitchTracker.recordedPitchX = ball.x
end
if utils.distanceBetween(fielders.pitcher.x, fielders.pitcher.y, PitchStartX, PitchStartY) < BaseHitbox then
secondsSincePitchAllowed = secondsSincePitchAllowed + deltaSeconds secondsSincePitchAllowed = secondsSincePitchAllowed + deltaSeconds
end
if secondsSincePitchAllowed > 3.5 and not catcherThrownBall then if secondsSincePitchAllowed > 3.5 and not catcherThrownBall then
recordPitch() local outcome = pitchTracker:updatePitchCounts()
if outcome == PitchOutcomes.StrikeOut then
strikeOut()
elseif outcome == PitchOutcomes.Walk then
walk()
end
throwBall(PitchStartX, PitchStartY, playdate.easingFunctions.linear, nil, true) throwBall(PitchStartX, PitchStartY, playdate.easingFunctions.linear, nil, true)
catcherThrownBall = true catcherThrownBall = true
end end
local batSpeed local batSpeed
if playerOnOffense then if playerOnOffense then
batAngleDeg, batSpeed = (playdate.getCrankPosition() + CrankOffsetDeg) % 360, crankChange batAngleDeg = (playdate.getCrankPosition() + CrankOffsetDeg) % 360
batSpeed = crankLimited
else else
batAngleDeg, batSpeed = npcBatAngle(), npcBatChange() batAngleDeg = npc.updateBatAngle(ball, catcherThrownBall, deltaSeconds)
batSpeed = npc.batSpeed() * deltaSeconds
end end
updateBatting(batAngleDeg, batSpeed) updateBatting(batAngleDeg, batSpeed)
-- TODO: Ensure batter can't be nil, here -- TODO: Ensure batter can't be nil, here
updateRunner(batter, nil, crankChange) updateRunner(batter, nil, crankLimited)
if secondsSincePitchAllowed > PitchAfterSeconds then if secondsSincePitchAllowed > PitchAfterSeconds then
if playerOnDefense then if playerOnDefense then
@ -892,7 +866,7 @@ function updateGameState()
end end
end end
elseif offenseMode == Offense.running then elseif offenseMode == Offense.running then
local appliedSpeed = playerOnOffense and crankChange or npcRunningSpeed() local appliedSpeed = playerOnOffense and crankLimited or npc.runningSpeed(Bases, runners)
if updateRunning(appliedSpeed) then if updateRunning(appliedSpeed) then
secondsSinceLastRunnerMove = 0 secondsSinceLastRunnerMove = 0
else else
@ -906,6 +880,11 @@ function updateGameState()
end end
end end
end end
elseif offenseMode == Offense.walking then
updateForcedRunners()
if not updateRunning(10, true) then
offenseMode = Offense.batting
end
end end
for _, fielder in pairs(fielders) do for _, fielder in pairs(fielders) do
@ -915,11 +894,11 @@ function updateGameState()
end end
local MinimapSizeX, MinimapSizeY <const> = Minimap:getSize() local MinimapSizeX, MinimapSizeY <const> = Minimap:getSize()
local MinimapPosX, MinimapPosY = Screen.W - MinimapSizeX, Screen.H - MinimapSizeY local MinimapPosX, MinimapPosY = C.Screen.W - MinimapSizeX, C.Screen.H - MinimapSizeY
local FieldHeight <const> = Bases[Home].y - Bases[Second].y local FieldHeight <const> = Bases[Home].y - Bases[Second].y
local MinimapMultX <const> = 0.75 * MinimapSizeX / Screen.W local MinimapMultX <const> = 0.75 * MinimapSizeX / C.Screen.W
local MinimapOffsetX <const> = MinimapPosX + 5 local MinimapOffsetX <const> = MinimapPosX + 5
local MinimapMultY <const> = 0.70 * MinimapSizeY / FieldHeight local MinimapMultY <const> = 0.70 * MinimapSizeY / FieldHeight
local MinimapOffsetY <const> = MinimapPosY - 15 local MinimapOffsetY <const> = MinimapPosY - 15
@ -934,21 +913,6 @@ function drawMinimap()
end end
end end
---@param fielder Fielder
---@return boolean isHoldingBall
function drawFielderGlove(fielder)
local distanceFromBall =
utils.distanceBetweenZ(fielder.x, fielder.y, 0, ball.x, ball.y, ballFloatAnimator:currentValue())
local shoulderX, shoulderY = fielder.x + 10, fielder.y + FielderDanceAnimator:currentValue() + 5
if distanceFromBall > 20 then
Glove:draw(shoulderX, shoulderY)
return false
else
GloveHoldingBall:draw(ball.x - GloveOffX, ball.y - GloveOffY)
return true
end
end
function playdate.update() function playdate.update()
playdate.timer.updateTimers() playdate.timer.updateTimers()
gfx.animation.blinker.updateAll() gfx.animation.blinker.updateAll()
@ -959,7 +923,7 @@ function playdate.update()
local offsetX, offsetY = 0, 0 local offsetX, offsetY = 0, 0
if ball.x < BallOffscreen then if ball.x < BallOffscreen then
offsetX, offsetY = getDrawOffset(Screen.W, Screen.H, ball.x, ball.y) offsetX, offsetY = getDrawOffset(C.Screen.W, C.Screen.H, ball.x, ball.y)
gfx.setDrawOffset(offsetX, offsetY) gfx.setDrawOffset(offsetX, offsetY)
end end
@ -968,9 +932,7 @@ function playdate.update()
local fielderDanceHeight = FielderDanceAnimator:currentValue() local fielderDanceHeight = FielderDanceAnimator:currentValue()
local ballIsHeld = false local ballIsHeld = false
for _, fielder in pairs(fielders) do for _, fielder in pairs(fielders) do
local fielderY = fielder.y + fielderDanceHeight ballIsHeld = drawFielder(ball, fielder.x, fielder.y + fielderDanceHeight) or ballIsHeld
gfx.fillRect(fielder.x, fielderY, 14, 25)
ballIsHeld = drawFielderGlove(fielder) or ballIsHeld
end end
if offenseMode == Offense.batting then if offenseMode == Offense.batting then
@ -1013,9 +975,9 @@ function playdate.update()
if math.abs(offsetX) > 10 or math.abs(offsetY) > 10 then if math.abs(offsetX) > 10 or math.abs(offsetY) > 10 then
drawMinimap() drawMinimap()
end end
drawScoreboard(0, Screen.H * 0.77, teams, outs, battingTeam, inning) drawScoreboard(0, C.Screen.H * 0.77, teams, outs, battingTeam, inning)
drawBallsAndStrikes(290, Screen.H - 20, balls, strikes) drawBallsAndStrikes(290, C.Screen.H - 20, pitchTracker.balls, pitchTracker.strikes)
announcer:draw(Center.x, 10) announcer:draw(C.Center.x, 10)
end end
function init() function init()

35
src/npc.lua Normal file
View File

@ -0,0 +1,35 @@
local npcBatDeg = 0
local BaseNpcBatSpeed <const> = 1500
local npcBatSpeed = 1500
-- selene: allow(unscoped_variables)
npc = {}
function npc.updateBatAngle(ball, catcherThrownBall, deltaSec)
if not catcherThrownBall and ball.y > 200 and ball.y < 230 and (ball.x < C.Center.x + 15) then
npcBatDeg = npcBatDeg + (deltaSec * npcBatSpeed)
else
npcBatSpeed = (1 + math.random()) * BaseNpcBatSpeed
npcBatDeg = 200
end
return npcBatDeg
end
function npc.batSpeed()
return npcBatSpeed
end
function npc.runningSpeed(baseConstants, runners)
if #runners == 0 then
return 0
end
local touchedBase = isTouchingBase(runners[1].x, runners[1].y)
if not touchedBase or touchedBase == baseConstants[4] then
return 10
end
return 0
end
if not playdate then
return utils
end

View File

@ -1,3 +1,6 @@
-- selene: allow(shadowing)
local gfx = playdate.graphics
local ScoreFont <const> = playdate.graphics.font.new("fonts/font-full-circle.pft") local ScoreFont <const> = playdate.graphics.font.new("fonts/font-full-circle.pft")
local OutBubbleRadius <const> = 5 local OutBubbleRadius <const> = 5
local ScoreboardMarginX <const> = 6 local ScoreboardMarginX <const> = 6
@ -25,7 +28,6 @@ function drawBallsAndStrikes(x, y, balls, strikes)
return return
end end
local gfx = playdate.graphics
gfx.setColor(gfx.kColorBlack) gfx.setColor(gfx.kColorBlack)
gfx.fillRect(x, y, BallStrikeWidth, BallStrikeHeight) gfx.fillRect(x, y, BallStrikeWidth, BallStrikeHeight)
local originalDrawMode = gfx.getImageDrawMode() local originalDrawMode = gfx.getImageDrawMode()
@ -39,7 +41,6 @@ function drawBallsAndStrikes(x, y, balls, strikes)
end end
function drawScoreboard(x, y, teams, outs, battingTeam, inning) function drawScoreboard(x, y, teams, outs, battingTeam, inning)
local gfx = playdate.graphics
local homeScore = teams.home.score local homeScore = teams.home.score
local awayScore = teams.away.score local awayScore = teams.away.score

View File

@ -142,8 +142,47 @@ function utils.getNearestOf(array, x, y, extraCondition)
return nearest, nearestDistance return nearest, nearestDistance
end end
---@alias Cache { get: fun(key: `Key`): `Value` } -- selene: allow(unscoped_variables)
PitchOutcomes = {
StrikeOut = {},
Walk = {},
}
-- selene: allow(unscoped_variables)
pitchTracker = {
--- Position of the pitch, or nil, if one has not been recorded.
---@type number | nil
recordedPitchX = nil,
strikes = 0,
balls = 0,
reset = function(self)
self.strikes = 0
self.balls = 0
end,
updatePitchCounts = function(self)
if not self.recordedPitchX then
return
end
if self.recordedPitchX > C.StrikeZoneStartX and self.recordedPitchX < C.StrikeZoneEndX then
self.strikes = self.strikes + 1
if self.strikes >= 3 then
self:reset()
return PitchOutcomes.StrikeOut
end
else
self.balls = self.balls + 1
if self.balls >= 4 then
self:reset()
return PitchOutcomes.Walk
end
end
end,
}
if not playdate then if not playdate then
return utils return utils, { pitchTracker = pitchTracker, PitchOutcomes = PitchOutcomes }
end end