Extract batting.lua

This commit is contained in:
Sage Vaillancourt 2025-02-27 19:31:00 -05:00
parent 8b66e2e826
commit 04d25127fc
6 changed files with 95 additions and 60 deletions

67
src/batting.lua Normal file
View File

@ -0,0 +1,67 @@
---@class BatRenderState
---@field batBase XyPair
---@field batTip XyPair
---@field batAngleDeg number
---@field batSpeed number
---@class Batting
---@field private Baserunning
---@field state BatRenderState Is updated by checkForHit()
Batting = {}
local SwingBackDeg <const> = 30
local SwingForwardDeg <const> = 170
local OffscreenPos <const> = utils.xy(-999, -999)
---@param baserunning Baserunning
function Batting.new(baserunning)
return setmetatable({
baserunning = baserunning,
state = {
batAngleDeg = 0,
batSpeed = 0,
batTip = OffscreenPos,
batBase = OffscreenPos,
},
}, { __index = Batting })
end
-- TODO? Make the bat angle work more like the throw meter.
-- Would instead constantly drift toward a default value, giving us a little more control,
-- and letting the user find a crank position and direction that works for them
--- Assumes the bat is being held by self.baserunning.batter
---@param batDeg number
---@param batSpeed number
---@param ball Point3d
---@return XyPair | nil, boolean, number | nil Ball destination or nil if no hit, true only if batter swung, power mult
function Batting:checkForHit(batDeg, batSpeed, ball)
local batter = self.baserunning.batter
local isSwinging = batDeg > SwingBackDeg and batDeg < SwingForwardDeg
local batRadians = math.rad(batDeg)
local base = batter and utils.xy(batter.x + C.BatterHandPos.x, batter.y + C.BatterHandPos.y) or OffscreenPos
local tip = utils.xy(base.x + (C.BatLength * math.sin(batRadians)), base.y + (C.BatLength * math.cos(batRadians)))
self.state.batSpeed = batSpeed
self.state.batAngleDeg = batDeg
self.state.batTip = tip
self.state.batBase = base
local ballWasHit = batSpeed > 0 and ball.y < 232 and utils.pointOnOrUnderLine(ball, base, tip, C.Screen.H)
if not ballWasHit then
return nil, isSwinging
end
local ballAngle = batRadians + 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)
if ballVelY > 0 then
ballVelX = ballVelX * -1
ballVelY = ballVelY * -1
end
return utils.xy(ball.x + ballVelX, ball.y + ballVelY), isSwinging, mult
end

View File

@ -7,8 +7,6 @@ Characters = {}
local gfx <const> = playdate.graphics local gfx <const> = playdate.graphics
---@alias BatState { batBase: XyPair, batTip: XyPair, batAngleDeg: number }
local GloveSizeX, GloveSizeY <const> = Glove:getSize() local GloveSizeX, GloveSizeY <const> = Glove:getSize()
local GloveOffX, GloveOffY <const> = GloveSizeX / 2, GloveSizeY / 2 local GloveOffX, GloveOffY <const> = GloveSizeX / 2, GloveSizeY / 2
@ -53,7 +51,7 @@ function drawFielder(fieldingTeamSprites, fielder, ball, flip)
return drawFielderGlove(ball, x, y) return drawFielderGlove(ball, x, y)
end end
---@param batState BatState ---@param batState BatRenderState
local function drawBat(batState) local function drawBat(batState)
gfx.setLineWidth(7) gfx.setLineWidth(7)
gfx.drawLine(batState.batBase.x, batState.batBase.y, batState.batTip.x, batState.batTip.y) gfx.drawLine(batState.batBase.x, batState.batBase.y, batState.batTip.x, batState.batTip.y)
@ -68,7 +66,7 @@ end
---@param battingTeamSprites SpriteCollection ---@param battingTeamSprites SpriteCollection
---@param batter Runner ---@param batter Runner
---@param batState BatState ---@param batState BatRenderState
local function drawBatter(battingTeamSprites, batter, batState) local function drawBatter(battingTeamSprites, batter, batState)
local spriteCollection = battingTeamSprites[batter.spriteIndex] local spriteCollection = battingTeamSprites[batter.spriteIndex]
if batState.batAngleDeg > 50 and batState.batAngleDeg < 200 then if batState.batAngleDeg > 50 and batState.batAngleDeg < 200 then
@ -91,7 +89,7 @@ end
---@param fielding Fielding ---@param fielding Fielding
---@param baserunning Baserunning ---@param baserunning Baserunning
---@param batState BatState ---@param batState BatRenderState
---@param battingTeam TeamId ---@param battingTeam TeamId
---@param ball Point3d ---@param ball Point3d
---@return Fielder | nil ballHeldBy ---@return Fielder | nil ballHeldBy

