Compare commits

..

No commits in common. "04d25127fcb6ec4e0c79af0c8df233fec5d81ba5" and "80015dbe621b41ccef165bdf5fbed7fc74764e4d" have entirely different histories.

9 changed files with 88 additions and 128 deletions

View File

@ -1,67 +0,0 @@
---@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

@ -1,11 +0,0 @@
local gfx <const> = playdate.graphics
function Ball:draw()
gfx.setLineWidth(2)
gfx.setColor(gfx.kColorWhite)
gfx.fillCircleAtPoint(self.x, self.y, self.size)
gfx.setColor(gfx.kColorBlack)
gfx.drawCircleAtPoint(self.x, self.y, self.size)
end

View File

@ -7,6 +7,8 @@ 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
@ -51,7 +53,7 @@ function drawFielder(fieldingTeamSprites, fielder, ball, flip)
return drawFielderGlove(ball, x, y) return drawFielderGlove(ball, x, y)
end end
---@param batState BatRenderState ---@param batState BatState
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)
@ -66,7 +68,7 @@ end
---@param battingTeamSprites SpriteCollection ---@param battingTeamSprites SpriteCollection
---@param batter Runner ---@param batter Runner
---@param batState BatRenderState ---@param batState BatState
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
@ -89,7 +91,7 @@ end
---@param fielding Fielding ---@param fielding Fielding
---@param baserunning Baserunning ---@param baserunning Baserunning
---@param batState BatRenderState ---@param batState BatState
---@param battingTeam TeamId ---@param battingTeam TeamId
---@param ball Point3d ---@param ball Point3d
---@return Fielder | nil ballHeldBy ---@return Fielder | nil ballHeldBy

View File

@ -145,8 +145,7 @@ end
local newStats = stats local newStats = stats
function drawScoreboard(x, y, statistics, outs, battingTeam, inning) function drawScoreboard(x, y, homeScore, awayScore, outs, battingTeam, inning)
local homeScore, awayScore = utils.totalScores(statistics)
if if
newStats.homeScore ~= homeScore newStats.homeScore ~= homeScore
or newStats.awayScore ~= awayScore or newStats.awayScore ~= awayScore

View File

