698 lines
24 KiB
Lua
698 lines
24 KiB
Lua
-- stylua: ignore start
|
|
import 'CoreLibs/animation.lua'
|
|
import 'CoreLibs/animator.lua'
|
|
import 'CoreLibs/easing.lua'
|
|
import 'CoreLibs/graphics.lua'
|
|
import 'CoreLibs/object.lua'
|
|
import 'CoreLibs/timer.lua'
|
|
import 'CoreLibs/ui.lua'
|
|
-- stylua: ignore end
|
|
|
|
--- @alias Scene { update: fun(self: self) }
|
|
|
|
--- @alias EasingFunc fun(number, number, number, number): number
|
|
|
|
--- @alias LaunchBall fun(
|
|
--- self: self,
|
|
--- destX: number,
|
|
--- destY: number,
|
|
--- easingFunc: EasingFunc,
|
|
--- flyTimeMs: number | nil,
|
|
--- floaty: boolean | nil,
|
|
--- customBallScaler: pd_animator | nil,
|
|
--- )
|
|
|
|
-- stylua: ignore start
|
|
import 'utils.lua'
|
|
import 'constants.lua'
|
|
import 'assets.lua'
|
|
import 'main-menu.lua'
|
|
|
|
import 'action-queue.lua'
|
|
import 'announcer.lua'
|
|
import 'ball.lua'
|
|
import 'baserunning.lua'
|
|
import 'dbg.lua'
|
|
import 'fielding.lua'
|
|
import 'graphics.lua'
|
|
import 'npc.lua'
|
|
import 'pitching.lua'
|
|
|
|
import 'draw/box-score.lua'
|
|
import 'draw/fielder.lua'
|
|
import 'draw/overlay.lua'
|
|
import 'draw/panner.lua'
|
|
import 'draw/player.lua'
|
|
import 'draw/throw-meter.lua'
|
|
import 'draw/transitions.lua'
|
|
-- stylua: ignore end
|
|
|
|
-- TODO: Customizable field structure. E.g. stands and ads etc.
|
|
|
|
local gfx <const>, C <const> = playdate.graphics, C
|
|
|
|
---@alias Team { benchPosition: XyPair }
|
|
---@type table<TeamId, Team>
|
|
local teams <const> = {
|
|
home = {
|
|
benchPosition = utils.xy(C.Screen.W + 10, C.Center.y),
|
|
},
|
|
away = {
|
|
benchPosition = utils.xy(-10, C.Center.y),
|
|
},
|
|
}
|
|
|
|
---@alias TeamId 'home' | 'away'
|
|
|
|
--- 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 TeamId | nil
|
|
---@field awayTeamSprites SpriteCollection
|
|
---@field homeTeamSprites SpriteCollection
|
|
|
|
---@class MutableState
|
|
---@field deltaSeconds number
|
|
---@field ball Ball
|
|
---@field battingTeam TeamId
|
|
---@field pitchIsOver boolean
|
|
---@field didSwing boolean
|
|
---@field offenseState OffenseState
|
|
---@field inning number
|
|
---@field stats Statistics
|
|
---@field batBase XyPair
|
|
---@field batTip XyPair
|
|
---@field batAngleDeg 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
|
|
---@field battingTeamSprites SpriteCollection
|
|
---@field fieldingTeamSprites SpriteCollection
|
|
|
|
---@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 panner Panner
|
|
---@field private state MutableState
|
|
Game = {}
|
|
|
|
---@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 = "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 = "away"
|
|
local runnerBlipper = battingTeam == "away" and awayTeamBlipper or homeTeamBlipper
|
|
local ball = Ball.new(gfx.animator)
|
|
|
|
local o = setmetatable({
|
|
settings = settings,
|
|
announcer = announcer,
|
|
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),
|
|
batAngleDeg = C.CrankOffsetDeg,
|
|
deltaSeconds = 0,
|
|
ball = ball,
|
|
battingTeam = battingTeam,
|
|
offenseState = C.Offense.batting,
|
|
inning = 1,
|
|
pitchIsOver = false,
|
|
didSwing = false,
|
|
battingTeamSprites = settings.awayTeamSprites,
|
|
fieldingTeamSprites = settings.homeTeamSprites,
|
|
runnerBlipper = runnerBlipper,
|
|
stats = Statistics.new(),
|
|
},
|
|
}, { __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()
|
|
o:returnToPitcher()
|
|
end)
|
|
|
|
BootTune:play()
|
|
BootTune:setFinishCallback(function()
|
|
TinnyBackground:play()
|
|
end)
|
|
|
|
return o
|
|
end
|
|
|
|
---@param teamId TeamId
|
|
---@return TeamId
|
|
local function getOppositeTeamId(teamId)
|
|
if teamId == "home" then
|
|
return "away"
|
|
elseif teamId == "away" then
|
|
return "home"
|
|
else
|
|
error("Unknown TeamId: " .. (teamId or "nil"))
|
|
end
|
|
end
|
|
|
|
function Game:getFieldingTeam()
|
|
return teams[getOppositeTeamId(self.state.battingTeam)]
|
|
end
|
|
|
|
---@param side Side
|
|
---@return boolean userIsOnSide, boolean userIsOnOtherSide
|
|
function Game:userIsOn(side)
|
|
if self.settings.userTeam == nil then
|
|
-- Both teams are NPC-driven
|
|
return false, false
|
|
end
|
|
local ret
|
|
if self.settings.userTeam == self.state.battingTeam then
|
|
ret = side == "offense"
|
|
else
|
|
ret = side == "defense"
|
|
end
|
|
return ret, not ret
|
|
end
|
|
|
|
---@param pitchFlyTimeMs number | nil
|
|
---@param pitchTypeIndex number | nil
|
|
function Game:pitch(pitchFlyTimeMs, pitchTypeIndex)
|
|
Fielding.markIneligible(self.fielding.fielders.pitcher)
|
|
self.state.ball.heldBy = nil
|
|
self.state.pitchIsOver = false
|
|
self.state.offenseState = C.Offense.batting
|
|
|
|
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 ball:launch() etc.
|
|
-- if current.z then
|
|
-- ball.floatAnimator = current.z
|
|
-- ball.floatAnimator:reset()
|
|
-- end
|
|
|
|
if pitchFlyTimeMs then
|
|
self.state.ball.xAnimator:reset(pitchFlyTimeMs)
|
|
self.state.ball.yAnimator:reset(pitchFlyTimeMs)
|
|
else
|
|
self.state.ball.xAnimator:reset()
|
|
self.state.ball.yAnimator:reset()
|
|
end
|
|
|
|
pitchTracker.secondsSinceLastPitch = 0
|
|
end
|
|
|
|
function Game:pitcherIsOnTheMound()
|
|
return utils.distanceBetweenPoints(self.fielding.fielders.pitcher, C.PitcherStartPos) < C.BaseHitbox
|
|
end
|
|
|
|
function Game:pitcherIsReady()
|
|
local pitcher = self.fielding.fielders.pitcher
|
|
return self: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()
|
|
pitchTracker:reset()
|
|
local homeScore, awayScore = utils.totalScores(self.state.stats)
|
|
local isFinalInning = self.state.inning >= self.settings.finalInning
|
|
local gameOver = isFinalInning and self.state.battingTeam == "home" and awayScore ~= homeScore
|
|
gameOver = gameOver or self.state.battingTeam == "away" and isFinalInning and homeScore > awayScore
|
|
Fielding.celebrate()
|
|
|
|
if gameOver then
|
|
self.announcer:say("THAT'S THE BALL GAME!")
|
|
playdate.timer.new(3000, function()
|
|
transitionTo(BoxScore.new(self.state.stats))
|
|
end)
|
|
return
|
|
end
|
|
|
|
self.fielding:benchTo(self:getFieldingTeam().benchPosition)
|
|
self.announcer:say("SWITCHING SIDES...")
|
|
|
|
self.fielding:resetFielderPositions()
|
|
if self.state.battingTeam == "home" then
|
|
self.state.inning = self.state.inning + 1
|
|
self.state.stats:pushInning()
|
|
end
|
|
self.state.battingTeam = getOppositeTeamId(self.state.battingTeam)
|
|
playdate.timer.new(2000, function()
|
|
if self.state.battingTeam == "home" then
|
|
self.state.battingTeamSprites = self.settings.homeTeamSprites
|
|
self.state.runnerBlipper = self.homeTeamBlipper
|
|
self.state.fieldingTeamSprites = self.settings.awayTeamSprites
|
|
else
|
|
self.state.battingTeamSprites = self.settings.awayTeamSprites
|
|
self.state.fieldingTeamSprites = self.settings.homeTeamSprites
|
|
self.state.runnerBlipper = self.awayTeamBlipper
|
|
end
|
|
end)
|
|
end
|
|
|
|
---@return TeamInningData
|
|
function Game:battingTeamCurrentInning()
|
|
return self.state.stats.innings[self.state.inning][self.state.battingTeam]
|
|
end
|
|
|
|
---@return TeamInningData
|
|
function Game:fieldingTeamCurrentInning()
|
|
return self.state.stats.innings[self.state.inning][getOppositeTeamId(self.state.battingTeam)]
|
|
end
|
|
|
|
---@param scoredRunCount number
|
|
function Game:score(scoredRunCount)
|
|
-- TODO: end the game when it's the bottom of the ninth the home team is now in the lead.
|
|
-- outRunners/scoredRunners split
|
|
self:battingTeamCurrentInning().score = self:battingTeamCurrentInning().score + scoredRunCount
|
|
|
|
self.announcer:say("SCORE!")
|
|
end
|
|
|
|
---@param throwFlyMs number
|
|
---@return boolean didThrow
|
|
function Game:buttonControlledThrow(throwFlyMs, forbidThrowHome)
|
|
local targetBase
|
|
if playdate.buttonIsPressed(playdate.kButtonLeft) then
|
|
targetBase = C.Bases[C.Third]
|
|
elseif playdate.buttonIsPressed(playdate.kButtonUp) then
|
|
targetBase = C.Bases[C.Second]
|
|
elseif playdate.buttonIsPressed(playdate.kButtonRight) then
|
|
targetBase = C.Bases[C.First]
|
|
elseif not forbidThrowHome and playdate.buttonIsPressed(playdate.kButtonDown) then
|
|
targetBase = C.Bases[C.Home]
|
|
else
|
|
return false
|
|
end
|
|
|
|
self.fielding:userThrowTo(targetBase, self.state.ball, throwFlyMs)
|
|
self.baserunning.secondsSinceLastRunnerMove = 0
|
|
self.state.offenseState = C.Offense.running
|
|
|
|
return true
|
|
end
|
|
|
|
function Game:nextBatter()
|
|
pitchTracker.secondsSinceLastPitch = -3
|
|
self.baserunning.batter = nil
|
|
playdate.timer.new(2000, function()
|
|
pitchTracker:reset()
|
|
if not self.baserunning.batter then
|
|
self.baserunning:pushNewBatter()
|
|
end
|
|
end)
|
|
end
|
|
|
|
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
|
|
|
|
function Game:strikeOut()
|
|
local outBatter = self.baserunning.batter
|
|
self.baserunning.batter = nil
|
|
self.baserunning:outRunner(outBatter, "Strike out!")
|
|
self:nextBatter()
|
|
end
|
|
|
|
local SwingBackDeg <const> = 30
|
|
local SwingForwardDeg <const> = 170
|
|
|
|
---@param batDeg number
|
|
function Game:updateBatting(batDeg, batSpeed)
|
|
if not self.state.pitchIsOver and batDeg > SwingBackDeg and batDeg < SwingForwardDeg then
|
|
self.state.didSwing = true
|
|
end
|
|
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
|
|
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
|
|
)
|
|
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 ballDestX = self.state.ball.x + (ballVelX * C.BattingPower)
|
|
local ballDestY = 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] = utils.xy(ballDestX, ballDestY)
|
|
|
|
if utils.isFoulBall(ballDestX, ballDestY) 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(ballDestX, ballDestY), C.OutfieldWall) then
|
|
playdate.timer.new(flyTimeMs, function()
|
|
-- Verify that the home run wasn't intercepted
|
|
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
|
|
|
|
local hitBallScaler = gfx.animator.new(2000, 9 + (mult * mult * 0.5), C.SmallestBallRadius, utils.easingHill)
|
|
self.state.ball:launch(ballDestX, ballDestY, playdate.easingFunctions.outQuint, flyTimeMs, nil, hitBallScaler)
|
|
|
|
self.baserunning:convertBatterToRunner()
|
|
self.fielding:haveSomeoneChase(ballDestX, ballDestY)
|
|
end
|
|
end
|
|
|
|
---@param appliedSpeed number | fun(runner: Runner): number
|
|
---@return boolean runnersStillMoving, number secondsSinceLastRunnerMove
|
|
function Game:updateNonBatterRunners(appliedSpeed, forcedOnly)
|
|
local runnersStillMoving, runnersScored, secondsSinceLastRunnerMove =
|
|
self.baserunning:updateNonBatterRunners(appliedSpeed, forcedOnly, self.state.deltaSeconds)
|
|
if runnersScored ~= 0 then
|
|
self:score(runnersScored)
|
|
end
|
|
return runnersStillMoving, secondsSinceLastRunnerMove
|
|
end
|
|
|
|
function Game:returnToPitcher()
|
|
self.fielding:resetFielderPositions()
|
|
actionQueue:upsert("returnToPitcher", 60 * 1000, function()
|
|
while not self:pitcherIsOnTheMound() do
|
|
coroutine.yield()
|
|
end
|
|
if not self.baserunning.batter then
|
|
self.baserunning:pushNewBatter()
|
|
end
|
|
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)
|
|
end
|
|
|
|
---@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
|
|
|
|
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 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)
|
|
|
|
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
|
|
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:updateBatting(self.state.batAngleDeg, batSpeed)
|
|
|
|
-- Walk batter to the plate
|
|
self.baserunning:updateRunner(self.baserunning.batter, nil, crankLimited, self.state.deltaSeconds)
|
|
|
|
if pitchTracker.secondsSinceLastPitch > C.PitchAfterSeconds then
|
|
if userOnDefense then
|
|
local powerRatio, isPerfect = throwMeter:readThrow(crankChange)
|
|
if powerRatio then
|
|
local throwFly = C.PitchFlyMs / powerRatio
|
|
if throwFly and not self:buttonControlledThrow(throwFly, true) then
|
|
self:userPitch(throwFly)
|
|
end
|
|
end
|
|
else
|
|
self:pitch(C.PitchFlyMs / self.npc:pitchSpeed(), 2)
|
|
end
|
|
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 _, 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
|
|
|
|
if userOnDefense then
|
|
local powerRatio, isPerfect = throwMeter:readThrow(crankChange)
|
|
if powerRatio then
|
|
local throwFly = C.PitchFlyMs / powerRatio
|
|
if throwFly then
|
|
self:buttonControlledThrow(throwFly)
|
|
end
|
|
end
|
|
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 * 2, false)
|
|
if #self.baserunning.runners == 0 then
|
|
-- Give the player a moment to enjoy their home run.
|
|
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
|
|
|
|
self.baserunning:walkAwayOutRunners(self.state.deltaSeconds)
|
|
actionQueue:runWaiting(self.state.deltaSeconds)
|
|
end
|
|
|
|
function Game:update()
|
|
playdate.timer.updateTimers()
|
|
gfx.animation.blinker.updateAll()
|
|
self:updateGameState()
|
|
local ball = self.state.ball
|
|
|
|
gfx.clear()
|
|
gfx.setColor(gfx.kColorBlack)
|
|
|
|
local offsetX, offsetY = self.panner:get(self.state.deltaSeconds)
|
|
gfx.setDrawOffset(offsetX, offsetY)
|
|
|
|
GrassBackground:draw(-400, -720)
|
|
|
|
---@type { y: number, drawAction: fun() }[]
|
|
local characterDraws = {}
|
|
function addDraw(y, drawAction)
|
|
characterDraws[#characterDraws + 1] = { y = y, drawAction = drawAction }
|
|
end
|
|
|
|
local danceOffset = FielderDanceAnimator:currentValue()
|
|
---@type Fielder | nil
|
|
local ballHeldBy
|
|
for _, fielder in pairs(self.fielding.fielders) do
|
|
addDraw(fielder.y + danceOffset, function()
|
|
local ballHeldByThisFielder = drawFielder(
|
|
self.state.fieldingTeamSprites,
|
|
self.state.ball,
|
|
fielder.x,
|
|
fielder.y + danceOffset
|
|
)
|
|
if ballHeldByThisFielder then
|
|
ballHeldBy = fielder
|
|
end
|
|
end)
|
|
end
|
|
|
|
local playerHeightOffset = 20
|
|
for _, runner in pairs(self.baserunning.runners) do
|
|
addDraw(runner.y, function()
|
|
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
|
|
self.state.battingTeamSprites.smiling:draw(runner.x, runner.y - playerHeightOffset)
|
|
end
|
|
else
|
|
-- TODO? Change blip speed depending on runner speed?
|
|
self.state.runnerBlipper:draw(false, runner.x, runner.y - playerHeightOffset)
|
|
end
|
|
end)
|
|
end
|
|
|
|
table.sort(characterDraws, function(a, b)
|
|
return a.y < b.y
|
|
end)
|
|
for _, character in pairs(characterDraws) do
|
|
character.drawAction()
|
|
end
|
|
|
|
if self.state.offenseState == C.Offense.batting then
|
|
gfx.setLineWidth(5)
|
|
gfx.drawLine(self.state.batBase.x, self.state.batBase.y, self.state.batTip.x, self.state.batTip.y)
|
|
end
|
|
|
|
for _, runner in pairs(self.baserunning.outRunners) do
|
|
self.state.battingTeamSprites.frowning:draw(runner.x, runner.y - playerHeightOffset)
|
|
end
|
|
for _, runner in pairs(self.baserunning.scoredRunners) do
|
|
self.state.battingTeamSprites.smiling:draw(runner.x, runner.y - playerHeightOffset)
|
|
end
|
|
|
|
throwMeter:drawNearFielder(ballHeldBy)
|
|
|
|
if not ballHeldBy then
|
|
gfx.setLineWidth(2)
|
|
|
|
gfx.setColor(gfx.kColorWhite)
|
|
gfx.fillCircleAtPoint(ball.x, ball.y, ball.size)
|
|
|
|
gfx.setColor(gfx.kColorBlack)
|
|
gfx.drawCircleAtPoint(ball.x, ball.y, ball.size)
|
|
end
|
|
|
|
gfx.setDrawOffset(0, 0)
|
|
if math.abs(offsetX) > 10 or math.abs(offsetY) > 10 then
|
|
drawMinimap(self.baserunning.runners, self.fielding.fielders)
|
|
end
|
|
local homeScore, awayScore = utils.totalScores(self.state.stats)
|
|
drawScoreboard(
|
|
0,
|
|
C.Screen.H * 0.77,
|
|
homeScore,
|
|
awayScore,
|
|
self.baserunning.outs,
|
|
self.state.battingTeam,
|
|
self.state.inning
|
|
)
|
|
drawBallsAndStrikes(290, C.Screen.H - 20, pitchTracker.balls, pitchTracker.strikes)
|
|
self.announcer:draw(C.Center.x, 10)
|
|
|
|
if playdate.isCrankDocked() then
|
|
-- luacheck: ignore
|
|
playdate.ui.crankIndicator:draw()
|
|
end
|
|
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?
|
|
|
|
MainMenu.start(Game)
|