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

View File

@ -15,7 +15,7 @@ import 'CoreLibs/utilities/where.lua'
---@class InputHandler
---@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 pitch fun(self)
---@field fielderAction fun(self, fielderHoldingBall: Fielder | nil, outedSomeRunner: boolean, ball: Ball)
@ -40,6 +40,7 @@ import 'action-queue.lua'
import 'announcer.lua'
import 'ball.lua'
import 'baserunning.lua'
import 'batting.lua'
import 'dbg.lua'
import 'fielding.lua'
import 'graphics.lua'
@ -94,15 +95,17 @@ local teams <const> = {
---@field offenseState OffenseState
---@field inning number
---@field stats Statistics
---@field batBase XyPair
---@field batTip XyPair
---@field batAngleDeg number
--- Ephemeral data ONLY used during rendering
---@class RenderState
---@field bat BatRenderState
---@class Game
---@field private settings Settings
---@field private announcer Announcer
---@field private fielding Fielding
---@field private baserunning Baserunning
---@field private batting Batting
---@field private characters Characters
---@field private npc InputHandler
---@field private userInput InputHandler
@ -124,16 +127,12 @@ function Game.new(settings, announcer, fielding, baserunning, npc, state)
local battingTeam = "away"
local ball = Ball.new(gfx.animator)
local o = setmetatable({
settings = settings,
announcer = announcer,
fielding = fielding,
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,
@ -148,6 +147,7 @@ function Game.new(settings, announcer, fielding, baserunning, npc, state)
o.baserunning = baserunning or Baserunning.new(announcer, function()
o:nextHalfInning()
end)
o.batting = Batting.new(o.baserunning)
o.userInput = UserInput.new(function(throwFly, forbidThrowHome)
return o:buttonControlledThrow(throwFly, forbidThrowHome)
end)
@ -362,7 +362,7 @@ function Game:strikeOut()
end
function Game:saveToFile()
playdate.datastore.write({ currentGame = self }, "data", true)
playdate.datastore.write({ currentGame = self.state }, "data", true)
end
function Game.load()
@ -380,60 +380,30 @@ function Game.load()
return ret
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
local batDeg, batSpeed = offenseHandler:updateBatAngle(ball, self.state.pitchIsOver, self.state.deltaSeconds)
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
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
if not ballDest then
return
end
-- Hit!
-- TODO: animate bat-flip or something
self:saveToFile()
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)
if ballVelY > 0 then
ballVelX = ballVelX * -1
ballVelY = ballVelY * -1
end
local ballDest = utils.xy(ball.x + ballVelX, ball.y + ballVelY)
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] = ballDest
if utils.isFoulBall(ballDest.x, ballDest.y) then
if utils.isFoulBall(ballDest) then
self.announcer:say("Foul ball!")
pitchTracker.strikes = math.min(pitchTracker.strikes + 1, 2)
-- TODO: Have a fielder chase for the fly-out
@ -592,7 +562,8 @@ function Game:update()
fans.draw()
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
throwMeter:drawNearFielder(ballHeldBy)

View File

@ -26,7 +26,7 @@ function Npc.update() end
---@param pitchIsOver boolean
---@param deltaSec number
---@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
npcBatDeg = npcBatDeg + (deltaSec * npcBatSpeed)
else

View File

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

View File

@ -175,14 +175,14 @@ end
---@param line2 XyPair
---@param bottomBound number
---@return boolean
function utils.pointDirectlyUnderLine(point, line1, line2, bottomBound)
function utils.pointOnOrUnderLine(point, line1, line2, bottomBound)
-- This check currently assumes right-handedness.
-- 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
return false
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
--- 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
--- Returns true if a ball landing at destX,destY will be foul.
---@param destX number
---@param destY number
function utils.isFoulBall(destX, destY)
---@param dest XyPair
function utils.isFoulBall(dest)
local leftLine = C.LeftFoulLine
local rightLine = C.RightFoulLine
return utils.pointUnderLine(destX, destY, leftLine.x1, leftLine.y1, leftLine.x2, leftLine.y2)
or utils.pointUnderLine(destX, destY, rightLine.x1, rightLine.y1, rightLine.x2, rightLine.y2)
return utils.pointUnderLine(dest.x, dest.y, leftLine.x1, leftLine.y1, leftLine.x2, leftLine.y2)
or utils.pointUnderLine(dest.x, dest.y, rightLine.x1, rightLine.y1, rightLine.x2, rightLine.y2)
end
--- Returns the nearest position object from the given point, as well as its distance from that point