@ -96,7 +96,7 @@ local function updateFielderPosition(deltaSeconds, fielder, ball)
local currentTarget = fielder.targets[#fielder.targets] local currentTarget = fielder.targets[#fielder.targets]
local willMove = utils.moveAtSpeed(nextFielderPos, fielder.speed * deltaSeconds, currentTarget) local willMove = utils.moveAtSpeed(nextFielderPos, fielder.speed * deltaSeconds, currentTarget)
if willMove and utils.pointIsAboveLine(nextFielderPos, C.BottomOfOutfieldWall) then if willMove and utils.pointIsSquarelyAboveLine(nextFielderPos, C.BottomOfOutfieldWall) then
local targetCount = #fielder.targets local targetCount = #fielder.targets
-- Back up a little -- Back up a little
fielder.targets[targetCount + 2] = utils.xy(fielder.x, fielder.y + 5) fielder.targets[targetCount + 2] = utils.xy(fielder.x, fielder.y + 5)

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 updateBatAngle fun(self, ball: Ball, pitchIsOver: boolean, deltaSeconds: number) ---@field updateBat 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,7 +40,6 @@ 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'
@ -49,7 +48,6 @@ import 'pitching.lua'
import 'statistics.lua' import 'statistics.lua'
import 'user-input.lua' import 'user-input.lua'
import 'draw/ball.lua'
import 'draw/box-score.lua' import 'draw/box-score.lua'
import 'draw/fans.lua' import 'draw/fans.lua'
import 'draw/characters.lua' import 'draw/characters.lua'
@ -95,17 +93,15 @@ local teams <const> = {
---@field offenseState OffenseState ---@field offenseState OffenseState
---@field inning number ---@field inning number
---@field stats Statistics ---@field stats Statistics
---@field batBase XyPair
--- Ephemeral data ONLY used during rendering ---@field batTip XyPair
---@class RenderState ---@field batAngleDeg number
---@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
@ -127,12 +123,16 @@ 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,
@ -147,7 +147,6 @@ 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 +361,7 @@ function Game:strikeOut()
end end
function Game:saveToFile() function Game:saveToFile()
playdate.datastore.write({ currentGame = self.state }, "data", true) playdate.datastore.write({ currentGame = self }, "data", true)
end end
function Game.load() function Game.load()
@ -380,41 +379,71 @@ 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:updateBatAngle(ball, self.state.pitchIsOver, self.state.deltaSeconds) local batDeg, batSpeed = offenseHandler:updateBat(ball, self.state.pitchIsOver, self.state.deltaSeconds)
local ballDest, isSwinging, mult = self.batting:checkForHit(batDeg, batSpeed, ball) self.state.batAngleDeg = batDeg
self.state.didSwing = self.state.didSwing or (isSwinging and not self.state.pitchIsOver)
if not ballDest then 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
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) then if utils.isFoulBall(ballDest.x, ballDest.y) 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
return return
end end
local isHomeRun = utils.pointIsAboveLine(ballDest, C.OutfieldWall) local isHomeRun = utils.pointIsSquarelyAboveLine(ballDest, C.OutfieldWall)
if isHomeRun then if isHomeRun then
playdate.timer.new(flyTimeMs, function() playdate.timer.new(flyTimeMs, function()
-- Verify that the home run wasn't intercepted -- Verify that the home run wasn't intercepted
if utils.distanceBetweenPoints(ball, ballDest) < 2 then if utils.within(1, ball.x, ballDest.x) and utils.within(1, ball.y, ballDest.y) then
self.announcer:say("HOME RUN!") self.announcer:say("HOME RUN!")
self.state.offenseState = C.Offense.homeRun self.state.offenseState = C.Offense.homeRun
-- Linger on the home-run ball for a moment, before panning to the bases. -- Linger on the home-run ball for a moment, before panning to the bases.
@ -562,15 +591,22 @@ function Game:update()
fans.draw() fans.draw()
GrassBackground:draw(-400, -720) GrassBackground:draw(-400, -720)
local ballHeldBy = local ball = state.ball
self.characters:drawAll(self.fielding, self.baserunning, self.batting.state, state.battingTeam, state.ball)
local ballHeldBy = self.characters:drawAll(self.fielding, self.baserunning, state, state.battingTeam, ball)
if self:userIsOn("defense") then if self:userIsOn("defense") then
throwMeter:drawNearFielder(ballHeldBy) throwMeter:drawNearFielder(ballHeldBy)
end end
if not ballHeldBy then if not ballHeldBy then
state.ball:draw() gfx.setLineWidth(2)
gfx.setColor(gfx.kColorWhite)
gfx.fillCircleAtPoint(ball.x, ball.y, ball.size)
gfx.setColor(gfx.kColorBlack)
gfx.drawCircleAtPoint(ball.x, ball.y, ball.size)
end end
gfx.setDrawOffset(0, 0) gfx.setDrawOffset(0, 0)
@ -578,7 +614,8 @@ function Game:update()
drawMinimap(self.baserunning.runners, self.fielding.fielders) drawMinimap(self.baserunning.runners, self.fielding.fielders)
end end
drawScoreboard(0, C.Screen.H * 0.77, state.stats, self.baserunning.outs, state.battingTeam, state.inning) local homeScore, awayScore = utils.totalScores(state.stats)
drawScoreboard(0, C.Screen.H * 0.77, homeScore, awayScore, self.baserunning.outs, state.battingTeam, state.inning)
drawBallsAndStrikes(290, C.Screen.H - 20, pitchTracker.balls, pitchTracker.strikes) drawBallsAndStrikes(290, C.Screen.H - 20, pitchTracker.balls, pitchTracker.strikes)
self.announcer:draw(C.Center.x, 10) self.announcer:draw(C.Center.x, 10)

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:updateBatAngle(ball, pitchIsOver, deltaSec) function Npc:updateBat(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

@ -11,16 +11,14 @@ end
function UserInput:update() function UserInput:update()
self.crankChange = playdate.getCrankChange() self.crankChange = playdate.getCrankChange()
self.crankLimited = self.crankChange == 0 and 0 or (math.log(math.abs(self.crankChange)) * C.CrankPower) local crankLimited = self.crankChange == 0 and 0 or (math.log(math.abs(self.crankChange)) * C.CrankPower)
if self.crankChange < 0 then self.crankLimited = math.abs(crankLimited)
self.crankLimited = self.crankLimited * -1
end
end end
---@return number batAngleDeg, number batSpeed ---@return number batAngleDeg, number batSpeed
function UserInput:updateBatAngle() function UserInput:updateBat()
local batAngleDeg = (playdate.getCrankPosition() + C.CrankOffsetDeg) % 360 local batAngleDeg = (playdate.getCrankPosition() + C.CrankOffsetDeg) % 360
local batSpeed = math.abs(self.crankLimited) local batSpeed = self.crankLimited
return batAngleDeg, batSpeed return batAngleDeg, batSpeed
end end

View File

@ -90,6 +90,14 @@ function utils.moveAtSpeed(mover, speed, target, tau)
return true return true
end end
---@param acceptableGap number
---@param n1 number
---@param n2 number
---@return boolean n1 is within acceptableGap of n2
function utils.within(acceptableGap, n1, n2)
return math.abs(n1 - n2) < acceptableGap
end
---@generic T ---@generic T
---@param array T[] ---@param array T[]
---@param condition fun(T): boolean ---@param condition fun(T): boolean
@ -175,26 +183,22 @@ end
---@param line2 XyPair ---@param line2 XyPair
---@param bottomBound number ---@param bottomBound number
---@return boolean ---@return boolean
function utils.pointOnOrUnderLine(point, line1, line2, bottomBound) function utils.pointDirectlyUnderLine(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 - 2, line2.x, line2.y - 2) return utils.pointUnderLine(point.x, point.y, line1.x, line1.y, line2.x, line2.y)
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.
--- This, used for home run calculations, does not *precesely* take into account balls that curve around the foul poles. --- This, if used for home run calculations, would not take into account balls that curve around the foul poles.
--- If left of first linePoint and above it, returns true. Similarly if right of the last linePoint.
---@param point XyPair ---@param point XyPair
---@param linePoints XyPair[] ---@param linePoints XyPair[]
---@return boolean ---@return boolean
function utils.pointIsAboveLine(point, linePoints) function utils.pointIsSquarelyAboveLine(point, linePoints)
if point.x < linePoints[1].x and point.y < linePoints[1].y then
return true
end
for i = 2, #linePoints do for i = 2, #linePoints do
local prev = linePoints[i - 1] local prev = linePoints[i - 1]
local next = linePoints[i] local next = linePoints[i]
@ -202,9 +206,6 @@ function utils.pointIsAboveLine(point, linePoints)
return not utils.pointUnderLine(point.x, point.y, prev.x, prev.y, next.x, next.y) return not utils.pointUnderLine(point.x, point.y, prev.x, prev.y, next.x, next.y)
end end
end end
if point.x > linePoints[#linePoints].x and point.y < linePoints[#linePoints].y then
return true
end
return false return false
end end
@ -230,13 +231,14 @@ 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 dest XyPair ---@param destX number
function utils.isFoulBall(dest) ---@param destY number
function utils.isFoulBall(destX, destY)
local leftLine = C.LeftFoulLine local leftLine = C.LeftFoulLine
local rightLine = C.RightFoulLine local rightLine = C.RightFoulLine
return utils.pointUnderLine(dest.x, dest.y, leftLine.x1, leftLine.y1, leftLine.x2, leftLine.y2) return utils.pointUnderLine(destX, destY, leftLine.x1, leftLine.y1, leftLine.x2, leftLine.y2)
or utils.pointUnderLine(dest.x, dest.y, rightLine.x1, rightLine.y1, rightLine.x2, rightLine.y2) or utils.pointUnderLine(destX, destY, 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