Compare commits

..

No commits in common. "30aa5bd6c62c7533d430a87ed70b7b38c1f34eb8" and "b44756ff57367f7f3393b667bb52a796cd16a98f" have entirely different histories.

4 changed files with 164 additions and 210 deletions

View File

@ -13,13 +13,6 @@ import 'CoreLibs/utilities/where.lua'
--- @alias EasingFunc fun(number, number, number, number): number
---@class InputHandler
---@field update fun(self, deltaSeconds: number)
---@field updateBat fun(self, ball: Ball, pitchIsOver: boolean, deltaSeconds: number)
---@field runningSpeed fun(self, runner: Runner, ball: Ball)
---@field pitch fun(self)
---@field fielderAction fun(self, fielderHoldingBall: Fielder | nil, outedSomeRunner: boolean, ball: Ball)
--- @alias LaunchBall fun(
--- self: self,
--- destX: number,
@ -46,7 +39,6 @@ import 'graphics.lua'
import 'npc.lua'
import 'pitching.lua'
import 'statistics.lua'
import 'user-input.lua'
import 'draw/box-score.lua'
import 'draw/fans.lua'
@ -106,8 +98,7 @@ local teams <const> = {
---@field private announcer Announcer
---@field private fielding Fielding
---@field private baserunning Baserunning
---@field private npc InputHandler
---@field private userInput InputHandler
---@field private npc Npc
---@field private homeTeamBlipper Blipper
---@field private awayTeamBlipper Blipper
---@field private panner Panner
@ -118,7 +109,7 @@ Game = {}
---@param announcer Announcer | nil
---@param fielding Fielding | nil
---@param baserunning Baserunning | nil
---@param npc InputHandler | nil
---@param npc Npc | nil
---@param state MutableState | nil
---@return Game
function Game.new(settings, announcer, fielding, baserunning, npc, state)
@ -160,9 +151,6 @@ function Game.new(settings, announcer, fielding, baserunning, npc, state)
o.baserunning = baserunning or Baserunning.new(announcer, function()
o:nextHalfInning()
end)
o.userInput = UserInput.new(function(throwFly, forbidThrowHome)
return o:buttonControlledThrow(throwFly, forbidThrowHome)
end)
o.npc = npc or Npc.new(o.baserunning.runners, o.fielding.fielders)
o.fielding:resetFielderPositions(teams.home.benchPosition)
@ -210,17 +198,6 @@ function Game:userIsOn(side)
return ret, not ret
end
---@return InputHandler offense, InputHandler defense
function Game:currentInputHandlers()
local userOnOffense, userOnDefense = self:userIsOn("offense")
local offenseInput = userOnOffense and self.userInput or self.npc
local defenseInput = userOnDefense and self.userInput or self.npc
offenseInput:update(self.state.deltaSeconds)
defenseInput:update(self.state.deltaSeconds)
return offenseInput, defenseInput
end
---@param pitchFlyTimeMs number | nil
---@param pitchTypeIndex number | nil
---@param accuracy number The closer to 1.0, the better
@ -331,18 +308,18 @@ end
function Game:buttonControlledThrow(throwFlyMs, forbidThrowHome)
local targetBase
if playdate.buttonIsPressed(playdate.kButtonLeft) then
targetBase = C.Third
targetBase = C.Bases[C.Third]
elseif playdate.buttonIsPressed(playdate.kButtonUp) then
targetBase = C.Second
targetBase = C.Bases[C.Second]
elseif playdate.buttonIsPressed(playdate.kButtonRight) then
targetBase = C.First
targetBase = C.Bases[C.First]
elseif not forbidThrowHome and playdate.buttonIsPressed(playdate.kButtonDown) then
targetBase = C.Home
targetBase = C.Bases[C.Home]
else
return false
end
self.fielding:userThrowTo(C.Bases[targetBase], self.state.ball, throwFlyMs)
self.fielding:userThrowTo(targetBase, self.state.ball, throwFlyMs)
self.baserunning.secondsSinceLastRunnerMove = 0
self.state.offenseState = C.Offense.running
@ -381,12 +358,8 @@ end
local SwingBackDeg <const> = 30
local SwingForwardDeg <const> = 170
---@param offenseHandler InputHandler
function Game:updateBatting(offenseHandler)
local ball = self.state.ball
local batDeg, batSpeed = offenseHandler:updateBat(ball, self.state.pitchIsOver, self.state.deltaSeconds)
self.state.batAngleDeg = batDeg
---@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
@ -395,42 +368,39 @@ function Game:updateBatting(offenseHandler)
-- and letting the user find a crank position and direction that works for them
local batAngle = math.rad(batDeg)
-- TODO: animate bat-flip or something
local batter = self.baserunning.batter
self.state.batBase.x = batter and (batter.x + C.BatterHandPos.x) or -999
self.state.batBase.y = batter and (batter.y + C.BatterHandPos.y) or -999
self.state.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))
local ballWasHit = batSpeed > 0
and ball.y < 232
if
batSpeed > 0
and self.state.ball.y < 232
and utils.pointDirectlyUnderLine(
ball.x,
ball.y,
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
)
if not ballWasHit then
return
end
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 * C.BattingPower * 10 * math.sin(ballAngle)
local ballVelY = mult * C.BattingPower * 5 * math.cos(ballAngle)
local ballVelX = mult * 10 * math.sin(ballAngle)
local ballVelY = mult * 5 * math.cos(ballAngle)
if ballVelY > 0 then
ballVelX = ballVelX * -1
ballVelY = ballVelY * -1
end
local ballDest = utils.xy(ball.x + ballVelX, ball.y + ballVelY)
local ballDest =
utils.xy(self.state.ball.x + (ballVelX * C.BattingPower), self.state.ball.y + (ballVelY * C.BattingPower))
pitchTracker:reset()
local flyTimeMs = 2000
@ -448,7 +418,9 @@ function Game:updateBatting(offenseHandler)
if utils.pointIsSquarelyAboveLine(utils.xy(ballDest.x, ballDest.y), C.OutfieldWall) then
playdate.timer.new(flyTimeMs, function()
-- Verify that the home run wasn't intercepted
if utils.within(1, ball.x, ballDest.x) and utils.within(1, ball.y, ballDest.y) then
if
utils.within(1, self.state.ball.x, ballDest.x) and utils.within(1, self.state.ball.y, ballDest.y)
then
self.announcer:say("HOME RUN!")
self.state.offenseState = C.Offense.homeRun
-- Linger on the home-run ball for a moment, before panning to the bases.
@ -462,13 +434,15 @@ function Game:updateBatting(offenseHandler)
end
local hitBallScaler = gfx.animator.new(2000, 9 + (mult * mult * 0.5), C.SmallestBallRadius, utils.easingHill)
ball:launch(ballDest.x, ballDest.y, playdate.easingFunctions.outQuint, flyTimeMs, nil, hitBallScaler)
self.state.ball:launch(ballDest.x, ballDest.y, playdate.easingFunctions.outQuint, flyTimeMs, nil, hitBallScaler)
self.baserunning:convertBatterToRunner()
self.fielding:haveSomeoneChase(ballDest.x, ballDest.y)
end
end
---@param appliedSpeed number | fun(runner: Runner): number
---
---@return boolean runnersStillMoving, number secondsSinceLastRunnerMove
function Game:updateNonBatterRunners(appliedSpeed, forcedOnly, isAutoRun)
local runnersStillMoving, runnersScored, secondsSinceLastRunnerMove =
@ -497,11 +471,48 @@ function Game:returnToPitcher()
end)
end
---@param defenseHandler InputHandler
function Game:updatePitching(defenseHandler)
---@param throwFly number
function Game:userPitch(throwFly, accuracy)
local aPressed = playdate.buttonIsPressed(playdate.kButtonA)
local bPressed = playdate.buttonIsPressed(playdate.kButtonB)
if not aPressed and not bPressed then
self:pitch(throwFly, 1, accuracy)
elseif aPressed and not bPressed then
self:pitch(throwFly, 2, accuracy)
elseif not aPressed and bPressed then
self:pitch(throwFly, 3, accuracy)
elseif aPressed and bPressed then
self:pitch(throwFly, 4, accuracy)
end
end
function Game:updateGameState()
self.state.deltaSeconds = playdate.getElapsedTime() or 0
playdate.resetElapsedTime()
local crankChange = playdate.getCrankChange() --[[@as number]]
local crankLimited = crankChange == 0 and 0 or (math.log(math.abs(crankChange)) * C.CrankPower)
if crankChange < 0 then
crankLimited = crankLimited * -1
end
self.state.ball:updatePosition()
local userOnOffense, userOnDefense = self:userIsOn("offense")
local 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)
if self:pitcherIsOnTheMound() then
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
@ -517,33 +528,45 @@ function Game:updatePitching(defenseHandler)
self.state.didSwing = false
end
if pitchTracker.secondsSinceLastPitch > C.PitchAfterSeconds then
self:pitch(defenseHandler:pitch())
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
end
function Game:updateGameState()
playdate.timer.updateTimers()
gfx.animation.blinker.updateAll()
self.state.deltaSeconds = playdate.getElapsedTime() or 0
playdate.resetElapsedTime()
self.state.ball:updatePosition()
local offenseHandler, defenseHandler = self:currentInputHandlers()
local fielderHoldingBall = self.fielding:updateFielderPositions(self.state.ball, self.state.deltaSeconds)
if self.state.offenseState == C.Offense.batting then
self:updatePitching(defenseHandler)
self:updateBatting(offenseHandler)
self:updateBatting(self.state.batAngleDeg, batSpeed)
-- Walk batter to the plate
self.baserunning:updateRunner(self.baserunning.batter, nil, 0, false, self.state.deltaSeconds)
elseif self.state.offenseState == C.Offense.running then
local appliedSpeed = function(runner)
return offenseHandler:runningSpeed(runner, self.state.ball)
end
self.baserunning:updateRunner(
self.baserunning.batter,
nil,
userOnOffense and crankLimited or 0,
false,
self.state.deltaSeconds
)
if pitchTracker.secondsSinceLastPitch > C.PitchAfterSeconds then
if userOnDefense then
local powerRatio, accuracy = throwMeter:readThrow(crankChange)
if powerRatio then
local throwFly = C.PitchFlyMs / powerRatio
if throwFly and not self:buttonControlledThrow(throwFly, true) then
self:userPitch(throwFly, accuracy)
end
end
else
self:pitch(C.PitchFlyMs / self.npc:pitchSpeed(), math.random(#Pitches))
end
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, false)
if secondsSinceLastRunnerMove > C.ResetFieldersAfterSeconds then
-- End of play. Throw the ball back to the pitcher
@ -551,11 +574,15 @@ function Game:updateGameState()
self:returnToPitcher()
end
local outedSomeRunner = false
if fielderHoldingBall then
outedSomeRunner = self.baserunning:outEligibleRunners(fielderHoldingBall)
if userOnDefense then
local powerRatio = throwMeter:readThrow(crankChange)
if powerRatio then
local throwFly = C.PitchFlyMs / powerRatio
if throwFly then
self:buttonControlledThrow(throwFly)
end
end
end
defenseHandler:fielderAction(fielderHoldingBall, outedSomeRunner, self.state.ball)
elseif self.state.offenseState == C.Offense.walking then
if not self:updateNonBatterRunners(C.WalkedRunnerSpeed, true, true) then
self.state.offenseState = C.Offense.batting
@ -581,7 +608,10 @@ function Game:updateGameState()
end
function Game:update()
playdate.timer.updateTimers()
gfx.animation.blinker.updateAll()
self:updateGameState()
local ball = self.state.ball
gfx.clear()
gfx.setColor(gfx.kColorBlack)
@ -598,8 +628,6 @@ function Game:update()
characterDraws[#characterDraws + 1] = { y = y, drawAction = drawAction }
end
local ball = self.state.ball
local danceOffset = FielderDanceAnimator:currentValue()
---@type Fielder | nil
local ballHeldBy
@ -607,7 +635,7 @@ function Game:update()
addDraw(fielder.y + danceOffset, function()
local ballHeldByThisFielder = drawFielder(
self.state.fieldingTeamSprites[fielder.spriteIndex],
ball,
self.state.ball,
fielder.x,
fielder.y + danceOffset
)

View File

@ -2,7 +2,7 @@ local npcBatDeg = 0
local BaseNpcBatSpeed <const> = 1500
local npcBatSpeed = 1500
---@class Npc: InputHandler
---@class Npc
---@field runners Runner[]
---@field fielders Fielder[]
-- selene: allow(unscoped_variables)
@ -18,32 +18,26 @@ function Npc.new(runners, fielders)
}, { __index = Npc })
end
function Npc.update() end
-- TODO: FAR more nuanced NPC batting.
---@param ball XyPair
---@param pitchIsOver boolean
---@param deltaSec number
---@return number batAngleDeg, number batSpeed
---@return number
-- luacheck: no unused
function Npc:updateBat(ball, pitchIsOver, deltaSec)
function Npc:updateBatAngle(ball, pitchIsOver, deltaSec)
if not pitchIsOver and ball.y > 200 and ball.y < 230 and (ball.x < C.Center.x + 15) then
npcBatDeg = npcBatDeg + (deltaSec * npcBatSpeed)
else
npcBatSpeed = (1 + math.random()) * BaseNpcBatSpeed
npcBatDeg = 230
end
return npcBatDeg, (self:batSpeed() * deltaSec)
return npcBatDeg
end
function Npc:batSpeed()
return npcBatSpeed / 1.5
end
function Npc:pitch()
return C.PitchFlyMs / self:pitchSpeed(), math.random(#Pitches), 0.9
end
local baseRunningSpeed = 25
---@param runner Runner
@ -144,9 +138,6 @@ end
---@param outedSomeRunner boolean
---@param ball { x: number, y: number, heldBy: Fielder | nil, launch: LaunchBall }
function Npc:fielderAction(fielder, outedSomeRunner, ball)
if not fielder then
return
end
if outedSomeRunner then
-- Delay a little before the next play
playdate.timer.new(750, function()

View File

@ -54,13 +54,12 @@ Pitches = {
y = gfx.animator.new(C.PitchFlyMs, C.PitchStart.y, C.PitchEndY, playdate.easingFunctions.linear),
}
end,
-- Wobbleball
-- Wobbbleball
function(accuracy, ball)
local missBy = getPitchMissBy(accuracy)
return {
x = {
currentValue = function()
return missBy
return getPitchMissBy(accuracy)
+ C.PitchStart.x
+ (10 * math.sin((ball.yAnimator:currentValue() - C.PitchStart.y) / 10))
end,

View File

@ -1,64 +0,0 @@
---@class UserInput: InputHandler
---@field buttonControlledThrow: fun(throwFlyMs: number, forbidThrowHome: boolean): boolean didThrow
UserInput = {}
function UserInput.new(buttonControlledThrow)
return setmetatable({
buttonControlledThrow = buttonControlledThrow,
}, { __index = UserInput })
end
function UserInput:update()
self.crankChange = playdate.getCrankChange()
local crankLimited = self.crankChange == 0 and 0 or (math.log(math.abs(self.crankChange)) * C.CrankPower)
self.crankLimited = math.abs(crankLimited)
end
function UserInput:updateBat()
local batAngleDeg = (playdate.getCrankPosition() + C.CrankOffsetDeg) % 360
local batSpeed = self.crankLimited
return batAngleDeg, batSpeed
end
function UserInput:runningSpeed()
return self.crankLimited
end
---@param throwFlyMs number
---@param accuracy number | nil
---@return number | nil pitchFlyTimeMs, number | nil pitchTypeIndex, number | nil accuracy
local function userPitch(throwFlyMs, accuracy)
local aPressed = playdate.buttonIsPressed(playdate.kButtonA)
local bPressed = playdate.buttonIsPressed(playdate.kButtonB)
if not aPressed and not bPressed then
return throwFlyMs, 1, accuracy
elseif aPressed and not bPressed then
return throwFlyMs, 2, accuracy
elseif not aPressed and bPressed then
return throwFlyMs, 3, accuracy
elseif aPressed and bPressed then
return throwFlyMs, 4, accuracy
end
return nil, nil, nil
end
function UserInput:pitch()
local powerRatio, accuracy = throwMeter:readThrow(self.crankChange)
if powerRatio then
local throwFly = C.PitchFlyMs / powerRatio
if throwFly and not self.buttonControlledThrow(throwFly, true) then
return userPitch(throwFly, accuracy)
end
end
end
function UserInput:fielderAction()
local powerRatio = throwMeter:readThrow(self.crankChange)
if powerRatio then
local throwFly = C.PitchFlyMs / powerRatio
if throwFly then
self.buttonControlledThrow(throwFly, false)
end
end
end