Compare commits

...

14 Commits

Author SHA1 Message Date
Sage Vaillancourt 4b9a94c2c2 Implement grounders and fly-outs.
Add a slight delay on npc fielder actions.
Speed up intro when userTeam == nil
2025-03-01 12:59:12 -05:00
Sage Vaillancourt decd1f7080 Fix inverted dance animation
Switch XyPair and Point3d to @class, over @alias
Seems to work better with autocomplete
2025-03-01 09:54:05 -05:00
Sage Vaillancourt 04d25127fc Extract batting.lua 2025-02-27 19:31:00 -05:00
Sage Vaillancourt 8b66e2e826 Drop within(). Just use distance between 2025-02-26 14:51:54 -05:00
Sage Vaillancourt 876f828117 Extract draw/ball.lua
Tweak pointIsSquarelyAboveLine() -> pointIsAboveLine()
2025-02-26 14:20:36 -05:00
Sage Vaillancourt ce9a2d335e Fix to re-enable user backwards baserunning 2025-02-26 13:14:19 -05:00
Sage Vaillancourt 80015dbe62 Extract draw/characters.lua
Pulls a bunch of draw logic out of main.lua; handles z-ordering.
Expand on save/load - though it's certainly not complete yet.
2025-02-26 13:04:38 -05:00
Sage Vaillancourt 176a7e6d5e Add saveToFile()
Not *likely* to work yet, but start scoping out good times to make a save.
Also, correct the Pitch type.
2025-02-25 17:14:47 -05:00
Sage Vaillancourt 55a3a7b0ee More type hints.
Also, move pointDirectlyUnderLine() to XyPair-based.
2025-02-24 23:55:03 -05:00
Sage Vaillancourt ddfdc8947a Bail early on pitch() if one was not actually requested 2025-02-24 22:37:15 -05:00
Sage Vaillancourt e035c0ca72 Missed a spot on the font-building 2025-02-24 20:18:44 -05:00
Sage Vaillancourt 668fa9ffd4 Add fonts to assets.lua2p
Include until-now missing Roobert 11 font
Rename font-full-circle to fix casing
2025-02-24 20:16:18 -05:00
Sage Vaillancourt b4ac028cd9 Consolidate assets into src/assets/ 2025-02-24 19:58:47 -05:00
sage 30aa5bd6c6 Merge pull request 'extract-input-controllers' (#4) from extract-input-controllers into main
Reviewed-on: #4
2025-02-24 19:50:23 -05:00
80 changed files with 916 additions and 354 deletions

View File

@ -26,6 +26,10 @@ function actionQueue:upsert(id, maxTimeMs, action)
} }
end end
--- The new action will not be added if an entry with the current id already exists in the queue.
---@param id any
---@param maxTimeMs number
---@param action Action
function actionQueue:newOnly(id, maxTimeMs, action) function actionQueue:newOnly(id, maxTimeMs, action)
if self.queue[id] then if self.queue[id] then
return return
@ -38,6 +42,7 @@ end
--- Must be called on every playdate.update() to check for (and run) any waiting tasks. --- Must be called on every playdate.update() to check for (and run) any waiting tasks.
--- Actions that return NeedsMoreTime will not be removed from the queue unless they have expired. --- Actions that return NeedsMoreTime will not be removed from the queue unless they have expired.
---@param deltaSeconds number
function actionQueue:runWaiting(deltaSeconds) function actionQueue:runWaiting(deltaSeconds)
local currentTimeMs = playdate.getCurrentTimeMilliseconds() local currentTimeMs = playdate.getCurrentTimeMilliseconds()

View File

@ -1,6 +1,6 @@
local gfx = playdate.graphics local gfx = playdate.graphics
local AnnouncementFont <const> = playdate.graphics.font.new("fonts/Roobert-20-Medium.pft") local AnnouncementFont <const> = Roobert20Medium
local AnnouncementTransitionMs <const> = 300 local AnnouncementTransitionMs <const> = 300
local AnnouncerMarginX <const> = 26 local AnnouncerMarginX <const> = 26
@ -52,6 +52,8 @@ function Announcer:say(text)
end end
end end
---@param x number
---@param y number
function Announcer:draw(x, y) function Announcer:draw(x, y)
if #self.textQueue == 0 then if #self.textQueue == 0 then
return return

View File

