223 lines
6.1 KiB
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
|