Fix some NPC positioning.

Basic foul-ball implementation.
Some balance tweaks. Generally faster play.
Move pitchMeter to utils.
Player -> User when talking about the human player instead of a baseball player.
Slightly delay scoreboard changes.
Animate ball-strike entry and exit.
This commit is contained in:
Sage Vaillancourt 2025-02-10 21:22:21 -05:00
parent 534a16ad67
commit f67d6262ac
8 changed files with 199 additions and 65 deletions

View File

@ -44,7 +44,7 @@ function Ball:launch(destX, destY, easingFunc, flyTimeMs, floaty, customBallScal
self.heldBy = nil self.heldBy = nil
if not flyTimeMs then if not flyTimeMs then
flyTimeMs = utils.distanceBetween(self.x, self.y, destX, destY) * 5 flyTimeMs = utils.distanceBetween(self.x, self.y, destX, destY) * C.DefaultLaunchPower
end end
if customBallScaler then if customBallScaler then

View File

@ -209,7 +209,7 @@ 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 someRunnerMoved, number runnersScored ---@return boolean someRunnerMoved, number runnersScored
function Baserunning:updateRunning(appliedSpeed, forcedOnly, deltaSeconds) function Baserunning:updateNonBatterRunners(appliedSpeed, forcedOnly, deltaSeconds)
local someRunnerMoved = false local someRunnerMoved = false
local runnersScored = 0 local runnersScored = 0

View File

@ -30,7 +30,7 @@ C.FieldHeight = C.Bases[C.Home].y - C.Bases[C.Second].y
-- Pseudo-base for batter to target -- Pseudo-base for batter to target
C.RightHandedBattersBox = { C.RightHandedBattersBox = {
x = C.Bases[C.Home].x - 35, x = C.Bases[C.Home].x - 30,
y = C.Bases[C.Home].y, y = C.Bases[C.Home].y,
} }
@ -42,21 +42,40 @@ C.NextBaseMap = {
[C.Bases[C.Third]] = C.Bases[C.Home], [C.Bases[C.Third]] = C.Bases[C.Home],
} }
C.LeftFoulLine = {
x1 = C.Center.x,
y1 = 220,
x2 = -1800,
y2 = -465,
}
C.RightFoulLine = {
x1 = C.Center.x,
y1 = 218,
x2 = 2120,
y2 = -465,
}
--- Angle to align the bat to --- Angle to align the bat to
C.CrankOffsetDeg = 90 C.CrankOffsetDeg = 90
C.DanceBounceMs = 500 C.DanceBounceMs = 500
C.DanceBounceCount = 4 C.DanceBounceCount = 4
C.ScoreboardDelayMs = 2000
--- Used to draw the ball well out of bounds, and --- Used to draw the ball well out of bounds, and
--- generally as a check for whether or not it's in play. --- generally as a check for whether or not it's in play.
C.BallOffscreen = 999 C.BallOffscreen = 999
C.PitchAfterSeconds = 7 C.PitchAfterSeconds = 6
C.ReturnToPitcherAfterSeconds = 2.4
C.PitchFlyMs = 1050 C.PitchFlyMs = 1050
C.PitchStartX = 195 C.PitchStartX = 190
C.PitchStartY, C.PitchEndY = 105, 240 C.PitchStartY, C.PitchEndY = 105, 240
C.DefaultLaunchPower = 4
--- The max distance at which a fielder can tag out a runner. --- The max distance at which a fielder can tag out a runner.
C.TagDistance = 15 C.TagDistance = 15
@ -69,7 +88,7 @@ C.BattingPower = 20
C.SmallestBallRadius = 6 C.SmallestBallRadius = 6
C.BatLength = 50 C.BatLength = 35
-- TODO: enums implemented this way are probably going to be difficult to serialize! -- TODO: enums implemented this way are probably going to be difficult to serialize!
@ -95,16 +114,16 @@ C.PitcherStartPos = {
y = C.Screen.H * 0.40, y = C.Screen.H * 0.40,
} }
C.ThrowMeterMax = 15 C.ThrowMeterMax = 10
C.ThrowMeterDrainPerSec = 150 C.ThrowMeterDrainPerSec = 150
--- Controls how hard the ball can be hit, and --- Controls how hard the ball can be hit, and
--- how fast the ball can be thrown. --- how fast the ball can be thrown.
C.CrankPower = 10 C.CrankPower = 10
C.PitchPower = 0.3 C.UserThrowPower = 0.3
--- How fast baserunners move after a walk --- How fast baserunners move after a walk
C.WalkedRunnerSpeed = 10 C.WalkedRunnerSpeed = 10
C.ResetFieldersAfterSeconds = 7 C.ResetFieldersAfterSeconds = 2.5

