From 3a465cb02d64112da9a31d18f8cbb12f0f31dfb2 Mon Sep 17 00:00:00 2001 From: Sage Vaillancourt Date: Mon, 24 Feb 2025 15:37:05 -0500 Subject: [PATCH 1/5] Extract UserInput as an InputHandler. Homogenous with Npc, which now also implements InputHandler. --- src/main.lua | 247 ++++++++++++++++++++------------------------- src/npc.lua | 15 ++- src/user-input.lua | 64 ++++++++++++ 3 files changed, 185 insertions(+), 141 deletions(-) create mode 100644 src/user-input.lua diff --git a/src/main.lua b/src/main.lua index ddef83e..82b6f0c 100644 --- a/src/main.lua +++ b/src/main.lua @@ -13,6 +13,13 @@ import 'CoreLibs/utilities/where.lua' --- @alias EasingFunc fun(number, number, number, number): number +---@class InputHandler +---@field update fun(self, deltaSeconds: number) +---@field updateBat fun(self, ball: Ball, pitchIsOver: boolean, deltaSeconds: number) +---@field runningSpeed fun(self, runner: Runner, ball: Ball) +---@field pitch fun(self) +---@field fielderAction fun(self, fielderHoldingBall: Fielder | nil, outedSomeRunner: boolean, ball: Ball) + --- @alias LaunchBall fun( --- self: self, --- destX: number, @@ -39,6 +46,7 @@ import 'graphics.lua' import 'npc.lua' import 'pitching.lua' import 'statistics.lua' +import 'user-input.lua' import 'draw/box-score.lua' import 'draw/fans.lua' @@ -98,7 +106,8 @@ local teams = { ---@field private announcer Announcer ---@field private fielding Fielding ---@field private baserunning Baserunning ----@field private npc Npc +---@field private npc InputHandler +---@field private userInput InputHandler ---@field private homeTeamBlipper Blipper ---@field private awayTeamBlipper Blipper ---@field private panner Panner @@ -109,7 +118,7 @@ Game = {} ---@param announcer Announcer | nil ---@param fielding Fielding | nil ---@param baserunning Baserunning | nil ----@param npc Npc | nil +---@param npc InputHandler | nil ---@param state MutableState | nil ---@return Game function Game.new(settings, announcer, fielding, baserunning, npc, state) @@ -151,6 +160,9 @@ function Game.new(settings, announcer, fielding, baserunning, npc, state) o.baserunning = baserunning or Baserunning.new(announcer, function() o:nextHalfInning() end) + o.userInput = UserInput.new(function(throwFly, forbidThrowHome) + return o:buttonControlledThrow(throwFly, forbidThrowHome) + end) o.npc = npc or Npc.new(o.baserunning.runners, o.fielding.fielders) o.fielding:resetFielderPositions(teams.home.benchPosition) @@ -198,6 +210,17 @@ function Game:userIsOn(side) return ret, not ret end +---@return InputHandler offense, InputHandler defense +function Game:currentInputHandlers() + local userOnOffense, userOnDefense = self:userIsOn("offense") + local offenseInput = userOnOffense and self.userInput or self.npc + local defenseInput = userOnDefense and self.userInput or self.npc + offenseInput:update(self.state.deltaSeconds) + defenseInput:update(self.state.deltaSeconds) + + return offenseInput, defenseInput +end + ---@param pitchFlyTimeMs number | nil ---@param pitchTypeIndex number | nil ---@param accuracy number The closer to 1.0, the better @@ -308,18 +331,18 @@ end function Game:buttonControlledThrow(throwFlyMs, forbidThrowHome) local targetBase if playdate.buttonIsPressed(playdate.kButtonLeft) then - targetBase = C.Bases[C.Third] + targetBase = C.Third elseif playdate.buttonIsPressed(playdate.kButtonUp) then - targetBase = C.Bases[C.Second] + targetBase = C.Second elseif playdate.buttonIsPressed(playdate.kButtonRight) then - targetBase = C.Bases[C.First] + targetBase = C.First elseif not forbidThrowHome and playdate.buttonIsPressed(playdate.kButtonDown) then - targetBase = C.Bases[C.Home] + targetBase = C.Home else return false end - self.fielding:userThrowTo(targetBase, self.state.ball, throwFlyMs) + self.fielding:userThrowTo(C.Bases[targetBase], self.state.ball, throwFlyMs) self.baserunning.secondsSinceLastRunnerMove = 0 self.state.offenseState = C.Offense.running @@ -368,77 +391,80 @@ function Game:updateBatting(batDeg, batSpeed) -- and letting the user find a crank position and direction that works for them local batAngle = math.rad(batDeg) -- TODO: animate bat-flip or something - self.state.batBase.x = self.baserunning.batter and (self.baserunning.batter.x + C.BatterHandPos.x) or 0 - self.state.batBase.y = self.baserunning.batter and (self.baserunning.batter.y + C.BatterHandPos.y) or 0 + local batter = self.baserunning.batter + self.state.batBase.x = batter and (batter.x + C.BatterHandPos.x) or 0 + self.state.batBase.y = batter and (batter.y + C.BatterHandPos.y) or 0 self.state.batTip.x = self.state.batBase.x + (C.BatLength * math.sin(batAngle)) self.state.batTip.y = self.state.batBase.y + (C.BatLength * math.cos(batAngle)) if - batSpeed > 0 - and self.state.ball.y < 232 - and utils.pointDirectlyUnderLine( - self.state.ball.x, - self.state.ball.y, - self.state.batBase.x, - self.state.batBase.y, - self.state.batTip.x, - self.state.batTip.y, - C.Screen.H + not ( + batSpeed > 0 + and self.state.ball.y < 232 + and utils.pointDirectlyUnderLine( + self.state.ball.x, + self.state.ball.y, + self.state.batBase.x, + self.state.batBase.y, + self.state.batTip.x, + self.state.batTip.y, + C.Screen.H + ) ) then - -- Hit! - BatCrackReverb:play() - self.state.offenseState = C.Offense.running - local ballAngle = batAngle + math.rad(90) - - local mult = math.abs(batSpeed / 15) - local ballVelX = mult * 10 * math.sin(ballAngle) - local ballVelY = mult * 5 * math.cos(ballAngle) - if ballVelY > 0 then - ballVelX = ballVelX * -1 - ballVelY = ballVelY * -1 - end - - local ballDest = - utils.xy(self.state.ball.x + (ballVelX * C.BattingPower), self.state.ball.y + (ballVelY * C.BattingPower)) - - pitchTracker:reset() - local flyTimeMs = 2000 - -- TODO? A dramatic eye-level view on a home-run could be sick. - local battingTeamStats = self:battingTeamCurrentInning() - battingTeamStats.hits[#battingTeamStats.hits + 1] = ballDest - - if utils.isFoulBall(ballDest.x, ballDest.y) then - self.announcer:say("Foul ball!") - pitchTracker.strikes = math.min(pitchTracker.strikes + 1, 2) - -- TODO: Have a fielder chase for the fly-out - return - end - - if utils.pointIsSquarelyAboveLine(utils.xy(ballDest.x, ballDest.y), C.OutfieldWall) then - playdate.timer.new(flyTimeMs, function() - -- Verify that the home run wasn't intercepted - if - utils.within(1, self.state.ball.x, ballDest.x) and utils.within(1, self.state.ball.y, ballDest.y) - 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 - - local hitBallScaler = gfx.animator.new(2000, 9 + (mult * mult * 0.5), C.SmallestBallRadius, utils.easingHill) - self.state.ball:launch(ballDest.x, ballDest.y, playdate.easingFunctions.outQuint, flyTimeMs, nil, hitBallScaler) - - self.baserunning:convertBatterToRunner() - self.fielding:haveSomeoneChase(ballDest.x, ballDest.y) + return end + + -- Hit! + BatCrackReverb:play() + self.state.offenseState = C.Offense.running + local ballAngle = batAngle + math.rad(90) + + local mult = math.abs(batSpeed / 15) + local ballVelX = mult * 10 * math.sin(ballAngle) + local ballVelY = mult * 5 * math.cos(ballAngle) + if ballVelY > 0 then + ballVelX = ballVelX * -1 + ballVelY = ballVelY * -1 + end + + local ballDest = + utils.xy(self.state.ball.x + (ballVelX * C.BattingPower), self.state.ball.y + (ballVelY * C.BattingPower)) + + pitchTracker:reset() + local flyTimeMs = 2000 + -- TODO? A dramatic eye-level view on a home-run could be sick. + local battingTeamStats = self:battingTeamCurrentInning() + battingTeamStats.hits[#battingTeamStats.hits + 1] = ballDest + + if utils.isFoulBall(ballDest.x, ballDest.y) then + self.announcer:say("Foul ball!") + pitchTracker.strikes = math.min(pitchTracker.strikes + 1, 2) + -- TODO: Have a fielder chase for the fly-out + return + end + + if utils.pointIsSquarelyAboveLine(utils.xy(ballDest.x, ballDest.y), C.OutfieldWall) then + playdate.timer.new(flyTimeMs, function() + -- Verify that the home run wasn't intercepted + if utils.within(1, self.state.ball.x, ballDest.x) and utils.within(1, self.state.ball.y, ballDest.y) 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 + + local hitBallScaler = gfx.animator.new(2000, 9 + (mult * mult * 0.5), C.SmallestBallRadius, utils.easingHill) + self.state.ball:launch(ballDest.x, ballDest.y, playdate.easingFunctions.outQuint, flyTimeMs, nil, hitBallScaler) + + self.baserunning:convertBatterToRunner() + self.fielding:haveSomeoneChase(ballDest.x, ballDest.y) end ---@param appliedSpeed number | fun(runner: Runner): number @@ -471,43 +497,16 @@ function Game:returnToPitcher() end) end ----@param throwFly number -function Game:userPitch(throwFly, accuracy) - local aPressed = playdate.buttonIsPressed(playdate.kButtonA) - local bPressed = playdate.buttonIsPressed(playdate.kButtonB) - if not aPressed and not bPressed then - self:pitch(throwFly, 1, accuracy) - elseif aPressed and not bPressed then - self:pitch(throwFly, 2, accuracy) - elseif not aPressed and bPressed then - self:pitch(throwFly, 3, accuracy) - elseif aPressed and bPressed then - self:pitch(throwFly, 4, accuracy) - end -end - function Game:updateGameState() self.state.deltaSeconds = playdate.getElapsedTime() or 0 playdate.resetElapsedTime() - local crankChange = playdate.getCrankChange() --[[@as number]] - local crankLimited = crankChange == 0 and 0 or (math.log(math.abs(crankChange)) * C.CrankPower) - if crankChange < 0 then - crankLimited = crankLimited * -1 - end self.state.ball:updatePosition() - local userOnOffense, userOnDefense = self:userIsOn("offense") + local offenseHandler, defenseHandler = self:currentInputHandlers() local fielderHoldingBall = self.fielding:updateFielderPositions(self.state.ball, self.state.deltaSeconds) - if fielderHoldingBall then - local outedSomeRunner = self.baserunning:outEligibleRunners(fielderHoldingBall) - if not userOnDefense and self.state.offenseState == C.Offense.running then - self.npc:fielderAction(fielderHoldingBall, outedSomeRunner, self.state.ball) - end - end - if self.state.offenseState == C.Offense.batting then pitchTracker:recordIfPassed(self.state.ball) @@ -529,44 +528,22 @@ function Game:updateGameState() end local batSpeed - if userOnOffense then - self.state.batAngleDeg = (playdate.getCrankPosition() + C.CrankOffsetDeg) % 360 - batSpeed = crankLimited - else - self.state.batAngleDeg = - self.npc:updateBatAngle(self.state.ball, self.state.pitchIsOver, self.state.deltaSeconds) - batSpeed = self.npc:batSpeed() * self.state.deltaSeconds - end + self.state.batAngleDeg, batSpeed = + offenseHandler:updateBat(self.state.ball, self.state.pitchIsOver, self.state.deltaSeconds) self:updateBatting(self.state.batAngleDeg, batSpeed) -- Walk batter to the plate - self.baserunning:updateRunner( - self.baserunning.batter, - nil, - userOnOffense and crankLimited or 0, - false, - self.state.deltaSeconds - ) + self.baserunning:updateRunner(self.baserunning.batter, nil, 0, false, self.state.deltaSeconds) if pitchTracker.secondsSinceLastPitch > C.PitchAfterSeconds then - if userOnDefense then - local powerRatio, accuracy = throwMeter:readThrow(crankChange) - if powerRatio then - local throwFly = C.PitchFlyMs / powerRatio - if throwFly and not self:buttonControlledThrow(throwFly, true) then - self:userPitch(throwFly, accuracy) - end - end - else - self:pitch(C.PitchFlyMs / self.npc:pitchSpeed(), math.random(#Pitches)) - end + defenseHandler:pitch() end elseif self.state.offenseState == C.Offense.running then - local appliedSpeed = userOnOffense and crankLimited - or function(runner) - return self.npc:runningSpeed(runner, self.state.ball) - end + local appliedSpeed = function(runner) + return offenseHandler:runningSpeed(runner, self.state.ball) + end + local _, secondsSinceLastRunnerMove = self:updateNonBatterRunners(appliedSpeed, false, false) if secondsSinceLastRunnerMove > C.ResetFieldersAfterSeconds then -- End of play. Throw the ball back to the pitcher @@ -574,15 +551,11 @@ function Game:updateGameState() self:returnToPitcher() end - if userOnDefense then - local powerRatio = throwMeter:readThrow(crankChange) - if powerRatio then - local throwFly = C.PitchFlyMs / powerRatio - if throwFly then - self:buttonControlledThrow(throwFly) - end - end + local outedSomeRunner = false + if fielderHoldingBall then + outedSomeRunner = self.baserunning:outEligibleRunners(fielderHoldingBall) end + defenseHandler:fielderAction(fielderHoldingBall, outedSomeRunner, self.state.ball) elseif self.state.offenseState == C.Offense.walking then if not self:updateNonBatterRunners(C.WalkedRunnerSpeed, true, true) then self.state.offenseState = C.Offense.batting diff --git a/src/npc.lua b/src/npc.lua index 533450c..57f6cb3 100644 --- a/src/npc.lua +++ b/src/npc.lua @@ -2,7 +2,7 @@ local npcBatDeg = 0 local BaseNpcBatSpeed = 1500 local npcBatSpeed = 1500 ----@class Npc +---@class Npc: InputHandler ---@field runners Runner[] ---@field fielders Fielder[] -- selene: allow(unscoped_variables) @@ -22,22 +22,26 @@ end ---@param ball XyPair ---@param pitchIsOver boolean ---@param deltaSec number ----@return number +---@return number batAngleDeg, number batSpeed -- luacheck: no unused -function Npc:updateBatAngle(ball, pitchIsOver, deltaSec) +function Npc:updateBat(ball, pitchIsOver, deltaSec) if not pitchIsOver and ball.y > 200 and ball.y < 230 and (ball.x < C.Center.x + 15) then npcBatDeg = npcBatDeg + (deltaSec * npcBatSpeed) else npcBatSpeed = (1 + math.random()) * BaseNpcBatSpeed npcBatDeg = 230 end - return npcBatDeg + return npcBatDeg, (self:batSpeed() * deltaSec) end function Npc:batSpeed() return npcBatSpeed / 1.5 end +function Npc:pitch() + return C.PitchFlyMs / self:pitchSpeed(), math.random(#Pitches), 0.9 +end + local baseRunningSpeed = 25 ---@param runner Runner @@ -138,6 +142,9 @@ end ---@param outedSomeRunner boolean ---@param ball { x: number, y: number, heldBy: Fielder | nil, launch: LaunchBall } function Npc:fielderAction(fielder, outedSomeRunner, ball) + if not fielder then + return + end if outedSomeRunner then -- Delay a little before the next play playdate.timer.new(750, function() diff --git a/src/user-input.lua b/src/user-input.lua new file mode 100644 index 0000000..8655411 --- /dev/null +++ b/src/user-input.lua @@ -0,0 +1,64 @@ +---@class UserInput: InputHandler +---@field buttonControlledThrow: fun(throwFlyMs: number, forbidThrowHome: boolean): boolean didThrow +UserInput = {} + +function UserInput.new(buttonControlledThrow) + return setmetatable({ + buttonControlledThrow = buttonControlledThrow, + }, { __index = UserInput }) +end + +function UserInput:update() + self.crankChange = playdate.getCrankChange() + local crankLimited = self.crankChange == 0 and 0 or (math.log(math.abs(self.crankChange)) * C.CrankPower) + self.crankLimited = math.abs(crankLimited) +end + +function UserInput:updateBat() + local batAngleDeg = (playdate.getCrankPosition() + C.CrankOffsetDeg) % 360 + local batSpeed = self.crankLimited + return batAngleDeg, batSpeed +end + +function UserInput:runningSpeed() + return self.crankLimited +end + +---@param throwFlyMs number +---@param accuracy number | nil +---@return number | nil pitchFlyTimeMs, number | nil pitchTypeIndex, number | nil accuracy +local function userPitch(throwFlyMs, accuracy) + local aPressed = playdate.buttonIsPressed(playdate.kButtonA) + local bPressed = playdate.buttonIsPressed(playdate.kButtonB) + if not aPressed and not bPressed then + return throwFlyMs, 1, accuracy + elseif aPressed and not bPressed then + return throwFlyMs, 2, accuracy + elseif not aPressed and bPressed then + return throwFlyMs, 3, accuracy + elseif aPressed and bPressed then + return throwFlyMs, 4, accuracy + end + + return nil, nil, nil +end + +function UserInput:pitch() + local powerRatio, accuracy = throwMeter:readThrow(self.crankChange) + if powerRatio then + local throwFly = C.PitchFlyMs / powerRatio + if throwFly and not self.buttonControlledThrow(throwFly, true) then + userPitch(throwFly, accuracy) + end + end +end + +function UserInput:fielderAction() + local powerRatio = throwMeter:readThrow(self.crankChange) + if powerRatio then + local throwFly = C.PitchFlyMs / powerRatio + if throwFly then + self.buttonControlledThrow(throwFly, false) + end + end +end -- 2.47.1 From b928ee36581df4b197491d9ec171731b558a897a Mon Sep 17 00:00:00 2001 From: Sage Vaillancourt Date: Mon, 24 Feb 2025 15:55:59 -0500 Subject: [PATCH 2/5] Tuck bat away if `batter` is `nil` --- src/main.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main.lua b/src/main.lua index 82b6f0c..ebd60fe 100644 --- a/src/main.lua +++ b/src/main.lua @@ -392,8 +392,8 @@ function Game:updateBatting(batDeg, batSpeed) local batAngle = math.rad(batDeg) -- TODO: animate bat-flip or something local batter = self.baserunning.batter - self.state.batBase.x = batter and (batter.x + C.BatterHandPos.x) or 0 - self.state.batBase.y = batter and (batter.y + C.BatterHandPos.y) or 0 + self.state.batBase.x = batter and (batter.x + C.BatterHandPos.x) or -999 + self.state.batBase.y = batter and (batter.y + C.BatterHandPos.y) or -999 self.state.batTip.x = self.state.batBase.x + (C.BatLength * math.sin(batAngle)) self.state.batTip.y = self.state.batBase.y + (C.BatLength * math.cos(batAngle)) -- 2.47.1 From 7c7b5ff76249f3f32f032c1509789e9832dabdec Mon Sep 17 00:00:00 2001 From: Sage Vaillancourt Date: Mon, 24 Feb 2025 16:24:21 -0500 Subject: [PATCH 3/5] Extract Game:updatePitching() Also, pull more from updateGameState() into updateBatting() --- src/main.lua | 68 +++++++++++++++++++++++++++------------------------- 1 file changed, 35 insertions(+), 33 deletions(-) diff --git a/src/main.lua b/src/main.lua index ebd60fe..dfa1881 100644 --- a/src/main.lua +++ b/src/main.lua @@ -381,8 +381,11 @@ end local SwingBackDeg = 30 local SwingForwardDeg = 170 ----@param batDeg number -function Game:updateBatting(batDeg, batSpeed) +---@param offenseHandler InputHandler +function Game:updateBatting(offenseHandler) + local batDeg, batSpeed = offenseHandler:updateBat(self.state.ball, self.state.pitchIsOver, self.state.deltaSeconds) + self.state.batAngleDeg = batDeg + if not self.state.pitchIsOver and batDeg > SwingBackDeg and batDeg < SwingForwardDeg then self.state.didSwing = true end @@ -468,7 +471,6 @@ function Game:updateBatting(batDeg, batSpeed) end ---@param appliedSpeed number | fun(runner: Runner): number ---- ---@return boolean runnersStillMoving, number secondsSinceLastRunnerMove function Game:updateNonBatterRunners(appliedSpeed, forcedOnly, isAutoRun) local runnersStillMoving, runnersScored, secondsSinceLastRunnerMove = @@ -497,6 +499,31 @@ function Game:returnToPitcher() end) end +---@param defenseHandler InputHandler +function Game:updatePitching(defenseHandler) + pitchTracker:recordIfPassed(self.state.ball) + + if self:pitcherIsOnTheMound() then + pitchTracker.secondsSinceLastPitch = pitchTracker.secondsSinceLastPitch + self.state.deltaSeconds + end + + 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 + self:returnToPitcher() + self.state.pitchIsOver = true + self.state.didSwing = false + end + + if pitchTracker.secondsSinceLastPitch > C.PitchAfterSeconds then + defenseHandler:pitch() + end +end + function Game:updateGameState() self.state.deltaSeconds = playdate.getElapsedTime() or 0 playdate.resetElapsedTime() @@ -508,37 +535,11 @@ function Game:updateGameState() local fielderHoldingBall = self.fielding:updateFielderPositions(self.state.ball, self.state.deltaSeconds) if self.state.offenseState == C.Offense.batting then - pitchTracker:recordIfPassed(self.state.ball) - - local pitcher = self.fielding.fielders.pitcher - if utils.distanceBetween(pitcher.x, pitcher.y, C.PitcherStartPos.x, C.PitcherStartPos.y) < C.BaseHitbox then - pitchTracker.secondsSinceLastPitch = pitchTracker.secondsSinceLastPitch + self.state.deltaSeconds - end - - 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 - self:returnToPitcher() - self.state.pitchIsOver = true - self.state.didSwing = false - end - - local batSpeed - self.state.batAngleDeg, batSpeed = - offenseHandler:updateBat(self.state.ball, self.state.pitchIsOver, self.state.deltaSeconds) - - self:updateBatting(self.state.batAngleDeg, batSpeed) + self:updatePitching(defenseHandler) + self:updateBatting(offenseHandler) -- Walk batter to the plate self.baserunning:updateRunner(self.baserunning.batter, nil, 0, false, self.state.deltaSeconds) - - if pitchTracker.secondsSinceLastPitch > C.PitchAfterSeconds then - defenseHandler:pitch() - end elseif self.state.offenseState == C.Offense.running then local appliedSpeed = function(runner) return offenseHandler:runningSpeed(runner, self.state.ball) @@ -584,7 +585,6 @@ function Game:update() playdate.timer.updateTimers() gfx.animation.blinker.updateAll() self:updateGameState() - local ball = self.state.ball gfx.clear() gfx.setColor(gfx.kColorBlack) @@ -601,6 +601,8 @@ function Game:update() characterDraws[#characterDraws + 1] = { y = y, drawAction = drawAction } end + local ball = self.state.ball + local danceOffset = FielderDanceAnimator:currentValue() ---@type Fielder | nil local ballHeldBy @@ -608,7 +610,7 @@ function Game:update() addDraw(fielder.y + danceOffset, function() local ballHeldByThisFielder = drawFielder( self.state.fieldingTeamSprites[fielder.spriteIndex], - self.state.ball, + ball, fielder.x, fielder.y + danceOffset ) -- 2.47.1 From 9bbd68c302a9b1bdc2e52f5eb9c21f2935582771 Mon Sep 17 00:00:00 2001 From: Sage Vaillancourt Date: Mon, 24 Feb 2025 17:01:51 -0500 Subject: [PATCH 4/5] Some other small main.lua refactoring --- src/main.lua | 47 ++++++++++++++++++++++------------------------- 1 file changed, 22 insertions(+), 25 deletions(-) diff --git a/src/main.lua b/src/main.lua index dfa1881..b1219b5 100644 --- a/src/main.lua +++ b/src/main.lua @@ -383,7 +383,8 @@ local SwingForwardDeg = 170 ---@param offenseHandler InputHandler function Game:updateBatting(offenseHandler) - local batDeg, batSpeed = offenseHandler:updateBat(self.state.ball, self.state.pitchIsOver, self.state.deltaSeconds) + local ball = self.state.ball + local batDeg, batSpeed = offenseHandler:updateBat(ball, self.state.pitchIsOver, self.state.deltaSeconds) self.state.batAngleDeg = batDeg if not self.state.pitchIsOver and batDeg > SwingBackDeg and batDeg < SwingForwardDeg then @@ -400,39 +401,36 @@ function Game:updateBatting(offenseHandler) self.state.batTip.x = self.state.batBase.x + (C.BatLength * math.sin(batAngle)) self.state.batTip.y = self.state.batBase.y + (C.BatLength * math.cos(batAngle)) - if - not ( - batSpeed > 0 - and self.state.ball.y < 232 - and utils.pointDirectlyUnderLine( - self.state.ball.x, - self.state.ball.y, - self.state.batBase.x, - self.state.batBase.y, - self.state.batTip.x, - self.state.batTip.y, - C.Screen.H - ) + local ballWasHit = batSpeed > 0 + and ball.y < 232 + and utils.pointDirectlyUnderLine( + ball.x, + ball.y, + self.state.batBase.x, + self.state.batBase.y, + self.state.batTip.x, + self.state.batTip.y, + C.Screen.H ) - then + + if not ballWasHit then return end -- Hit! BatCrackReverb:play() self.state.offenseState = C.Offense.running - local ballAngle = batAngle + math.rad(90) + local ballAngle = batAngle + math.rad(90) local mult = math.abs(batSpeed / 15) - local ballVelX = mult * 10 * math.sin(ballAngle) - local ballVelY = mult * 5 * math.cos(ballAngle) + local ballVelX = mult * C.BattingPower * 10 * math.sin(ballAngle) + local ballVelY = mult * C.BattingPower * 5 * math.cos(ballAngle) if ballVelY > 0 then ballVelX = ballVelX * -1 ballVelY = ballVelY * -1 end - local ballDest = - utils.xy(self.state.ball.x + (ballVelX * C.BattingPower), self.state.ball.y + (ballVelY * C.BattingPower)) + local ballDest = utils.xy(ball.x + ballVelX, ball.y + ballVelY) pitchTracker:reset() local flyTimeMs = 2000 @@ -450,7 +448,7 @@ function Game:updateBatting(offenseHandler) if utils.pointIsSquarelyAboveLine(utils.xy(ballDest.x, ballDest.y), C.OutfieldWall) then playdate.timer.new(flyTimeMs, function() -- Verify that the home run wasn't intercepted - if utils.within(1, self.state.ball.x, ballDest.x) and utils.within(1, self.state.ball.y, ballDest.y) then + if utils.within(1, ball.x, ballDest.x) and utils.within(1, ball.y, ballDest.y) 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. @@ -464,7 +462,7 @@ function Game:updateBatting(offenseHandler) end local hitBallScaler = gfx.animator.new(2000, 9 + (mult * mult * 0.5), C.SmallestBallRadius, utils.easingHill) - self.state.ball:launch(ballDest.x, ballDest.y, playdate.easingFunctions.outQuint, flyTimeMs, nil, hitBallScaler) + ball:launch(ballDest.x, ballDest.y, playdate.easingFunctions.outQuint, flyTimeMs, nil, hitBallScaler) self.baserunning:convertBatterToRunner() self.fielding:haveSomeoneChase(ballDest.x, ballDest.y) @@ -525,9 +523,10 @@ function Game:updatePitching(defenseHandler) end function Game:updateGameState() + playdate.timer.updateTimers() + gfx.animation.blinker.updateAll() self.state.deltaSeconds = playdate.getElapsedTime() or 0 playdate.resetElapsedTime() - self.state.ball:updatePosition() local offenseHandler, defenseHandler = self:currentInputHandlers() @@ -582,8 +581,6 @@ function Game:updateGameState() end function Game:update() - playdate.timer.updateTimers() - gfx.animation.blinker.updateAll() self:updateGameState() gfx.clear() -- 2.47.1 From 09e48b65b4c3c6b539f95bbfd2f6816c16ca1199 Mon Sep 17 00:00:00 2001 From: Sage Vaillancourt Date: Mon, 24 Feb 2025 19:49:32 -0500 Subject: [PATCH 5/5] Fix pitching --- src/main.lua | 2 +- src/npc.lua | 2 ++ src/pitching.lua | 5 +++-- src/user-input.lua | 2 +- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/main.lua b/src/main.lua index b1219b5..709f7f9 100644 --- a/src/main.lua +++ b/src/main.lua @@ -518,7 +518,7 @@ function Game:updatePitching(defenseHandler) end if pitchTracker.secondsSinceLastPitch > C.PitchAfterSeconds then - defenseHandler:pitch() + self:pitch(defenseHandler:pitch()) end end diff --git a/src/npc.lua b/src/npc.lua index 57f6cb3..be0eb24 100644 --- a/src/npc.lua +++ b/src/npc.lua @@ -18,6 +18,8 @@ function Npc.new(runners, fielders) }, { __index = Npc }) end +function Npc.update() end + -- TODO: FAR more nuanced NPC batting. ---@param ball XyPair ---@param pitchIsOver boolean diff --git a/src/pitching.lua b/src/pitching.lua index 42381a3..18d9aa3 100644 --- a/src/pitching.lua +++ b/src/pitching.lua @@ -54,12 +54,13 @@ Pitches = { y = gfx.animator.new(C.PitchFlyMs, C.PitchStart.y, C.PitchEndY, playdate.easingFunctions.linear), } end, - -- Wobbbleball + -- Wobbleball function(accuracy, ball) + local missBy = getPitchMissBy(accuracy) return { x = { currentValue = function() - return getPitchMissBy(accuracy) + return missBy + C.PitchStart.x + (10 * math.sin((ball.yAnimator:currentValue() - C.PitchStart.y) / 10)) end, diff --git a/src/user-input.lua b/src/user-input.lua index 8655411..02a78b9 100644 --- a/src/user-input.lua +++ b/src/user-input.lua @@ -48,7 +48,7 @@ function UserInput:pitch() if powerRatio then local throwFly = C.PitchFlyMs / powerRatio if throwFly and not self.buttonControlledThrow(throwFly, true) then - userPitch(throwFly, accuracy) + return userPitch(throwFly, accuracy) end end end -- 2.47.1