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.
This commit is contained in:
parent
bb95ef5a63
commit
51855e13cf
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 <const> = 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
|
||||
---@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)
|
||||
|
|
515
src/main.lua
515
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 <const>, C <const> = 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 <const> = utils.xy(C.Center.x - 34, 215)
|
||||
local batTip <const> = utils.xy(0, 0)
|
||||
local batAngleDeg = C.CrankOffsetDeg
|
||||
|
||||
local catcherThrownBall = false
|
||||
|
||||
---@alias Team { score: number, benchPosition: XyPair }
|
||||
---@type table<string, Team>
|
||||
local teams <const> = {
|
||||
|
@ -85,49 +58,133 @@ local teams <const> = {
|
|||
},
|
||||
}
|
||||
|
||||
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 <const> = 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 <const> = {
|
||||
-- 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),
|
||||
},
|
||||
}
|
||||
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),
|
||||
},
|
||||
}
|
||||
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),
|
||||
},
|
||||
}
|
||||
end,
|
||||
-- Wobbbleball
|
||||
{
|
||||
function(ball)
|
||||
return {
|
||||
x = {
|
||||
currentValue = function()
|
||||
return C.PitchStartX + (10 * math.sin((ball.yAnimator:currentValue() - C.PitchStartY) / 10))
|
||||
|
@ -135,17 +192,18 @@ local Pitches <const> = {
|
|||
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
|
||||
|
||||
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)
|
||||
|
|
15
src/npc.lua
15
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
|
||||
|
||||
|
|
Loading…
Reference in New Issue