View File

@ -38,11 +38,32 @@ local BallStrikeMarginY <const> = 4
local BallStrikeWidth <const> = 60 local BallStrikeWidth <const> = 60
local BallStrikeHeight <const> = (BallStrikeMarginY * 2) + ScoreFont:getHeight() local BallStrikeHeight <const> = (BallStrikeMarginY * 2) + ScoreFont:getHeight()
local BallStrikeAnimatorIn <const> =
playdate.graphics.animator.new(500, BallStrikeHeight, 0, playdate.easingFunctions.outBounce)
local BallStrikeAnimatorOut <const> =
playdate.graphics.animator.new(500, 0, BallStrikeHeight, playdate.easingFunctions.linear)
-- Start out of frame.
local currentBallStrikeAnimator = utils.staticAnimator(20)
function drawBallsAndStrikes(x, y, balls, strikes) function drawBallsAndStrikes(x, y, balls, strikes)
if balls == 0 and strikes == 0 then if balls == 0 and strikes == 0 then
if currentBallStrikeAnimator == BallStrikeAnimatorIn then
currentBallStrikeAnimator = BallStrikeAnimatorOut
currentBallStrikeAnimator:reset()
end
if currentBallStrikeAnimator:ended() then
return return
end end
end
if balls + strikes == 1 and currentBallStrikeAnimator ~= BallStrikeAnimatorIn then
-- First pitch - should pop in now.
currentBallStrikeAnimator = BallStrikeAnimatorIn
currentBallStrikeAnimator:reset()
end
y = y + currentBallStrikeAnimator:currentValue()
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()
@ -72,11 +93,19 @@ function getIndicators(teams, battingTeam)
return "", IndicatorWidth, Indicator, 0 return "", IndicatorWidth, Indicator, 0
end end
function drawScoreboard(x, y, teams, outs, battingTeam, inning) local stats = {
local homeScore = teams.home.score homeScore = 0,
local awayScore = teams.away.score awayScore = 0,
outs = 0,
inning = 1,
battingTeam = nil,
}
local homeIndicator, homeOffset, awayIndicator, awayOffset = getIndicators(teams, battingTeam) function drawScoreboardImpl(x, y, teams)
local homeScore = stats.homeScore
local awayScore = stats.awayScore
local homeIndicator, homeOffset, awayIndicator, awayOffset = getIndicators(teams, stats.battingTeam)
local homeScoreText = homeIndicator .. "HOME " .. (homeScore > 9 and homeScore or " " .. homeScore) local homeScoreText = homeIndicator .. "HOME " .. (homeScore > 9 and homeScore or " " .. homeScore)
local awayScoreText = awayIndicator .. "AWAY " .. (awayScore > 9 and awayScore or " " .. awayScore) local awayScoreText = awayIndicator .. "AWAY " .. (awayScore > 9 and awayScore or " " .. awayScore)
@ -96,7 +125,7 @@ function drawScoreboard(x, y, teams, outs, battingTeam, inning)
ScoreFont:drawText(homeScoreText, x + ScoreboardMarginX + homeOffset, y + 6) ScoreFont:drawText(homeScoreText, x + ScoreboardMarginX + homeOffset, y + 6)
ScoreFont:drawText(awayScoreText, x + ScoreboardMarginX + awayOffset, y + 22) ScoreFont:drawText(awayScoreText, x + ScoreboardMarginX + awayOffset, y + 22)
local inningOffsetX = (x + ScoreboardMarginX + IndicatorWidth) + (4 * 2.5 * OutBubbleRadius) local inningOffsetX = (x + ScoreboardMarginX + IndicatorWidth) + (4 * 2.5 * OutBubbleRadius)
ScoreFont:drawText(inning, inningOffsetX, y + 39) ScoreFont:drawText(tostring(stats.inning), inningOffsetX, y + 39)
gfx.setImageDrawMode(originalDrawMode) gfx.setImageDrawMode(originalDrawMode)
@ -107,10 +136,34 @@ function drawScoreboard(x, y, teams, outs, battingTeam, inning)
return (x + ScoreboardMarginX + OutBubbleRadius + IndicatorWidth) + circleOffset, y + 46, OutBubbleRadius return (x + ScoreboardMarginX + OutBubbleRadius + IndicatorWidth) + circleOffset, y + 46, OutBubbleRadius
end end
for i = outs, 2 do for i = stats.outs, 2 do
gfx.drawCircleAtPoint(circleParams(i)) gfx.drawCircleAtPoint(circleParams(i))
end end
for i = 0, (outs - 1) do for i = 0, (stats.outs - 1) do
gfx.fillCircleAtPoint(circleParams(i)) gfx.fillCircleAtPoint(circleParams(i))
end end
end end
local newStats = stats
function drawScoreboard(x, y, teams, outs, battingTeam, inning)
if
newStats.homeScore ~= teams.home.score
or newStats.awayScore ~= teams.away.score
or newStats.outs ~= outs
or newStats.inning ~= inning
or newStats.battingTeam ~= battingTeam
then
newStats = {
homeScore = teams.home.score,
awayScore = teams.away.score,
outs = outs,
inning = inning,
battingTeam = battingTeam,
}
playdate.timer.new(C.ScoreboardDelayMs, function()
stats = newStats
end)
end
drawScoreboardImpl(x, y, teams)
end

