From 04d25127fcb6ec4e0c79af0c8df233fec5d81ba5 Mon Sep 17 00:00:00 2001 From: Sage Vaillancourt Date: Thu, 27 Feb 2025 19:31:00 -0500 Subject: [PATCH] Extract batting.lua --- src/batting.lua | 67 +++++++++++++++++++++++++++++++++++++++++ src/draw/characters.lua | 8 ++--- src/main.lua | 63 +++++++++++--------------------------- src/npc.lua | 2 +- src/user-input.lua | 2 +- src/utils.lua | 13 ++++---- 6 files changed, 95 insertions(+), 60 deletions(-) create mode 100644 src/batting.lua diff --git a/src/batting.lua b/src/batting.lua new file mode 100644 index 0000000..b8d50ad --- /dev/null +++ b/src/batting.lua @@ -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 = 30 +local SwingForwardDeg = 170 +local OffscreenPos = 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 diff --git a/src/draw/characters.lua b/src/draw/characters.lua index 7f81848..395e6df 100644 --- a/src/draw/characters.lua +++ b/src/draw/characters.lua @@ -7,8 +7,6 @@ Characters = {} local gfx = playdate.graphics ----@alias BatState { batBase: XyPair, batTip: XyPair, batAngleDeg: number } - local GloveSizeX, GloveSizeY = Glove:getSize() local GloveOffX, GloveOffY = 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 diff --git a/src/main.lua b/src/main.lua index 85bb973..ad36301 100644 --- a/src/main.lua +++ b/src/main.lua @@ -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 = { ---@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 = 30 -local SwingForwardDeg = 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) diff --git a/src/npc.lua b/src/npc.lua index 897076c..ad2e247 100644 --- a/src/npc.lua +++ b/src/npc.lua @@ -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 diff --git a/src/user-input.lua b/src/user-input.lua index 4e1bd29..c821570 100644 --- a/src/user-input.lua +++ b/src/user-input.lua @@ -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 diff --git a/src/utils.lua b/src/utils.lua index f79486a..8da3ac7 100644 --- a/src/utils.lua +++ b/src/utils.lua @@ -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