View File

@ -15,7 +15,7 @@ import 'CoreLibs/utilities/where.lua'
---@class InputHandler ---@class InputHandler
---@field update fun(self, deltaSeconds: number) ---@field update fun(self, deltaSeconds: number)
---@field updateBat fun(self, ball: Ball, pitchIsOver: boolean, deltaSeconds: number) ---@field updateBatAngle fun(self, ball: Ball, pitchIsOver: boolean, deltaSeconds: number)
---@field runningSpeed fun(self, runner: Runner, ball: Ball) ---@field runningSpeed fun(self, runner: Runner, ball: Ball)
---@field pitch fun(self) ---@field pitch fun(self)
---@field fielderAction fun(self, fielderHoldingBall: Fielder | nil, outedSomeRunner: boolean, ball: Ball) ---@field fielderAction fun(self, fielderHoldingBall: Fielder | nil, outedSomeRunner: boolean, ball: Ball)
@ -40,6 +40,7 @@ import 'action-queue.lua'
import 'announcer.lua' import 'announcer.lua'
import 'ball.lua' import 'ball.lua'
import 'baserunning.lua' import 'baserunning.lua'
import 'batting.lua'
import 'dbg.lua' import 'dbg.lua'
import 'fielding.lua' import 'fielding.lua'
import 'graphics.lua' import 'graphics.lua'
@ -94,15 +95,17 @@ local teams <const> = {
---@field offenseState OffenseState ---@field offenseState OffenseState
---@field inning number ---@field inning number
---@field stats Statistics ---@field stats Statistics
---@field batBase XyPair
---@field batTip XyPair --- Ephemeral data ONLY used during rendering
---@field batAngleDeg number ---@class RenderState
---@field bat BatRenderState
---@class Game ---@class Game
---@field private settings Settings ---@field private settings Settings
---@field private announcer Announcer ---@field private announcer Announcer
---@field private fielding Fielding ---@field private fielding Fielding
---@field private baserunning Baserunning ---@field private baserunning Baserunning
---@field private batting Batting
---@field private characters Characters ---@field private characters Characters
---@field private npc InputHandler ---@field private npc InputHandler
---@field private userInput InputHandler ---@field private userInput InputHandler
@ -124,16 +127,12 @@ function Game.new(settings, announcer, fielding, baserunning, npc, state)
local battingTeam = "away" local battingTeam = "away"
local ball = Ball.new(gfx.animator) local ball = Ball.new(gfx.animator)
local o = setmetatable({ local o = setmetatable({
settings = settings, settings = settings,
announcer = announcer, announcer = announcer,
fielding = fielding, fielding = fielding,
panner = Panner.new(ball), panner = Panner.new(ball),
state = state or { state = state or {
batBase = utils.xy(C.Center.x - 34, 215),
batTip = utils.xy(0, 0),
batAngleDeg = C.CrankOffsetDeg,
deltaSeconds = 0, deltaSeconds = 0,
ball = ball, ball = ball,
battingTeam = battingTeam, battingTeam = battingTeam,
@ -148,6 +147,7 @@ function Game.new(settings, announcer, fielding, baserunning, npc, state)
o.baserunning = baserunning or Baserunning.new(announcer, function() o.baserunning = baserunning or Baserunning.new(announcer, function()
o:nextHalfInning() o:nextHalfInning()
end) end)
o.batting = Batting.new(o.baserunning)
o.userInput = UserInput.new(function(throwFly, forbidThrowHome) o.userInput = UserInput.new(function(throwFly, forbidThrowHome)
return o:buttonControlledThrow(throwFly, forbidThrowHome) return o:buttonControlledThrow(throwFly, forbidThrowHome)
end) end)
@ -362,7 +362,7 @@ function Game:strikeOut()
end end
function Game:saveToFile() function Game:saveToFile()
playdate.datastore.write({ currentGame = self }, "data", true) playdate.datastore.write({ currentGame = self.state }, "data", true)
end end
function Game.load() function Game.load()
@ -380,60 +380,30 @@ function Game.load()
return ret return ret
end end
local SwingBackDeg <const> = 30
local SwingForwardDeg <const> = 170
---@param offenseHandler InputHandler ---@param offenseHandler InputHandler
function Game:updateBatting(offenseHandler) function Game:updateBatting(offenseHandler)
local ball = self.state.ball local ball = self.state.ball
local batDeg, batSpeed = offenseHandler:updateBat(ball, self.state.pitchIsOver, self.state.deltaSeconds) local batDeg, batSpeed = offenseHandler:updateBatAngle(ball, self.state.pitchIsOver, self.state.deltaSeconds)
self.state.batAngleDeg = batDeg local ballDest, isSwinging, mult = self.batting:checkForHit(batDeg, batSpeed, ball)
self.state.didSwing = self.state.didSwing or (isSwinging and not self.state.pitchIsOver)
if not self.state.pitchIsOver and batDeg > SwingBackDeg and batDeg < SwingForwardDeg then if not ballDest then
self.state.didSwing = true
end
-- TODO? Make the bat angle work more like the throw meter.
-- Would instead constantly drift toward a default value, giving us a little more control,
-- 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.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
and utils.pointDirectlyUnderLine(ball, self.state.batBase, self.state.batTip, C.Screen.H)
if not ballWasHit then
return return
end end
-- Hit! -- Hit!
-- TODO: animate bat-flip or something
self:saveToFile() self:saveToFile()
BatCrackReverb:play() BatCrackReverb:play()
self.state.offenseState = C.Offense.running 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)
if ballVelY > 0 then
ballVelX = ballVelX * -1
ballVelY = ballVelY * -1
end
local ballDest = utils.xy(ball.x + ballVelX, ball.y + ballVelY)
pitchTracker:reset() pitchTracker:reset()
local flyTimeMs = 2000 local flyTimeMs = 2000
-- TODO? A dramatic eye-level view on a home-run could be sick. -- TODO? A dramatic eye-level view on a home-run could be sick.
local battingTeamStats = self:battingTeamCurrentInning() local battingTeamStats = self:battingTeamCurrentInning()
battingTeamStats.hits[#battingTeamStats.hits + 1] = ballDest battingTeamStats.hits[#battingTeamStats.hits + 1] = ballDest
if utils.isFoulBall(ballDest.x, ballDest.y) then if utils.isFoulBall(ballDest) then
self.announcer:say("Foul ball!") self.announcer:say("Foul ball!")
pitchTracker.strikes = math.min(pitchTracker.strikes + 1, 2) pitchTracker.strikes = math.min(pitchTracker.strikes + 1, 2)
-- TODO: Have a fielder chase for the fly-out -- TODO: Have a fielder chase for the fly-out
@ -592,7 +562,8 @@ function Game:update()
fans.draw() fans.draw()
GrassBackground:draw(-400, -720) GrassBackground:draw(-400, -720)
local ballHeldBy = self.characters:drawAll(self.fielding, self.baserunning, state, state.battingTeam, state.ball) local ballHeldBy =
self.characters:drawAll(self.fielding, self.baserunning, self.batting.state, state.battingTeam, state.ball)
if self:userIsOn("defense") then if self:userIsOn("defense") then
throwMeter:drawNearFielder(ballHeldBy) throwMeter:drawNearFielder(ballHeldBy)

View File

@ -26,7 +26,7 @@ function Npc.update() end
---@param pitchIsOver boolean ---@param pitchIsOver boolean
---@param deltaSec number ---@param deltaSec number
---@return number batAngleDeg, number batSpeed ---@return number batAngleDeg, number batSpeed
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 if not pitchIsOver and ball.y > 200 and ball.y < 230 and (ball.x < C.Center.x + 15) then
npcBatDeg = npcBatDeg + (deltaSec * npcBatSpeed) npcBatDeg = npcBatDeg + (deltaSec * npcBatSpeed)
else else

View File

@ -18,7 +18,7 @@ function UserInput:update()
end end
---@return number batAngleDeg, number batSpeed ---@return number batAngleDeg, number batSpeed
function UserInput:updateBat() function UserInput:updateBatAngle()
local batAngleDeg = (playdate.getCrankPosition() + C.CrankOffsetDeg) % 360 local batAngleDeg = (playdate.getCrankPosition() + C.CrankOffsetDeg) % 360
local batSpeed = math.abs(self.crankLimited) local batSpeed = math.abs(self.crankLimited)
return batAngleDeg, batSpeed return batAngleDeg, batSpeed

View File

@ -175,14 +175,14 @@ end
---@param line2 XyPair ---@param line2 XyPair
---@param bottomBound number ---@param bottomBound number
---@return boolean ---@return boolean
function utils.pointDirectlyUnderLine(point, line1, line2, bottomBound) function utils.pointOnOrUnderLine(point, line1, line2, bottomBound)
-- This check currently assumes right-handedness. -- This check currently assumes right-handedness.
-- I.e. it assumes the ball is to the right of batBaseX -- I.e. it assumes the ball is to the right of batBaseX
if point.x < line1.x or point.x > line2.x or point.y > bottomBound then if point.x < line1.x or point.x > line2.x or point.y > bottomBound then
return false return false
end end
return utils.pointUnderLine(point.x, point.y, line1.x, line1.y, line2.x, line2.y) return utils.pointUnderLine(point.x, point.y, line1.x, line1.y - 2, line2.x, line2.y - 2)
end end
--- Returns true if the given point is anywhere above the given line, with no upper bound. --- Returns true if the given point is anywhere above the given line, with no upper bound.
@ -230,14 +230,13 @@ function utils.pointUnderLine(pointX, pointY, lineX1, lineY1, lineX2, lineY2)
end end
--- Returns true if a ball landing at destX,destY will be foul. --- Returns true if a ball landing at destX,destY will be foul.
---@param destX number ---@param dest XyPair
---@param destY number function utils.isFoulBall(dest)
function utils.isFoulBall(destX, destY)
local leftLine = C.LeftFoulLine local leftLine = C.LeftFoulLine
local rightLine = C.RightFoulLine local rightLine = C.RightFoulLine
return utils.pointUnderLine(destX, destY, leftLine.x1, leftLine.y1, leftLine.x2, leftLine.y2) return utils.pointUnderLine(dest.x, dest.y, leftLine.x1, leftLine.y1, leftLine.x2, leftLine.y2)
or utils.pointUnderLine(destX, destY, rightLine.x1, rightLine.y1, rightLine.x2, rightLine.y2) or utils.pointUnderLine(dest.x, dest.y, rightLine.x1, rightLine.y1, rightLine.x2, rightLine.y2)
end end
--- Returns the nearest position object from the given point, as well as its distance from that point --- Returns the nearest position object from the given point, as well as its distance from that point