Compare commits
14 Commits
extract-in
...
main
Author | SHA1 | Date |
---|---|---|
|
4b9a94c2c2 | |
|
decd1f7080 | |
|
04d25127fc | |
|
8b66e2e826 | |
|
876f828117 | |
|
ce9a2d335e | |
|
80015dbe62 | |
|
176a7e6d5e | |
|
55a3a7b0ee | |
|
ddfdc8947a | |
|
e035c0ca72 | |
|
668fa9ffd4 | |
|
b4ac028cd9 | |
|
30aa5bd6c6 |
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
103
src/assets.lua
|
@ -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") },
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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))
|
||||||
}
|
}
|
||||||
|
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.1 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 7.5 KiB |
|
@ -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
|
Before Width: | Height: | Size: 9.4 KiB After Width: | Height: | Size: 9.4 KiB |
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 4.6 KiB |
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 4.8 KiB |
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 4.3 KiB |
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 4.6 KiB |
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 4.8 KiB |
Before Width: | Height: | Size: 626 B After Width: | Height: | Size: 626 B |
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 4.6 KiB |
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 5.2 KiB |
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.5 KiB |
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 42 KiB |
Before Width: | Height: | Size: 7.0 KiB After Width: | Height: | Size: 7.0 KiB |
Before Width: | Height: | Size: 592 B After Width: | Height: | Size: 592 B |
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 4.4 KiB |
Before Width: | Height: | Size: 738 B After Width: | Height: | Size: 738 B |
Before Width: | Height: | Size: 601 B After Width: | Height: | Size: 601 B |
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 4.9 KiB |
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 5.1 KiB |
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 4.6 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 654 B After Width: | Height: | Size: 654 B |
Before Width: | Height: | Size: 611 B After Width: | Height: | Size: 611 B |
Before Width: | Height: | Size: 614 B After Width: | Height: | Size: 614 B |
Before Width: | Height: | Size: 592 B After Width: | Height: | Size: 592 B |
Before Width: | Height: | Size: 589 B After Width: | Height: | Size: 589 B |
Before Width: | Height: | Size: 600 B After Width: | Height: | Size: 600 B |
Before Width: | Height: | Size: 579 B After Width: | Height: | Size: 579 B |
Before Width: | Height: | Size: 589 B After Width: | Height: | Size: 589 B |
Before Width: | Height: | Size: 593 B After Width: | Height: | Size: 593 B |
Before Width: | Height: | Size: 596 B After Width: | Height: | Size: 596 B |
Before Width: | Height: | Size: 587 B After Width: | Height: | Size: 587 B |
Before Width: | Height: | Size: 598 B After Width: | Height: | Size: 598 B |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
33
src/ball.lua
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
|
@ -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")
|
||||||
|
|
|
@ -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
|
|
@ -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
|
|
|
@ -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
|
||||||
|
|
|
@ -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?
|
||||||
|
|
|
@ -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:
|
||||||
---
|
---
|
||||||
|
|
|
@ -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)
|
||||||
|
|
266
src/main.lua
|
@ -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)
|
||||||
|
|
||||||
|
|
40
src/npc.lua
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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 = {
|
||||||
|
|
|
@ -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
|
||||||
|
|
140
src/utils.lua
|
@ -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
|
||||||
|
|