From 51855e13cf165e0d4b6483b2046fc5c99c9020c2 Mon Sep 17 00:00:00 2001 From: Sage Vaillancourt Date: Sat, 15 Feb 2025 17:38:56 -0500 Subject: [PATCH] Convert main into a Game object. Much BATTER encapsulation of its dependencies and mutable state. Only the team scores are still global and mutable, but that shouldn't be too hard to fix. --- src/ball.lua | 1 + src/fielding.lua | 12 +- src/graphics.lua | 8 +- src/main-menu.lua | 20 +- src/main.lua | 549 ++++++++++++++++++++++++---------------------- src/npc.lua | 15 +- 6 files changed, 317 insertions(+), 288 deletions(-) diff --git a/src/ball.lua b/src/ball.lua index 9b7016c..2632e7f 100644 --- a/src/ball.lua +++ b/src/ball.lua @@ -58,6 +58,7 @@ end ---@param floaty boolean | nil ---@param customBallScaler pd_animator | nil function Ball:launch(destX, destY, easingFunc, flyTimeMs, floaty, customBallScaler) + throwMeter:reset() self.heldBy = nil if not flyTimeMs then diff --git a/src/fielding.lua b/src/fielding.lua index f682213..942e668 100644 --- a/src/fielding.lua +++ b/src/fielding.lua @@ -151,9 +151,9 @@ end -- TODO? Start moving target fielders close sooner? ---@param field Fielding ---@param targetBase Base ----@param launchBall LaunchBall +---@param ball { launch: LaunchBall } ---@param throwFlyMs number -local function userThrowToCoroutine(field, targetBase, launchBall, throwFlyMs) +local function userThrowToCoroutine(field, targetBase, ball, throwFlyMs) while true do if field.fielderHoldingBall == nil then coroutine.yield() @@ -163,7 +163,7 @@ local function userThrowToCoroutine(field, targetBase, launchBall, throwFlyMs) end) closestFielder.target = targetBase - launchBall(targetBase.x, targetBase.y, playdate.easingFunctions.linear, throwFlyMs) + ball:launch(targetBase.x, targetBase.y, playdate.easingFunctions.linear, throwFlyMs) Fielding.markIneligible(field.fielderHoldingBall) return @@ -174,12 +174,12 @@ end --- Buffer in a fielder throw action. ---@param self table ---@param targetBase Base ----@param launchBall LaunchBall +---@param ball { launch: LaunchBall } ---@param throwFlyMs number -function Fielding:userThrowTo(targetBase, launchBall, throwFlyMs) +function Fielding:userThrowTo(targetBase, ball, throwFlyMs) local maxTryTimeMs = 5000 actionQueue:upsert("userThrowTo", maxTryTimeMs, function() - userThrowToCoroutine(self, targetBase, launchBall, throwFlyMs) + userThrowToCoroutine(self, targetBase, ball, throwFlyMs) end) end diff --git a/src/graphics.lua b/src/graphics.lua index 9776473..9a34b25 100644 --- a/src/graphics.lua +++ b/src/graphics.lua @@ -20,6 +20,8 @@ function getDrawOffset(ballX, ballY) return offsetX * 1.3, offsetY * 1.5 end +---@class Blipper +---@field draw fun(self: self, disableBlipping: boolean, x: number, y: number) -- selene: allow(unscoped_variables) blipper = {} @@ -39,9 +41,3 @@ function blipper.new(msInterval, smiling, lowHat) end, } end - ---selene: allow(unscoped_variables) -HomeTeamBlipper = blipper.new(100, HomeTeamSprites.smiling, HomeTeamSprites.lowHat) - ---selene: allow(unscoped_variables) -AwayTeamBlipper = blipper.new(100, AwayTeamSprites.smiling, AwayTeamSprites.lowHat) diff --git a/src/main-menu.lua b/src/main-menu.lua index a306901..9ec9ddf 100644 --- a/src/main-menu.lua +++ b/src/main-menu.lua @@ -1,9 +1,8 @@ -- selene: allow(unscoped_variables) ---@class MainMenu MainMenu = { - mainGameUpdateFunction = nil, - ---@type fun(settings: Settings) - mainGameInitFunction = nil, + ---@type { new: fun(settings: Settings): { update: fun(self) } } + next = nil, } -- selene: allow(shadowing) local gfx = playdate.graphics @@ -12,24 +11,25 @@ local gfx = playdate.graphics local StartFont = gfx.font.new("fonts/Roobert-20-Medium.pft") --- Take control of playdate.update ----@param config MainMenu Function that controls the main gameplay loop. ---- Will replace playdate.update when the menu is done. -function MainMenu.start(config) - MainMenu.mainGameUpdateFunction = config.mainGameUpdateFunction - MainMenu.mainGameInitFunction = config.mainGameInitFunction +--- Will replace playdate.update when the menu is done. +---@param next { new: fun(settings: Settings): { update: fun(self) } } +function MainMenu.start(next) + MainMenu.next = next playdate.update = MainMenu.update end local inningCountSelection = 3 local function startGame() - MainMenu.mainGameInitFunction({ + local next = MainMenu.next.new({ finalInning = inningCountSelection, homeTeamSprites = HomeTeamSprites, awayTeamSprites = AwayTeamSprites, }) playdate.resetElapsedTime() - playdate.update = MainMenu.mainGameUpdateFunction + playdate.update = function() + next:update() + end end local function pausingEaser(baseEaser) diff --git a/src/main.lua b/src/main.lua index beb81a6..c67d019 100644 --- a/src/main.lua +++ b/src/main.lua @@ -11,6 +11,7 @@ import 'CoreLibs/ui.lua' --- @alias EasingFunc fun(number, number, number, number): number --- @alias LaunchBall fun( +--- self: self, --- destX: number, --- destY: number, --- easingFunc: EasingFunc, @@ -44,34 +45,6 @@ import 'npc.lua' -- selene: allow(shadowing) local gfx , C = playdate.graphics, C -local announcer = Announcer.new() -local fielding = Fielding.new() --- TODO: Find a way to get baserunning and npc instantiated closer to the top, here. --- Currently difficult because they depend on nextHalfInning/each other. - ------------------- --- GLOBAL STATE -- ------------------- - ---- Well, maybe not "Settings", but passive state that probably won't change much, if at all, during a game. ----@class Settings -local settings = { - finalInning = 3, - ---@type SpriteCollection - awayTeamSprites = nil, - ---@type SpriteCollection - homeTeamSprites = nil, -} - -local deltaSeconds = 0 -local ball = Ball.new(gfx.animator) - -local batBase = utils.xy(C.Center.x - 34, 215) -local batTip = utils.xy(0, 0) -local batAngleDeg = C.CrankOffsetDeg - -local catcherThrownBall = false - ---@alias Team { score: number, benchPosition: XyPair } ---@type table local teams = { @@ -85,67 +58,152 @@ local teams = { }, } -local inning = 1 - -local battingTeam = teams.away -local offenseState = C.Offense.batting +--- Well, maybe not "Settings", but passive state that probably won't change much, if at all, during a game. +---@class Settings +---@field finalInning number +---@field userTeam Team | nil +---@field awayTeamSprites SpriteCollection +---@field homeTeamSprites SpriteCollection +---@class MutableState +---@field deltaSeconds number +---@field ball Ball +---@field battingTeam Team +---@field catcherThrownBall boolean +---@field offenseState OffenseState +---@field inning number +---@field batBase XyPair +---@field batTip XyPair +---@field batAngleDeg number -- TODO: Replace with timers, repeatedly reset, instead of constantly setting to 0 -local secondsSinceLastRunnerMove = 0 -local secondsSincePitchAllowed = 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. -local runnerBlipper -local battingTeamSprites -local fieldingTeamSprites +---@field runnerBlipper Blipper +---@field battingTeamSprites SpriteCollection +---@field fieldingTeamSprites SpriteCollection -------------------------- --- END OF GLOBAL STATE -- -------------------------- +---@class Game +---@field private settings Settings +---@field private announcer Announcer +---@field private fielding Fielding +---@field private baserunning Baserunning +---@field private npc Npc +---@field private homeTeamBlipper Blipper +---@field private awayTeamBlipper Blipper +---@field private state MutableState +-- selene: allow(unscoped_variables) +Game = {} -local UserTeam = teams.away +---@param settings Settings +---@param announcer Announcer | nil +---@param fielding Fielding | nil +---@param baserunning Baserunning | nil +---@param npc Npc | nil +---@param state MutableState | nil +---@return Game +function Game.new(settings, announcer, fielding, baserunning, npc, state) + announcer = announcer or Announcer.new() + fielding = fielding or Fielding.new() + settings.userTeam = teams.away + + local homeTeamBlipper = blipper.new(100, settings.homeTeamSprites.smiling, settings.homeTeamSprites.lowHat) + local awayTeamBlipper = blipper.new(100, settings.awayTeamSprites.smiling, settings.awayTeamSprites.lowHat) + local battingTeam = teams.away + local runnerBlipper = battingTeam == teams.away and awayTeamBlipper or homeTeamBlipper + local ball = Ball.new(gfx.animator) + + local o = setmetatable({ + settings = settings, + announcer = announcer, + fielding = fielding, + state = state or { + batBase = utils.xy(C.Center.x - 34, 215), + batTip = utils.xy(0, 0), + batAngleDeg = C.CrankOffsetDeg, + deltaSeconds = 0, + ball = ball, + battingTeam = battingTeam, + offenseState = C.Offense.batting, + inning = 1, + catcherThrownBall = false, + secondsSinceLastRunnerMove = 0, + secondsSincePitchAllowed = 0, + battingTeamSprites = settings.awayTeamSprites, + fieldingTeamSprites = settings.homeTeamSprites, + homeTeamBlipper = homeTeamBlipper, + awayTeamBlipper = awayTeamBlipper, + runnerBlipper = runnerBlipper, + }, + }, { __index = Game }) + + o.baserunning = baserunning or Baserunning.new(announcer, function() + o:nextHalfInning() + end) + o.npc = npc or Npc.new(o.baserunning.runners, o.fielding.fielders) + + o.fielding:resetFielderPositions(teams.home.benchPosition) + playdate.timer.new(2000, function() + ball:launch(C.PitchStartX, C.PitchStartY, playdate.easingFunctions.linear, nil, false) + end) + + BootTune:play() + BootTune:setFinishCallback(function() + TinnyBackground:play() + end) + + return o +end ---@alias SimpleAnimator { currentValue: fun(self): number; reset: fun(self, durationMs: number | nil) } ----@alias Pitch { x: SimpleAnimator, y: SimpleAnimator, z: SimpleAnimator | nil } +---@alias Pitch fun(ball: Ball): { x: SimpleAnimator, y: SimpleAnimator, z: SimpleAnimator | nil } ---@type Pitch[] local Pitches = { -- Fastball - { - 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), - }, + 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), + } + end, -- Curve ball - { - 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), - }, + 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), + } + end, -- Slider - { - 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), - }, + 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), + } + end, -- Wobbbleball - { - x = { - currentValue = function() - return C.PitchStartX + (10 * math.sin((ball.yAnimator:currentValue() - C.PitchStartY) / 10)) - end, - reset = function() end, - }, - y = gfx.animator.new(C.PitchFlyMs * 1.3, C.PitchStartY, C.PitchEndY, playdate.easingFunctions.linear), - }, + function(ball) + return { + x = { + currentValue = function() + return C.PitchStartX + (10 * math.sin((ball.yAnimator:currentValue() - C.PitchStartY) / 10)) + end, + reset = function() end, + }, + y = gfx.animator.new(C.PitchFlyMs * 1.3, C.PitchStartY, C.PitchEndY, playdate.easingFunctions.linear), + } + end, } ---@return boolean userIsOnSide, boolean userIsOnOtherSide -local function userIsOn(side) - if UserTeam == nil then +function Game:userIsOn(side) + if self.settings.userTeam == nil then -- Both teams are NPC-driven return false, false end local ret - if UserTeam == battingTeam then + if self.settings.userTeam == self.state.battingTeam then ret = side == C.Sides.offense else ret = side == C.Sides.defense @@ -153,86 +211,77 @@ local function userIsOn(side) return ret, not ret end ----@type LaunchBall -local function launchBall(destX, destY, easingFunc, flyTimeMs, floaty, customBallScaler) - throwMeter:reset() - ball:launch(destX, destY, easingFunc, flyTimeMs, floaty, customBallScaler) -end - ---@param pitchFlyTimeMs number | nil ---@param pitchTypeIndex number | nil -local function pitch(pitchFlyTimeMs, pitchTypeIndex) - Fielding.markIneligible(fielding.fielders.pitcher) - ball.heldBy = nil - catcherThrownBall = false - offenseState = C.Offense.batting +function Game:pitch(pitchFlyTimeMs, pitchTypeIndex) + Fielding.markIneligible(self.fielding.fielders.pitcher) + self.state.ball.heldBy = nil + self.state.catcherThrownBall = false + self.state.offenseState = C.Offense.batting - local current = Pitches[pitchTypeIndex] - ball.xAnimator = current.x - ball.yAnimator = current.y or Pitches[1].y + local current = Pitches[pitchTypeIndex](self.state.ball) + self.state.ball.xAnimator = current.x + self.state.ball.yAnimator = current.y or Pitches[1](self.state.ball).y - -- TODO: This would need to be sanely replaced in launchBall() etc. + -- TODO: This would need to be sanely replaced in ball:launch() etc. -- if current.z then -- ball.floatAnimator = current.z -- ball.floatAnimator:reset() -- end if pitchFlyTimeMs then - ball.xAnimator:reset(pitchFlyTimeMs) - ball.yAnimator:reset(pitchFlyTimeMs) + self.state.ball.xAnimator:reset(pitchFlyTimeMs) + self.state.ball.yAnimator:reset(pitchFlyTimeMs) else - ball.xAnimator:reset() - ball.yAnimator:reset() + self.state.ball.xAnimator:reset() + self.state.ball.yAnimator:reset() end - secondsSincePitchAllowed = 0 + self.state.secondsSincePitchAllowed = 0 end -local function nextHalfInning() +function Game:nextHalfInning() pitchTracker:reset() - local currentlyFieldingTeam = battingTeam == teams.home and teams.away or teams.home - local gameOver = inning == settings.finalInning and teams.away.score ~= teams.home.score + local currentlyFieldingTeam = self.state.battingTeam == teams.home and teams.away or teams.home + local gameOver = self.state.inning == self.settings.finalInning and teams.away.score ~= teams.home.score if not gameOver then - fielding:celebrate() - secondsSinceLastRunnerMove = -7 - fielding:benchTo(currentlyFieldingTeam.benchPosition) - announcer:say("SWITCHING SIDES...") + self.fielding:celebrate() + self.state.secondsSinceLastRunnerMove = -7 + self.fielding:benchTo(currentlyFieldingTeam.benchPosition) + self.announcer:say("SWITCHING SIDES...") end if gameOver then - announcer:say("AND THAT'S THE BALL GAME!") + self.announcer:say("AND THAT'S THE BALL GAME!") else - fielding:resetFielderPositions() - if battingTeam == teams.home then - inning = inning + 1 + self.fielding:resetFielderPositions() + if self.state.battingTeam == teams.home then + self.state.inning = self.state.inning + 1 end - battingTeam = currentlyFieldingTeam + self.state.battingTeam = currentlyFieldingTeam playdate.timer.new(2000, function() - if battingTeam == teams.home then - battingTeamSprites = settings.homeTeamSprites - runnerBlipper = HomeTeamBlipper - fieldingTeamSprites = settings.awayTeamSprites + if self.state.battingTeam == teams.home then + self.state.battingTeamSprites = self.settings.homeTeamSprites + self.state.runnerBlipper = self.homeTeamBlipper + self.state.fieldingTeamSprites = self.settings.awayTeamSprites else - battingTeamSprites = settings.awayTeamSprites - fieldingTeamSprites = settings.homeTeamSprites - runnerBlipper = AwayTeamBlipper + self.state.battingTeamSprites = self.settings.awayTeamSprites + self.state.fieldingTeamSprites = self.settings.homeTeamSprites + self.state.runnerBlipper = self.awayTeamBlipper end end) end end -local baserunning = Baserunning.new(announcer, nextHalfInning) -local npc = Npc.new(baserunning.runners, fielding.fielders) - ---@param scoredRunCount number -local function score(scoredRunCount) - battingTeam.score = battingTeam.score + scoredRunCount - announcer:say("SCORE!") +function Game:score(scoredRunCount) + self.state.battingTeam.score = self.state.battingTeam.score + scoredRunCount + self.announcer:say("SCORE!") end ---@param throwFlyMs number ---@return boolean didThrow -local function buttonControlledThrow(throwFlyMs, forbidThrowHome) +function Game:buttonControlledThrow(throwFlyMs, forbidThrowHome) local targetBase if playdate.buttonIsPressed(playdate.kButtonLeft) then targetBase = C.Bases[C.Third] @@ -246,62 +295,67 @@ local function buttonControlledThrow(throwFlyMs, forbidThrowHome) return false end - -- Power for this throw has already been determined - throwMeter:reset() - - fielding:userThrowTo(targetBase, launchBall, throwFlyMs) - secondsSinceLastRunnerMove = 0 - offenseState = C.Offense.running + self.fielding:userThrowTo(targetBase, self.state.ball, throwFlyMs) + self.state.secondsSinceLastRunnerMove = 0 + self.state.offenseState = C.Offense.running return true end -local function nextBatter() - secondsSincePitchAllowed = -3 - baserunning.batter = nil +function Game:nextBatter() + self.state.secondsSincePitchAllowed = -3 + self.baserunning.batter = nil playdate.timer.new(2000, function() pitchTracker:reset() - if not baserunning.batter then - baserunning:pushNewBatter() + if not self.baserunning.batter then + self.baserunning:pushNewBatter() end end) end -local function walk() - announcer:say("Walk!") - -- TODO? Use baserunning:convertBatterToRunner() - baserunning.batter.nextBase = C.Bases[C.First] - baserunning.batter.prevBase = C.Bases[C.Home] - offenseState = C.Offense.walking - baserunning.batter = nil - baserunning:updateForcedRunners() - nextBatter() +function Game:walk() + self.announcer:say("Walk!") + -- TODO? Use self.baserunning:convertBatterToRunner() + self.baserunning.batter.nextBase = C.Bases[C.First] + self.baserunning.batter.prevBase = C.Bases[C.Home] + self.state.offenseState = C.Offense.walking + self.baserunning.batter = nil + self.baserunning:updateForcedRunners() + self:nextBatter() end -local function strikeOut() - local outBatter = baserunning.batter - baserunning.batter = nil - baserunning:outRunner(outBatter --[[@as Runner]], "Strike out!") - nextBatter() +function Game:strikeOut() + local outBatter = self.baserunning.batter + self.baserunning.batter = nil + self.baserunning:outRunner(outBatter --[[@as Runner]], "Strike out!") + self:nextBatter() end ---@param batDeg number -local function updateBatting(batDeg, batSpeed) +function Game:updateBatting(batDeg, batSpeed) local batAngle = math.rad(batDeg) -- TODO: animate bat-flip or something - batBase.x = baserunning.batter and (baserunning.batter.x + C.BatterHandPos.x) or 0 - batBase.y = baserunning.batter and (baserunning.batter.y + C.BatterHandPos.y) or 0 - batTip.x = batBase.x + (C.BatLength * math.sin(batAngle)) - batTip.y = batBase.y + (C.BatLength * math.cos(batAngle)) + 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 + 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 utils.pointDirectlyUnderLine(ball.x, ball.y, batBase.x, batBase.y, batTip.x, batTip.y, C.Screen.H) - and 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 + ) + and self.state.ball.y < 232 then -- Hit! BatCrackReverb:play() - offenseState = C.Offense.running + self.state.offenseState = C.Offense.running local ballAngle = batAngle + math.rad(90) local mult = math.abs(batSpeed / 15) @@ -311,52 +365,54 @@ local function updateBatting(batDeg, batSpeed) ballVelX = ballVelX * -1 ballVelY = ballVelY * -1 end - local ballDestX = ball.x + (ballVelX * C.BattingPower) - local ballDestY = ball.y + (ballVelY * C.BattingPower) + local ballDestX = self.state.ball.x + (ballVelX * C.BattingPower) + local ballDestY = self.state.ball.y + (ballVelY * C.BattingPower) pitchTracker:reset() local hitBallScaler = gfx.animator.new(2000, 9 + (mult * mult * 0.5), C.SmallestBallRadius, utils.easingHill) - launchBall(ballDestX, ballDestY, playdate.easingFunctions.outQuint, 2000, nil, hitBallScaler) + self.state.ball:launch(ballDestX, ballDestY, playdate.easingFunctions.outQuint, 2000, nil, hitBallScaler) -- TODO? A dramatic eye-level view on a home-run could be sick. if utils.isFoulBall(ballDestX, ballDestY) then - announcer:say("Foul ball!") + self.announcer:say("Foul ball!") pitchTracker.strikes = math.min(pitchTracker.strikes + 1, 2) -- TODO: Have a fielder chase for the fly-out return end - baserunning:convertBatterToRunner() + self.baserunning:convertBatterToRunner() - fielding:haveSomeoneChase(ballDestX, ballDestY) + self.fielding:haveSomeoneChase(ballDestX, ballDestY) end end ---@param appliedSpeed number | fun(runner: Runner): number ---@return boolean someRunnerMoved -local function updateNonBatterRunners(appliedSpeed, forcedOnly) - local runnerMoved, runnersScored = baserunning:updateNonBatterRunners(appliedSpeed, forcedOnly, deltaSeconds) +function Game:updateNonBatterRunners(appliedSpeed, forcedOnly) + local runnerMoved, runnersScored = + self.baserunning:updateNonBatterRunners(appliedSpeed, forcedOnly, self.state.deltaSeconds) if runnersScored ~= 0 then - score(runnersScored) + self:score(runnersScored) end return runnerMoved end -local function userPitch(throwFly) - local aButton = playdate.buttonIsPressed(playdate.kButtonA) - local bButton = playdate.buttonIsPressed(playdate.kButtonB) - if not aButton and not bButton then - pitch(throwFly, 1) - elseif aButton and not bButton then - pitch(throwFly, 2) - elseif not aButton and bButton then - pitch(throwFly, 3) - elseif aButton and bButton then - pitch(throwFly, 4) +---@param throwFly number +function Game:userPitch(throwFly) + local aPressed = playdate.buttonIsPressed(playdate.kButtonA) + local bPressed = playdate.buttonIsPressed(playdate.kButtonB) + if not aPressed and not bPressed then + self:pitch(throwFly, 1) + elseif aPressed and not bPressed then + self:pitch(throwFly, 2) + elseif not aPressed and bPressed then + self:pitch(throwFly, 3) + elseif aPressed and bPressed then + self:pitch(throwFly, 4) end end -local function updateGameState() - deltaSeconds = playdate.getElapsedTime() or 0 +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) @@ -364,115 +420,117 @@ local function updateGameState() crankLimited = crankLimited * -1 end - ball:updatePosition() + self.state.ball:updatePosition() - local userOnOffense, userOnDefense = userIsOn(C.Sides.offense) + local userOnOffense, userOnDefense = self:userIsOn(C.Sides.offense) if userOnDefense then - throwMeter:applyCharge(deltaSeconds, crankLimited) + throwMeter:applyCharge(self.state.deltaSeconds, crankLimited) end - if offenseState == C.Offense.batting then - if ball.y < C.StrikeZoneStartY then + if self.state.offenseState == C.Offense.batting then + if self.state.ball.y < C.StrikeZoneStartY then pitchTracker.recordedPitchX = nil elseif not pitchTracker.recordedPitchX then - pitchTracker.recordedPitchX = ball.x + pitchTracker.recordedPitchX = self.state.ball.x end - local pitcher = fielding.fielders.pitcher + local pitcher = self.fielding.fielders.pitcher if utils.distanceBetween(pitcher.x, pitcher.y, C.PitcherStartPos.x, C.PitcherStartPos.y) < C.BaseHitbox then - secondsSincePitchAllowed = secondsSincePitchAllowed + deltaSeconds + self.state.secondsSincePitchAllowed = self.state.secondsSincePitchAllowed + self.state.deltaSeconds end - if secondsSincePitchAllowed > C.ReturnToPitcherAfterSeconds and not catcherThrownBall then + if self.state.secondsSincePitchAllowed > C.ReturnToPitcherAfterSeconds and not self.state.catcherThrownBall then local outcome = pitchTracker:updatePitchCounts() if outcome == PitchOutcomes.StrikeOut then - strikeOut() + self:strikeOut() elseif outcome == PitchOutcomes.Walk then - walk() + self:walk() end -- Catcher has the ball. Throw it back to the pitcher - launchBall(C.PitchStartX, C.PitchStartY, playdate.easingFunctions.linear, nil, true) - catcherThrownBall = true + self.state.ball:launch(C.PitchStartX, C.PitchStartY, playdate.easingFunctions.linear, nil, true) + self.state.catcherThrownBall = true end local batSpeed if userOnOffense then - batAngleDeg = (playdate.getCrankPosition() + C.CrankOffsetDeg) % 360 + self.state.batAngleDeg = (playdate.getCrankPosition() + C.CrankOffsetDeg) % 360 batSpeed = crankLimited else - batAngleDeg = npc:updateBatAngle(ball, catcherThrownBall, deltaSeconds) - batSpeed = npc:batSpeed() * deltaSeconds + self.state.batAngleDeg = + self.npc:updateBatAngle(self.state.ball, self.state.catcherThrownBall, self.state.deltaSeconds) + batSpeed = self.npc:batSpeed() * self.state.deltaSeconds end - updateBatting(batAngleDeg, batSpeed) + self:updateBatting(self.state.batAngleDeg, batSpeed) -- Walk batter to the plate -- TODO: Ensure batter can't be nil, here - baserunning:updateRunner(baserunning.batter, nil, crankLimited, deltaSeconds) + self.baserunning:updateRunner(self.baserunning.batter, nil, crankLimited, self.state.deltaSeconds) - if secondsSincePitchAllowed > C.PitchAfterSeconds then + if self.state.secondsSincePitchAllowed > C.PitchAfterSeconds then if userOnDefense then local throwFly = throwMeter:readThrow() - if throwFly and not buttonControlledThrow(throwFly, true) then - userPitch(throwFly) + if throwFly and not self:buttonControlledThrow(throwFly, true) then + self:userPitch(throwFly) end else - pitch(C.PitchFlyMs / npc:pitchSpeed(), math.random(#Pitches)) + self:pitch(C.PitchFlyMs / self.npc:pitchSpeed(), math.random(#Pitches)) end end - elseif offenseState == C.Offense.running then + elseif self.state.offenseState == C.Offense.running then local appliedSpeed = userOnOffense and crankLimited or function(runner) - return npc:runningSpeed(runner, ball) + return self.npc:runningSpeed(runner, self.state.ball) end - if updateNonBatterRunners(appliedSpeed) then - secondsSinceLastRunnerMove = 0 + if self:updateNonBatterRunners(appliedSpeed) then + self.state.secondsSinceLastRunnerMove = 0 else - secondsSinceLastRunnerMove = secondsSinceLastRunnerMove + deltaSeconds - if secondsSinceLastRunnerMove > C.ResetFieldersAfterSeconds then + 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 - launchBall(C.PitchStartX, C.PitchStartY, playdate.easingFunctions.linear, nil, true) - fielding:markAllIneligible() -- This is ugly, and ideally would not be necessary if Fielding handled the return throw directly. - fielding:resetFielderPositions() - offenseState = C.Offense.batting + self.state.ball:launch(C.PitchStartX, C.PitchStartY, playdate.easingFunctions.linear, nil, true) + self.fielding:markAllIneligible() -- This is ugly, and ideally would not be necessary if Fielding handled the return throw directly. + self.fielding:resetFielderPositions() + self.state.offenseState = C.Offense.batting -- TODO: Remove, or replace with nextBatter() - if not baserunning.batter then - baserunning:pushNewBatter() + if not self.baserunning.batter then + self.baserunning:pushNewBatter() end end end - elseif offenseState == C.Offense.walking then - if not updateNonBatterRunners(C.WalkedRunnerSpeed, true) then - offenseState = C.Offense.batting + elseif self.state.offenseState == C.Offense.walking then + if not self:updateNonBatterRunners(C.WalkedRunnerSpeed, true) then + self.state.offenseState = C.Offense.batting end end - local fielderHoldingBall = fielding:updateFielderPositions(ball, deltaSeconds) + local fielderHoldingBall = self.fielding:updateFielderPositions(self.state.ball, self.state.deltaSeconds) if userOnDefense then local throwFly = throwMeter:readThrow() if throwFly then - buttonControlledThrow(throwFly) + self:buttonControlledThrow(throwFly) end end if fielderHoldingBall then - local outedSomeRunner = baserunning:outEligibleRunners(fielderHoldingBall) - if not userOnDefense and offenseState == C.Offense.running then - npc:fielderAction(fielderHoldingBall, outedSomeRunner, ball, launchBall) + 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 - baserunning:walkAwayOutRunners(deltaSeconds) - actionQueue:runWaiting(deltaSeconds) + self.baserunning:walkAwayOutRunners(self.state.deltaSeconds) + actionQueue:runWaiting(self.state.deltaSeconds) end -- TODO: Swappable update() for main menu, etc. -function mainGameUpdate() +function Game:update() playdate.timer.updateTimers() gfx.animation.blinker.updateAll() - updateGameState() + self:updateGameState() + local ball = self.state.ball gfx.clear() gfx.setColor(gfx.kColorBlack) @@ -482,30 +540,30 @@ function mainGameUpdate() GrassBackground:draw(-400, -240) - local ballIsHeld = fielding:drawFielders(fieldingTeamSprites, ball) + local ballIsHeld = self.fielding:drawFielders(self.state.fieldingTeamSprites, ball) - if offenseState == C.Offense.batting then + if self.state.offenseState == C.Offense.batting then gfx.setLineWidth(5) - gfx.drawLine(batBase.x, batBase.y, batTip.x, batTip.y) + gfx.drawLine(self.state.batBase.x, self.state.batBase.y, self.state.batTip.x, self.state.batTip.y) end local playerHeightOffset = 10 -- TODO? Scale sprites down as y increases - for _, runner in pairs(baserunning.runners) do - if runner == baserunning.batter then - if batAngleDeg > 50 and batAngleDeg < 200 then - battingTeamSprites.back:draw(runner.x, runner.y - playerHeightOffset) + for _, runner in pairs(self.baserunning.runners) do + if runner == self.baserunning.batter then + if self.state.batAngleDeg > 50 and self.state.batAngleDeg < 200 then + self.state.battingTeamSprites.back:draw(runner.x, runner.y - playerHeightOffset) else - battingTeamSprites.smiling:draw(runner.x, runner.y - playerHeightOffset) + self.state.battingTeamSprites.smiling:draw(runner.x, runner.y - playerHeightOffset) end else -- TODO? Change blip speed depending on runner speed? - runnerBlipper:draw(false, runner.x, runner.y - playerHeightOffset) + self.state.runnerBlipper:draw(false, runner.x, runner.y - playerHeightOffset) end end - for _, runner in pairs(baserunning.outRunners) do - battingTeamSprites.frowning:draw(runner.x, runner.y - playerHeightOffset) + for _, runner in pairs(self.baserunning.outRunners) do + self.state.battingTeamSprites.frowning:draw(runner.x, runner.y - playerHeightOffset) end if not ballIsHeld then @@ -520,45 +578,20 @@ function mainGameUpdate() gfx.setDrawOffset(0, 0) if math.abs(offsetX) > 10 or math.abs(offsetY) > 10 then - drawMinimap(baserunning.runners, fielding.fielders) + drawMinimap(self.baserunning.runners, self.fielding.fielders) end - drawScoreboard(0, C.Screen.H * 0.77, teams, baserunning.outs, battingTeam, inning) + drawScoreboard(0, C.Screen.H * 0.77, teams, self.baserunning.outs, self.state.battingTeam, self.state.inning) drawBallsAndStrikes(290, C.Screen.H - 20, pitchTracker.balls, pitchTracker.strikes) - announcer:draw(C.Center.x, 10) + self.announcer:draw(C.Center.x, 10) if playdate.isCrankDocked() then playdate.ui.crankIndicator:draw() end end ----@param s Settings -local function mainGameInit(s) - settings = s - fielding:resetFielderPositions(teams.home.benchPosition) - playdate.timer.new(2000, function() - launchBall(C.PitchStartX, C.PitchStartY, playdate.easingFunctions.linear, nil, false) - end) - BootTune:play() - BootTune:setFinishCallback(function() - TinnyBackground:play() - end) - battingTeamSprites = settings.awayTeamSprites - fieldingTeamSprites = settings.homeTeamSprites - HomeTeamBlipper = blipper.new(100, settings.homeTeamSprites.smiling, settings.homeTeamSprites.lowHat) - AwayTeamBlipper = blipper.new(100, settings.awayTeamSprites.smiling, settings.awayTeamSprites.lowHat) - runnerBlipper = battingTeam == teams.away and AwayTeamBlipper or HomeTeamBlipper -end +playdate.display.setRefreshRate(50) +gfx.setBackgroundColor(gfx.kColorWhite) +playdate.setMenuImage(gfx.image.new("images/game/menu-image.png")) +playdate.getSystemMenu():addMenuItem("Restart game", function() end) -- TODO? -local function init() - playdate.display.setRefreshRate(50) - gfx.setBackgroundColor(gfx.kColorWhite) - playdate.setMenuImage(gfx.image.new("images/game/menu-image.png")) - playdate.getSystemMenu():addMenuItem("Restart game", function() end) -- TODO? - - MainMenu.start({ - mainGameUpdateFunction = mainGameUpdate, - mainGameInitFunction = mainGameInit, - }) -end - -init() +MainMenu.start(Game) diff --git a/src/npc.lua b/src/npc.lua index ca17096..781ce3c 100644 --- a/src/npc.lua +++ b/src/npc.lua @@ -119,8 +119,8 @@ end ---@param fielders Fielder[] ---@param fielder Fielder ---@param runners Runner[] ----@param launchBall LaunchBall -local function tryToMakeAPlay(fielders, fielder, runners, ball, launchBall) +---@param ball { x: number, y: number, heldBy: Fielder | nil, launch: LaunchBall } +local function tryToMakeAPlay(fielders, fielder, runners, ball) local targetX, targetY = getNextOutTarget(runners) if targetX ~= nil and targetY ~= nil then local nearestFielder = utils.getNearestOf(fielders, targetX, targetY, function(grabCandidate) @@ -130,7 +130,7 @@ local function tryToMakeAPlay(fielders, fielder, runners, ball, launchBall) if nearestFielder == fielder then ball.heldBy = fielder else - launchBall(targetX, targetY, playdate.easingFunctions.linear, nil, true) + ball:launch(targetX, targetY, playdate.easingFunctions.linear, nil, true) Fielding.markIneligible(nearestFielder) end end @@ -138,16 +138,15 @@ end ---@param fielder Fielder ---@param outedSomeRunner boolean ----@param ball { x: number, y: number, heldBy: Fielder | nil } ----@param launchBall LaunchBall -function Npc:fielderAction(fielder, outedSomeRunner, ball, launchBall) +---@param ball { x: number, y: number, heldBy: Fielder | nil, launch: LaunchBall } +function Npc:fielderAction(fielder, outedSomeRunner, ball) if outedSomeRunner then -- Delay a little before the next play playdate.timer.new(750, function() - tryToMakeAPlay(self.fielders, fielder, self.runners, ball, launchBall) + tryToMakeAPlay(self.fielders, fielder, self.runners, ball) end) else - tryToMakeAPlay(self.fielders, fielder, self.runners, ball, launchBall) + tryToMakeAPlay(self.fielders, fielder, self.runners, ball) end end