@ -3,169 +3,190 @@
-- luacheck: ignore -- luacheck: ignore
---@type pd_image ---@type pd_image
BallBackground = playdate.graphics.image.new("images/game/BallBackground.png") BallBackground = playdate.graphics.image.new("assets/images/game/BallBackground.png")
-- luacheck: ignore -- luacheck: ignore
---@type pd_image ---@type pd_image
BigBat = playdate.graphics.image.new("images/game/BigBat.png") BigBat = playdate.graphics.image.new("assets/images/game/BigBat.png")
-- luacheck: ignore -- luacheck: ignore
---@type pd_image ---@type pd_image
DarkPlayerAwayBack = playdate.graphics.image.new("images/game/DarkPlayerAwayBack.png") DarkPlayerAwayBack = playdate.graphics.image.new("assets/images/game/DarkPlayerAwayBack.png")
-- luacheck: ignore -- luacheck: ignore
---@type pd_image ---@type pd_image
DarkPlayerAwayBase = playdate.graphics.image.new("images/game/DarkPlayerAwayBase.png") DarkPlayerAwayBase = playdate.graphics.image.new("assets/images/game/DarkPlayerAwayBase.png")
-- luacheck: ignore -- luacheck: ignore
---@type pd_image ---@type pd_image
DarkPlayerFrown = playdate.graphics.image.new("images/game/DarkPlayerFrown.png") DarkPlayerFrown = playdate.graphics.image.new("assets/images/game/DarkPlayerFrown.png")
-- luacheck: ignore -- luacheck: ignore
---@type pd_image ---@type pd_image
DarkPlayerHomeBack = playdate.graphics.image.new("images/game/DarkPlayerHomeBack.png") DarkPlayerHomeBack = playdate.graphics.image.new("assets/images/game/DarkPlayerHomeBack.png")
-- luacheck: ignore -- luacheck: ignore
---@type pd_image ---@type pd_image
DarkPlayerHomeBase = playdate.graphics.image.new("images/game/DarkPlayerHomeBase.png") DarkPlayerHomeBase = playdate.graphics.image.new("assets/images/game/DarkPlayerHomeBase.png")
-- luacheck: ignore -- luacheck: ignore
---@type pd_image ---@type pd_image
DarkPlayerSmile = playdate.graphics.image.new("images/game/DarkPlayerSmile.png") DarkPlayerSmile = playdate.graphics.image.new("assets/images/game/DarkPlayerSmile.png")
-- luacheck: ignore -- luacheck: ignore
---@type pd_image ---@type pd_image
DarkSkinFan = playdate.graphics.image.new("images/game/DarkSkinFan.png") DarkSkinFan = playdate.graphics.image.new("assets/images/game/DarkSkinFan.png")
-- luacheck: ignore -- luacheck: ignore
---@type pd_image ---@type pd_image
GameLogo = playdate.graphics.image.new("images/game/GameLogo.png") GameLogo = playdate.graphics.image.new("assets/images/game/GameLogo.png")
-- luacheck: ignore -- luacheck: ignore
---@type pd_image ---@type pd_image
GloveHoldingBall = playdate.graphics.image.new("images/game/GloveHoldingBall.png") GloveHoldingBall = playdate.graphics.image.new("assets/images/game/GloveHoldingBall.png")
-- luacheck: ignore -- luacheck: ignore
---@type pd_image ---@type pd_image
Glove = playdate.graphics.image.new("images/game/Glove.png") Glove = playdate.graphics.image.new("assets/images/game/Glove.png")
-- luacheck: ignore -- luacheck: ignore
---@type pd_image ---@type pd_image
GrassBackground = playdate.graphics.image.new("images/game/GrassBackground.png") GrassBackground = playdate.graphics.image.new("assets/images/game/GrassBackground.png")
-- luacheck: ignore -- luacheck: ignore
---@type pd_image ---@type pd_image
GrassBackgroundSmall = playdate.graphics.image.new("images/game/GrassBackgroundSmall.png") GrassBackgroundSmall = playdate.graphics.image.new("assets/images/game/GrassBackgroundSmall.png")
-- luacheck: ignore -- luacheck: ignore
---@type pd_image ---@type pd_image
Hat = playdate.graphics.image.new("images/game/Hat.png") Hat = playdate.graphics.image.new("assets/images/game/Hat.png")
-- luacheck: ignore -- luacheck: ignore
---@type pd_image ---@type pd_image
LightPlayerAwayBack = playdate.graphics.image.new("images/game/LightPlayerAwayBack.png") LightPlayerAwayBack = playdate.graphics.image.new("assets/images/game/LightPlayerAwayBack.png")
-- luacheck: ignore -- luacheck: ignore
---@type pd_image ---@type pd_image
LightPlayerAwayBase = playdate.graphics.image.new("images/game/LightPlayerAwayBase.png") LightPlayerAwayBase = playdate.graphics.image.new("assets/images/game/LightPlayerAwayBase.png")
-- luacheck: ignore -- luacheck: ignore
---@type pd_image ---@type pd_image
LightPlayerFrown = playdate.graphics.image.new("images/game/LightPlayerFrown.png") LightPlayerFrown = playdate.graphics.image.new("assets/images/game/LightPlayerFrown.png")
-- luacheck: ignore -- luacheck: ignore
---@type pd_image ---@type pd_image
LightPlayerHomeBack = playdate.graphics.image.new("images/game/LightPlayerHomeBack.png") LightPlayerHomeBack = playdate.graphics.image.new("assets/images/game/LightPlayerHomeBack.png")
-- luacheck: ignore -- luacheck: ignore
---@type pd_image ---@type pd_image
LightPlayerHomeBase = playdate.graphics.image.new("images/game/LightPlayerHomeBase.png") LightPlayerHomeBase = playdate.graphics.image.new("assets/images/game/LightPlayerHomeBase.png")
-- luacheck: ignore -- luacheck: ignore
---@type pd_image ---@type pd_image
LightPlayerSmile = playdate.graphics.image.new("images/game/LightPlayerSmile.png") LightPlayerSmile = playdate.graphics.image.new("assets/images/game/LightPlayerSmile.png")
-- luacheck: ignore -- luacheck: ignore
---@type pd_image ---@type pd_image
LightSkinFan = playdate.graphics.image.new("images/game/LightSkinFan.png") LightSkinFan = playdate.graphics.image.new("assets/images/game/LightSkinFan.png")
-- luacheck: ignore -- luacheck: ignore
---@type pd_image ---@type pd_image
MenuImage = playdate.graphics.image.new("images/game/MenuImage.png") MenuImage = playdate.graphics.image.new("assets/images/game/MenuImage.png")
-- luacheck: ignore -- luacheck: ignore
---@type pd_image ---@type pd_image
Minimap = playdate.graphics.image.new("images/game/Minimap.png") Minimap = playdate.graphics.image.new("assets/images/game/Minimap.png")
-- luacheck: ignore -- luacheck: ignore
---@type pd_image ---@type pd_image
PerfectPowerBg = playdate.graphics.image.new("images/game/PerfectPowerBg.png") PerfectPowerBg = playdate.graphics.image.new("assets/images/game/PerfectPowerBg.png")
-- luacheck: ignore -- luacheck: ignore
---@type pd_image ---@type pd_image
PerfectPowerFlickerLeft = playdate.graphics.image.new("images/game/PerfectPowerFlickerLeft.png") PerfectPowerFlickerLeft = playdate.graphics.image.new("assets/images/game/PerfectPowerFlickerLeft.png")
-- luacheck: ignore -- luacheck: ignore
---@type pd_image ---@type pd_image
PerfectPowerFlickerRight = playdate.graphics.image.new("images/game/PerfectPowerFlickerRight.png") PerfectPowerFlickerRight = playdate.graphics.image.new("assets/images/game/PerfectPowerFlickerRight.png")
-- luacheck: ignore -- luacheck: ignore
---@type pd_sampleplayer ---@type pd_sampleplayer
BatCrackReverb = playdate.sound.sampleplayer.new("sounds/BatCrackReverb.wav") BatCrackReverb = playdate.sound.sampleplayer.new("assets/sounds/BatCrackReverb.wav")
-- luacheck: ignore -- luacheck: ignore
---@type pd_sampleplayer ---@type pd_sampleplayer
BootTuneOrgany = playdate.sound.sampleplayer.new("music/BootTuneOrgany.wav") BootTuneOrgany = playdate.sound.sampleplayer.new("assets/music/BootTuneOrgany.wav")
-- luacheck: ignore -- luacheck: ignore
---@type pd_sampleplayer ---@type pd_sampleplayer
BootTune = playdate.sound.sampleplayer.new("music/BootTune.wav") BootTune = playdate.sound.sampleplayer.new("assets/music/BootTune.wav")
-- luacheck: ignore -- luacheck: ignore
---@type pd_sampleplayer ---@type pd_sampleplayer
MenuMusic = playdate.sound.sampleplayer.new("music/MenuMusic.wav") MenuMusic = playdate.sound.sampleplayer.new("assets/music/MenuMusic.wav")
-- luacheck: ignore -- luacheck: ignore
---@type pd_sampleplayer ---@type pd_sampleplayer
TinnyBackground = playdate.sound.sampleplayer.new("music/TinnyBackground.wav") TinnyBackground = playdate.sound.sampleplayer.new("assets/music/TinnyBackground.wav")
-- luacheck: ignore
---@type pd_font
AshevilleSans14Bold = playdate.graphics.font.new("assets/fonts/Asheville-Sans-14-Bold.pft")
-- luacheck: ignore
---@type pd_font
FontFullCircle = playdate.graphics.font.new("assets/fonts/Font-Full-Circle.pft")
-- luacheck: ignore
---@type pd_font
NanoSans = playdate.graphics.font.new("assets/fonts/Nano Sans.pft")
-- luacheck: ignore
---@type pd_font
Roobert11Medium = playdate.graphics.font.new("assets/fonts/Roobert-11-Medium.pft")
-- luacheck: ignore
---@type pd_font
Roobert20Medium = playdate.graphics.font.new("assets/fonts/Roobert-20-Medium.pft")
Logos = { Logos = {
{ name = "Base", image = playdate.graphics.image.new("images/game/logos/Base.png") }, { name = "Base", image = playdate.graphics.image.new("assets/images/game/logos/Base.png") },
-- luacheck: ignore -- luacheck: ignore
---@type pd_image ---@type pd_image
{ name = "Arrows", image = playdate.graphics.image.new("images/game/logos/Arrows.png") }, { name = "Arrows", image = playdate.graphics.image.new("assets/images/game/logos/Arrows.png") },
-- luacheck: ignore -- luacheck: ignore
---@type pd_image ---@type pd_image
{ name = "Cats", image = playdate.graphics.image.new("images/game/logos/Cats.png") }, { name = "Cats", image = playdate.graphics.image.new("assets/images/game/logos/Cats.png") },
-- luacheck: ignore -- luacheck: ignore
---@type pd_image ---@type pd_image
{ name = "Checkmarks", image = playdate.graphics.image.new("images/game/logos/Checkmarks.png") }, { name = "Checkmarks", image = playdate.graphics.image.new("assets/images/game/logos/Checkmarks.png") },
-- luacheck: ignore -- luacheck: ignore
---@type pd_image ---@type pd_image
{ name = "FingerGuns", image = playdate.graphics.image.new("images/game/logos/FingerGuns.png") }, { name = "FingerGuns", image = playdate.graphics.image.new("assets/images/game/logos/FingerGuns.png") },
-- luacheck: ignore -- luacheck: ignore
---@type pd_image ---@type pd_image
{ name = "Frown", image = playdate.graphics.image.new("images/game/logos/Frown.png") }, { name = "Frown", image = playdate.graphics.image.new("assets/images/game/logos/Frown.png") },
-- luacheck: ignore -- luacheck: ignore
---@type pd_image ---@type pd_image
{ name = "Hearts", image = playdate.graphics.image.new("images/game/logos/Hearts.png") }, { name = "Hearts", image = playdate.graphics.image.new("assets/images/game/logos/Hearts.png") },
-- luacheck: ignore -- luacheck: ignore
---@type pd_image ---@type pd_image
{ name = "Smiles", image = playdate.graphics.image.new("images/game/logos/Smiles.png") }, { name = "Smiles", image = playdate.graphics.image.new("assets/images/game/logos/Smiles.png") },
-- luacheck: ignore -- luacheck: ignore
---@type pd_image ---@type pd_image
{ name = "Turds", image = playdate.graphics.image.new("images/game/logos/Turds.png") }, { name = "Turds", image = playdate.graphics.image.new("assets/images/game/logos/Turds.png") },
} }

View File

@ -25,13 +25,16 @@ function generatedFileWarning()
return "-- GENERATED FILE - DO NOT EDIT\n-- Instead, edit the source file directly: assets.lua2p." return "-- GENERATED FILE - DO NOT EDIT\n-- Instead, edit the source file directly: assets.lua2p."
end)!!(generatedFileWarning()) end)!!(generatedFileWarning())
!!(dirLookup('images/game', 'png', 'playdate.graphics.image.new', 'pd_image')) !!(dirLookup('assets/images/game', 'png', 'playdate.graphics.image.new', 'pd_image'))
!!(dirLookup('sounds', 'wav', 'playdate.sound.sampleplayer.new', 'pd_sampleplayer')) !!(dirLookup('assets/sounds', 'wav', 'playdate.sound.sampleplayer.new', 'pd_sampleplayer'))
!!(dirLookup('music', 'wav', 'playdate.sound.sampleplayer.new', 'pd_sampleplayer')) !!(dirLookup('assets/music', 'wav', 'playdate.sound.sampleplayer.new', 'pd_sampleplayer'))
!!(dirLookup('assets/fonts', 'fnt', 'playdate.graphics.font.new', 'pd_font', nil, nil, function(varName, value)
return varName:gsub("[- ]", "") .. " = " .. value:gsub("fnt", "pft")
end))
Logos = { Logos = {
{ name = "Base", image = playdate.graphics.image.new("images/game/logos/Base.png") }, { name = "Base", image = playdate.graphics.image.new("assets/images/game/logos/Base.png") },
!!(dirLookup('images/game/logos -not -name "Base.png"', 'png', 'playdate.graphics.image.new', 'pd_image', ",\n\n", " ", function(varName, value) !!(dirLookup('assets/images/game/logos -not -name "Base.png"', 'png', 'playdate.graphics.image.new', 'pd_image', ",\n\n", " ", function(varName, value)
return '{ name = "' .. varName .. '", image = ' .. value .. ' }' return '{ name = "' .. varName .. '", image = ' .. value .. ' }'
end)) end))
} }

View File

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

View File

