---@alias SimpleAnimator { currentValue: fun(self): number; reset: fun(self, durationMs: number | nil) } ---@alias Pitch fun(accuracy: number, ball: Ball): { x: SimpleAnimator, y: SimpleAnimator, z: SimpleAnimator | nil } ---@type pd_graphics_lib local gfx = playdate.graphics local StrikeZoneWidth = 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 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 = 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 = 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