Compare commits
1 Commits
main
...
slightly-b
Author | SHA1 | Date |
---|---|---|
|
58c6ab9fe6 |
56
src/ball.lua
56
src/ball.lua
|
@ -2,10 +2,11 @@
|
|||
---@field x number
|
||||
---@field y number
|
||||
---@field z number
|
||||
---@field trajectory Point3d
|
||||
---@field size number
|
||||
---@field heldBy Fielder | nil
|
||||
---@field catchable boolean
|
||||
---@field isFlyBall boolean
|
||||
---@field catchable boolean
|
||||
---@field xAnimator SimpleAnimator
|
||||
---@field yAnimator SimpleAnimator
|
||||
---@field sizeAnimator SimpleAnimator
|
||||
|
@ -13,10 +14,6 @@
|
|||
---@field private animatorLib pd_animator_lib
|
||||
Ball = {}
|
||||
|
||||
local function defaultFloatAnimator(animatorLib)
|
||||
return animatorLib.new(2000, -60, 0, utils.easingHill)
|
||||
end
|
||||
|
||||
---@param animatorLib pd_animator_lib
|
||||
---@return Ball
|
||||
function Ball.new(animatorLib)
|
||||
|
@ -25,6 +22,7 @@ function Ball.new(animatorLib)
|
|||
x = C.Center.x --[[@as number]],
|
||||
y = C.Center.y --[[@as number]],
|
||||
z = 0,
|
||||
isFlyBall = false,
|
||||
catchable = true,
|
||||
size = C.SmallestBallRadius,
|
||||
heldBy = nil --[[@type Runner | nil]],
|
||||
|
@ -35,26 +33,43 @@ function Ball.new(animatorLib)
|
|||
-- TODO? Replace these with a ballAnimatorZ?
|
||||
-- ...that might lose some of the magic of both. Compromise available? idk
|
||||
sizeAnimator = utils.staticAnimator(C.SmallestBallRadius),
|
||||
floatAnimator = defaultFloatAnimator(animatorLib),
|
||||
floatAnimator = animatorLib.new(2000, -60, 0, utils.easingHill),
|
||||
}, { __index = Ball })
|
||||
end
|
||||
|
||||
---@param deltaSeconds number
|
||||
function Ball:updatePosition(deltaSeconds)
|
||||
if self.heldBy then
|
||||
utils.moveAtSpeedZ(self, 100 * deltaSeconds, { x = self.heldBy.x, y = self.heldBy.y, z = C.GloveZ })
|
||||
self.x = self.heldBy.x
|
||||
self.y = self.heldBy.y
|
||||
self.z = C.GloveZ
|
||||
self.size = C.SmallestBallRadius
|
||||
else
|
||||
elseif self.floatAnimator:ended() then
|
||||
-- Try to bounce?
|
||||
if self.trajectory then
|
||||
printTable(self.trajectory)
|
||||
-- if utils.distanceBetweenPoints(self, self.trajectory) > 1 then
|
||||
-- self:launch()
|
||||
-- end
|
||||
-- local easing = playdate.easingFunctions.outQuint
|
||||
-- self:launch(self.x + self.trajectory.x, self.y + self.trajectory.y, easing, nil, true)
|
||||
self.trajectory = nil
|
||||
end
|
||||
--else
|
||||
local lastPos = { x = self.x, y = self.y, z = self.z }
|
||||
self.x = self.xAnimator:currentValue()
|
||||
local z = self.floatAnimator:currentValue()
|
||||
-- TODO: This `+ z` is more graphics logic than physics logic
|
||||
self.y = self.yAnimator:currentValue() + z
|
||||
self.z = z
|
||||
if self.z < 2 and self.isFlyBall then
|
||||
print("Ball hit the ground!")
|
||||
self.isFlyBall = false
|
||||
end
|
||||
self.size = C.SmallestBallRadius + math.max(0, (self.floatAnimator:currentValue() - C.GloveZ) / 2)
|
||||
self.size = self.sizeAnimator:currentValue()
|
||||
self.trajectory = { x = self.x - lastPos.x, y = self.y - lastPos.y, z = self.z - lastPos.z }
|
||||
self.trajectory.x = self.trajectory.x / deltaSeconds
|
||||
self.trajectory.y = self.trajectory.y / deltaSeconds
|
||||
self.trajectory.z = self.trajectory.z / deltaSeconds
|
||||
end
|
||||
if self.z < 1 then
|
||||
self.isFlyBall = false
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -71,11 +86,9 @@ end
|
|||
---@param easingFunc EasingFunc
|
||||
---@param flyTimeMs number | nil
|
||||
---@param floaty boolean | nil
|
||||
---@param customFloater pd_animator | nil
|
||||
---@param isHit boolean
|
||||
function Ball:launch(destX, destY, easingFunc, flyTimeMs, floaty, customFloater, isHit)
|
||||
---@param customBallScaler pd_animator | nil
|
||||
function Ball:launch(destX, destY, easingFunc, flyTimeMs, floaty, customBallScaler)
|
||||
self.heldBy = nil
|
||||
self.isFlyBall = isHit
|
||||
|
||||
-- Prevent silly insta-catches
|
||||
self:markUncatchable()
|
||||
|
@ -84,16 +97,15 @@ function Ball:launch(destX, destY, easingFunc, flyTimeMs, floaty, customFloater,
|
|||
flyTimeMs = utils.distanceBetween(self.x, self.y, destX, destY) * C.DefaultLaunchPower
|
||||
end
|
||||
|
||||
if customFloater then
|
||||
self.floatAnimator = customFloater
|
||||
if customBallScaler then
|
||||
self.sizeAnimator = customBallScaler
|
||||
else
|
||||
self.sizeAnimator = self.animatorLib.new(flyTimeMs, C.SmallestBallRadius, 9, utils.easingHill)
|
||||
self.floatAnimator = defaultFloatAnimator(self.animatorLib)
|
||||
self.sizeAnimator = self.animatorLib.new(flyTimeMs, 9, C.SmallestBallRadius, utils.easingHill)
|
||||
end
|
||||
self.yAnimator = self.animatorLib.new(flyTimeMs, self.y, destY, easingFunc)
|
||||
self.xAnimator = self.animatorLib.new(flyTimeMs, self.x, destX, easingFunc)
|
||||
if floaty then
|
||||
self.floatAnimator:reset(flyTimeMs)
|
||||
self.floatAnimator:reset(flyTimeMs / 2)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -176,10 +176,6 @@ function Baserunning:pushNewBatter()
|
|||
return new
|
||||
end
|
||||
|
||||
function Baserunning:getNewestRunner()
|
||||
return self.runners[#self.runners]
|
||||
end
|
||||
|
||||
---@param runnerIndex number
|
||||
function Baserunning:runnerScored(runnerIndex)
|
||||
self.scoredRunners[#self.scoredRunners + 1] = self.runners[runnerIndex]
|
||||
|
@ -205,7 +201,7 @@ function Baserunning:updateRunner(runner, runnerIndex, appliedSpeed, isAutoRun,
|
|||
if
|
||||
nearestBaseDistance < 5
|
||||
and runnerIndex ~= nil
|
||||
and runner ~= self.batter
|
||||
and runner ~= self.batter --runner.prevBase
|
||||
and runner.nextBase == C.Bases[C.Home]
|
||||
and nearestBase == C.Bases[C.Home]
|
||||
then
|
||||
|
|
|
@ -1,68 +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
|
||||
--- Mutates self.state for later rendering.
|
||||
---@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
|
|
@ -99,7 +99,6 @@ C.Offense = {
|
|||
running = "running",
|
||||
walking = "walking",
|
||||
homeRun = "homeRun",
|
||||
fliedOut = "running",
|
||||
}
|
||||
|
||||
---@alias Side "offense" | "defense"
|
||||
|
@ -115,8 +114,6 @@ C.CrankPower = 10
|
|||
|
||||
C.FielderRunMult = 1.3
|
||||
|
||||
C.PlayerHeightOffset = 20
|
||||
|
||||
C.UserThrowPower = 0.3
|
||||
|
||||
--- How fast baserunners move after a walk
|
||||
|
|
|
@ -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
|
|
@ -1,151 +0,0 @@
|
|||
---@class Characters
|
||||
---@field homeSprites SpriteCollection
|
||||
---@field awaySprites SpriteCollection
|
||||
---@field homeBlipper table
|
||||
---@field awayBlipper table
|
||||
Characters = {}
|
||||
|
||||
local gfx <const> = playdate.graphics
|
||||
|
||||
local GloveSizeX, GloveSizeY <const> = Glove:getSize()
|
||||
local GloveOffX, GloveOffY <const> = GloveSizeX / 2, GloveSizeY / 2
|
||||
|
||||
---@param homeSprites SpriteCollection
|
||||
---@param awaySprites SpriteCollection
|
||||
function Characters.new(homeSprites, awaySprites)
|
||||
return setmetatable({
|
||||
homeSprites = homeSprites,
|
||||
awaySprites = awaySprites,
|
||||
homeBlipper = blipper.new(100, homeSprites),
|
||||
awayBlipper = blipper.new(100, awaySprites),
|
||||
}, { __index = Characters })
|
||||
end
|
||||
|
||||
---@param ball Point3d
|
||||
---@param fielderX number
|
||||
---@param fielderY number
|
||||
---@return boolean isHoldingBall
|
||||
local function drawFielderGlove(ball, fielderX, fielderY, flip)
|
||||
local distanceFromBall = utils.distanceBetweenZ(fielderX, fielderY, 0, ball.x, ball.y, ball.z)
|
||||
local shoulderX, shoulderY = fielderX + 10, fielderY - 5
|
||||
if distanceFromBall > 20 then
|
||||
Glove:draw(shoulderX, shoulderY, flip)
|
||||
return false
|
||||
else
|
||||
GloveHoldingBall:draw(ball.x - GloveOffX, ball.y - GloveOffY, flip)
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
---@param fieldingTeamSprites SpriteCollection
|
||||
---@param fielder Fielder
|
||||
---@param ball Point3d
|
||||
---@param flip boolean | nil
|
||||
---@return boolean isHoldingBall
|
||||
function drawFielder(fieldingTeamSprites, fielder, ball, flip)
|
||||
local danceOffset = FielderDanceAnimator:currentValue()
|
||||
|
||||
local x = fielder.x
|
||||
local y = fielder.y - danceOffset
|
||||
fieldingTeamSprites[fielder.spriteIndex].smiling:draw(fielder.x, y - 20, flip)
|
||||
return drawFielderGlove(ball, x, y)
|
||||
end
|
||||
|
||||
---@param batState BatRenderState
|
||||
local function drawBat(batState)
|
||||
gfx.setLineWidth(7)
|
||||
gfx.drawLine(batState.batBase.x, batState.batBase.y, batState.batTip.x, batState.batTip.y)
|
||||
|
||||
gfx.setColor(gfx.kColorWhite)
|
||||
gfx.setLineCapStyle(gfx.kLineCapStyleRound)
|
||||
gfx.setLineWidth(3)
|
||||
gfx.drawLine(batState.batBase.x, batState.batBase.y, batState.batTip.x, batState.batTip.y)
|
||||
|
||||
gfx.setColor(gfx.kColorBlack)
|
||||
end
|
||||
|
||||
---@param battingTeamSprites SpriteCollection
|
||||
---@param batter Runner
|
||||
---@param batState BatRenderState
|
||||
local function drawBatter(battingTeamSprites, batter, batState)
|
||||
local spriteCollection = battingTeamSprites[batter.spriteIndex]
|
||||
if batState.batAngleDeg > 50 and batState.batAngleDeg < 200 then
|
||||
drawBat(batState)
|
||||
spriteCollection.back:draw(batter.x, batter.y - C.PlayerHeightOffset)
|
||||
else
|
||||
spriteCollection.smiling:draw(batter.x, batter.y - C.PlayerHeightOffset)
|
||||
drawBat(batState)
|
||||
end
|
||||
end
|
||||
|
||||
---@param battingTeam TeamId
|
||||
---@return SpriteCollection battingTeam, SpriteCollection fieldingTeam, table runnerBlipper
|
||||
function Characters:getSpriteCollections(battingTeam)
|
||||
if battingTeam == "home" then
|
||||
return self.homeSprites, self.awaySprites, self.homeBlipper
|
||||
end
|
||||
return self.awaySprites, self.homeSprites, self.awayBlipper
|
||||
end
|
||||
|
||||
---@param fielding Fielding
|
||||
---@param baserunning Baserunning
|
||||
---@param batState BatRenderState
|
||||
---@param battingTeam TeamId
|
||||
---@param ball Point3d
|
||||
---@return Fielder | nil ballHeldBy
|
||||
function Characters:drawAll(fielding, baserunning, batState, battingTeam, ball)
|
||||
---@type { y: number, drawAction: fun() }[]
|
||||
local characterDraws = {}
|
||||
function addDraw(y, drawAction)
|
||||
characterDraws[#characterDraws + 1] = { y = y, drawAction = drawAction }
|
||||
end
|
||||
|
||||
local battingTeamSprites, fieldingTeamSprites, runnerBlipper = self:getSpriteCollections(battingTeam)
|
||||
---@type Fielder | nil
|
||||
local ballHeldBy
|
||||
for _, fielder in pairs(fielding.fielders) do
|
||||
addDraw(fielder.y, function()
|
||||
local ballHeldByThisFielder = drawFielder(fieldingTeamSprites, fielder, ball)
|
||||
if ballHeldByThisFielder then
|
||||
ballHeldBy = fielder
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
for _, runner in pairs(baserunning.runners) do
|
||||
addDraw(runner.y, function()
|
||||
local currentBatter = baserunning.batter
|
||||
if runner == currentBatter then
|
||||
drawBatter(battingTeamSprites, currentBatter, batState)
|
||||
else
|
||||
-- TODO? Change blip speed depending on runner speed?
|
||||
runnerBlipper:draw(false, runner.x, runner.y - C.PlayerHeightOffset, runner)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
for _, runner in pairs(baserunning.outRunners) do
|
||||
addDraw(runner.y, function()
|
||||
battingTeamSprites[runner.spriteIndex].frowning:draw(runner.x, runner.y - C.PlayerHeightOffset)
|
||||
end)
|
||||
end
|
||||
for _, runner in pairs(baserunning.scoredRunners) do
|
||||
addDraw(runner.y, function()
|
||||
runnerBlipper:draw(false, runner.x, runner.y - C.PlayerHeightOffset, runner)
|
||||
end)
|
||||
end
|
||||
|
||||
table.sort(characterDraws, function(a, b)
|
||||
return a.y < b.y
|
||||
end)
|
||||
for _, character in pairs(characterDraws) do
|
||||
character.drawAction()
|
||||
end
|
||||
|
||||
return ballHeldBy
|
||||
end
|
||||
|
||||
-- luacheck: ignore
|
||||
if not playdate or playdate.TEST_MODE then
|
||||
return Characters
|
||||
end
|
|
@ -0,0 +1,28 @@
|
|||
local GloveSizeX, GloveSizeY <const> = Glove:getSize()
|
||||
local GloveOffX, GloveOffY <const> = GloveSizeX / 2, GloveSizeY / 2
|
||||
|
||||
---@param ball Point3d
|
||||
---@param fielderX number
|
||||
---@param fielderY number
|
||||
---@return boolean isHoldingBall
|
||||
local function drawFielderGlove(ball, fielderX, fielderY, flip)
|
||||
local distanceFromBall = utils.distanceBetweenZ(fielderX, fielderY, 0, ball.x, ball.y, ball.z)
|
||||
local shoulderX, shoulderY = fielderX + 10, fielderY - 5
|
||||
if distanceFromBall > 20 then
|
||||
Glove:draw(shoulderX, shoulderY, flip)
|
||||
return false
|
||||
else
|
||||
GloveHoldingBall:draw(ball.x - GloveOffX, ball.y - GloveOffY, flip)
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
---@param playerSprites PlayerImageBundle
|
||||
---@param ball Point3d
|
||||
---@param x number
|
||||
---@param y number
|
||||
---@return boolean isHoldingBall
|
||||
function drawFielder(playerSprites, ball, x, y, flip)
|
||||
playerSprites.smiling:draw(x, y - 20, flip)
|
||||
return drawFielderGlove(ball, x, y)
|
||||
end
|
|
@ -145,8 +145,7 @@ end
|
|||
|
||||
local newStats = stats
|
||||
|
||||
function drawScoreboard(x, y, statistics, outs, battingTeam, inning)
|
||||
local homeScore, awayScore = utils.totalScores(statistics)
|
||||
function drawScoreboard(x, y, homeScore, awayScore, outs, battingTeam, inning)
|
||||
if
|
||||
newStats.homeScore ~= homeScore
|
||||
or newStats.awayScore ~= awayScore
|
||||
|
|
|
@ -67,8 +67,7 @@ end
|
|||
|
||||
--- Resets the target positions of all fielders to their defaults (at their field positions).
|
||||
---@param fromOffTheField XyPair | nil If provided, also sets all runners' current position to one centralized location.
|
||||
---@param immediate boolean | nil
|
||||
function Fielding:resetFielderPositions(fromOffTheField, immediate)
|
||||
function Fielding:resetFielderPositions(fromOffTheField)
|
||||
if fromOffTheField then
|
||||
for _, fielder in pairs(self.fielders) do
|
||||
fielder.x = fromOffTheField.x
|
||||
|
@ -85,13 +84,6 @@ function Fielding:resetFielderPositions(fromOffTheField, immediate)
|
|||
self.fielders.left.targets = { utils.xy(C.Screen.W * -0.6, C.Screen.H * -0.1) }
|
||||
self.fielders.center.targets = { utils.xy(C.Center.x, C.Screen.H * -0.4) }
|
||||
self.fielders.right.targets = { utils.xy(C.Screen.W * 1.6, self.fielders.left.targets[1].y) }
|
||||
|
||||
if immediate then
|
||||
for _, fielder in pairs(self.fielders) do
|
||||
fielder.x = fielder.targets[1].x
|
||||
fielder.y = fielder.targets[1].y
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@param deltaSeconds number
|
||||
|
@ -104,7 +96,7 @@ local function updateFielderPosition(deltaSeconds, fielder, ball)
|
|||
local currentTarget = fielder.targets[#fielder.targets]
|
||||
local willMove = utils.moveAtSpeed(nextFielderPos, fielder.speed * deltaSeconds, currentTarget)
|
||||
|
||||
if willMove and utils.pointIsAboveLine(nextFielderPos, C.BottomOfOutfieldWall, 40) then
|
||||
if willMove and utils.pointIsSquarelyAboveLine(nextFielderPos, C.BottomOfOutfieldWall) then
|
||||
local targetCount = #fielder.targets
|
||||
-- Back up a little
|
||||
fielder.targets[targetCount + 2] = utils.xy(fielder.x, fielder.y + 5)
|
||||
|
@ -133,12 +125,11 @@ end
|
|||
|
||||
--- Selects the nearest fielder to move toward the given coordinates.
|
||||
--- Other fielders should attempt to cover their bases
|
||||
---@param ball Point3d
|
||||
---@param ballDest XyPair
|
||||
function Fielding:haveSomeoneChase(ball, ballDest)
|
||||
local chasingFielder = utils.getNearestOf(self.fielders, ballDest.x, ballDest.y)
|
||||
-- Start moving toward the ball directly after reaching ballDest
|
||||
chasingFielder.targets = { ball, ballDest }
|
||||
---@param ballDestX number
|
||||
---@param ballDestY number
|
||||
function Fielding:haveSomeoneChase(ballDestX, ballDestY)
|
||||
local chasingFielder = utils.getNearestOf(self.fielders, ballDestX, ballDestY)
|
||||
chasingFielder.targets = { utils.xy(ballDestX, ballDestY) }
|
||||
|
||||
for _, base in ipairs(C.Bases) do
|
||||
local nearest = utils.getNearestOf(self.fielders, base.x, base.y, function(fielder)
|
||||
|
@ -155,24 +146,19 @@ end
|
|||
--- **Also updates `ball.heldby`**
|
||||
---@param ball Ball
|
||||
---@param deltaSeconds number
|
||||
---@return Fielder | nil, boolean fielderHoldingBall nil if no fielder is currently touching the ball, true if caught a fly ball
|
||||
---@return Fielder | nil fielderHoldingBall nil if no fielder is currently touching the ball
|
||||
function Fielding:updateFielderPositions(ball, deltaSeconds)
|
||||
local fielderHoldingBall
|
||||
local caughtAFlyBall = false
|
||||
for _, fielder in pairs(self.fielders) do
|
||||
-- TODO: Base this catch on fielder skill?
|
||||
local canCatch = updateFielderPosition(deltaSeconds, fielder, ball)
|
||||
if canCatch then
|
||||
fielderHoldingBall = fielder
|
||||
ball.heldBy = fielder -- How much havoc will this wreak?
|
||||
if ball.isFlyBall then
|
||||
ball.isFlyBall = false
|
||||
caughtAFlyBall = true
|
||||
end
|
||||
end
|
||||
end
|
||||
self.fielderHoldingBall = fielderHoldingBall
|
||||
return fielderHoldingBall, caughtAFlyBall
|
||||
return fielderHoldingBall
|
||||
end
|
||||
|
||||
-- TODO? Start moving target fielders close sooner?
|
||||
|
|
|
@ -136,11 +136,8 @@ function MainMenu:update()
|
|||
size = 6,
|
||||
}
|
||||
|
||||
local fielder1 = { x = 30, y = 200, spriteIndex = 1 }
|
||||
local ballIsHeld = drawFielder(AwayTeamSpriteGroup, fielder1, ball)
|
||||
|
||||
local fielder2 = { x = 350, y = 200, spriteIndex = 2 }
|
||||
ballIsHeld = drawFielder(HomeTeamSpriteGroup, fielder2, ball, playdate.graphics.kImageFlippedX) or ballIsHeld
|
||||
local ballIsHeld = drawFielder(AwayTeamSpriteGroup[1], ball, 30, 200)
|
||||
ballIsHeld = drawFielder(HomeTeamSpriteGroup[2], ball, 350, 200, playdate.graphics.kImageFlippedX) or ballIsHeld
|
||||
|
||||
if not ballIsHeld then
|
||||
gfx.setLineWidth(2)
|
||||
|
|
280
src/main.lua
280
src/main.lua
|
@ -15,7 +15,7 @@ import 'CoreLibs/utilities/where.lua'
|
|||
|
||||
---@class InputHandler
|
||||
---@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 pitch fun(self)
|
||||
---@field fielderAction fun(self, fielderHoldingBall: Fielder | nil, outedSomeRunner: boolean, ball: Ball)
|
||||
|
@ -40,7 +40,6 @@ 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'
|
||||
|
@ -49,14 +48,12 @@ import 'pitching.lua'
|
|||
import 'statistics.lua'
|
||||
import 'user-input.lua'
|
||||
|
||||
import 'draw/ball.lua'
|
||||
import 'draw/box-score.lua'
|
||||
import 'draw/fans.lua'
|
||||
import 'draw/characters.lua'
|
||||
import 'draw/fielder.lua'
|
||||
import 'draw/overlay.lua'
|
||||
import 'draw/panner.lua'
|
||||
import 'draw/character-sprites.lua'
|
||||
import 'draw/characters.lua'
|
||||
import 'draw/player.lua'
|
||||
import 'draw/throw-meter.lua'
|
||||
import 'draw/transitions.lua'
|
||||
-- stylua: ignore end
|
||||
|
@ -64,9 +61,7 @@ import 'draw/transitions.lua'
|
|||
-- TODO: Customizable field structure. E.g. stands and ads etc.
|
||||
|
||||
---@type pd_graphics_lib
|
||||
local gfx <const> = playdate.graphics
|
||||
|
||||
local C <const> = C
|
||||
local gfx <const>, C <const> = playdate.graphics, C
|
||||
|
||||
---@alias Team { benchPosition: XyPair }
|
||||
---@type table<TeamId, Team>
|
||||
|
@ -97,20 +92,24 @@ local teams <const> = {
|
|||
---@field offenseState OffenseState
|
||||
---@field inning number
|
||||
---@field stats Statistics
|
||||
|
||||
--- Ephemeral data ONLY used during rendering
|
||||
---@class RenderState
|
||||
---@field bat BatRenderState
|
||||
---@field batBase XyPair
|
||||
---@field batTip XyPair
|
||||
---@field batAngleDeg number
|
||||
-- These are only sort-of global state. They are purely graphical,
|
||||
-- but they need to be kept in sync with the rest of the globals.
|
||||
---@field runnerBlipper Blipper
|
||||
---@field battingTeamSprites SpriteCollection
|
||||
---@field fieldingTeamSprites SpriteCollection
|
||||
|
||||
---@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
|
||||
---@field private homeTeamBlipper Blipper
|
||||
---@field private awayTeamBlipper Blipper
|
||||
---@field private panner Panner
|
||||
---@field private state MutableState
|
||||
Game = {}
|
||||
|
@ -125,16 +124,25 @@ Game = {}
|
|||
function Game.new(settings, announcer, fielding, baserunning, npc, state)
|
||||
announcer = announcer or Announcer.new()
|
||||
fielding = fielding or Fielding.new()
|
||||
settings.userTeam = "away"
|
||||
settings.userTeam = "home" -- "away"
|
||||
|
||||
local homeTeamBlipper = blipper.new(100, settings.homeTeamSpriteGroup)
|
||||
local awayTeamBlipper = blipper.new(100, settings.awayTeamSpriteGroup)
|
||||
local battingTeam = "away"
|
||||
local runnerBlipper = battingTeam == "away" and awayTeamBlipper or homeTeamBlipper
|
||||
local ball = Ball.new(gfx.animator)
|
||||
|
||||
local o = setmetatable({
|
||||
settings = settings,
|
||||
announcer = announcer,
|
||||
fielding = fielding,
|
||||
homeTeamBlipper = homeTeamBlipper,
|
||||
awayTeamBlipper = awayTeamBlipper,
|
||||
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,
|
||||
|
@ -142,6 +150,9 @@ function Game.new(settings, announcer, fielding, baserunning, npc, state)
|
|||
inning = 1,
|
||||
pitchIsOver = true,
|
||||
didSwing = false,
|
||||
battingTeamSprites = settings.awayTeamSpriteGroup,
|
||||
fieldingTeamSprites = settings.homeTeamSpriteGroup,
|
||||
runnerBlipper = runnerBlipper,
|
||||
stats = Statistics.new(),
|
||||
},
|
||||
}, { __index = Game })
|
||||
|
@ -149,17 +160,15 @@ 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)
|
||||
o.npc = npc or Npc.new(o.baserunning.runners, o.fielding.fielders)
|
||||
|
||||
o.fielding:resetFielderPositions(teams.home.benchPosition, settings.userTeam == nil)
|
||||
playdate.timer.new(settings.userTeam == nil and 10 or 2000, function()
|
||||
o.fielding:resetFielderPositions(teams.home.benchPosition)
|
||||
playdate.timer.new(2000, function()
|
||||
o:returnToPitcher()
|
||||
end)
|
||||
o.characters = Characters.new(settings.homeTeamSpriteGroup, settings.awayTeamSpriteGroup)
|
||||
|
||||
BootTune:play()
|
||||
BootTune:setFinishCallback(function()
|
||||
|
@ -260,6 +269,8 @@ function Game:pitcherIsReady()
|
|||
end
|
||||
|
||||
function Game:checkForGameOver()
|
||||
Fielding.celebrate()
|
||||
|
||||
local state = self.state
|
||||
if state.stats:gameIsOver(state.inning, self.settings.finalInning, state.battingTeam) then
|
||||
self.announcer:say("THAT'S THE BALL GAME!")
|
||||
|
@ -274,8 +285,6 @@ end
|
|||
|
||||
function Game:nextHalfInning()
|
||||
pitchTracker:reset()
|
||||
Fielding.celebrate()
|
||||
|
||||
if self:checkForGameOver() then
|
||||
return
|
||||
end
|
||||
|
@ -288,8 +297,17 @@ function Game:nextHalfInning()
|
|||
self.state.inning = self.state.inning + 1
|
||||
self.state.stats:pushInning()
|
||||
end
|
||||
self.state.battingTeam = getOppositeTeamId(self.state.battingTeam)
|
||||
playdate.timer.new(2000, function()
|
||||
self.state.battingTeam = getOppositeTeamId(self.state.battingTeam)
|
||||
if self.state.battingTeam == "home" then
|
||||
self.state.battingTeamSprites = self.settings.homeTeamSpriteGroup
|
||||
self.state.runnerBlipper = self.homeTeamBlipper
|
||||
self.state.fieldingTeamSprites = self.settings.awayTeamSpriteGroup
|
||||
else
|
||||
self.state.battingTeamSprites = self.settings.awayTeamSpriteGroup
|
||||
self.state.fieldingTeamSprites = self.settings.homeTeamSpriteGroup
|
||||
self.state.runnerBlipper = self.awayTeamBlipper
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
|
@ -363,88 +381,86 @@ function Game:strikeOut()
|
|||
self:nextBatter()
|
||||
end
|
||||
|
||||
function Game:saveToFile()
|
||||
playdate.datastore.write({ currentGame = self.state }, "data", true)
|
||||
end
|
||||
|
||||
function Game.load()
|
||||
local loaded = playdate.datastore.read("data")
|
||||
---@type Game
|
||||
local loadedGame = loaded.currentGame
|
||||
loadedGame.state.ball = Ball.new(gfx.animator)
|
||||
local settings = {
|
||||
homeTeamSpriteGroup = HomeTeamSpriteGroup,
|
||||
awayTeamSpriteGroup = AwayTeamSpriteGroup,
|
||||
finalInning = loadedGame.settings.finalInning,
|
||||
}
|
||||
local ret = Game.new(settings, nil, loadedGame.fielding, nil, nil, loadedGame.state)
|
||||
ret.baserunning.outs = loadedGame.outs
|
||||
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: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)
|
||||
local batDeg, batSpeed = offenseHandler:updateBat(ball, self.state.pitchIsOver, self.state.deltaSeconds)
|
||||
self.state.batAngleDeg = batDeg
|
||||
|
||||
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
|
||||
end
|
||||
|
||||
-- Hit!
|
||||
-- TODO: animate bat-flip or something
|
||||
local isFlyBall = math.random() > 0.5
|
||||
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 = 8000
|
||||
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) then
|
||||
if utils.isFoulBall(ballDest.x, ballDest.y) then
|
||||
self.announcer:say("Foul ball!")
|
||||
pitchTracker.strikes = math.min(pitchTracker.strikes + 1, 2)
|
||||
-- TODO: Have a fielder chase for the fly-out
|
||||
return
|
||||
end
|
||||
|
||||
local isPastOutfieldWall, nearbyPointAbove = utils.pointIsAboveLine(ballDest, C.OutfieldWall)
|
||||
|
||||
if isPastOutfieldWall then
|
||||
if not isFlyBall then
|
||||
-- Grounder at the wall!
|
||||
ballDest.y = nearbyPointAbove.y - 8
|
||||
else
|
||||
-- Home run!
|
||||
playdate.timer.new(flyTimeMs, function()
|
||||
-- Verify that the home run wasn't intercepted
|
||||
if utils.distanceBetweenPoints(ball, ballDest) < 2 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.
|
||||
playdate.timer.new(1000, function()
|
||||
self.panner:panTo(utils.xy(0, 0), function()
|
||||
return self:pitcherIsReady()
|
||||
end)
|
||||
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
|
||||
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.
|
||||
playdate.timer.new(1000, function()
|
||||
self.panner:panTo(utils.xy(0, 0), function()
|
||||
return self:pitcherIsReady()
|
||||
end)
|
||||
end
|
||||
end)
|
||||
end
|
||||
end)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
local ballHeightAnimator = isFlyBall
|
||||
and gfx.animator.new(flyTimeMs, C.GloveZ, 10 + (2 * mult * mult * 0.5), utils.hitEasingHill)
|
||||
or gfx.animator.new(flyTimeMs, 2 * (mult * mult), 0, utils.createBouncer(4))
|
||||
|
||||
ball:launch(ballDest.x, ballDest.y, playdate.easingFunctions.outQuint, flyTimeMs, nil, ballHeightAnimator, true)
|
||||
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.baserunning:convertBatterToRunner()
|
||||
self.fielding:haveSomeoneChase(ball, ballDest)
|
||||
self.fielding:haveSomeoneChase(ballDest.x, ballDest.y)
|
||||
end
|
||||
|
||||
---@param appliedSpeed number | fun(runner: Runner): number
|
||||
|
@ -487,7 +503,6 @@ function Game:updatePitching(defenseHandler)
|
|||
end
|
||||
|
||||
if pitchTracker.secondsSinceLastPitch > C.ReturnToPitcherAfterSeconds and not self.state.pitchIsOver then
|
||||
self:saveToFile()
|
||||
local outcome = pitchTracker:updatePitchCounts(self.state.didSwing, self:fieldingTeamCurrentInning())
|
||||
if outcome == PitchOutcomes.StrikeOut then
|
||||
self:strikeOut()
|
||||
|
@ -509,20 +524,12 @@ function Game:updateGameState()
|
|||
gfx.animation.blinker.updateAll()
|
||||
self.state.deltaSeconds = playdate.getElapsedTime() or 0
|
||||
playdate.resetElapsedTime()
|
||||
self.state.ball:updatePosition()
|
||||
|
||||
local fielderHoldingBall, caughtAFlyBall =
|
||||
self.fielding:updateFielderPositions(self.state.ball, self.state.deltaSeconds)
|
||||
if caughtAFlyBall then
|
||||
local fliedOut = self.baserunning:getNewestRunner()
|
||||
self.baserunning:outRunner(fliedOut, "Fly out!")
|
||||
self.state.offenseState = C.Offense.fliedOut
|
||||
self.baserunning:pushNewBatter()
|
||||
pitchTracker.secondsSinceLastPitch = -1
|
||||
end
|
||||
self.state.ball:updatePosition(self.state.deltaSeconds)
|
||||
|
||||
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)
|
||||
|
@ -576,30 +583,111 @@ function Game:update()
|
|||
gfx.clear()
|
||||
gfx.setColor(gfx.kColorBlack)
|
||||
|
||||
local state = self.state
|
||||
local offsetX, offsetY = self.panner:get(state.deltaSeconds)
|
||||
local offsetX, offsetY = self.panner:get(self.state.deltaSeconds)
|
||||
gfx.setDrawOffset(offsetX, offsetY)
|
||||
|
||||
fans.draw()
|
||||
GrassBackground:draw(-400, -720)
|
||||
|
||||
local ballHeldBy =
|
||||
self.characters:drawAll(self.fielding, self.baserunning, self.batting.state, state.battingTeam, state.ball)
|
||||
---@type { y: number, drawAction: fun() }[]
|
||||
local characterDraws = {}
|
||||
function addDraw(y, drawAction)
|
||||
characterDraws[#characterDraws + 1] = { y = y, drawAction = drawAction }
|
||||
end
|
||||
|
||||
local ball = self.state.ball
|
||||
|
||||
local danceOffset = FielderDanceAnimator:currentValue()
|
||||
---@type Fielder | nil
|
||||
local ballHeldBy
|
||||
for _, fielder in pairs(self.fielding.fielders) do
|
||||
addDraw(fielder.y + danceOffset, function()
|
||||
local ballHeldByThisFielder = drawFielder(
|
||||
self.state.fieldingTeamSprites[fielder.spriteIndex],
|
||||
ball,
|
||||
fielder.x,
|
||||
fielder.y + danceOffset
|
||||
)
|
||||
if ballHeldByThisFielder then
|
||||
ballHeldBy = fielder
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
local playerHeightOffset = 20
|
||||
for _, runner in pairs(self.baserunning.runners) do
|
||||
addDraw(runner.y, function()
|
||||
if runner == self.baserunning.batter then
|
||||
if self.state.batAngleDeg > 50 and self.state.batAngleDeg < 200 then
|
||||
self.state.battingTeamSprites[runner.spriteIndex].back:draw(runner.x, runner.y - playerHeightOffset)
|
||||
else
|
||||
self.state.battingTeamSprites[runner.spriteIndex].smiling:draw(
|
||||
runner.x,
|
||||
runner.y - playerHeightOffset
|
||||
)
|
||||
end
|
||||
else
|
||||
-- TODO? Change blip speed depending on runner speed?
|
||||
self.state.runnerBlipper:draw(false, runner.x, runner.y - playerHeightOffset, runner)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
table.sort(characterDraws, function(a, b)
|
||||
return a.y < b.y
|
||||
end)
|
||||
for _, character in pairs(characterDraws) do
|
||||
character.drawAction()
|
||||
end
|
||||
|
||||
if self.state.offenseState == C.Offense.batting then
|
||||
gfx.setLineWidth(7)
|
||||
gfx.drawLine(self.state.batBase.x, self.state.batBase.y, self.state.batTip.x, self.state.batTip.y)
|
||||
|
||||
gfx.setColor(gfx.kColorWhite)
|
||||
gfx.setLineCapStyle(gfx.kLineCapStyleRound)
|
||||
gfx.setLineWidth(3)
|
||||
gfx.drawLine(self.state.batBase.x, self.state.batBase.y, self.state.batTip.x, self.state.batTip.y)
|
||||
|
||||
gfx.setColor(gfx.kColorBlack)
|
||||
end
|
||||
|
||||
for _, runner in pairs(self.baserunning.outRunners) do
|
||||
self.state.battingTeamSprites[runner.spriteIndex].frowning:draw(runner.x, runner.y - playerHeightOffset)
|
||||
end
|
||||
for _, runner in pairs(self.baserunning.scoredRunners) do
|
||||
self.state.runnerBlipper:draw(false, runner.x, runner.y - playerHeightOffset, runner)
|
||||
end
|
||||
|
||||
if self:userIsOn("defense") then
|
||||
throwMeter:drawNearFielder(ballHeldBy)
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
gfx.setDrawOffset(0, 0)
|
||||
if math.abs(offsetX) > 10 or math.abs(offsetY) > 20 then
|
||||
if math.abs(offsetX) > 10 or math.abs(offsetY) > 10 then
|
||||
drawMinimap(self.baserunning.runners, self.fielding.fielders)
|
||||
end
|
||||
|
||||
drawScoreboard(0, C.Screen.H * 0.77, state.stats, self.baserunning.outs, state.battingTeam, state.inning)
|
||||
local homeScore, awayScore = utils.totalScores(self.state.stats)
|
||||
drawScoreboard(
|
||||
0,
|
||||
C.Screen.H * 0.77,
|
||||
homeScore,
|
||||
awayScore,
|
||||
self.baserunning.outs,
|
||||
self.state.battingTeam,
|
||||
self.state.inning
|
||||
)
|
||||
drawBallsAndStrikes(290, C.Screen.H - 20, pitchTracker.balls, pitchTracker.strikes)
|
||||
self.announcer:draw(C.Center.x, 10)
|
||||
|
||||
|
|
50
src/npc.lua
50
src/npc.lua
|
@ -1,6 +1,6 @@
|
|||
local npcBatDeg = 0
|
||||
local BaseNpcBatSpeed <const> = 1000
|
||||
local npcBatSpeed = BaseNpcBatSpeed
|
||||
local BaseNpcBatSpeed <const> = 1500
|
||||
local npcBatSpeed = 1500
|
||||
|
||||
---@class Npc: InputHandler
|
||||
---@field runners Runner[]
|
||||
|
@ -26,25 +26,19 @@ function Npc.update() end
|
|||
---@param pitchIsOver boolean
|
||||
---@param deltaSec number
|
||||
---@return number batAngleDeg, number batSpeed
|
||||
function Npc:updateBatAngle(ball, pitchIsOver, deltaSec)
|
||||
if
|
||||
not pitchIsOver
|
||||
and ball.y > 200
|
||||
and ball.y < 230
|
||||
and (ball.x < C.Center.x + 15)
|
||||
and (ball.x > C.Center.x - 12)
|
||||
then
|
||||
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
|
||||
npcBatDeg = npcBatDeg + (deltaSec * npcBatSpeed)
|
||||
else
|
||||
npcBatSpeed = (1 + math.random()) * BaseNpcBatSpeed
|
||||
npcBatDeg = utils.moveAtSpeed1d(npcBatDeg, deltaSec * BaseNpcBatSpeed, 230 - 360)
|
||||
npcBatDeg = 230
|
||||
end
|
||||
return npcBatDeg, (self:batSpeed() * deltaSec)
|
||||
end
|
||||
|
||||
---@return number
|
||||
function Npc:batSpeed()
|
||||
return npcBatSpeed * 1.25
|
||||
return npcBatSpeed / 1.5
|
||||
end
|
||||
|
||||
---@return number flyTimeMs, number pitchId, number accuracy
|
||||
|
@ -137,16 +131,14 @@ end
|
|||
---@param ball { x: number, y: number, heldBy: Fielder | nil, launch: LaunchBall }
|
||||
local function tryToMakeAPlay(fielders, fielder, runners, ball)
|
||||
local targetX, targetY = getNextOutTarget(runners)
|
||||
if targetX == nil or targetY == nil then
|
||||
return
|
||||
end
|
||||
|
||||
local nearestFielder = utils.getNearestOf(fielders, targetX, targetY)
|
||||
nearestFielder.targets = { utils.xy(targetX, targetY) }
|
||||
if nearestFielder == fielder then
|
||||
ball.heldBy = fielder
|
||||
else
|
||||
ball:launch(targetX, targetY, playdate.easingFunctions.linear, nil, true)
|
||||
if targetX ~= nil and targetY ~= nil then
|
||||
local nearestFielder = utils.getNearestOf(fielders, targetX, targetY)
|
||||
nearestFielder.targets = { utils.xy(targetX, targetY) }
|
||||
if nearestFielder == fielder then
|
||||
ball.heldBy = fielder
|
||||
else
|
||||
ball:launch(targetX, targetY, playdate.easingFunctions.linear, nil, true)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -157,14 +149,14 @@ function Npc:fielderAction(fielder, outedSomeRunner, ball)
|
|||
if not fielder then
|
||||
return
|
||||
end
|
||||
local playDelay = outedSomeRunner and 0.5 or 0.1
|
||||
actionQueue:newOnly("npcFielderAction", 2000, function()
|
||||
local dt = 0
|
||||
while dt < playDelay do
|
||||
dt = dt + coroutine.yield()
|
||||
end
|
||||
if outedSomeRunner then
|
||||
-- Delay a little before the next play
|
||||
playdate.timer.new(750, function()
|
||||
tryToMakeAPlay(self.fielders, fielder, self.runners, ball)
|
||||
end)
|
||||
else
|
||||
tryToMakeAPlay(self.fielders, fielder, self.runners, ball)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
---@return number
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
---@alias SimpleAnimator { currentValue: fun(self): number; reset: fun(self, durationMs: number | nil) }
|
||||
---@alias Pitch fun(accuracy: number, ball: Ball): { x: SimpleAnimator, y: SimpleAnimator, z: SimpleAnimator | nil }
|
||||
---@alias Pitch fun(ball: Ball): { x: SimpleAnimator, y: SimpleAnimator, z: SimpleAnimator | nil }
|
||||
|
||||
---@type pd_graphics_lib
|
||||
local gfx <const> = playdate.graphics
|
||||
|
|
|
@ -13,13 +13,6 @@ import = function(target)
|
|||
require(target:sub(1, #target - 4))
|
||||
end
|
||||
|
||||
Glove = {
|
||||
getSize = function()
|
||||
return 10, 10
|
||||
end,
|
||||
}
|
||||
Characters = require("draw/characters")
|
||||
|
||||
local Game = require("main")
|
||||
|
||||
local settings = {
|
||||
|
|
|
@ -11,16 +11,14 @@ end
|
|||
|
||||
function UserInput:update()
|
||||
self.crankChange = playdate.getCrankChange()
|
||||
self.crankLimited = self.crankChange == 0 and 0 or (math.log(math.abs(self.crankChange)) * C.CrankPower)
|
||||
if self.crankChange < 0 then
|
||||
self.crankLimited = self.crankLimited * -1
|
||||
end
|
||||
local crankLimited = self.crankChange == 0 and 0 or (math.log(math.abs(self.crankChange)) * C.CrankPower)
|
||||
self.crankLimited = math.abs(crankLimited)
|
||||
end
|
||||
|
||||
---@return number batAngleDeg, number batSpeed
|
||||
function UserInput:updateBatAngle()
|
||||
function UserInput:updateBat()
|
||||
local batAngleDeg = (playdate.getCrankPosition() + C.CrankOffsetDeg) % 360
|
||||
local batSpeed = math.abs(self.crankLimited)
|
||||
local batSpeed = self.crankLimited
|
||||
return batAngleDeg, batSpeed
|
||||
end
|
||||
|
||||
|
|
121
src/utils.lua
121
src/utils.lua
|
@ -1,14 +1,16 @@
|
|||
-- luacheck no new globals
|
||||
utils = {}
|
||||
|
||||
---@class XyPair
|
||||
---@field x: number,
|
||||
---@field y: number,
|
||||
--- @alias XyPair {
|
||||
--- x: number,
|
||||
--- y: number,
|
||||
--- }
|
||||
|
||||
---@class Point3d
|
||||
---@field x number,
|
||||
---@field y number,
|
||||
---@field z number,
|
||||
--- @alias Point3d {
|
||||
--- x: number,
|
||||
--- y: number,
|
||||
--- z: number,
|
||||
--- }
|
||||
|
||||
local sqrt <const> = math.sqrt
|
||||
|
||||
|
@ -20,25 +22,6 @@ function utils.easingHill(t, b, c, d)
|
|||
return (c * t) + b
|
||||
end
|
||||
|
||||
function utils.hitEasingHill(t, b, c, d)
|
||||
c = c + 0.0 -- convert to float to prevent integer overflow
|
||||
t = 1 - (t / d)
|
||||
local extraDrop = -C.GloveZ * t
|
||||
t = ((t * 2) - 1)
|
||||
t = t * t
|
||||
return (c * t) + b + extraDrop
|
||||
end
|
||||
|
||||
function utils.createBouncer(bounceCount)
|
||||
return function(t, b, c, d)
|
||||
c = c + 0.0 -- convert to float to prevent integer overflow
|
||||
local percComplete = t / d
|
||||
local x = percComplete * math.pi * bounceCount
|
||||
local weird = -math.abs((2 / x) * math.sin(x)) / math.pi * 2
|
||||
return b + c + (c * weird)
|
||||
end
|
||||
end
|
||||
|
||||
--- @alias StaticAnimator {
|
||||
--- currentValue: fun(self): number;
|
||||
--- reset: fun(self, durationMs: number | nil);
|
||||
|
@ -82,29 +65,6 @@ function utils.normalizeVector(x1, y1, x2, y2)
|
|||
return x / distance, y / distance, distance
|
||||
end
|
||||
|
||||
function utils.normalizeVectorZ(x1, y1, z1, x2, y2, z2)
|
||||
local distance, x, y, z = utils.distanceBetweenZ(x1, y1, z1, x2, y2, z2)
|
||||
return x / distance, y / distance, z / distance, distance
|
||||
end
|
||||
|
||||
---@param current number
|
||||
---@param speed number Must not be negative!
|
||||
---@param target number
|
||||
---@return number newValue, boolean didMove
|
||||
function utils.moveAtSpeed1d(current, speed, target)
|
||||
local distance = math.abs(current - target)
|
||||
if distance == 0 then
|
||||
return target, false
|
||||
end
|
||||
if distance < speed then
|
||||
return target, true
|
||||
end
|
||||
if target > current then
|
||||
return current + speed, true
|
||||
end
|
||||
return current - speed, true
|
||||
end
|
||||
|
||||
--- Push the given obect at the given speed toward a target. Speed should be pre-multiplied by the frame's delta time.
|
||||
--- Stops when within 1. Returns true only if the object did actually move.
|
||||
---@param mover { x: number, y: number }
|
||||
|
@ -130,29 +90,12 @@ function utils.moveAtSpeed(mover, speed, target, tau)
|
|||
return true
|
||||
end
|
||||
|
||||
---@param mover Point3d
|
||||
---@param speed number
|
||||
---@param target Point3d
|
||||
---@param tau number | nil
|
||||
---@return boolean isStillMoving
|
||||
function utils.moveAtSpeedZ(mover, speed, target, tau)
|
||||
local x, y, distance = utils.normalizeVectorZ(mover.x, mover.y, mover.z, target.x, target.y, target.z)
|
||||
|
||||
if distance == 0 then
|
||||
return false
|
||||
end
|
||||
|
||||
if distance > (tau or 1) then
|
||||
mover.x = mover.x - (x * speed)
|
||||
mover.y = mover.y - (y * speed)
|
||||
mover.z = mover.z - (z * speed)
|
||||
else
|
||||
mover.x = target.x
|
||||
mover.y = target.y
|
||||
mover.z = target.z
|
||||
end
|
||||
|
||||
return true
|
||||
---@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
|
||||
|
@ -240,41 +183,30 @@ end
|
|||
---@param line2 XyPair
|
||||
---@param bottomBound number
|
||||
---@return boolean
|
||||
function utils.pointOnOrUnderLine(point, line1, line2, bottomBound)
|
||||
function utils.pointDirectlyUnderLine(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 - 2, line2.x, line2.y - 2)
|
||||
return utils.pointUnderLine(point.x, point.y, line1.x, line1.y, line2.x, line2.y)
|
||||
end
|
||||
|
||||
--- 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.
|
||||
--- If left of first linePoint and above it, returns true. Similarly if right of the last linePoint.
|
||||
--- This, if used for home run calculations, would not take into account balls that curve around the foul poles.
|
||||
---@param point XyPair
|
||||
---@param linePoints XyPair[]
|
||||
---@return boolean, XyPair | nil nearbyPointAbove
|
||||
function utils.pointIsAboveLine(point, linePoints, by)
|
||||
by = by or 0
|
||||
local pointY = point.y + by
|
||||
if point.x < linePoints[1].x and pointY < linePoints[1].y then
|
||||
return true, linePoints[1]
|
||||
end
|
||||
---@return boolean
|
||||
function utils.pointIsSquarelyAboveLine(point, linePoints)
|
||||
for i = 2, #linePoints do
|
||||
local prev = linePoints[i - 1]
|
||||
local next = linePoints[i]
|
||||
if point.x >= prev.x and point.x <= next.x then
|
||||
if not utils.pointUnderLine(point.x, pointY, prev.x, prev.y, next.x, next.y) then
|
||||
return true, prev
|
||||
end
|
||||
return not utils.pointUnderLine(point.x, point.y, prev.x, prev.y, next.x, next.y)
|
||||
end
|
||||
end
|
||||
if point.x > linePoints[#linePoints].x and pointY < linePoints[#linePoints].y then
|
||||
return true, linePoints[#linePoints]
|
||||
end
|
||||
return false, nil
|
||||
return false
|
||||
end
|
||||
|
||||
--- Returns true only if the point is below the given line.
|
||||
|
@ -299,13 +231,14 @@ function utils.pointUnderLine(pointX, pointY, lineX1, lineY1, lineX2, lineY2)
|
|||
end
|
||||
|
||||
--- Returns true if a ball landing at destX,destY will be foul.
|
||||
---@param dest XyPair
|
||||
function utils.isFoulBall(dest)
|
||||
---@param destX number
|
||||
---@param destY number
|
||||
function utils.isFoulBall(destX, destY)
|
||||
local leftLine = C.LeftFoulLine
|
||||
local rightLine = C.RightFoulLine
|
||||
|
||||
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)
|
||||
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)
|
||||
end
|
||||
|
||||
--- Returns the nearest position object from the given point, as well as its distance from that point
|
||||
|
|
Loading…
Reference in New Issue