@ -0,0 +1,242 @@
--metrics={"baseline":0,"xHeight":0,"capHeight":0,"pairs":{"ac":[0,0],"ad":[0,0],"ae":[0,0],"af":[-1,0,0,0],"ag":[0,0],"ap":[0,0],"ar":[1,0,0,0],"at":[-1,0,0,0],"au":[0,0],"av":[-1,0,0,0],"aw":[-1,0,0,0],"ay":[-1,0,0,0],"b,":[-1,0,0,0],"b.":[-1,0,0,0],"bl":[0,0],"br":[0,0],"bu":[0,0],"by":[-1,0,0,0],"ca":[0,0],"ch":[0,0],"ck":[0,0],"d,":[-1,0,0,0],"d.":[0,0],"da":[0,0],"dc":[0,0],"de":[0,0],"dg":[0,0],"do":[0,0],"dt":[0,0],"du":[0,0],"dv":[0,0],"dw":[0,0],"dy":[0,0],"e,":[-1,0,0,0],"e.":[-1,0,0,0],"ea":[0,0],"ei":[0,0],"el":[0,0],"em":[0,0],"en":[0,0],"ep":[0,0],"er":[0,0],"et":[-1,0,0,0],"eu":[0,0],"ev":[-1,0,0,0],"ew":[-1,0,0,0],"ey":[-1,0,0,0],"f,":[-2,0,0,0],"f.":[-2,0,0,0],"fa":[-1,0,0,0],"fe":[-1,0,0,0],"ff":[-2,0,0,0],"fi":[0,0],"fl":[-1,0,0,0],"fo":[-2,0,0,0],"g,":[0,0],"g.":[0,0],"ga":[0,0],"ge":[0,0],"gg":[0,0],"gh":[0,0],"gl":[0,0],"go":[0,0],"hc":[0,0],"hd":[0,0],"he":[0,0],"hg":[0,0],"ho":[0,0],"hp":[0,0],"ht":[-1,0,0,0],"hu":[0,0],"hv":[-1,0,0,0],"hw":[-1,0,0,0],"hy":[-1,0,0,0],"ic":[-1,0,0,0],"id":[-1,0,0,0],"ie":[-1,0,0,0],"ig":[-1,0,0,0],"io":[-1,0,0,0],"ip":[-1,0,0,0],"it":[-2,0,0,0],"iu":[-1,0,0,0],"iv":[-1,0,0,0],"j,":[0,0],"j.":[0,0],"ja":[0,0],"je":[0,0],"jo":[0,0],"ju":[0,0],"ka":[-2,0,0,0],"kc":[-2,0,0,0],"kd":[-2,0,0,0],"ke":[-2,0,0,0],"kg":[-2,0,0,0],"ko":[-2,0,0,0],"la":[0,0],"lc":[0,0],"ld":[0,0],"le":[0,0],"lf":[0,0],"lg":[0,0],"lo":[0,0],"Lo":[-1,0,0,0],"lp":[0,0],"lq":[0,0],"lu":[0,0],"lv":[0,0],"lw":[0,0],"ly":[0,0],"ma":[0,0],"mc":[0,0],"md":[0,0],"me":[0,0],"mg":[0,0],"mn":[0,0],"mo":[0,0],"mp":[0,0],"mt":[-1,0,0,0],"mu":[0,0],"mv":[-1,0,0,0],"my":[-1,0,0,0],"nc":[0,0],"nd":[0,0],"ne":[0,0],"ng":[0,0],"no":[0,0],"np":[0,0],"nt":[-1,0,0,0],"nu":[0,0],"nv":[-1,0,0,0],"nw":[-1,0,0,0],"ny":[-1,0,0,0],"o,":[-2,0,0,0],"o.":[-1,0,0,0],"ob":[0,0],"of":[-2,0,0,0],"oh":[0,0],"oj":[-2,0,0,0],"ok":[0,0],"ol":[0,0],"om":[0,0],"on":[0,0],"op":[0,0],"or":[0,0],"ou":[0,0],"ov":[-1,0,0,0],"ow":[-1,0,0,0],"ox":[-1,0,0,0],"oy":[-1,0,0,0],"p,":[-1,0,0,0],"p.":[-1,0,0,0],"pa":[0,0],"ph":[0,0],"pi":[0,0],"pl":[0,0],"pp":[0,0],"pu":[0,0],"qu":[0,0],"r,":[-3,0,0,0],"r.":[-2,0,0,0],"ra":[-1,0,0,0],"rd":[-1,0,0,0],"re":[-1,0,0,0],"rg":[-1,0,0,0],"rk":[0,0],"rl":[0,0],"rm":[0,0],"rn":[0,0],"ro":[-2,0,0,0],"rq":[-1,0,0,0],"rr":[0,0],"rt":[-1,0,0,0],"rv":[0,0],"ry":[0,0],"s,":[-1,0,0,0],"s.":[-1,0,0,0],"sh":[0,0],"st":[-1,0,0,0],"su":[0,0],"t,":[0,0],"t.":[1,0,0,0],"ta":[1,0,0,0],"td":[0,0],"te":[0,0],"th":[0,0],"ti":[1,0,0,0],"tl":[1,0,0,0],"to":[0,0],"ua":[0,0],"uc":[0,0],"ud":[0,0],"ue":[0,0],"ug":[0,0],"uo":[0,0],"up":[1,0,0,0],"uq":[0,0],"ur":[1,0,0,0],"ut":[0,0],"uv":[0,0],"uw":[0,0],"uy":[0,0],"v,":[-2,0,0,0],"v.":[-2,0,0,0],"va":[0,0],"vb":[0,0],"vc":[-1,0,0,0],"vd":[-1,0,0,0],"ve":[-1,0,0,0],"vg":[-1,0,0,0],"vo":[-1,0,0,0],"vv":[0,0],"vy":[-1,0,0,0],"w,":[-2,0,0,0],"w.":[-1,0,0,0],"wa":[-1,0,0,0],"wd":[-1,0,0,0],"we":[-1,0,0,0],"wg":[-1,0,0,0],"wh":[0,0],"wo":[-1,0,0,0],"wx":[-1,0,0,0],"xa":[-1,0,0,0],"xe":[-1,0,0,0],"xo":[-1,0,0,0],"y,":[-3,0,0,0],"y.":[-2,0,0,0],"ya":[-1,0,0,0],"yc":[-1,0,0,0],"yd":[-1,0,0,0],"ye":[-1,0,0,0],"Yo":[-2,0,0,0],"yo":[-1,0,0,0],"LO":[-2,0,0,0],"AT":[-3,0,0,0],"AY":[-3,0,0,0],"//":[-4,0,0,0],"/d":[-2,0,0,0],"/p":[-1,0,0,0],"tp":[1,0,0,0],"t:":[1,0,0,0],"/w":[-1,0,0,0],"ot":[-1,0,0,0],"Wo":[-2,0,0,0],"Fo":[-2,0,0,0],"Fu":[-2,0,0,0],"Vu":[-1,0,0,0],"Tu":[-2,0,0,0],"To":[-3,0,0,0],"Vo":[-2,0,0,0],"Yu":[-1,0,0,0],"Zo":[-1,0,0,0],"ty":[-1,0,0,0],"is":[-1,0,0,0]},"left":[],"right":[]}
tracking=1
0 12
1 5
2 11
3 12
4 12
5 11
6 12
7 11
8 11
9 12
space 3
! 2
" 6
# 14
$ 11
% 15
& 13
' 2
( 5
) 5
* 8
+ 10
, 3
- 8
. 2
/ 9
: 2
; 4
< 9
= 11
> 9
? 9
@ 18
A 13
B 11
C 14
D 12
E 10
F 10
G 14
H 12
I 2
J 5
K 12
L 9
M 15
N 11
O 15
P 10
Q 15
R 10
S 11
T 12
U 12
V 12
W 18
X 11
Y 10
Z 11
[ 5
\ 9
] 5
^ 7
_ 11
` 3
a 9
b 10
c 10
d 10
e 10
f 7
g 10
h 9
i 3
j 4
k 10
l 2
m 16
n 9
o 11
p 10
q 10
r 6
s 8
t 7
u 9
v 8
w 14
x 9
y 10
z 9
{ 6
| 2
} 6
~ 10
¥ 10
… 12
™ 16
‼ 6
© 15
® 15
<EFBFBD> 15
Ⓐ 18
Ⓑ 18
🌐 18
14
▸ 12
⊙ 18
3
3
“ 6
” 6
af -1
ar 1
at -1
av -1
aw -1
ay -1
b, -1
b. -1
by -1
d, -1
e, -1
e. -1
et -1
ev -1
ew -1
ey -1
f, -2
f. -2
fa -1
fe -1
ff -2
fl -1
fo -2
ht -1
hv -1
hw -1
hy -1
ic -1
id -1
ie -1
ig -1
io -1
ip -1
it -2
iu -1
iv -1
ka -2
kc -2
kd -2
ke -2
kg -2
ko -2
Lo -1
mt -1
mv -1
my -1
nt -1
nv -1
nw -1
ny -1
o, -2
o. -1
of -2
oj -2
ov -1
ow -1
ox -1
oy -1
p, -1
p. -1
r, -3
r. -2
ra -1
rd -1
re -1
rg -1
ro -2
rq -1
rt -1
s, -1
s. -1
st -1
t. 1
ta 1
ti 1
tl 1
up 1
ur 1
v, -2
v. -2
vc -1
vd -1
ve -1
vg -1
vo -1
vy -1
w, -2
w. -1
wa -1
wd -1
we -1
wg -1
wo -1
wx -1
xa -1
xe -1
xo -1
y, -3
y. -2
ya -1
yc -1
yd -1
ye -1
Yo -2
yo -1
LO -2
AT -3
AY -3
// -4
/d -2
/p -1
tp 1
t: 1
/w -1
ot -1
Wo -2
Fo -2
Fu -2
Vu -1
Tu -2
To -3
Vo -2
Yu -1
Zo -1
ty -1
is -1

View File

Before

Width:  |  Height:  |  Size: 9.4 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

View File

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

View File

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

Before

Width:  |  Height:  |  Size: 626 B

After

Width:  |  Height:  |  Size: 626 B

View File

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

View File

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 42 KiB

View File

Before

Width:  |  Height:  |  Size: 7.0 KiB

After

Width:  |  Height:  |  Size: 7.0 KiB

View File

Before

Width:  |  Height:  |  Size: 592 B

After

Width:  |  Height:  |  Size: 592 B

View File

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

Before

Width:  |  Height:  |  Size: 738 B

After

Width:  |  Height:  |  Size: 738 B

View File

Before

Width:  |  Height:  |  Size: 601 B

After

Width:  |  Height:  |  Size: 601 B

View File

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

View File

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

Before

Width:  |  Height:  |  Size: 654 B

After

Width:  |  Height:  |  Size: 654 B

View File

Before

Width:  |  Height:  |  Size: 611 B

After

Width:  |  Height:  |  Size: 611 B

View File

Before

Width:  |  Height:  |  Size: 614 B

After

Width:  |  Height:  |  Size: 614 B

View File

Before

Width:  |  Height:  |  Size: 592 B

After

Width:  |  Height:  |  Size: 592 B

View File

Before

Width:  |  Height:  |  Size: 589 B

After

Width:  |  Height:  |  Size: 589 B

View File

Before

Width:  |  Height:  |  Size: 600 B

After

Width:  |  Height:  |  Size: 600 B

View File

Before

Width:  |  Height:  |  Size: 579 B

After

Width:  |  Height:  |  Size: 579 B

View File

Before

Width:  |  Height:  |  Size: 589 B

After

Width:  |  Height:  |  Size: 589 B

View File

Before

Width:  |  Height:  |  Size: 593 B

After

Width:  |  Height:  |  Size: 593 B

View File

Before

Width:  |  Height:  |  Size: 596 B

After

Width:  |  Height:  |  Size: 596 B

View File

Before

Width:  |  Height:  |  Size: 587 B

After

Width:  |  Height:  |  Size: 587 B

View File

Before

Width:  |  Height:  |  Size: 598 B

After

Width:  |  Height:  |  Size: 598 B

View File

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -5,6 +5,7 @@
---@field size number ---@field size number
---@field heldBy Fielder | nil ---@field heldBy Fielder | nil
---@field catchable boolean ---@field catchable boolean
---@field isFlyBall boolean
---@field xAnimator SimpleAnimator ---@field xAnimator SimpleAnimator
---@field yAnimator SimpleAnimator ---@field yAnimator SimpleAnimator
---@field sizeAnimator SimpleAnimator ---@field sizeAnimator SimpleAnimator
@ -12,6 +13,10 @@
---@field private animatorLib pd_animator_lib ---@field private animatorLib pd_animator_lib
Ball = {} Ball = {}
local function defaultFloatAnimator(animatorLib)
return animatorLib.new(2000, -60, 0, utils.easingHill)
end
---@param animatorLib pd_animator_lib ---@param animatorLib pd_animator_lib
---@return Ball ---@return Ball
function Ball.new(animatorLib) function Ball.new(animatorLib)
@ -30,15 +35,14 @@ function Ball.new(animatorLib)
-- TODO? Replace these with a ballAnimatorZ? -- TODO? Replace these with a ballAnimatorZ?
-- ...that might lose some of the magic of both. Compromise available? idk -- ...that might lose some of the magic of both. Compromise available? idk
sizeAnimator = utils.staticAnimator(C.SmallestBallRadius), sizeAnimator = utils.staticAnimator(C.SmallestBallRadius),
floatAnimator = animatorLib.new(2000, -60, 0, utils.easingHill), floatAnimator = defaultFloatAnimator(animatorLib),
}, { __index = Ball }) }, { __index = Ball })
end end
function Ball:updatePosition() ---@param deltaSeconds number
function Ball:updatePosition(deltaSeconds)
if self.heldBy then if self.heldBy then
self.x = self.heldBy.x utils.moveAtSpeedZ(self, 100 * deltaSeconds, { x = self.heldBy.x, y = self.heldBy.y, z = C.GloveZ })
self.y = self.heldBy.y
self.z = C.GloveZ
self.size = C.SmallestBallRadius self.size = C.SmallestBallRadius
else else
self.x = self.xAnimator:currentValue() self.x = self.xAnimator:currentValue()
@ -46,7 +50,11 @@ function Ball:updatePosition()
-- TODO: This `+ z` is more graphics logic than physics logic -- TODO: This `+ z` is more graphics logic than physics logic
self.y = self.yAnimator:currentValue() + z self.y = self.yAnimator:currentValue() + z
self.z = z self.z = z
self.size = self.sizeAnimator:currentValue() 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)
end end
end end
@ -63,9 +71,11 @@ end
---@param easingFunc EasingFunc ---@param easingFunc EasingFunc
---@param flyTimeMs number | nil ---@param flyTimeMs number | nil
---@param floaty boolean | nil ---@param floaty boolean | nil
---@param customBallScaler pd_animator | nil ---@param customFloater pd_animator | nil
function Ball:launch(destX, destY, easingFunc, flyTimeMs, floaty, customBallScaler) ---@param isHit boolean
function Ball:launch(destX, destY, easingFunc, flyTimeMs, floaty, customFloater, isHit)
self.heldBy = nil self.heldBy = nil
self.isFlyBall = isHit
-- Prevent silly insta-catches -- Prevent silly insta-catches
self:markUncatchable() self:markUncatchable()
@ -74,10 +84,11 @@ function Ball:launch(destX, destY, easingFunc, flyTimeMs, floaty, customBallScal
flyTimeMs = utils.distanceBetween(self.x, self.y, destX, destY) * C.DefaultLaunchPower flyTimeMs = utils.distanceBetween(self.x, self.y, destX, destY) * C.DefaultLaunchPower
end end
if customBallScaler then if customFloater then
self.sizeAnimator = customBallScaler self.floatAnimator = customFloater
else else
self.sizeAnimator = self.animatorLib.new(flyTimeMs, 9, C.SmallestBallRadius, utils.easingHill) self.sizeAnimator = self.animatorLib.new(flyTimeMs, C.SmallestBallRadius, 9, utils.easingHill)
self.floatAnimator = defaultFloatAnimator(self.animatorLib)
end end
self.yAnimator = self.animatorLib.new(flyTimeMs, self.y, destY, easingFunc) self.yAnimator = self.animatorLib.new(flyTimeMs, self.y, destY, easingFunc)
self.xAnimator = self.animatorLib.new(flyTimeMs, self.x, destX, easingFunc) self.xAnimator = self.animatorLib.new(flyTimeMs, self.x, destX, easingFunc)

View File

@ -21,8 +21,9 @@ Baserunning = {}
-- TODO: Implement slides? Would require making fielders' gloves "real objects" whose state is tracked. -- TODO: Implement slides? Would require making fielders' gloves "real objects" whose state is tracked.
---@param announcer Announcer ---@param announcer Announcer
---@param onThirdOutCallback fun()
---@return Baserunning ---@return Baserunning
function Baserunning.new(announcer, onThirdOut) function Baserunning.new(announcer, onThirdOutCallback)
local o = setmetatable({ local o = setmetatable({
runners = {}, runners = {},
outRunners = {}, outRunners = {},
@ -32,7 +33,7 @@ function Baserunning.new(announcer, onThirdOut)
--- it seems sensible to store the value here. --- it seems sensible to store the value here.
outs = 0, outs = 0,
announcer = announcer, announcer = announcer,
onThirdOut = onThirdOut, onThirdOut = onThirdOutCallback,
}, { __index = Baserunning }) }, { __index = Baserunning })
o:pushNewBatter() o:pushNewBatter()
@ -133,6 +134,9 @@ function Baserunning:convertBatterToRunner()
self.batter = nil -- Demote batter to a mere runner self.batter = nil -- Demote batter to a mere runner
end end
---@param deltaSeconds number
---@param runner Runner
---@return boolean isStillWalking
local function walkWayOutRunner(deltaSeconds, runner) local function walkWayOutRunner(deltaSeconds, runner)
if runner.x < C.Screen.W + 50 and runner.y < C.Screen.H + 50 then if runner.x < C.Screen.W + 50 and runner.y < C.Screen.H + 50 then
runner.x = runner.x + (deltaSeconds * 25) runner.x = runner.x + (deltaSeconds * 25)
@ -156,7 +160,7 @@ function Baserunning:walkAwayOutRunners(deltaSeconds)
end end
end end
---@return Runner ---@return Runner theBatterPushed
function Baserunning:pushNewBatter() function Baserunning:pushNewBatter()
local new = { local new = {
-- imageSet = math.random() < C.WokeMeter and FemmeSet or MascSet, -- TODO? lol. -- imageSet = math.random() < C.WokeMeter and FemmeSet or MascSet, -- TODO? lol.
@ -172,6 +176,10 @@ function Baserunning:pushNewBatter()
return new return new
end end
function Baserunning:getNewestRunner()
return self.runners[#self.runners]
end
---@param runnerIndex number ---@param runnerIndex number
function Baserunning:runnerScored(runnerIndex) function Baserunning:runnerScored(runnerIndex)
self.scoredRunners[#self.scoredRunners + 1] = self.runners[runnerIndex] self.scoredRunners[#self.scoredRunners + 1] = self.runners[runnerIndex]
@ -182,6 +190,7 @@ end
---@param runner Runner | nil ---@param runner Runner | nil
---@param runnerIndex number | nil May only be nil if runner == batter ---@param runnerIndex number | nil May only be nil if runner == batter
---@param appliedSpeed number ---@param appliedSpeed number
---@param isAutoRun boolean
---@param deltaSeconds number ---@param deltaSeconds number
---@return boolean runnerMoved, boolean runnerScored ---@return boolean runnerMoved, boolean runnerScored
function Baserunning:updateRunner(runner, runnerIndex, appliedSpeed, isAutoRun, deltaSeconds) function Baserunning:updateRunner(runner, runnerIndex, appliedSpeed, isAutoRun, deltaSeconds)
@ -196,7 +205,7 @@ function Baserunning:updateRunner(runner, runnerIndex, appliedSpeed, isAutoRun,
if if
nearestBaseDistance < 5 nearestBaseDistance < 5
and runnerIndex ~= nil and runnerIndex ~= nil
and runner ~= self.batter --runner.prevBase and runner ~= self.batter
and runner.nextBase == C.Bases[C.Home] and runner.nextBase == C.Bases[C.Home]
and nearestBase == C.Bases[C.Home] and nearestBase == C.Bases[C.Home]
then then
@ -248,7 +257,9 @@ end
--- Update non-batter runners. --- Update non-batter runners.
--- Returns true only if at least one of the given runners moved during this update --- Returns true only if at least one of the given runners moved during this update
---@param appliedSpeed number | fun(runner: Runner): number ---@param appliedSpeed number | fun(runner: Runner): number
---@param forcedOnly boolean If true, only move forced runners (e.g. for a walk)
---@param isAutoRun boolean If true, does not attempt to hug the bases ---@param isAutoRun boolean If true, does not attempt to hug the bases
---@param deltaSeconds number
---@return boolean runnersStillMoving, number runnersScored, number secondsSinceLastMove ---@return boolean runnersStillMoving, number runnersScored, number secondsSinceLastMove
function Baserunning:updateNonBatterRunners(appliedSpeed, forcedOnly, isAutoRun, deltaSeconds) function Baserunning:updateNonBatterRunners(appliedSpeed, forcedOnly, isAutoRun, deltaSeconds)
local runnersStillMoving = false local runnersStillMoving = false

68
src/batting.lua Normal file
View File

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

View File

@ -99,6 +99,7 @@ C.Offense = {
running = "running", running = "running",
walking = "walking", walking = "walking",
homeRun = "homeRun", homeRun = "homeRun",
fliedOut = "running",
} }
---@alias Side "offense" | "defense" ---@alias Side "offense" | "defense"
@ -114,6 +115,8 @@ C.CrankPower = 10
C.FielderRunMult = 1.3 C.FielderRunMult = 1.3
C.PlayerHeightOffset = 20
C.UserThrowPower = 0.3 C.UserThrowPower = 0.3
--- How fast baserunners move after a walk --- How fast baserunners move after a walk

View File

@ -1,7 +1,7 @@
local gfx = playdate.graphics local gfx = playdate.graphics
local HeaderFont <const> = playdate.graphics.font.new("fonts/Roobert-11-Medium.pft") local HeaderFont <const> = Roobert11Medium
local DetailFont <const> = playdate.graphics.font.new("fonts/font-full-circle.pft") local DetailFont <const> = FontFullCircle
---@alias TextObject { text: string, font: pd_font } ---@alias TextObject { text: string, font: pd_font }
@ -52,7 +52,7 @@ local function detail(text)
return { text = text, font = DetailFont } return { text = text, font = DetailFont }
end end
---@class ---@class ControlScreen
---@field sceneToReturnTo Scene ---@field sceneToReturnTo Scene
---@field private renderedImage pd_image Static image doesn't need to be constantly re-rendered. ---@field private renderedImage pd_image Static image doesn't need to be constantly re-rendered.
ControlScreen = {} ControlScreen = {}
@ -81,6 +81,7 @@ local function draw()
end end
---@param sceneToReturnTo Scene ---@param sceneToReturnTo Scene
---@return ControlScreen
function ControlScreen.new(sceneToReturnTo) function ControlScreen.new(sceneToReturnTo)
return setmetatable({ return setmetatable({
sceneToReturnTo = sceneToReturnTo, sceneToReturnTo = sceneToReturnTo,

View File

@ -16,7 +16,8 @@ function dbg.label(value, name)
return value return value
end end
-- Only works if called with the bases empty (i.e. the only runner should be the batter. --- Only works if called with the bases empty (i.e. the only runner should be the batter.
---@param br Baserunning
function dbg.loadTheBases(br) function dbg.loadTheBases(br)
br:pushNewBatter() br:pushNewBatter()
br:pushNewBatter() br:pushNewBatter()
@ -73,6 +74,7 @@ local hitSamples = {
}, },
} }
---@param inningCount number Number of innings to mock
---@return Statistics ---@return Statistics
function dbg.mockStatistics(inningCount) function dbg.mockStatistics(inningCount)
inningCount = inningCount or 9 inningCount = inningCount or 9

11
src/draw/ball.lua Normal file
View File

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

@ -1,7 +1,7 @@
local MarginY <const> = 70 local MarginY <const> = 70
local SmallFont <const> = playdate.graphics.font.new("fonts/font-full-circle.pft") local SmallFont <const> = FontFullCircle
local ScoreFont <const> = playdate.graphics.font.new("fonts/Asheville-Sans-14-Bold.pft") local ScoreFont <const> = AshevilleSans14Bold
local NumWidth <const> = ScoreFont:getTextWidth("0") local NumWidth <const> = ScoreFont:getTextWidth("0")
local NumHeight <const> = ScoreFont:getHeight() local NumHeight <const> = ScoreFont:getHeight()
local AwayWidth <const> = ScoreFont:getTextWidth("AWAY") local AwayWidth <const> = ScoreFont:getTextWidth("AWAY")

151
src/draw/characters.lua Normal file
View File

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

View File

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

View File

@ -1,7 +1,7 @@
-- selene: allow(shadowing) -- selene: allow(shadowing)
local gfx = playdate.graphics local gfx = playdate.graphics
local ScoreFont <const> = playdate.graphics.font.new("fonts/font-full-circle.pft") local ScoreFont <const> = FontFullCircle
local MinimapSizeX, MinimapSizeY <const> = Minimap:getSize() local MinimapSizeX, MinimapSizeY <const> = Minimap:getSize()
local MinimapPosX, MinimapPosY = C.Screen.W - MinimapSizeX, C.Screen.H - MinimapSizeY local MinimapPosX, MinimapPosY = C.Screen.W - MinimapSizeX, C.Screen.H - MinimapSizeY
@ -145,7 +145,8 @@ end
local newStats = stats local newStats = stats
function drawScoreboard(x, y, homeScore, awayScore, outs, battingTeam, inning) function drawScoreboard(x, y, statistics, 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

@ -67,7 +67,8 @@ end
--- Resets the target positions of all fielders to their defaults (at their field positions). --- 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 fromOffTheField XyPair | nil If provided, also sets all runners' current position to one centralized location.
function Fielding:resetFielderPositions(fromOffTheField) ---@param immediate boolean | nil
function Fielding:resetFielderPositions(fromOffTheField, immediate)
if fromOffTheField then if fromOffTheField then
for _, fielder in pairs(self.fielders) do for _, fielder in pairs(self.fielders) do
fielder.x = fromOffTheField.x fielder.x = fromOffTheField.x
@ -84,6 +85,13 @@ function Fielding:resetFielderPositions(fromOffTheField)
self.fielders.left.targets = { utils.xy(C.Screen.W * -0.6, C.Screen.H * -0.1) } 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.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) } 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 end
---@param deltaSeconds number ---@param deltaSeconds number
@ -96,7 +104,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.pointIsSquarelyAboveLine(nextFielderPos, C.BottomOfOutfieldWall) then if willMove and utils.pointIsAboveLine(nextFielderPos, C.BottomOfOutfieldWall, 40) 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)
@ -125,11 +133,12 @@ end
--- Selects the nearest fielder to move toward the given coordinates. --- Selects the nearest fielder to move toward the given coordinates.
--- Other fielders should attempt to cover their bases --- Other fielders should attempt to cover their bases
---@param ballDestX number ---@param ball Point3d
---@param ballDestY number ---@param ballDest XyPair
function Fielding:haveSomeoneChase(ballDestX, ballDestY) function Fielding:haveSomeoneChase(ball, ballDest)
local chasingFielder = utils.getNearestOf(self.fielders, ballDestX, ballDestY) local chasingFielder = utils.getNearestOf(self.fielders, ballDest.x, ballDest.y)
chasingFielder.targets = { utils.xy(ballDestX, ballDestY) } -- Start moving toward the ball directly after reaching ballDest
chasingFielder.targets = { ball, ballDest }
for _, base in ipairs(C.Bases) do for _, base in ipairs(C.Bases) do
local nearest = utils.getNearestOf(self.fielders, base.x, base.y, function(fielder) local nearest = utils.getNearestOf(self.fielders, base.x, base.y, function(fielder)
@ -146,19 +155,24 @@ end
--- **Also updates `ball.heldby`** --- **Also updates `ball.heldby`**
---@param ball Ball ---@param ball Ball
---@param deltaSeconds number ---@param deltaSeconds number
---@return Fielder | nil fielderHoldingBall nil if no fielder is currently touching the ball ---@return Fielder | nil, boolean fielderHoldingBall nil if no fielder is currently touching the ball, true if caught a fly ball
function Fielding:updateFielderPositions(ball, deltaSeconds) function Fielding:updateFielderPositions(ball, deltaSeconds)
local fielderHoldingBall local fielderHoldingBall
local caughtAFlyBall = false
for _, fielder in pairs(self.fielders) do for _, fielder in pairs(self.fielders) do
-- TODO: Base this catch on fielder skill? -- TODO: Base this catch on fielder skill?
local canCatch = updateFielderPosition(deltaSeconds, fielder, ball) local canCatch = updateFielderPosition(deltaSeconds, fielder, ball)
if canCatch then if canCatch then
fielderHoldingBall = fielder fielderHoldingBall = fielder
ball.heldBy = fielder -- How much havoc will this wreak? ball.heldBy = fielder -- How much havoc will this wreak?
if ball.isFlyBall then
ball.isFlyBall = false
caughtAFlyBall = true
end
end end
end end
self.fielderHoldingBall = fielderHoldingBall self.fielderHoldingBall = fielderHoldingBall
return fielderHoldingBall return fielderHoldingBall, caughtAFlyBall
end end
-- TODO? Start moving target fielders close sooner? -- TODO? Start moving target fielders close sooner?

View File

@ -1,6 +1,6 @@
local gfx <const> = playdate.graphics local gfx <const> = playdate.graphics
local ButtonFont <const> = gfx.font.new("fonts/font-full-circle.pft") local ButtonFont <const> = FontFullCircle
--- Assumes that background image is of size: --- Assumes that background image is of size:
--- ---

View File

@ -9,8 +9,8 @@ MainMenu = {
} }
local gfx = playdate.graphics local gfx = playdate.graphics
local ScoreFont <const> = playdate.graphics.font.new("fonts/font-full-circle.pft") local ScoreFont <const> = FontFullCircle
local TinyFont <const> = gfx.font.new("fonts/Nano Sans.pft") local TinyFont <const> = NanoSans
--- Take control of playdate.update --- Take control of playdate.update
--- Will replace playdate.update when the menu is done. --- Will replace playdate.update when the menu is done.
@ -41,6 +41,8 @@ local function startGame()
MenuMusic:setPaused(true) MenuMusic:setPaused(true)
end end
---@param baseEaser EasingFunc
---@return EasingFunc
local function pausingEaser(baseEaser) local function pausingEaser(baseEaser)
--- t: elapsedTime --- t: elapsedTime
--- d: duration --- d: duration
@ -65,6 +67,7 @@ local animatorY = gfx.animator.new(2000, 60, 200, pausingEaser(utils.easingHill)
animatorY.repeatCount = -1 animatorY.repeatCount = -1
animatorY.reverses = true animatorY.reverses = true
---@type number
local crankStartPos local crankStartPos
---@generic T ---@generic T
@ -76,6 +79,7 @@ local function arrayElementFromCrank(array, crankPosition)
return array[i] return array[i]
end end
---@type pd_image
local currentLogo local currentLogo
--luacheck: ignore --luacheck: ignore
@ -132,8 +136,11 @@ function MainMenu:update()
size = 6, size = 6,
} }
local ballIsHeld = drawFielder(AwayTeamSpriteGroup[1], ball, 30, 200) local fielder1 = { x = 30, y = 200, spriteIndex = 1 }
ballIsHeld = drawFielder(HomeTeamSpriteGroup[2], ball, 350, 200, playdate.graphics.kImageFlippedX) or ballIsHeld local ballIsHeld = drawFielder(AwayTeamSpriteGroup, fielder1, ball)
local fielder2 = { x = 350, y = 200, spriteIndex = 2 }
ballIsHeld = drawFielder(HomeTeamSpriteGroup, fielder2, ball, playdate.graphics.kImageFlippedX) or ballIsHeld
if not ballIsHeld then if not ballIsHeld then
gfx.setLineWidth(2) gfx.setLineWidth(2)

View File

@ -15,7 +15,7 @@ import 'CoreLibs/utilities/where.lua'
---@class InputHandler ---@class InputHandler
---@field update fun(self, deltaSeconds: number) ---@field update fun(self, deltaSeconds: number)
---@field updateBat fun(self, ball: Ball, pitchIsOver: boolean, deltaSeconds: number) ---@field updateBatAngle fun(self, ball: Ball, pitchIsOver: boolean, deltaSeconds: number)
---@field runningSpeed fun(self, runner: Runner, ball: Ball) ---@field runningSpeed fun(self, runner: Runner, ball: Ball)
---@field pitch fun(self) ---@field pitch fun(self)
---@field fielderAction fun(self, fielderHoldingBall: Fielder | nil, outedSomeRunner: boolean, ball: Ball) ---@field fielderAction fun(self, fielderHoldingBall: Fielder | nil, outedSomeRunner: boolean, ball: Ball)
@ -40,6 +40,7 @@ import 'action-queue.lua'
import 'announcer.lua' import 'announcer.lua'
import 'ball.lua' import 'ball.lua'
import 'baserunning.lua' import 'baserunning.lua'
import 'batting.lua'
import 'dbg.lua' import 'dbg.lua'
import 'fielding.lua' import 'fielding.lua'
import 'graphics.lua' import 'graphics.lua'
@ -48,12 +49,14 @@ 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/fielder.lua' import 'draw/characters.lua'
import 'draw/overlay.lua' import 'draw/overlay.lua'
import 'draw/panner.lua' import 'draw/panner.lua'
import 'draw/player.lua' import 'draw/character-sprites.lua'
import 'draw/characters.lua'
import 'draw/throw-meter.lua' import 'draw/throw-meter.lua'
import 'draw/transitions.lua' import 'draw/transitions.lua'
-- stylua: ignore end -- stylua: ignore end
@ -61,7 +64,9 @@ import 'draw/transitions.lua'
-- TODO: Customizable field structure. E.g. stands and ads etc. -- TODO: Customizable field structure. E.g. stands and ads etc.
---@type pd_graphics_lib ---@type pd_graphics_lib
local gfx <const>, C <const> = playdate.graphics, C local gfx <const> = playdate.graphics
local C <const> = C
---@alias Team { benchPosition: XyPair } ---@alias Team { benchPosition: XyPair }
---@type table<TeamId, Team> ---@type table<TeamId, Team>
@ -92,24 +97,20 @@ local teams <const> = {
---@field offenseState OffenseState ---@field offenseState OffenseState
---@field inning number ---@field inning number
---@field stats Statistics ---@field stats Statistics
---@field batBase XyPair
---@field batTip XyPair --- Ephemeral data ONLY used during rendering
---@field batAngleDeg number ---@class RenderState
-- These are only sort-of global state. They are purely graphical, ---@field bat BatRenderState
-- 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 ---@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 npc InputHandler ---@field private npc InputHandler
---@field private userInput InputHandler ---@field private userInput InputHandler
---@field private homeTeamBlipper Blipper
---@field private awayTeamBlipper Blipper
---@field private panner Panner ---@field private panner Panner
---@field private state MutableState ---@field private state MutableState
Game = {} Game = {}
@ -126,23 +127,14 @@ function Game.new(settings, announcer, fielding, baserunning, npc, state)
fielding = fielding or Fielding.new() fielding = fielding or Fielding.new()
settings.userTeam = "away" settings.userTeam = "away"
local homeTeamBlipper = blipper.new(100, settings.homeTeamSpriteGroup)
local awayTeamBlipper = blipper.new(100, settings.awayTeamSpriteGroup)
local battingTeam = "away" local battingTeam = "away"
local runnerBlipper = battingTeam == "away" and awayTeamBlipper or homeTeamBlipper
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,
homeTeamBlipper = homeTeamBlipper,
awayTeamBlipper = awayTeamBlipper,
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,
@ -150,9 +142,6 @@ function Game.new(settings, announcer, fielding, baserunning, npc, state)
inning = 1, inning = 1,
pitchIsOver = true, pitchIsOver = true,
didSwing = false, didSwing = false,
battingTeamSprites = settings.awayTeamSpriteGroup,
fieldingTeamSprites = settings.homeTeamSpriteGroup,
runnerBlipper = runnerBlipper,
stats = Statistics.new(), stats = Statistics.new(),
}, },
}, { __index = Game }) }, { __index = Game })
@ -160,15 +149,17 @@ 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)
o.npc = npc or Npc.new(o.baserunning.runners, o.fielding.fielders) o.npc = npc or Npc.new(o.baserunning.runners, o.fielding.fielders)
o.fielding:resetFielderPositions(teams.home.benchPosition) o.fielding:resetFielderPositions(teams.home.benchPosition, settings.userTeam == nil)
playdate.timer.new(2000, function() playdate.timer.new(settings.userTeam == nil and 10 or 2000, function()
o:returnToPitcher() o:returnToPitcher()
end) end)
o.characters = Characters.new(settings.homeTeamSpriteGroup, settings.awayTeamSpriteGroup)
BootTune:play() BootTune:play()
BootTune:setFinishCallback(function() BootTune:setFinishCallback(function()
@ -225,6 +216,9 @@ end
---@param pitchTypeIndex number | nil ---@param pitchTypeIndex number | nil
---@param accuracy number The closer to 1.0, the better ---@param accuracy number The closer to 1.0, the better
function Game:pitch(pitchFlyTimeMs, pitchTypeIndex, accuracy) function Game:pitch(pitchFlyTimeMs, pitchTypeIndex, accuracy)
if pitchTypeIndex == nil then
return -- No throw!
end
self.state.ball:markUncatchable() self.state.ball:markUncatchable()
self.state.ball.heldBy = nil self.state.ball.heldBy = nil
self.state.pitchIsOver = false self.state.pitchIsOver = false
@ -266,8 +260,6 @@ function Game:pitcherIsReady()
end end
function Game:checkForGameOver() function Game:checkForGameOver()
Fielding.celebrate()
local state = self.state local state = self.state
if state.stats:gameIsOver(state.inning, self.settings.finalInning, state.battingTeam) then if state.stats:gameIsOver(state.inning, self.settings.finalInning, state.battingTeam) then
self.announcer:say("THAT'S THE BALL GAME!") self.announcer:say("THAT'S THE BALL GAME!")
@ -282,6 +274,8 @@ end
function Game:nextHalfInning() function Game:nextHalfInning()
pitchTracker:reset() pitchTracker:reset()
Fielding.celebrate()
if self:checkForGameOver() then if self:checkForGameOver() then
return return
end end
@ -294,17 +288,8 @@ function Game:nextHalfInning()
self.state.inning = self.state.inning + 1 self.state.inning = self.state.inning + 1
self.state.stats:pushInning() self.state.stats:pushInning()
end end
self.state.battingTeam = getOppositeTeamId(self.state.battingTeam)
playdate.timer.new(2000, function() playdate.timer.new(2000, function()
if self.state.battingTeam == "home" then self.state.battingTeam = getOppositeTeamId(self.state.battingTeam)
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)
end end
@ -378,77 +363,67 @@ function Game:strikeOut()
self:nextBatter() self:nextBatter()
end end
local SwingBackDeg <const> = 30 function Game:saveToFile()
local SwingForwardDeg <const> = 170 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
---@param offenseHandler InputHandler ---@param offenseHandler InputHandler
function Game:updateBatting(offenseHandler) function Game:updateBatting(offenseHandler)
local ball = self.state.ball local ball = self.state.ball
local batDeg, batSpeed = offenseHandler:updateBat(ball, self.state.pitchIsOver, self.state.deltaSeconds) local batDeg, batSpeed = offenseHandler:updateBatAngle(ball, self.state.pitchIsOver, self.state.deltaSeconds)
self.state.batAngleDeg = batDeg local ballDest, isSwinging, mult = self.batting:checkForHit(batDeg, batSpeed, ball)
self.state.didSwing = self.state.didSwing or (isSwinging and not self.state.pitchIsOver)
if not self.state.pitchIsOver and batDeg > SwingBackDeg and batDeg < SwingForwardDeg then if not ballDest then
self.state.didSwing = true
end
-- TODO? Make the bat angle work more like the throw meter.
-- Would instead constantly drift toward a default value, giving us a little more control,
-- and letting the user find a crank position and direction that works for them
local batAngle = math.rad(batDeg)
-- TODO: animate bat-flip or something
local batter = self.baserunning.batter
self.state.batBase.x = batter and (batter.x + C.BatterHandPos.x) or -999
self.state.batBase.y = batter and (batter.y + C.BatterHandPos.y) or -999
self.state.batTip.x = self.state.batBase.x + (C.BatLength * math.sin(batAngle))
self.state.batTip.y = self.state.batBase.y + (C.BatLength * math.cos(batAngle))
local ballWasHit = batSpeed > 0
and ball.y < 232
and utils.pointDirectlyUnderLine(
ball.x,
ball.y,
self.state.batBase.x,
self.state.batBase.y,
self.state.batTip.x,
self.state.batTip.y,
C.Screen.H
)
if not ballWasHit then
return return
end end
-- Hit! -- Hit!
-- TODO: animate bat-flip or something
local isFlyBall = math.random() > 0.5
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 = 8000
-- TODO? A dramatic eye-level view on a home-run could be sick. -- TODO? A dramatic eye-level view on a home-run could be sick.
local battingTeamStats = self:battingTeamCurrentInning() local battingTeamStats = self:battingTeamCurrentInning()
battingTeamStats.hits[#battingTeamStats.hits + 1] = ballDest battingTeamStats.hits[#battingTeamStats.hits + 1] = ballDest
if utils.isFoulBall(ballDest.x, ballDest.y) then if utils.isFoulBall(ballDest) then
self.announcer:say("Foul ball!") self.announcer:say("Foul ball!")
pitchTracker.strikes = math.min(pitchTracker.strikes + 1, 2) pitchTracker.strikes = math.min(pitchTracker.strikes + 1, 2)
-- TODO: Have a fielder chase for the fly-out -- TODO: Have a fielder chase for the fly-out
return return
end end
if utils.pointIsSquarelyAboveLine(utils.xy(ballDest.x, ballDest.y), C.OutfieldWall) then 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() playdate.timer.new(flyTimeMs, function()
-- Verify that the home run wasn't intercepted -- 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 if utils.distanceBetweenPoints(ball, ballDest) < 2 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.
@ -460,15 +435,21 @@ function Game:updateBatting(offenseHandler)
end end
end) end)
end end
end
local hitBallScaler = gfx.animator.new(2000, 9 + (mult * mult * 0.5), C.SmallestBallRadius, utils.easingHill) local ballHeightAnimator = isFlyBall
ball:launch(ballDest.x, ballDest.y, playdate.easingFunctions.outQuint, flyTimeMs, nil, hitBallScaler) 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)
self.baserunning:convertBatterToRunner() self.baserunning:convertBatterToRunner()
self.fielding:haveSomeoneChase(ballDest.x, ballDest.y) self.fielding:haveSomeoneChase(ball, ballDest)
end end
---@param appliedSpeed number | fun(runner: Runner): number ---@param appliedSpeed number | fun(runner: Runner): number
---@param forcedOnly boolean
---@param isAutoRun boolean
---@return boolean runnersStillMoving, number secondsSinceLastRunnerMove ---@return boolean runnersStillMoving, number secondsSinceLastRunnerMove
function Game:updateNonBatterRunners(appliedSpeed, forcedOnly, isAutoRun) function Game:updateNonBatterRunners(appliedSpeed, forcedOnly, isAutoRun)
local runnersStillMoving, runnersScored, secondsSinceLastRunnerMove = local runnersStillMoving, runnersScored, secondsSinceLastRunnerMove =
@ -506,6 +487,7 @@ function Game:updatePitching(defenseHandler)
end end
if pitchTracker.secondsSinceLastPitch > C.ReturnToPitcherAfterSeconds and not self.state.pitchIsOver then if pitchTracker.secondsSinceLastPitch > C.ReturnToPitcherAfterSeconds and not self.state.pitchIsOver then
self:saveToFile()
local outcome = pitchTracker:updatePitchCounts(self.state.didSwing, self:fieldingTeamCurrentInning()) local outcome = pitchTracker:updatePitchCounts(self.state.didSwing, self:fieldingTeamCurrentInning())
if outcome == PitchOutcomes.StrikeOut then if outcome == PitchOutcomes.StrikeOut then
self:strikeOut() self:strikeOut()
@ -529,9 +511,17 @@ function Game:updateGameState()
playdate.resetElapsedTime() playdate.resetElapsedTime()
self.state.ball:updatePosition() self.state.ball:updatePosition()
local offenseHandler, defenseHandler = self:currentInputHandlers() 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
local fielderHoldingBall = self.fielding:updateFielderPositions(self.state.ball, self.state.deltaSeconds) local offenseHandler, defenseHandler = self:currentInputHandlers()
if self.state.offenseState == C.Offense.batting then if self.state.offenseState == C.Offense.batting then
self:updatePitching(defenseHandler) self:updatePitching(defenseHandler)
@ -586,108 +576,30 @@ function Game:update()
gfx.clear() gfx.clear()
gfx.setColor(gfx.kColorBlack) gfx.setColor(gfx.kColorBlack)
local offsetX, offsetY = self.panner:get(self.state.deltaSeconds) local state = self.state
local offsetX, offsetY = self.panner:get(state.deltaSeconds)
gfx.setDrawOffset(offsetX, offsetY) gfx.setDrawOffset(offsetX, offsetY)
fans.draw() fans.draw()
GrassBackground:draw(-400, -720) GrassBackground:draw(-400, -720)
---@type { y: number, drawAction: fun() }[] local ballHeldBy =
local characterDraws = {} self.characters:drawAll(self.fielding, self.baserunning, self.batting.state, state.battingTeam, state.ball)
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 if self:userIsOn("defense") then
throwMeter:drawNearFielder(ballHeldBy) throwMeter:drawNearFielder(ballHeldBy)
end end
if not ballHeldBy then if not ballHeldBy then
gfx.setLineWidth(2) state.ball:draw()
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)
if math.abs(offsetX) > 10 or math.abs(offsetY) > 10 then if math.abs(offsetX) > 10 or math.abs(offsetY) > 20 then
drawMinimap(self.baserunning.runners, self.fielding.fielders) drawMinimap(self.baserunning.runners, self.fielding.fielders)
end end
local homeScore, awayScore = utils.totalScores(self.state.stats)
drawScoreboard( drawScoreboard(0, C.Screen.H * 0.77, state.stats, self.baserunning.outs, state.battingTeam, state.inning)
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) 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

@ -1,6 +1,6 @@
local npcBatDeg = 0 local npcBatDeg = 0
local BaseNpcBatSpeed <const> = 1500 local BaseNpcBatSpeed <const> = 1000
local npcBatSpeed = 1500 local npcBatSpeed = BaseNpcBatSpeed
---@class Npc: InputHandler ---@class Npc: InputHandler
---@field runners Runner[] ---@field runners Runner[]
@ -21,25 +21,33 @@ end
function Npc.update() end function Npc.update() end
-- TODO: FAR more nuanced NPC batting. -- TODO: FAR more nuanced NPC batting.
-- luacheck: no unused
---@param ball XyPair ---@param ball XyPair
---@param pitchIsOver boolean ---@param pitchIsOver boolean
---@param deltaSec number ---@param deltaSec number
---@return number batAngleDeg, number batSpeed ---@return number batAngleDeg, number batSpeed
-- luacheck: no unused function Npc:updateBatAngle(ball, pitchIsOver, deltaSec)
function Npc:updateBat(ball, pitchIsOver, deltaSec) if
if not pitchIsOver and ball.y > 200 and ball.y < 230 and (ball.x < C.Center.x + 15) then 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
npcBatDeg = npcBatDeg + (deltaSec * npcBatSpeed) npcBatDeg = npcBatDeg + (deltaSec * npcBatSpeed)
else else
npcBatSpeed = (1 + math.random()) * BaseNpcBatSpeed npcBatSpeed = (1 + math.random()) * BaseNpcBatSpeed
npcBatDeg = 230 npcBatDeg = utils.moveAtSpeed1d(npcBatDeg, deltaSec * BaseNpcBatSpeed, 230 - 360)
end end
return npcBatDeg, (self:batSpeed() * deltaSec) return npcBatDeg, (self:batSpeed() * deltaSec)
end end
---@return number
function Npc:batSpeed() function Npc:batSpeed()
return npcBatSpeed / 1.5 return npcBatSpeed * 1.25
end end
---@return number flyTimeMs, number pitchId, number accuracy
function Npc:pitch() function Npc:pitch()
return C.PitchFlyMs / self:pitchSpeed(), math.random(#Pitches), 0.9 return C.PitchFlyMs / self:pitchSpeed(), math.random(#Pitches), 0.9
end end
@ -129,7 +137,10 @@ end
---@param ball { x: number, y: number, heldBy: Fielder | nil, launch: LaunchBall } ---@param ball { x: number, y: number, heldBy: Fielder | nil, launch: LaunchBall }
local function tryToMakeAPlay(fielders, fielder, runners, ball) local function tryToMakeAPlay(fielders, fielder, runners, ball)
local targetX, targetY = getNextOutTarget(runners) local targetX, targetY = getNextOutTarget(runners)
if targetX ~= nil and targetY ~= nil then if targetX == nil or targetY == nil then
return
end
local nearestFielder = utils.getNearestOf(fielders, targetX, targetY) local nearestFielder = utils.getNearestOf(fielders, targetX, targetY)
nearestFielder.targets = { utils.xy(targetX, targetY) } nearestFielder.targets = { utils.xy(targetX, targetY) }
if nearestFielder == fielder then if nearestFielder == fielder then
@ -138,7 +149,6 @@ local function tryToMakeAPlay(fielders, fielder, runners, ball)
ball:launch(targetX, targetY, playdate.easingFunctions.linear, nil, true) ball:launch(targetX, targetY, playdate.easingFunctions.linear, nil, true)
end end
end end
end
---@param fielder Fielder ---@param fielder Fielder
---@param outedSomeRunner boolean ---@param outedSomeRunner boolean
@ -147,14 +157,14 @@ function Npc:fielderAction(fielder, outedSomeRunner, ball)
if not fielder then if not fielder then
return return
end end
if outedSomeRunner then local playDelay = outedSomeRunner and 0.5 or 0.1
-- Delay a little before the next play actionQueue:newOnly("npcFielderAction", 2000, function()
playdate.timer.new(750, function() local dt = 0
while dt < playDelay do
dt = dt + coroutine.yield()
end
tryToMakeAPlay(self.fielders, fielder, self.runners, ball) tryToMakeAPlay(self.fielders, fielder, self.runners, ball)
end) end)
else
tryToMakeAPlay(self.fielders, fielder, self.runners, ball)
end
end end
---@return number ---@return number

View File

@ -2,6 +2,6 @@ name=Batter Up!
author=Sage Vaillancourt author=Sage Vaillancourt
description=Crush dingers and hustle around the bases! description=Crush dingers and hustle around the bases!
bundleID=space.sagev.batterup bundleID=space.sagev.batterup
imagePath=images/launcher imagePath=assets/images/launcher
version=0.1 version=0.1
buildNumber=1 buildNumber=1

View File

@ -1,5 +1,5 @@
---@alias SimpleAnimator { currentValue: fun(self): number; reset: fun(self, durationMs: number | nil) } ---@alias SimpleAnimator { currentValue: fun(self): number; reset: fun(self, durationMs: number | nil) }
---@alias Pitch fun(ball: Ball): { x: SimpleAnimator, y: SimpleAnimator, z: SimpleAnimator | nil } ---@alias Pitch fun(accuracy: number, ball: Ball): { x: SimpleAnimator, y: SimpleAnimator, z: SimpleAnimator | nil }
---@type pd_graphics_lib ---@type pd_graphics_lib
local gfx <const> = playdate.graphics local gfx <const> = playdate.graphics
@ -7,6 +7,8 @@ local gfx <const> = playdate.graphics
local StrikeZoneWidth <const> = C.StrikeZoneEndX - C.StrikeZoneStartX local StrikeZoneWidth <const> = C.StrikeZoneEndX - C.StrikeZoneStartX
-- TODO? Also degrade speed -- TODO? Also degrade speed
---@param accuracy number
---@return number xValueToMissBy
function getPitchMissBy(accuracy) function getPitchMissBy(accuracy)
accuracy = accuracy or 1.0 accuracy = accuracy or 1.0
local missBy = (1 - accuracy) * StrikeZoneWidth * 3 local missBy = (1 - accuracy) * StrikeZoneWidth * 3
@ -71,6 +73,9 @@ Pitches = {
end, end,
} }
---@alias PitchOutcome "StrikeOut" | "Walk"
---@type table<string, PitchOutcome>
PitchOutcomes = { PitchOutcomes = {
StrikeOut = "StrikeOut", StrikeOut = "StrikeOut",
Walk = "Walk", Walk = "Walk",
@ -93,6 +98,7 @@ function pitchTracker:reset()
self.balls = 0 self.balls = 0
end end
---@param ball XyPair
function pitchTracker:recordIfPassed(ball) function pitchTracker:recordIfPassed(ball)
if ball.y < C.StrikeZoneStartY then if ball.y < C.StrikeZoneStartY then
self.recordedPitchX = nil self.recordedPitchX = nil
@ -103,6 +109,7 @@ end
---@param didSwing boolean ---@param didSwing boolean
---@param fieldingTeamInningData TeamInningData ---@param fieldingTeamInningData TeamInningData
---@return PitchOutcome | nil
function pitchTracker:updatePitchCounts(didSwing, fieldingTeamInningData) function pitchTracker:updatePitchCounts(didSwing, fieldingTeamInningData)
if not self.recordedPitchX then if not self.recordedPitchX then
return return
@ -149,8 +156,6 @@ throwMeter = {
wasPerfect = false, wasPerfect = false,
} }
local crankQueue = {}
local MaxPowerRatio <const> = 1.5 local MaxPowerRatio <const> = 1.5
--- Returns nil when a throw is NOT requested. --- Returns nil when a throw is NOT requested.
@ -176,6 +181,11 @@ end
local CrankRecordSec <const> = 0.33 local CrankRecordSec <const> = 0.33
---@alias CrankQueueEntry { time: number, chargeAmount: number }
---@type CrankQueueEntry[]
local crankQueue = {}
--- If (within approx. a third of a second) the crank has moved more than 45 degrees, call that a throw. --- If (within approx. a third of a second) the crank has moved more than 45 degrees, call that a throw.
---@param chargeAmount number ---@param chargeAmount number
---@return number | nil ---@return number | nil

View File

@ -5,6 +5,7 @@
-- + Batting average -- + Batting average
-- + Farthest hit ball -- + Farthest hit ball
---@return TeamInningData
local function newTeamInning() local function newTeamInning()
return { return {
score = 0, score = 0,

View File

@ -13,6 +13,13 @@ import = function(target)
require(target:sub(1, #target - 4)) require(target:sub(1, #target - 4))
end end
Glove = {
getSize = function()
return 10, 10
end,
}
Characters = require("draw/characters")
local Game = require("main") local Game = require("main")
local settings = { local settings = {

View File

@ -2,6 +2,7 @@
---@field buttonControlledThrow: fun(throwFlyMs: number, forbidThrowHome: boolean): boolean didThrow ---@field buttonControlledThrow: fun(throwFlyMs: number, forbidThrowHome: boolean): boolean didThrow
UserInput = {} UserInput = {}
---@return UserInput
function UserInput.new(buttonControlledThrow) function UserInput.new(buttonControlledThrow)
return setmetatable({ return setmetatable({
buttonControlledThrow = buttonControlledThrow, buttonControlledThrow = buttonControlledThrow,
@ -10,13 +11,16 @@ end
function UserInput:update() function UserInput:update()
self.crankChange = playdate.getCrankChange() self.crankChange = playdate.getCrankChange()
local crankLimited = self.crankChange == 0 and 0 or (math.log(math.abs(self.crankChange)) * C.CrankPower) self.crankLimited = self.crankChange == 0 and 0 or (math.log(math.abs(self.crankChange)) * C.CrankPower)
self.crankLimited = math.abs(crankLimited) if self.crankChange < 0 then
self.crankLimited = self.crankLimited * -1
end
end end
function UserInput:updateBat() ---@return number batAngleDeg, number batSpeed
function UserInput:updateBatAngle()
local batAngleDeg = (playdate.getCrankPosition() + C.CrankOffsetDeg) % 360 local batAngleDeg = (playdate.getCrankPosition() + C.CrankOffsetDeg) % 360
local batSpeed = self.crankLimited local batSpeed = math.abs(self.crankLimited)
return batAngleDeg, batSpeed return batAngleDeg, batSpeed
end end
@ -43,6 +47,7 @@ local function userPitch(throwFlyMs, accuracy)
return nil, nil, nil return nil, nil, nil
end end
---@return number | nil pitchFlyTimeMs, number | nil pitchTypeIndex, number | nil accuracy
function UserInput:pitch() function UserInput:pitch()
local powerRatio, accuracy = throwMeter:readThrow(self.crankChange) local powerRatio, accuracy = throwMeter:readThrow(self.crankChange)
if powerRatio then if powerRatio then

View File

@ -1,16 +1,14 @@
-- luacheck no new globals -- luacheck no new globals
utils = {} utils = {}
--- @alias XyPair { ---@class XyPair
--- x: number, ---@field x: number,
--- y: number, ---@field y: number,
--- }
--- @alias Point3d { ---@class Point3d
--- x: number, ---@field x number,
--- y: number, ---@field y number,
--- z: number, ---@field z number,
--- }
local sqrt <const> = math.sqrt local sqrt <const> = math.sqrt
@ -22,6 +20,25 @@ function utils.easingHill(t, b, c, d)
return (c * t) + b return (c * t) + b
end 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 { --- @alias StaticAnimator {
--- currentValue: fun(self): number; --- currentValue: fun(self): number;
--- reset: fun(self, durationMs: number | nil); --- reset: fun(self, durationMs: number | nil);
@ -65,6 +82,29 @@ function utils.normalizeVector(x1, y1, x2, y2)
return x / distance, y / distance, distance return x / distance, y / distance, distance
end 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. --- 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. --- Stops when within 1. Returns true only if the object did actually move.
---@param mover { x: number, y: number } ---@param mover { x: number, y: number }
@ -90,8 +130,29 @@ function utils.moveAtSpeed(mover, speed, target, tau)
return true return true
end end
function utils.within(within, n1, n2) ---@param mover Point3d
return math.abs(n1 - n2) < within ---@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
end end
---@generic T ---@generic T
@ -121,6 +182,10 @@ function utils.first(array, condition)
return nil return nil
end end
---@param x1 number
---@param y1 number
---@param x2 number
---@param y2 number
---@return number distance, number x, number y ---@return number distance, number x, number y
function utils.distanceBetween(x1, y1, x2, y2) function utils.distanceBetween(x1, y1, x2, y2)
local x = x1 - x2 local x = x1 - x2
@ -137,6 +202,12 @@ function utils.distanceBetweenPoints(point1, point2)
return sqrt((x * x) + (y * y)), x, y return sqrt((x * x) + (y * y)), x, y
end end
---@param x1 number
---@param y1 number
---@param z1 number
---@param x2 number
---@param y2 number
---@param z2 number
---@return number distance, number x, number y, number z ---@return number distance, number x, number y, number z
function utils.distanceBetweenZ(x1, y1, z1, x2, y2, z2) function utils.distanceBetweenZ(x1, y1, z1, x2, y2, z2)
local x = x1 - x2 local x = x1 - x2
@ -164,34 +235,55 @@ function utils.getRunnerWithNextBase(runners, base)
end end
--- Returns true only if the point is below the given line, within the x bounds of said line, and above the bottomBound. --- Returns true only if the point is below the given line, within the x bounds of said line, and above the bottomBound.
---@param point XyPair
---@param line1 XyPair
---@param line2 XyPair
---@param bottomBound number
---@return boolean ---@return boolean
function utils.pointDirectlyUnderLine(pointX, pointY, lineX1, lineY1, lineX2, lineY2, bottomBound) function utils.pointOnOrUnderLine(point, line1, line2, bottomBound)
-- This check currently assumes right-handedness. -- This check currently assumes right-handedness.
-- I.e. it assumes the ball is to the right of batBaseX -- I.e. it assumes the ball is to the right of batBaseX
if pointX < lineX1 or pointX > lineX2 or pointY > 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(pointX, pointY, lineX1, lineY1, lineX2, lineY2) return utils.pointUnderLine(point.x, point.y, line1.x, line1.y - 2, line2.x, line2.y - 2)
end end
--- Returns true if the given point is anywhere above the given line, with no upper bound. --- Returns true if the given point is anywhere above the given line, with no upper bound.
--- This, if used for home run calculations, would not take into account balls that curve around the foul poles. --- 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.
---@param point XyPair ---@param point XyPair
---@param linePoints XyPair[] ---@param linePoints XyPair[]
---@return boolean ---@return boolean, XyPair | nil nearbyPointAbove
function utils.pointIsSquarelyAboveLine(point, linePoints) 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
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]
if point.x >= prev.x and point.x <= next.x then if point.x >= prev.x and point.x <= next.x then
return not utils.pointUnderLine(point.x, point.y, prev.x, prev.y, next.x, next.y) if not utils.pointUnderLine(point.x, pointY, prev.x, prev.y, next.x, next.y) then
return true, prev
end end
end end
return false end
if point.x > linePoints[#linePoints].x and pointY < linePoints[#linePoints].y then
return true, linePoints[#linePoints]
end
return false, nil
end end
--- Returns true only if the point is below the given line. --- Returns true only if the point is below the given line.
---@param pointX number
---@param pointY number
---@param lineX1 number
---@param lineY1 number
---@param lineX2 number
---@param lineY2 number
---@return boolean ---@return boolean
function utils.pointUnderLine(pointX, pointY, lineX1, lineY1, lineX2, lineY2) function utils.pointUnderLine(pointX, pointY, lineX1, lineY1, lineX2, lineY2)
local m = (lineY2 - lineY1) / (lineX2 - lineX1) local m = (lineY2 - lineY1) / (lineX2 - lineX1)
@ -207,14 +299,13 @@ function utils.pointUnderLine(pointX, pointY, lineX1, lineY1, lineX2, lineY2)
end end
--- Returns true if a ball landing at destX,destY will be foul. --- Returns true if a ball landing at destX,destY will be foul.
---@param destX number ---@param dest XyPair
---@param destY number function utils.isFoulBall(dest)
function utils.isFoulBall(destX, destY)
local leftLine = C.LeftFoulLine local leftLine = C.LeftFoulLine
local rightLine = C.RightFoulLine local rightLine = C.RightFoulLine
return utils.pointUnderLine(destX, destY, leftLine.x1, leftLine.y1, leftLine.x2, leftLine.y2) return utils.pointUnderLine(dest.x, dest.y, leftLine.x1, leftLine.y1, leftLine.x2, leftLine.y2)
or utils.pointUnderLine(destX, destY, rightLine.x1, rightLine.y1, rightLine.x2, rightLine.y2) or utils.pointUnderLine(dest.x, dest.y, rightLine.x1, rightLine.y1, rightLine.x2, rightLine.y2)
end end
--- Returns the nearest position object from the given point, as well as its distance from that point --- Returns the nearest position object from the given point, as well as its distance from that point
@ -222,6 +313,7 @@ end
---@param array T[] ---@param array T[]
---@param x number ---@param x number
---@param y number ---@param y number
---@param extraCondition fun(t: T): boolean
---@return T nearest,number |nil distance ---@return T nearest,number |nil distance
function utils.getNearestOf(array, x, y, extraCondition) function utils.getNearestOf(array, x, y, extraCondition)
local nearest, nearestDistance = nil, nil local nearest, nearestDistance = nil, nil