diff --git a/src/baserunning.lua b/src/baserunning.lua index b8a74c4..7f26082 100644 --- a/src/baserunning.lua +++ b/src/baserunning.lua @@ -12,6 +12,8 @@ ---@field scoredRunners Runner[] ---@field batter Runner | nil ---@field outs number +-- TODO: Replace with timer, repeatedly reset, instead of constantly setting to 0 +---@field secondsSinceLastRunnerMove number ---@field announcer Announcer ---@field onThirdOut fun() Baserunning = {} @@ -237,9 +239,9 @@ end --- Update non-batter runners. --- Returns true only if at least one of the given runners moved during this update ---@param appliedSpeed number | fun(runner: Runner): number ----@return boolean someRunnerMoved, number runnersScored +---@return boolean runnersStillMoving, number runnersScored, number secondsSinceLastMove function Baserunning:updateNonBatterRunners(appliedSpeed, forcedOnly, deltaSeconds) - local someRunnerMoved = false + local runnersStillMoving = false local runnersScored = 0 local speedIsFunction = type(appliedSpeed) == "function" @@ -251,18 +253,21 @@ function Baserunning:updateNonBatterRunners(appliedSpeed, forcedOnly, deltaSecon speed = appliedSpeed(runner) end local thisRunnerMoved, thisRunnerScored = self:updateRunner(runner, runnerIndex, speed, deltaSeconds) - someRunnerMoved = someRunnerMoved or thisRunnerMoved + runnersStillMoving = runnersStillMoving or thisRunnerMoved if thisRunnerScored then runnersScored = runnersScored + 1 end end end - if someRunnerMoved then + if runnersStillMoving then + self.secondsSinceLastRunnerMove = 0 self:updateForcedRunners() + else + self.secondsSinceLastRunnerMove = self.secondsSinceLastRunnerMove + deltaSeconds end - return someRunnerMoved, runnersScored + return runnersStillMoving, runnersScored, self.secondsSinceLastRunnerMove end -- luacheck: ignore diff --git a/src/constants.lua b/src/constants.lua index 4ac30ca..fddcfc1 100644 --- a/src/constants.lua +++ b/src/constants.lua @@ -70,8 +70,8 @@ C.BallOffscreen = 999 C.PitchAfterSeconds = 6 C.ReturnToPitcherAfterSeconds = 2.4 C.PitchFlyMs = 1050 -C.PitchStartX = 195 -C.PitchStartY, C.PitchEndY = 105, 240 +C.PitchStart = utils.xy(195, 105) +C.PitchEndY = 240 C.DefaultLaunchPower = 4 @@ -91,7 +91,7 @@ C.SmallestBallRadius = 6 C.BatLength = 35 ----@alias OffenseState "batting" | "running" | "walking" +---@alias OffenseState "batting" | "running" | "walking" | "homeRun" --- An enum for what state the offense is in ---@type table C.Offense = { diff --git a/src/draw/panner.lua b/src/draw/panner.lua new file mode 100644 index 0000000..20841d2 --- /dev/null +++ b/src/draw/panner.lua @@ -0,0 +1,52 @@ +---@class Panner +Panner = {} + +local function panCoroutine(ball) + local offset = utils.xy(getDrawOffset(ball.x, ball.y)) + while true do + local target, deltaSeconds = coroutine.yield(offset.x, offset.y) + if target == nil then + offset = utils.xy(getDrawOffset(ball.x, ball.y)) + else + while utils.moveAtSpeed(offset, 200 * deltaSeconds, target, 20) do + target, deltaSeconds = coroutine.yield(offset.x, offset.y) + end + -- -- Pan back to ball + -- while utils.moveAtSpeed(offset, 200 * deltaSeconds, ball, 20) do + -- target, deltaSeconds = coroutine.yield(offset.x, offset.y) + -- end + end + end +end + +---@param ball XyPair +function Panner.new(ball) + return setmetatable({ + coroutine = coroutine.create(function() + panCoroutine(ball) + end), + panTarget = nil, + }, { __index = Panner }) +end + +---@param deltaSeconds number +---@return number offsetX, number offsetY +function Panner:get(deltaSeconds) + if self.holdUntil and self.holdUntil() then + self:reset() + end + local _, offsetX, offsetY = coroutine.resume(self.coroutine, self.panTarget, deltaSeconds) + return offsetX, offsetY +end + +---@param panTarget XyPair +---@param holdUntil fun(): boolean +function Panner:panTo(panTarget, holdUntil) + self.panTarget = panTarget + self.holdUntil = holdUntil +end + +function Panner:reset() + self.holdUntil = nil + self.panTarget = nil +end diff --git a/src/fielding.lua b/src/fielding.lua index b524f23..223cef6 100644 --- a/src/fielding.lua +++ b/src/fielding.lua @@ -5,8 +5,9 @@ --- @field target XyPair | nil --- @field speed number +-- luacheck: ignore 631 ---@class Fielding ----@field fielders table +---@field fielders { first: Fielder, second: Fielder, shortstop: Fielder, third: Fielder, pitcher: Fielder, catcher: Fielder, left: Fielder, center: Fielder, right: Fielder } ---@field fielderHoldingBall Fielder | nil Fielding = {} @@ -113,7 +114,7 @@ end ---@param deltaSeconds number ---@return Fielder | nil fielderHoldingBall nil if no fielder is currently touching the ball function Fielding:updateFielderPositions(ball, deltaSeconds) - local fielderHoldingBall = nil + local fielderHoldingBall for _, fielder in pairs(self.fielders) do local inCatchingRange = updateFielderPosition(deltaSeconds, fielder, ball) if inCatchingRange and fielder.catchEligible then @@ -137,10 +138,14 @@ function Fielding.markIneligible(fielder) end) end -function Fielding:markAllIneligible() +function Fielding:markAllEligible(eligible) for _, fielder in pairs(self.fielders) do - fielder.catchEligible = false + fielder.catchEligible = eligible end +end + +function Fielding:resetEligibility() + self:markAllEligible(false) playdate.timer.new(750, function() for _, fielder in pairs(self.fielders) do fielder.catchEligible = true diff --git a/src/graphics.lua b/src/graphics.lua index a5bfb95..7bd8479 100644 --- a/src/graphics.lua +++ b/src/graphics.lua @@ -7,7 +7,8 @@ function getDrawOffset(ballX, ballY) if ballY > C.Screen.H or ballX >= C.BallOffscreen then return 0, 0 end - offsetY = math.max(0, -1 * ballY) + -- Keep the ball approximately in the center, once it's past C.Center.y - 30 + offsetY = math.max(0, (-1 * ballY) + C.Center.y - 30) if ballX > 0 and ballX < C.Screen.W then offsetX = 0 @@ -17,7 +18,7 @@ function getDrawOffset(ballX, ballY) offsetX = math.min(C.Screen.W * 2, (ballX * -1) + C.Screen.W) end - return offsetX * 1.3, offsetY * 1.5 + return offsetX * 1.3, offsetY end ---@class Blipper diff --git a/src/images/game/GrassBackground.png b/src/images/game/GrassBackground.png index b01de02..00bae54 100644 Binary files a/src/images/game/GrassBackground.png and b/src/images/game/GrassBackground.png differ diff --git a/src/main.lua b/src/main.lua index 4a24d61..f6d30cf 100644 --- a/src/main.lua +++ b/src/main.lua @@ -29,6 +29,7 @@ import 'assets.lua' import 'draw/box-score.lua' import 'draw/fielder.lua' import 'draw/overlay.lua' +import 'draw/panner.lua' import 'draw/player.lua' import 'draw/transitions.lua' @@ -42,6 +43,7 @@ import 'dbg.lua' import 'fielding.lua' import 'graphics.lua' import 'npc.lua' +import 'pitching.lua' -- stylua: ignore end -- TODO: Customizable field structure. E.g. stands and ads etc. @@ -80,9 +82,6 @@ local teams = { ---@field batBase XyPair ---@field batTip XyPair ---@field batAngleDeg number --- TODO: Replace with timers, repeatedly reset, instead of constantly setting to 0 ----@field secondsSinceLastRunnerMove number ----@field secondsSincePitchAllowed number -- These are only sort-of global state. They are purely graphical, -- but they need to be kept in sync with the rest of the globals. ---@field runnerBlipper Blipper @@ -97,6 +96,7 @@ local teams = { ---@field private npc Npc ---@field private homeTeamBlipper Blipper ---@field private awayTeamBlipper Blipper +---@field private panner Panner ---@field private state MutableState Game = {} @@ -124,6 +124,7 @@ function Game.new(settings, announcer, fielding, baserunning, npc, state) fielding = fielding, homeTeamBlipper = homeTeamBlipper, awayTeamBlipper = awayTeamBlipper, + panner = Panner.new(ball), state = state or { batBase = utils.xy(C.Center.x - 34, 215), batTip = utils.xy(0, 0), @@ -135,8 +136,6 @@ function Game.new(settings, announcer, fielding, baserunning, npc, state) inning = 1, pitchIsOver = false, didSwing = false, - secondsSinceLastRunnerMove = 0, - secondsSincePitchAllowed = 0, battingTeamSprites = settings.awayTeamSprites, fieldingTeamSprites = settings.homeTeamSprites, runnerBlipper = runnerBlipper, @@ -151,7 +150,7 @@ function Game.new(settings, announcer, fielding, baserunning, npc, state) o.fielding:resetFielderPositions(teams.home.benchPosition) playdate.timer.new(2000, function() - ball:launch(C.PitchStartX, C.PitchStartY, playdate.easingFunctions.linear, nil, false) + o:returnToPitcher() end) BootTune:play() @@ -170,22 +169,22 @@ local Pitches = { -- Fastball function() return { - x = gfx.animator.new(0, C.PitchStartX, C.PitchStartX, playdate.easingFunctions.linear), - y = gfx.animator.new(C.PitchFlyMs / 1.3, C.PitchStartY, C.PitchEndY, playdate.easingFunctions.linear), + x = gfx.animator.new(0, C.PitchStart.x, 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() return { - x = gfx.animator.new(C.PitchFlyMs, C.PitchStartX + 20, C.PitchStartX, utils.easingHill), - y = gfx.animator.new(C.PitchFlyMs, C.PitchStartY, C.PitchEndY, playdate.easingFunctions.linear), + x = gfx.animator.new(C.PitchFlyMs, 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() return { - x = gfx.animator.new(C.PitchFlyMs, C.PitchStartX - 20, C.PitchStartX, utils.easingHill), - y = gfx.animator.new(C.PitchFlyMs, C.PitchStartY, C.PitchEndY, playdate.easingFunctions.linear), + x = gfx.animator.new(C.PitchFlyMs, C.PitchStart.x - 20, C.PitchStart.x, utils.easingHill), + y = gfx.animator.new(C.PitchFlyMs, C.PitchStart.y, C.PitchEndY, playdate.easingFunctions.linear), } end, -- Wobbbleball @@ -193,11 +192,11 @@ local Pitches = { return { x = { currentValue = function() - return C.PitchStartX + (10 * math.sin((ball.yAnimator:currentValue() - C.PitchStartY) / 10)) + return 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.PitchStartY, C.PitchEndY, playdate.easingFunctions.linear), + y = gfx.animator.new(C.PitchFlyMs * 1.3, C.PitchStart.y, C.PitchEndY, playdate.easingFunctions.linear), } end, } @@ -260,7 +259,18 @@ function Game:pitch(pitchFlyTimeMs, pitchTypeIndex) self.state.ball.yAnimator:reset() end - self.state.secondsSincePitchAllowed = 0 + pitchTracker.secondsSinceLastPitch = 0 +end + +function Game:pitcherIsReady() + local pitcher = self.fielding.fielders.pitcher + local pitcherIsOnTheMound = utils.distanceBetweenPoints(pitcher, C.PitcherStartPos) < C.BaseHitbox + return pitcherIsOnTheMound + and ( + self.state.ball.heldBy == pitcher + or utils.distanceBetweenPoints(pitcher, self.state.ball) < C.BallCatchHitbox + or utils.distanceBetweenPoints(self.state.ball, C.PitchStart) < 2 + ) end function Game:nextHalfInning() @@ -279,7 +289,6 @@ function Game:nextHalfInning() end Fielding.celebrate() - self.state.secondsSinceLastRunnerMove = -7 self.fielding:benchTo(self:getFieldingTeam().benchPosition) self.announcer:say("SWITCHING SIDES...") @@ -338,14 +347,14 @@ function Game:buttonControlledThrow(throwFlyMs, forbidThrowHome) end self.fielding:userThrowTo(targetBase, self.state.ball, throwFlyMs) - self.state.secondsSinceLastRunnerMove = 0 + self.baserunning.secondsSinceLastRunnerMove = 0 self.state.offenseState = C.Offense.running return true end function Game:nextBatter() - self.state.secondsSincePitchAllowed = -3 + pitchTracker.secondsSinceLastPitch = -3 self.baserunning.batter = nil playdate.timer.new(2000, function() pitchTracker:reset() @@ -390,6 +399,7 @@ function Game:updateBatting(batDeg, batSpeed) if batSpeed > 0 + and self.state.ball.y < 232 and utils.pointDirectlyUnderLine( self.state.ball.x, self.state.ball.y, @@ -399,7 +409,6 @@ function Game:updateBatting(batDeg, batSpeed) self.state.batTip.y, C.Screen.H ) - and self.state.ball.y < 232 then -- Hit! BatCrackReverb:play() @@ -434,6 +443,12 @@ function Game:updateBatting(batDeg, batSpeed) if utils.within(1, self.state.ball.x, ballDestX) and utils.within(1, self.state.ball.y, ballDestY) then self.announcer:say("HOME RUN!") self.state.offenseState = C.Offense.homeRun + -- Linger on the home-run ball for a moment, before panning to the bases. + playdate.timer.new(1000, function() + self.panner:panTo(utils.xy(0, 0), function() + return self:pitcherIsReady() + end) + end) end end) end @@ -447,14 +462,20 @@ function Game:updateBatting(batDeg, batSpeed) end ---@param appliedSpeed number | fun(runner: Runner): number ----@return boolean someRunnerMoved +---@return boolean runnersStillMoving, number secondsSinceLastRunnerMove function Game:updateNonBatterRunners(appliedSpeed, forcedOnly) - local runnerMoved, runnersScored = + local runnersStillMoving, runnersScored, secondsSinceLastRunnerMove = self.baserunning:updateNonBatterRunners(appliedSpeed, forcedOnly, self.state.deltaSeconds) if runnersScored ~= 0 then self:score(runnersScored) end - return runnerMoved + return runnersStillMoving, secondsSinceLastRunnerMove +end + +function Game:returnToPitcher() + self.fielding:resetEligibility() + self.fielding.fielders.pitcher.catchEligible = true + self.state.ball:launch(C.PitchStart.x, C.PitchStart.y, playdate.easingFunctions.linear, nil, true) end ---@param throwFly number @@ -494,18 +515,17 @@ function Game:updateGameState() local pitcher = self.fielding.fielders.pitcher if utils.distanceBetween(pitcher.x, pitcher.y, C.PitcherStartPos.x, C.PitcherStartPos.y) < C.BaseHitbox then - self.state.secondsSincePitchAllowed = self.state.secondsSincePitchAllowed + self.state.deltaSeconds + pitchTracker.secondsSinceLastPitch = pitchTracker.secondsSinceLastPitch + self.state.deltaSeconds end - if self.state.secondsSincePitchAllowed > C.ReturnToPitcherAfterSeconds and not self.state.pitchIsOver then + if pitchTracker.secondsSinceLastPitch > C.ReturnToPitcherAfterSeconds and not self.state.pitchIsOver then local outcome = pitchTracker:updatePitchCounts(self.state.didSwing, self:fieldingTeamCurrentInning()) if outcome == PitchOutcomes.StrikeOut then self:strikeOut() elseif outcome == PitchOutcomes.Walk then self:walk() end - -- Catcher has the ball. Throw it back to the pitcher - self.state.ball:launch(C.PitchStartX, C.PitchStartY, playdate.easingFunctions.linear, nil, true) + self:returnToPitcher() self.state.pitchIsOver = true self.state.didSwing = false end @@ -525,7 +545,7 @@ function Game:updateGameState() -- Walk batter to the plate self.baserunning:updateRunner(self.baserunning.batter, nil, crankLimited, self.state.deltaSeconds) - if self.state.secondsSincePitchAllowed > C.PitchAfterSeconds then + if pitchTracker.secondsSinceLastPitch > C.PitchAfterSeconds then if userOnDefense then local throwFly = throwMeter:readThrow() if throwFly and not self:buttonControlledThrow(throwFly, true) then @@ -540,32 +560,32 @@ function Game:updateGameState() or function(runner) return self.npc:runningSpeed(runner, self.state.ball) end - if self:updateNonBatterRunners(appliedSpeed) then - self.state.secondsSinceLastRunnerMove = 0 - else - self.state.secondsSinceLastRunnerMove = self.state.secondsSinceLastRunnerMove + self.state.deltaSeconds - if self.state.secondsSinceLastRunnerMove > C.ResetFieldersAfterSeconds then - -- End of play. Throw the ball back to the pitcher - self.state.ball:launch(C.PitchStartX, C.PitchStartY, playdate.easingFunctions.linear, nil, true) - -- This is ugly, and ideally would not be necessary if Fielding handled the return throw directly. - self.fielding:markAllIneligible() - self.fielding:resetFielderPositions() - self.state.offenseState = C.Offense.batting - -- TODO: Remove, or replace with nextBatter() - if not self.baserunning.batter then - self.baserunning:pushNewBatter() - end - end + local _, secondsSinceLastRunnerMove = self:updateNonBatterRunners(appliedSpeed, false) + if secondsSinceLastRunnerMove > C.ResetFieldersAfterSeconds then + -- End of play. Throw the ball back to the pitcher + self.state.offenseState = C.Offense.batting + self:returnToPitcher() end elseif self.state.offenseState == C.Offense.walking then if not self:updateNonBatterRunners(C.WalkedRunnerSpeed, true) then self.state.offenseState = C.Offense.batting end elseif self.state.offenseState == C.Offense.homeRun then - self:updateNonBatterRunners(C.WalkedRunnerSpeed * 3, false) + self:updateNonBatterRunners(C.WalkedRunnerSpeed * 2, false) if #self.baserunning.runners == 0 then - self.state.offenseState = C.Offense.batting self.baserunning:pushNewBatter() + -- Give the player a moment to enjoy their home run. + self.fielding:resetFielderPositions() + playdate.timer.new(1500, function() + self:returnToPitcher() + end) + actionQueue:upsert("waitForPitcherToHaveBall", 10000, function() + while not self:pitcherIsReady() do + coroutine.yield() + end + self.fielding:markAllEligible(true) + self.state.offenseState = C.Offense.batting + end) end end @@ -597,10 +617,10 @@ function Game:update() gfx.clear() gfx.setColor(gfx.kColorBlack) - local offsetX, offsetY = getDrawOffset(ball.x, ball.y) + local offsetX, offsetY = self.panner:get(self.state.deltaSeconds) gfx.setDrawOffset(offsetX, offsetY) - GrassBackground:draw(-400, -240) + GrassBackground:draw(-400, -720) ---@type { y: number, drawAction: fun() }[] local characterDraws = {} diff --git a/src/pitching.lua b/src/pitching.lua new file mode 100644 index 0000000..94412e0 --- /dev/null +++ b/src/pitching.lua @@ -0,0 +1,83 @@ +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 + +function pitchTracker:recordIfPassed(ball) + if ball.y < C.StrikeZoneStartY then + self.recordedPitchX = nil + elseif not pitchTracker.recordedPitchX then + self.recordedPitchX = ball.x + end +end + +---@param didSwing boolean +---@param fieldingTeamInningData TeamInningData +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 = { + 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 deltaSeconds number +---@param chargeAmount number +function throwMeter:applyCharge(deltaSeconds, chargeAmount) + self.value = math.max(0, self.value - (deltaSeconds * C.ThrowMeterDrainPerSec)) + self.value = self.value + math.abs(chargeAmount * C.UserThrowPower) +end diff --git a/src/utils.lua b/src/utils.lua index 34af323..5bc364b 100644 --- a/src/utils.lua +++ b/src/utils.lua @@ -70,17 +70,24 @@ end ---@param mover { x: number, y: number } ---@param speed number ---@param target { x: number, y: number } ----@return boolean -function utils.moveAtSpeed(mover, speed, target) +---@param tau number | nil +---@return boolean isStillMoving +function utils.moveAtSpeed(mover, speed, target, tau) local x, y, distance = utils.normalizeVector(mover.x, mover.y, target.x, target.y) - if distance > 1 then - mover.x = mover.x - (x * speed) - mover.y = mover.y - (y * speed) - return true + if distance == 0 then + return false end - return false + if distance > (tau or 1) then + mover.x = mover.x - (x * speed) + mover.y = mover.y - (y * speed) + else + mover.x = target.x + mover.y = target.y + end + + return true end function utils.within(within, n1, n2) @@ -249,87 +256,6 @@ function utils.totalScores(stats) return homeScore, awayScore end -PitchOutcomes = { - StrikeOut = "StrikeOut", - Walk = "Walk", -} - -pitchTracker = { - --- Position of the pitch, or nil, if one has not been recorded. - ---@type number | nil - recordedPitchX = nil, - - strikes = 0, - balls = 0, -} - -function pitchTracker:reset() - self.strikes = 0 - self.balls = 0 -end - -function pitchTracker:recordIfPassed(ball) - if ball.y < C.StrikeZoneStartY then - self.recordedPitchX = nil - elseif not pitchTracker.recordedPitchX then - self.recordedPitchX = ball.x - end -end - ----@param didSwing boolean ----@param fieldingTeamInningData TeamInningData -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 = { - 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 } + return utils end