View File

@ -124,7 +124,7 @@ end
---@param launchBall LaunchBall ---@param launchBall LaunchBall
---@param throwFlyMs number ---@param throwFlyMs number
---@return ActionResult ---@return ActionResult
local function playerThrowToImpl(field, targetBase, launchBall, throwFlyMs) local function userThrowToImpl(field, targetBase, launchBall, throwFlyMs)
if field.fielderTouchingBall == nil then if field.fielderTouchingBall == nil then
return ActionResult.NeedsMoreTime return ActionResult.NeedsMoreTime
end end
@ -142,9 +142,9 @@ end
---@param targetBase Base ---@param targetBase Base
---@param launchBall LaunchBall ---@param launchBall LaunchBall
---@param throwFlyMs number ---@param throwFlyMs number
function Fielding:playerThrowTo(targetBase, launchBall, throwFlyMs) function Fielding:userThrowTo(targetBase, launchBall, throwFlyMs)
local maxTryTimeMs = 5000 local maxTryTimeMs = 5000
actionQueue:upsert("playerThrowTo", maxTryTimeMs, function() actionQueue:upsert("userThrowTo", maxTryTimeMs, function()
return playerThrowToImpl(self, targetBase, launchBall, throwFlyMs) return userThrowToImpl(self, targetBase, launchBall, throwFlyMs)
end) end)
end end

View File

