BatterUp/src/pitching.lua

223 lines
6.1 KiB
Lua

---@alias SimpleAnimator { currentValue: fun(self): number; reset: fun(self, durationMs: number | nil) }
---@alias Pitch fun(ball: Ball): { x: SimpleAnimator, y: SimpleAnimator, z: SimpleAnimator | nil }
---@type pd_graphics_lib
local gfx <const> = playdate.graphics
local StrikeZoneWidth <const> = C.StrikeZoneEndX - C.StrikeZoneStartX
-- TODO? Also degrade speed
---@param accuracy number
---@return number xValueToMissBy
function getPitchMissBy(accuracy)
accuracy = accuracy or 1.0
local missBy = (1 - accuracy) * StrikeZoneWidth * 3
if math.random() > 0.5 then
missBy = missBy * -1
end
return missBy
end
---@type Pitch[]
Pitches = {
-- Fastball
function(accuracy)
return {
x = gfx.animator.new(
0,
C.PitchStart.x,
getPitchMissBy(accuracy) + C.PitchStart.x,
playdate.easingFunctions.linear
),
y = gfx.animator.new(C.PitchFlyMs / 1.3, C.PitchStart.y, C.PitchEndY, playdate.easingFunctions.linear),
}
end,
-- Curve ball
function(accuracy)
return {
x = gfx.animator.new(
C.PitchFlyMs,
getPitchMissBy(accuracy) + C.PitchStart.x + 20,
C.PitchStart.x,
utils.easingHill
),
y = gfx.animator.new(C.PitchFlyMs, C.PitchStart.y, C.PitchEndY, playdate.easingFunctions.linear),
}
end,
-- Slider
function(accuracy)
return {
x = gfx.animator.new(
C.PitchFlyMs,
getPitchMissBy(accuracy) + C.PitchStart.x - 20,
C.PitchStart.x,
utils.easingHill
),
y = gfx.animator.new(C.PitchFlyMs, C.PitchStart.y, C.PitchEndY, playdate.easingFunctions.linear),
}
end,
-- Wobbleball
function(accuracy, ball)
local missBy = getPitchMissBy(accuracy)
return {
x = {
currentValue = function()
return missBy
+ C.PitchStart.x
+ (10 * math.sin((ball.yAnimator:currentValue() - C.PitchStart.y) / 10))
end,
reset = function() end,
},
y = gfx.animator.new(C.PitchFlyMs * 1.3, C.PitchStart.y, C.PitchEndY, playdate.easingFunctions.linear),
}
end,
}
---@alias PitchOutcome "StrikeOut" | "Walk"
---@type table<string, PitchOutcome>
PitchOutcomes = {
StrikeOut = "StrikeOut",
Walk = "Walk",
}
pitchTracker = {
--- Position of the pitch, or nil, if one has not been recorded.
---@type number | nil
recordedPitchX = nil,
-- TODO: Replace with timer, repeatedly reset, instead of constantly setting to 0
secondsSinceLastPitch = 0,
strikes = 0,
balls = 0,
}
function pitchTracker:reset()
self.strikes = 0
self.balls = 0
end
---@param ball XyPair
function pitchTracker:recordIfPassed(ball)
if ball.y < C.StrikeZoneStartY then
self.recordedPitchX = nil
elseif not self.recordedPitchX then
self.recordedPitchX = ball.x
end
end
---@param didSwing boolean
---@param fieldingTeamInningData TeamInningData
---@return PitchOutcome | nil
function pitchTracker:updatePitchCounts(didSwing, fieldingTeamInningData)
if not self.recordedPitchX then
return
end
local currentPitchingStats = fieldingTeamInningData.pitching
if didSwing or self.recordedPitchX > C.StrikeZoneStartX and self.recordedPitchX < C.StrikeZoneEndX then
self.strikes = self.strikes + 1
currentPitchingStats.strikes = currentPitchingStats.strikes + 1
if self.strikes >= 3 then
self:reset()
return PitchOutcomes.StrikeOut
end
else
self.balls = self.balls + 1
currentPitchingStats.balls = currentPitchingStats.balls + 1
if self.balls >= 4 then
self:reset()
return PitchOutcomes.Walk
end
end
end
-----------------
-- Throw Meter --
-----------------
throwMeter = {
MinCharge = 25,
idealPower = 50,
--- Used at draw-time only.
---@type number
lastReadThrow = nil,
--- Used at draw-time only.
---@type Fielder | nil
lastThrower = nil,
--- Used at draw-time only.
---@type boolean
wasPerfect = false,
}
local MaxPowerRatio <const> = 1.5
--- Returns nil when a throw is NOT requested.
---@param chargeAmount number
---@return number | nil powerRatio, number | nil accuracy, boolean isPerfect
function throwMeter:readThrow(chargeAmount)
local power = self:readCharge(chargeAmount)
if not power then
return nil, nil, false
end
local ratio = math.min(power / self.idealPower, MaxPowerRatio)
self.wasPerfect = math.abs(ratio - 1) < 0.05
local accuracy = 1
-- Only throw off accuracy on slow throws
if ratio >= 1 and not self.wasPerfect then
accuracy = 1 / ratio
end
return ratio * 1.5, accuracy, self.wasPerfect
end
local CrankRecordSec <const> = 0.33
---@alias CrankQueueEntry { time: number, chargeAmount: number }
---@type CrankQueueEntry[]
local crankQueue = {}
--- If (within approx. a third of a second) the crank has moved more than 45 degrees, call that a throw.
---@param chargeAmount number
---@return number | nil
function throwMeter:readCharge(chargeAmount)
if chargeAmount == 0 then
return nil
end
local currentTimeMs = playdate.getCurrentTimeMilliseconds()
local minTimeHasPassed = false
while #crankQueue ~= 0 and (currentTimeMs - crankQueue[1].time) > CrankRecordSec do
table.remove(crankQueue, 1)
minTimeHasPassed = true
end
crankQueue[#crankQueue + 1] = { time = currentTimeMs, chargeAmount = math.abs(chargeAmount) }
if not minTimeHasPassed then
return nil
end
local currentCharge = 0
for _, v in ipairs(crankQueue) do
currentCharge = currentCharge + v.chargeAmount
end
if currentCharge > throwMeter.MinCharge then
self.lastReadThrow = currentCharge
crankQueue = {}
return currentCharge
else
return nil
end
end