@ -62,20 +62,18 @@ local teams <const> = {
}, },
} }
local PlayerTeam <const> = teams.away local UserTeam <const> = teams.away
local battingTeam = teams.away local battingTeam = teams.away
local inning = 1 local inning = 1
local offenseState = C.Offense.batting local offenseState = C.Offense.batting
local throwMeter = 0
-- TODO: Replace with timers, repeatedly reset, instead of constantly setting to 0 -- TODO: Replace with timers, repeatedly reset, instead of constantly setting to 0
local secondsSinceLastRunnerMove = 0 local secondsSinceLastRunnerMove = 0
local secondsSincePitchAllowed = -5 local secondsSincePitchAllowed = -5
local catcherThrownBall = false local catcherThrownBall = false
local BatterHandPos <const> = utils.xy(10, 15) local BatterHandPos <const> = utils.xy(25, 15)
local batBase <const> = utils.xy(C.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)
@ -118,10 +116,10 @@ local Pitches <const> = {
}, },
} }
---@return boolean playerIsOnSide, boolean playerIsOnOtherSide ---@return boolean userIsOnSide, boolean playerIsOnOtherSide
local function playerIsOn(side) local function userIsOn(side)
local ret local ret
if PlayerTeam == battingTeam then if UserTeam == battingTeam then
ret = side == C.Sides.offense ret = side == C.Sides.offense
else else
ret = side == C.Sides.defense ret = side == C.Sides.defense
@ -137,7 +135,7 @@ end
---@param floaty boolean | nil ---@param floaty boolean | nil
---@param customBallScaler pd_animator | nil ---@param customBallScaler pd_animator | nil
local function launchBall(destX, destY, easingFunc, flyTimeMs, floaty, customBallScaler) local function launchBall(destX, destY, easingFunc, flyTimeMs, floaty, customBallScaler)
throwMeter = 0 throwMeter:reset()
ball:launch(destX, destY, easingFunc, flyTimeMs, floaty, customBallScaler) ball:launch(destX, destY, easingFunc, flyTimeMs, floaty, customBallScaler)
end end
@ -203,13 +201,6 @@ local function score(scoredRunCount)
announcer:say("SCORE!") announcer:say("SCORE!")
end end
local function readThrow()
if throwMeter > C.ThrowMeterMax then
return (C.PitchFlyMs / (throwMeter / C.ThrowMeterMax))
end
return nil
end
---@param throwFlyMs number ---@param throwFlyMs number
---@return boolean didThrow ---@return boolean didThrow
local function buttonControlledThrow(throwFlyMs, forbidThrowHome) local function buttonControlledThrow(throwFlyMs, forbidThrowHome)
@ -227,9 +218,9 @@ local function buttonControlledThrow(throwFlyMs, forbidThrowHome)
end end
-- Power for this throw has already been determined -- Power for this throw has already been determined
throwMeter = 0 throwMeter:reset()
fielding:playerThrowTo(targetBase, launchBall, throwFlyMs) fielding:userThrowTo(targetBase, launchBall, throwFlyMs)
secondsSinceLastRunnerMove = 0 secondsSinceLastRunnerMove = 0
offenseState = C.Offense.running offenseState = C.Offense.running
@ -237,6 +228,7 @@ local function buttonControlledThrow(throwFlyMs, forbidThrowHome)
end end
local function nextBatter() local function nextBatter()
secondsSincePitchAllowed = -3
baserunning.batter = nil baserunning.batter = nil
playdate.timer.new(2000, function() playdate.timer.new(2000, function()
pitchTracker:reset() pitchTracker:reset()
@ -277,6 +269,7 @@ local function updateBatting(batDeg, batSpeed)
and utils.pointDirectlyUnderLine(ball.x, ball.y, batBase.x, batBase.y, batTip.x, batTip.y, C.Screen.H) and utils.pointDirectlyUnderLine(ball.x, ball.y, batBase.x, batBase.y, batTip.x, batTip.y, C.Screen.H)
and ball.y < 232 and ball.y < 232
then then
-- Hit!
BatCrackReverb:play() BatCrackReverb:play()
offenseState = C.Offense.running offenseState = C.Offense.running
local ballAngle = batAngle + math.rad(90) local ballAngle = batAngle + math.rad(90)
@ -290,10 +283,17 @@ local function updateBatting(batDeg, batSpeed)
end end
local ballDestX = ball.x + (ballVelX * C.BattingPower) local ballDestX = ball.x + (ballVelX * C.BattingPower)
local ballDestY = ball.y + (ballVelY * C.BattingPower) local ballDestY = ball.y + (ballVelY * C.BattingPower)
-- Hit! pitchTracker:reset()
local hitBallScaler = gfx.animator.new(2000, 9 + (mult * mult * 0.5), C.SmallestBallRadius, utils.easingHill) local hitBallScaler = gfx.animator.new(2000, 9 + (mult * mult * 0.5), C.SmallestBallRadius, utils.easingHill)
launchBall(ballDestX, ballDestY, playdate.easingFunctions.outQuint, 2000, nil, hitBallScaler) launchBall(ballDestX, ballDestY, playdate.easingFunctions.outQuint, 2000, nil, hitBallScaler)
if utils.isFoulBall(ballDestX, ballDestY) then
announcer:say("Foul ball!")
pitchTracker.strikes = math.min(pitchTracker.strikes + 1, 2)
-- TODO: Have a fielder chase for the fly-out
return
end
baserunning.batter.nextBase = C.Bases[C.First] baserunning.batter.nextBase = C.Bases[C.First]
baserunning.batter.prevBase = C.Bases[C.Home] baserunning.batter.prevBase = C.Bases[C.Home]
baserunning:updateForcedRunners() baserunning:updateForcedRunners()
@ -307,14 +307,14 @@ end
---@param appliedSpeed number ---@param appliedSpeed number
---@return boolean someRunnerMoved ---@return boolean someRunnerMoved
local function updateNonBatterRunners(appliedSpeed, forcedOnly) local function updateNonBatterRunners(appliedSpeed, forcedOnly)
local runnerMoved, runnersScored = baserunning:updateRunning(appliedSpeed, forcedOnly, deltaSeconds) local runnerMoved, runnersScored = baserunning:updateNonBatterRunners(appliedSpeed, forcedOnly, deltaSeconds)
if runnersScored ~= 0 then if runnersScored ~= 0 then
score(runnersScored) score(runnersScored)
end end
return runnerMoved return runnerMoved
end end
local function playerPitch(throwFly) local function userPitch(throwFly)
local aButton = playdate.buttonIsPressed(playdate.kButtonA) local aButton = playdate.buttonIsPressed(playdate.kButtonA)
local bButton = playdate.buttonIsPressed(playdate.kButtonB) local bButton = playdate.buttonIsPressed(playdate.kButtonB)
if not aButton and not bButton then if not aButton and not bButton then
@ -348,11 +348,10 @@ local function updateGameState()
ball.size = ball.sizeAnimator:currentValue() ball.size = ball.sizeAnimator:currentValue()
end end
local playerOnOffense, playerOnDefense = playerIsOn(C.Sides.offense) local userOnOffense, userOnDefense = userIsOn(C.Sides.offense)
if playerOnDefense then if userOnDefense then
throwMeter = math.max(0, throwMeter - (deltaSeconds * C.ThrowMeterDrainPerSec)) throwMeter:applyCharge(deltaSeconds, crankLimited)
throwMeter = throwMeter + math.abs(crankLimited * C.PitchPower)
end end
if offenseState == C.Offense.batting then if offenseState == C.Offense.batting then
@ -363,23 +362,24 @@ local function updateGameState()
end end
local pitcher = fielding.fielders.pitcher local pitcher = fielding.fielders.pitcher
if utils.distanceBetween(pitcher.x, pitcher.y, C.PitchStartX, C.PitchStartY) < C.BaseHitbox then if utils.distanceBetween(pitcher.x, pitcher.y, C.PitcherStartPos.x, C.PitcherStartPos.y) < C.BaseHitbox then
secondsSincePitchAllowed = secondsSincePitchAllowed + deltaSeconds secondsSincePitchAllowed = secondsSincePitchAllowed + deltaSeconds
end end
if secondsSincePitchAllowed > 3.5 and not catcherThrownBall then if secondsSincePitchAllowed > C.ReturnToPitcherAfterSeconds and not catcherThrownBall then
local outcome = pitchTracker:updatePitchCounts() local outcome = pitchTracker:updatePitchCounts()
if outcome == PitchOutcomes.StrikeOut then if outcome == PitchOutcomes.StrikeOut then
strikeOut() strikeOut()
elseif outcome == PitchOutcomes.Walk then elseif outcome == PitchOutcomes.Walk then
walk() walk()
end end
-- Catcher has the ball. Throw it back to the pitcher
launchBall(C.PitchStartX, C.PitchStartY, playdate.easingFunctions.linear, nil, true) launchBall(C.PitchStartX, C.PitchStartY, playdate.easingFunctions.linear, nil, true)
catcherThrownBall = true catcherThrownBall = true
end end
local batSpeed local batSpeed
if playerOnOffense then if userOnOffense then
batAngleDeg = (playdate.getCrankPosition() + C.CrankOffsetDeg) % 360 batAngleDeg = (playdate.getCrankPosition() + C.CrankOffsetDeg) % 360
batSpeed = crankLimited batSpeed = crankLimited
else else
@ -394,25 +394,27 @@ local function updateGameState()
baserunning:updateRunner(baserunning.batter, nil, crankLimited, deltaSeconds) baserunning:updateRunner(baserunning.batter, nil, crankLimited, deltaSeconds)
if secondsSincePitchAllowed > C.PitchAfterSeconds then if secondsSincePitchAllowed > C.PitchAfterSeconds then
if playerOnDefense then if userOnDefense then
local throwFly = readThrow() local throwFly = throwMeter:readThrow()
if throwFly and not buttonControlledThrow(throwFly, true) then if throwFly and not buttonControlledThrow(throwFly, true) then
playerPitch(throwFly) userPitch(throwFly)
end end
else else
pitch(C.PitchFlyMs, math.random(#Pitches)) pitch(C.PitchFlyMs / npc:pitchSpeed(), math.random(#Pitches))
end end
end end
elseif offenseState == C.Offense.running then elseif offenseState == C.Offense.running then
local appliedSpeed = playerOnOffense and crankLimited or npc:runningSpeed() local appliedSpeed = userOnOffense and crankLimited or npc:runningSpeed()
if updateNonBatterRunners(appliedSpeed) then if updateNonBatterRunners(appliedSpeed) then
secondsSinceLastRunnerMove = 0 secondsSinceLastRunnerMove = 0
else else
secondsSinceLastRunnerMove = secondsSinceLastRunnerMove + deltaSeconds secondsSinceLastRunnerMove = secondsSinceLastRunnerMove + deltaSeconds
if secondsSinceLastRunnerMove > C.ResetFieldersAfterSeconds then if secondsSinceLastRunnerMove > C.ResetFieldersAfterSeconds then
-- End of play. Throw the ball back to the pitcher
launchBall(C.PitchStartX, C.PitchStartY, playdate.easingFunctions.linear, nil, true) launchBall(C.PitchStartX, C.PitchStartY, playdate.easingFunctions.linear, nil, true)
fielding:resetFielderPositions() fielding:resetFielderPositions()
offenseState = C.Offense.batting offenseState = C.Offense.batting
-- TODO: Remove, or replace with nextBatter()
if not baserunning.batter then if not baserunning.batter then
baserunning.batter = baserunning:newRunner() baserunning.batter = baserunning:newRunner()
end end
@ -426,15 +428,15 @@ local function updateGameState()
local fielderHoldingBall = fielding:updateFielderPositions(ball, deltaSeconds) local fielderHoldingBall = fielding:updateFielderPositions(ball, deltaSeconds)
if playerOnDefense then if userOnDefense then
local throwFly = readThrow() local throwFly = throwMeter:readThrow()
if throwFly then if throwFly then
buttonControlledThrow(throwFly) buttonControlledThrow(throwFly)
end end
end end
if fielderHoldingBall then if fielderHoldingBall then
local outedSomeRunner = baserunning:outEligibleRunners(fielderHoldingBall) local outedSomeRunner = baserunning:outEligibleRunners(fielderHoldingBall)
if playerOnOffense then if userOnOffense then
npc:fielderAction(offenseState, fielderHoldingBall, outedSomeRunner, ball, launchBall) npc:fielderAction(offenseState, fielderHoldingBall, outedSomeRunner, ball, launchBall)
end end
end end
@ -467,10 +469,6 @@ 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
playdate.ui.crankIndicator:draw()
end
local playerHeightOffset = 10 local playerHeightOffset = 10
-- TODO? Scale sprites down as y increases -- TODO? Scale sprites down as y increases
for _, runner in pairs(baserunning.runners) do for _, runner in pairs(baserunning.runners) do
@ -507,6 +505,10 @@ function playdate.update()
drawScoreboard(0, C.Screen.H * 0.77, teams, baserunning.outs, battingTeam, inning) drawScoreboard(0, C.Screen.H * 0.77, teams, baserunning.outs, battingTeam, inning)
drawBallsAndStrikes(290, C.Screen.H - 20, pitchTracker.balls, pitchTracker.strikes) drawBallsAndStrikes(290, C.Screen.H - 20, pitchTracker.balls, pitchTracker.strikes)
announcer:draw(C.Center.x, 10) announcer:draw(C.Center.x, 10)
if playdate.isCrankDocked() then
playdate.ui.crankIndicator:draw()
end
end end
local function init() local function init()

View File

@ -133,6 +133,11 @@ function Npc:fielderAction(offenseState, fielder, outedSomeRunner, ball, launchB
end end
end end
---@return number
function Npc:pitchSpeed()
return 2
end
if not playdate then if not playdate then
return Npc return Npc
end end

View File

@ -22,16 +22,25 @@ function utils.easingHill(t, b, c, d)
return (c * t) + b return (c * t) + b
end end
--- @alias StaticAnimator {
--- currentValue: fun(self): number;
--- reset: fun(self, durationMs: number | nil);
--- ended: fun(self): boolean;
--- }
--- Build an "animator" whose `:currentValue()` always returns the given value. --- Build an "animator" whose `:currentValue()` always returns the given value.
--- Essentially an "empty object" pattern for initial object positions. --- Essentially an "empty object" pattern for initial object positions.
---@param value number ---@param value number
---@return SimpleAnimator ---@return StaticAnimator
function utils.staticAnimator(value) function utils.staticAnimator(value)
return { return {
currentValue = function(_) currentValue = function(_)
return value return value
end, end,
reset = function(_) end, reset = function(_) end,
ended = function(_)
return true
end,
} }
end end
@ -143,7 +152,7 @@ function utils.getRunnerWithNextBase(runners, base)
end) end)
end end
--- Returns true only if the point is below the given line, within the x bounds of said line, and above the bottomBound --- Returns true only if the point is below the given line, within the x bounds of said line, and above the bottomBound.
---@return boolean ---@return boolean
function utils.pointDirectlyUnderLine(pointX, pointY, lineX1, lineY1, lineX2, lineY2, bottomBound) function utils.pointDirectlyUnderLine(pointX, pointY, lineX1, lineY1, lineX2, lineY2, bottomBound)
-- This check currently assumes right-handedness. -- This check currently assumes right-handedness.
@ -152,6 +161,12 @@ function utils.pointDirectlyUnderLine(pointX, pointY, lineX1, lineY1, lineX2, li
return false return false
end end
return utils.pointUnderLine(pointX, pointY, lineX1, lineY1, lineX2, lineY2)
end
--- Returns true only if the point is below the given line.
---@return boolean
function utils.pointUnderLine(pointX, pointY, lineX1, lineY1, lineX2, lineY2)
local m = (lineY2 - lineY1) / (lineX2 - lineX1) local m = (lineY2 - lineY1) / (lineX2 - lineX1)
-- y = mx + b -- y = mx + b
@ -164,8 +179,19 @@ function utils.pointDirectlyUnderLine(pointX, pointY, lineX1, lineY1, lineX2, li
return yDelta <= 0 return yDelta <= 0
end end
--- Returns true if a ball landing at destX,destY will be foul.
---@param destX number
---@param destY number
function utils.isFoulBall(destX, destY)
local leftLine = C.LeftFoulLine
local rightLine = C.RightFoulLine
return utils.pointUnderLine(destX, destY, leftLine.x1, leftLine.y1, leftLine.x2, leftLine.y2)
or utils.pointUnderLine(destX, destY, rightLine.x1, rightLine.y1, rightLine.x2, rightLine.y2)
end
--- Returns the nearest position object from the given point, as well as its distance from that point --- Returns the nearest position object from the given point, as well as its distance from that point
---@generic T : {x: number, y: number | nil} ---@generic T : { x: number, y: number | nil }
---@param array T[] ---@param array T[]
---@param x number ---@param x number
---@param y number ---@param y number
@ -231,6 +257,35 @@ pitchTracker = {
end, end,
} }
if not playdate then -----------------
return utils, { pitchTracker = pitchTracker, PitchOutcomes = PitchOutcomes } -- Throw Meter --
-----------------
-- selene: allow(unscoped_variables)
throwMeter = {
value = 0,
}
function throwMeter:reset()
self.value = 0
end
---@return number | nil flyTimeMs Returns nil when a throw is NOT requested.
function throwMeter:readThrow()
if self.value > C.ThrowMeterMax then
return (C.PitchFlyMs / (self.value / C.ThrowMeterMax))
end
return nil
end
--- Applies the given charge, but drains some meter for how much time has passed
---@param delta number
---@param chargeAmount number
function throwMeter:applyCharge(delta, chargeAmount)
self.value = math.max(0, self.value - (delta * C.ThrowMeterDrainPerSec))
self.value = self.value + math.abs(chargeAmount * C.UserThrowPower)
end
if not playdate then
return utils, { pitchTracker = pitchTracker, PitchOutcomes = PitchOutcomes, throwMeter = throwMeter }
end end