Compare commits

..

3 Commits

Author SHA1 Message Date
Sage Vaillancourt 4c9fbcdee7 More advanced statistics and displays. 2025-02-17 20:17:26 -05:00
Sage Vaillancourt 1ccf8765ee Remove some outdated TODOs and comments 2025-02-17 13:26:51 -05:00
Sage Vaillancourt 5c45b7bba0 Add box score and transitions
Add constants defining the top of the outfield wall (not used yet)
Take scores out of mutable global state (that might be just about all of it sewn up)
Finish switching batttingTeam to a TeamId value
2025-02-17 13:21:28 -05:00
18 changed files with 818 additions and 63 deletions

View File

@ -44,6 +44,7 @@ function Announcer:popIn()
end) end)
end end
---@param text string
function Announcer:say(text) function Announcer:say(text)
self.textQueue[#self.textQueue + 1] = text self.textQueue[#self.textQueue + 1] = text
if #self.textQueue == 1 then if #self.textQueue == 1 then

View File

@ -8,6 +8,8 @@ Glove = playdate.graphics.image.new("images/game/Glove.png")
-- luacheck: ignore -- luacheck: ignore
PlayerFrown = playdate.graphics.image.new("images/game/PlayerFrown.png") PlayerFrown = playdate.graphics.image.new("images/game/PlayerFrown.png")
-- luacheck: ignore -- luacheck: ignore
BigBat = playdate.graphics.image.new("images/game/BigBat.png")
-- luacheck: ignore
GloveHoldingBall = playdate.graphics.image.new("images/game/GloveHoldingBall.png") GloveHoldingBall = playdate.graphics.image.new("images/game/GloveHoldingBall.png")
-- luacheck: ignore -- luacheck: ignore
GameLogo = playdate.graphics.image.new("images/game/GameLogo.png") GameLogo = playdate.graphics.image.new("images/game/GameLogo.png")
@ -24,6 +26,8 @@ Minimap = playdate.graphics.image.new("images/game/Minimap.png")
-- luacheck: ignore -- luacheck: ignore
GrassBackground = playdate.graphics.image.new("images/game/GrassBackground.png") GrassBackground = playdate.graphics.image.new("images/game/GrassBackground.png")
-- luacheck: ignore -- luacheck: ignore
GrassBackgroundSmall = playdate.graphics.image.new("images/game/GrassBackgroundSmall.png")
-- luacheck: ignore
LightPlayerBase = playdate.graphics.image.new("images/game/LightPlayerBase.png") LightPlayerBase = playdate.graphics.image.new("images/game/LightPlayerBase.png")
-- luacheck: ignore -- luacheck: ignore
LightPlayerBack = playdate.graphics.image.new("images/game/LightPlayerBack.png") LightPlayerBack = playdate.graphics.image.new("images/game/LightPlayerBack.png")

View File

@ -91,8 +91,6 @@ C.SmallestBallRadius = 6
C.BatLength = 35 C.BatLength = 35
-- TODO: enums implemented this way are probably going to be difficult to serialize!
---@alias OffenseState "batting" | "running" | "walking" ---@alias OffenseState "batting" | "running" | "walking"
--- An enum for what state the offense is in --- An enum for what state the offense is in
---@type table<string, OffenseState> ---@type table<string, OffenseState>
@ -125,6 +123,18 @@ C.WalkedRunnerSpeed = 10
C.ResetFieldersAfterSeconds = 2.5 C.ResetFieldersAfterSeconds = 2.5
C.OutfieldWall = {
{ x = 0, y = 137 },
{ x = 233, y = 32 },
{ x = 450, y = 29 },
{ x = 550, y = 59 },
{ x = 739, y = 64 },
{ x = 850, y = 19 },
{ x = 1100, y = 31 },
{ x = 1185, y = 181 },
{ x = 1201, y = 224 },
}
if not playdate then if not playdate then
return C return C
end end

View File

@ -35,6 +35,67 @@ function dbg.loadTheBases(br)
br.runners[4].nextBase = C.Bases[C.Home] br.runners[4].nextBase = C.Bases[C.Home]
end end
local hitSamples = {
away = {
{
utils.xy(7.88733, -16.3434),
utils.xy(378.3376, 30.49521),
utils.xy(367.1036, 21.55336),
},
{
utils.xy(379.8051, -40.82794),
utils.xy(-444.5791, -30.30901),
utils.xy(-30.43079, -30.50307),
},
{
utils.xy(227.8881, -14.56854),
utils.xy(293.5208, 39.38919),
utils.xy(154.4738, -26.55899),
},
},
home = {
{
utils.xy(146.2505, -89.12155),
utils.xy(429.5428, 59.62944),
utils.xy(272.4666, -78.578),
},
{
utils.xy(485.0516, 112.8341),
utils.xy(290.9232, -4.946442),
utils.xy(263.4262, -6.482407),
},
{
utils.xy(260.6927, -63.63049),
utils.xy(392.1548, -44.22421),
utils.xy(482.5545, 105.3476),
utils.xy(125.5928, 18.53091),
},
},
}
---@return Statistics
function dbg.mockStatistics(inningCount)
inningCount = inningCount or 9
local stats = Statistics.new()
for i = 1, inningCount - 1 do
stats.innings[i].home.score = math.floor(math.random() * 5)
stats.innings[i].away.score = math.floor(math.random() * 5)
if hitSamples.home[i] ~= nil then
stats.innings[i].home.hits = hitSamples.home[i]
end
if hitSamples.away[i] ~= nil then
stats.innings[i].away.hits = hitSamples.away[i]
end
stats:pushInning()
end
local homeScore, awayScore = utils.totalScores(stats)
if homeScore == awayScore then
stats.innings[#stats.innings].home.score = 1 + stats.innings[#stats.innings].home.score
end
return stats
end
---@param points XyPair[] ---@param points XyPair[]
function dbg.drawLine(points) function dbg.drawLine(points)
for i = 2, #points do for i = 2, #points do

248
src/draw/box-score.lua Normal file
View File

@ -0,0 +1,248 @@
---@alias TeamInningData { score: number, pitching: { balls: number, strikes: number }, hits: XyPair[] }
--- E.g. statistics[1].home.pitching.balls
---@class Statistics
---@field innings: (table<TeamId, TeamInningData>)[]
Statistics = {}
local function newTeamInning()
return {
score = 0,
pitching = {
balls = 0,
strikes = 0,
},
hits = {},
}
end
---@return table<TeamId, TeamInningData>
local function newInning()
return {
home = newTeamInning(),
away = newTeamInning(),
}
end
---@return Statistics
function Statistics.new()
return setmetatable({
innings = { newInning() },
}, { __index = Statistics })
end
function Statistics:pushInning()
self.innings[#self.innings + 1] = newInning()
end
---@class BoxScore
---@field stats Statistics
---@field private targetY number
BoxScore = {}
---@param stats Statistics
function BoxScore.new(stats)
return setmetatable({
stats = stats,
targetY = 0,
}, { __index = BoxScore })
end
-- TODO: Convert the box-score into a whole "scene"
-- * Scroll left and right through games that go into extra innings
-- * Scroll up and down through other stats.
-- + Balls and strikes
-- + Batting average
-- + Graph of team scores over time
-- + Farthest hit ball
local MarginY <const> = 70
local SmallFont <const> = playdate.graphics.font.new("fonts/font-full-circle.pft")
local ScoreFont <const> = playdate.graphics.font.new("fonts/Asheville-Sans-14-Bold.pft")
local NumWidth <const> = ScoreFont:getTextWidth("0")
local NumHeight <const> = ScoreFont:getHeight()
local AwayWidth <const> = ScoreFont:getTextWidth("AWAY")
local InningMargin = 4
local InningDrawWidth <const> = (InningMargin * 2) + (NumWidth * 2)
local ScoreDrawHeight = NumHeight * 2
-- luacheck: ignore 143
---@type pd_graphics_lib
local gfx = playdate.graphics
local function formatScore(n)
if n <= 9 then
return " " .. n
elseif n <= 19 then
return " " .. n
else
return tostring(n)
end
end
local HomeY <const> = -4 + (NumHeight * 2) + MarginY
local AwayY <const> = -4 + (NumHeight * 3) + MarginY
local function drawInning(x, inningNumber, homeScore, awayScore)
gfx.setColor(gfx.kColorBlack)
gfx.setColor(gfx.kColorWhite)
gfx.setLineWidth(1)
gfx.drawRect(x, 34 + MarginY, InningDrawWidth, ScoreDrawHeight)
inningNumber = " " .. inningNumber
homeScore = formatScore(homeScore)
awayScore = formatScore(awayScore)
x = x - 8 + (InningDrawWidth / 2)
ScoreFont:drawTextAligned(inningNumber, x, -4 + NumHeight + MarginY, gfx.kAlignRight)
ScoreFont:drawTextAligned(awayScore, x, HomeY, gfx.kAlignRight)
ScoreFont:drawTextAligned(homeScore, x, AwayY, gfx.kAlignRight)
end
function BoxScore:drawBoxScore()
local inningStart = 4 + (AwayWidth * 1.5)
local widthAndMarg = InningDrawWidth + 4
ScoreFont:drawTextAligned(" HOME", 10, HomeY, gfx.kAlignRight)
ScoreFont:drawTextAligned("AWAY", 10, AwayY, gfx.kAlignRight)
for i = 1, #self.stats.innings do
local inningStats = self.stats.innings[i]
drawInning(inningStart + ((i - 1) * widthAndMarg), i, inningStats.home.score, inningStats.away.score)
end
local homeScore, awayScore = utils.totalScores(self.stats)
drawInning(4 + inningStart + (widthAndMarg * #self.stats.innings), "F", homeScore, awayScore)
ScoreFont:drawTextAligned("v", C.Center.x, C.Screen.H - 40, gfx.kAlignCenter)
end
local GraphM = 10
local GraphW = C.Screen.W - (GraphM * 2)
local GraphH = C.Screen.H - (GraphM * 2)
function BoxScore:drawScoreGraph(y)
-- TODO: Actually draw score legend
-- Offset by 2 to support a) the zero-index b) the score legend
local segmentWidth = GraphW / (#self.stats.innings + 2)
local legendX = segmentWidth * (#self.stats.innings + 2) - GraphM
gfx.drawLine(GraphM / 2, y + GraphM + GraphH, legendX, y + GraphM + GraphH)
gfx.drawLine(legendX, y + GraphM, legendX, y + GraphH + GraphM)
gfx.setLineWidth(3)
local homeScore, awayScore = utils.totalScores(self.stats)
local highestScore = math.max(homeScore, awayScore)
local heightPerPoint = (GraphH - 6) / highestScore
function point(inning, score)
return utils.xy(GraphM + (inning * segmentWidth), y + GraphM + GraphH + (score * -heightPerPoint))
end
function drawLine(teamId)
local linePoints = { point(0, 0) }
local scoreTotal = 0
for i, inning in ipairs(self.stats.innings) do
scoreTotal = scoreTotal + inning[teamId].score
linePoints[#linePoints + 1] = point(i, scoreTotal)
end
dbg.drawLine(linePoints)
local finalPoint = linePoints[#linePoints]
SmallFont:drawTextAligned(string.upper(teamId), finalPoint.x + 3, finalPoint.y - 7, gfx.kAlignRight)
end
drawLine("home")
gfx.setDitherPattern(0.5)
drawLine("away")
gfx.setDitherPattern(0)
end
---@param realHit XyPair
---@return XyPair
function convertHitToMini(realHit)
-- Convert to all-positive y
local y = realHit.y + C.Screen.H
y = y / 2
local x = realHit.x + C.Screen.W
x = x / 3
return utils.xy(x, y)
end
function BoxScore:drawHitChart(y)
local leftMargin = 8
GrassBackgroundSmall:drawCentered(C.Center.x, y + C.Center.y + 54)
gfx.setLineWidth(1)
ScoreFont:drawTextAligned("AWAY", leftMargin, y + C.Screen.H - NumHeight, gfx.kAlignRight)
gfx.setColor(gfx.kColorBlack)
gfx.setDitherPattern(0.5, gfx.image.kDitherTypeBayer2x2)
gfx.fillRect(leftMargin, y + C.Screen.H - NumHeight, ScoreFont:getTextWidth("AWAY"), NumHeight)
gfx.setColor(gfx.kColorWhite)
gfx.setDitherPattern(0.5)
for _, inning in ipairs(self.stats.innings) do
for _, hit in ipairs(inning.away.hits) do
local miniHitPos = convertHitToMini(hit)
gfx.fillCircleAtPoint(miniHitPos.x + 10, miniHitPos.y + y, 4)
end
end
gfx.setColor(gfx.kColorWhite)
gfx.setDitherPattern(0)
ScoreFont:drawTextAligned(" HOME", leftMargin, y + C.Screen.H - (NumHeight * 2), gfx.kAlignRight)
for _, inning in ipairs(self.stats.innings) do
for _, hit in ipairs(inning.home.hits) do
local miniHitPos = convertHitToMini(hit)
gfx.fillCircleAtPoint(miniHitPos.x + 10, miniHitPos.y + y, 4)
end
end
end
local screens = {
BoxScore.drawBoxScore,
BoxScore.drawScoreGraph,
BoxScore.drawHitChart,
}
function BoxScore:render()
local originalDrawMode = gfx.getImageDrawMode()
gfx.clear(gfx.kColorBlack)
gfx.setImageDrawMode(gfx.kDrawModeInverted)
gfx.setColor(gfx.kColorBlack)
for i, screen in ipairs(screens) do
screen(self, (i - 1) * C.Screen.H)
end
gfx.setImageDrawMode(originalDrawMode)
end
local renderedImage
function BoxScore:update()
if not renderedImage then
renderedImage = gfx.image.new(C.Screen.W, C.Screen.H * #screens)
gfx.pushContext(renderedImage)
self:render()
gfx.popContext()
end
local deltaSeconds = playdate.getElapsedTime()
playdate.resetElapsedTime()
gfx.setDrawOffset(0, self.targetY)
renderedImage:draw(0, 0)
local crankChange = playdate.getCrankChange()
if crankChange ~= 0 then
self.targetY = self.targetY - (crankChange * 0.8)
else
local closestScreen = math.floor(0.5 + (self.targetY / C.Screen.H)) * C.Screen.H
if math.abs(self.targetY - closestScreen) > 3 then
local needsIncrease = self.targetY < closestScreen
local change = needsIncrease and 200 * deltaSeconds or -200 * deltaSeconds
self.targetY = self.targetY + change
end
end
self.targetY = math.max(math.min(self.targetY, 0), -C.Screen.H * (#screens - 1))
end

View File

@ -83,11 +83,10 @@ local ScoreboardHeight <const> = 55
local Indicator = "> " local Indicator = "> "
local IndicatorWidth <const> = ScoreFont:getTextWidth(Indicator) local IndicatorWidth <const> = ScoreFont:getTextWidth(Indicator)
---@param teams any
---@param battingTeam any ---@param battingTeam any
---@return string, number, string, number ---@return string, number, string, number
function getIndicators(teams, battingTeam) function getIndicators(battingTeam)
if teams.home == battingTeam then if battingTeam == "home" then
return Indicator, 0, "", IndicatorWidth return Indicator, 0, "", IndicatorWidth
end end
return "", IndicatorWidth, Indicator, 0 return "", IndicatorWidth, Indicator, 0
@ -101,11 +100,11 @@ local stats = {
battingTeam = nil, battingTeam = nil,
} }
function drawScoreboardImpl(x, y, teams) function drawScoreboardImpl(x, y)
local homeScore = stats.homeScore local homeScore = stats.homeScore
local awayScore = stats.awayScore local awayScore = stats.awayScore
local homeIndicator, homeOffset, awayIndicator, awayOffset = getIndicators(teams, stats.battingTeam) local homeIndicator, homeOffset, awayIndicator, awayOffset = getIndicators(stats.battingTeam)
local homeScoreText = homeIndicator .. "HOME " .. (homeScore > 9 and homeScore or " " .. homeScore) local homeScoreText = homeIndicator .. "HOME " .. (homeScore > 9 and homeScore or " " .. homeScore)
local awayScoreText = awayIndicator .. "AWAY " .. (awayScore > 9 and awayScore or " " .. awayScore) local awayScoreText = awayIndicator .. "AWAY " .. (awayScore > 9 and awayScore or " " .. awayScore)
@ -146,17 +145,17 @@ end
local newStats = stats local newStats = stats
function drawScoreboard(x, y, teams, outs, battingTeam, inning) function drawScoreboard(x, y, homeScore, awayScore, outs, battingTeam, inning)
if if
newStats.homeScore ~= teams.home.score newStats.homeScore ~= homeScore
or newStats.awayScore ~= teams.away.score or newStats.awayScore ~= awayScore
or newStats.outs ~= outs or newStats.outs ~= outs
or newStats.inning ~= inning or newStats.inning ~= inning
or newStats.battingTeam ~= battingTeam or newStats.battingTeam ~= battingTeam
then then
newStats = { newStats = {
homeScore = teams.home.score, homeScore = homeScore,
awayScore = teams.away.score, awayScore = awayScore,
outs = outs, outs = outs,
inning = inning, inning = inning,
battingTeam = battingTeam, battingTeam = battingTeam,
@ -165,5 +164,5 @@ function drawScoreboard(x, y, teams, outs, battingTeam, inning)
stats = newStats stats = newStats
end) end)
end end
drawScoreboardImpl(x, y, teams) drawScoreboardImpl(x, y)
end end

92
src/draw/transitions.lua Normal file
View File

@ -0,0 +1,92 @@
Transitions = {
---@type Scene | nil
nextScene = nil,
---@type Scene | nil
previousScene = nil,
}
local gfx = playdate.graphics
local previousSceneImage
local previousSceneMask
local nextSceneImage
local batImageTable = {}
local batOffset = 80
local degStep = 3
function loadBatImageTable()
for deg = 90 - (degStep * 3), 270 + (degStep * 3), degStep do
local img = gfx.image.new(C.Screen.W, C.Screen.H)
gfx.pushContext(img)
BigBat:drawRotated(C.Center.x, C.Screen.H + batOffset, 90 + deg)
gfx.popContext()
batImageTable[deg] = img
end
end
loadBatImageTable()
local function update()
local lastAngle
local seamAngle = math.rad(270)
while seamAngle > math.rad(90) do
local deltaSeconds = playdate.getElapsedTime()
playdate.resetElapsedTime()
seamAngle = seamAngle - (deltaSeconds * 3)
local seamAngleDeg = math.floor(math.deg(seamAngle))
seamAngleDeg = seamAngleDeg - (seamAngleDeg % degStep)
-- Skip re-drawing if no change
if lastAngle ~= seamAngleDeg then
lastAngle = seamAngleDeg
nextSceneImage:draw(0, 0)
gfx.pushContext(previousSceneMask)
gfx.setImageDrawMode(gfx.kDrawModeFillBlack)
batImageTable[seamAngleDeg]:draw(0, 0)
gfx.popContext()
previousSceneImage:setMaskImage(previousSceneMask)
previousSceneImage:draw(0, 0)
batImageTable[seamAngleDeg]:draw(0, 0)
end
coroutine.yield()
end
playdate.update = function()
Transitions.nextScene:update()
end
end
---@param nextScene fun() The next playdate.update function
function transitionTo(nextScene)
if not Transitions.nextScene then
error("Expected Transitions to already have nextScene defined! E.g. by calling transitionBetween")
end
local previousScene = Transitions.nextScene
transitionBetween(previousScene, nextScene)
end
---@param previousScene Scene Has the current playdate.update function
---@param nextScene Scene Has the next playdate.update function
function transitionBetween(previousScene, nextScene)
playdate.wait(2) -- TODO: There's some sort of timing wack here.
playdate.update = update
previousSceneImage = gfx.image.new(C.Screen.W, C.Screen.H)
gfx.pushContext(previousSceneImage)
previousScene:update()
gfx.popContext()
nextSceneImage = gfx.image.new(C.Screen.W, C.Screen.H)
gfx.pushContext(nextSceneImage)
nextScene:update()
gfx.popContext()
previousSceneMask = gfx.image.new(C.Screen.W, C.Screen.H, gfx.kColorWhite)
previousSceneImage:setMaskImage(previousSceneMask)
Transitions.nextScene = nextScene
Transitions.previousScene = previousScene
end

View File

@ -5,8 +5,6 @@
--- @field target XyPair | nil --- @field target XyPair | nil
--- @field speed number --- @field speed number
-- TODO: Run down baserunners in a pickle.
---@class Fielding ---@class Fielding
---@field fielders table<string, Fielder> ---@field fielders table<string, Fielder>
---@field fielderHoldingBall Fielder | nil ---@field fielderHoldingBall Fielder | nil
@ -182,7 +180,7 @@ function Fielding:userThrowTo(targetBase, ball, throwFlyMs)
end) end)
end end
function Fielding:celebrate() function Fielding.celebrate()
FielderDanceAnimator:reset(C.DanceBounceMs) FielderDanceAnimator:reset(C.DanceBounceMs)
end end

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -0,0 +1,295 @@
tracking=1
space 3
! 2
" 5
# 9
$ 8
% 12
& 11
' 3
( 5
) 5
* 8
+ 8
, 3
- 6
. 2
/ 6
0 9
1 4
2 9
3 9
4 9
5 9
6 9
7 9
8 10
9 9
: 2
; 2
< 7
= 7
> 7
? 9
@ 11
A 10
B 9
C 9
D 9
E 8
F 8
G 9
H 9
I 2
J 8
K 10
L 9
M 12
N 9
O 9
P 9
Q 9
R 9
S 9
T 10
U 9
V 10
W 14
X 8
Y 8
Z 8
[ 3
\ 6
] 3
^ 6
_ 8
` 3
a 8
b 8
c 8
d 8
e 8
f 6
g 8
h 8
i 2
j 4
k 8
l 2
m 12
n 8
o 8
p 8
q 8
r 6
s 8
t 6
u 8
v 8
w 12
x 9
y 8
z 8
{ 6
| 2
} 6
~ 10
… 8
¥ 8
‼ 5
™ 8
© 11
® 11
。 16
、 16
ぁ 16
あ 16
ぃ 16
い 16
ぅ 16
う 16
ぇ 16
え 16
ぉ 16
お 16
か 16
が 16
き 16
ぎ 16
く 16
ぐ 16
け 16
げ 16
こ 16
ご 16
さ 16
ざ 16
し 16
じ 16
す 16
ず 16
せ 16
ぜ 16
そ 16
ぞ 16
た 16
だ 16
ち 16
ぢ 16
っ 16
つ 16
づ 16
て 16
で 16
と 16
ど 16
な 16
に 16
ぬ 16
ね 16
の 16
は 16
ば 16
ぱ 16
ひ 16
び 16
ぴ 16
ふ 16
ぶ 16
ぷ 16
へ 16
べ 16
ぺ 16
ほ 16
ぼ 16
ぽ 16
ま 16
み 16
む 16
め 16
も 16
ゃ 16
や 16
ゅ 16
ゆ 16
ょ 16
よ 16
ら 16
り 16
る 16
れ 16
ろ 16
ゎ 16
わ 16
ゐ 16
ゑ 16
を 16
ん 16
ゔ 16
ゕ 16
ゖ 16
゛ 1
゜ 0
ゝ 16
ゞ 16
ゟ 16
16
ァ 16
ア 16
ィ 16
イ 16
ゥ 16
ウ 16
ェ 16
エ 16
ォ 16
オ 16
カ 16
ガ 16
キ 16
ギ 16
ク 16
グ 16
ケ 16
ゲ 16
コ 16
ゴ 16
サ 16
ザ 16
シ 16
ジ 16
ス 16
ズ 16
セ 16
ゼ 16
ソ 16
ゾ 16
タ 16
ダ 16
チ 16
ヂ 16
ッ 16
ツ 16
ヅ 16
テ 16
デ 16
ト 16
ド 16
ナ 16
ニ 16
ヌ 16
ネ 16
16
ハ 16
バ 16
パ 16
ヒ 16
ビ 16
ピ 16
フ 16
ブ 16
プ 16
ヘ 16
ベ 16
ペ 16
ホ 16
ボ 16
ポ 16
マ 16
ミ 16
ム 16
メ 16
モ 16
ャ 16
ヤ 16
ュ 16
ユ 16
ョ 16
ヨ 16
ラ 16
リ 16
ル 16
レ 16
ロ 16
ヮ 16
ワ 16
ヰ 16
ヱ 16
ヲ 16
ン 16
ヴ 16
ヵ 16
ヶ 16
ヷ 16
ヸ 16
ヹ 16
ヺ 16
・ 16
ー 16
ヽ 16
ヾ 16
ヿ 16
「 16
」 16
円 16
<EFBFBD> 13

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

BIN
src/images/game/BigBat.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

View File

@ -24,9 +24,7 @@ local function startGame()
awayTeamSprites = AwayTeamSprites, awayTeamSprites = AwayTeamSprites,
}) })
playdate.resetElapsedTime() playdate.resetElapsedTime()
playdate.update = function() transitionBetween(MainMenu, next)
next:update()
end
end end
local function pausingEaser(baseEaser) local function pausingEaser(baseEaser)
@ -64,9 +62,10 @@ local function arrayElementFromCrank(array, crankPosition)
return array[i] return array[i]
end end
local currentLogo = nil local currentLogo
function MainMenu.update() --luacheck: ignore
function MainMenu:update()
playdate.timer.updateTimers() playdate.timer.updateTimers()
crankStartPos = crankStartPos or playdate.getCrankPosition() crankStartPos = crankStartPos or playdate.getCrankPosition()

View File

@ -8,6 +8,8 @@ import 'CoreLibs/timer.lua'
import 'CoreLibs/ui.lua' import 'CoreLibs/ui.lua'
-- stylua: ignore end -- stylua: ignore end
--- @alias Scene { update: fun(self: self) }
--- @alias EasingFunc fun(number, number, number, number): number --- @alias EasingFunc fun(number, number, number, number): number
--- @alias LaunchBall fun( --- @alias LaunchBall fun(
@ -24,9 +26,11 @@ import 'CoreLibs/ui.lua'
import 'utils.lua' import 'utils.lua'
import 'constants.lua' import 'constants.lua'
import 'assets.lua' import 'assets.lua'
import 'draw/player.lua' import 'draw/box-score.lua'
import 'draw/overlay.lua'
import 'draw/fielder.lua' import 'draw/fielder.lua'
import 'draw/overlay.lua'
import 'draw/player.lua'
import 'draw/transitions.lua'
import 'main-menu.lua' import 'main-menu.lua'
@ -44,15 +48,13 @@ import 'npc.lua'
local gfx <const>, C <const> = playdate.graphics, C local gfx <const>, C <const> = playdate.graphics, C
---@alias Team { score: number, benchPosition: XyPair } ---@alias Team { benchPosition: XyPair }
---@type table<TeamId, Team> ---@type table<TeamId, Team>
local teams <const> = { local teams <const> = {
home = { home = {
score = 0, -- TODO: Extract this last bit of global mutable state.
benchPosition = utils.xy(C.Screen.W + 10, C.Center.y), benchPosition = utils.xy(C.Screen.W + 10, C.Center.y),
}, },
away = { away = {
score = 0,
benchPosition = utils.xy(-10, C.Center.y), benchPosition = utils.xy(-10, C.Center.y),
}, },
} }
@ -73,6 +75,7 @@ local teams <const> = {
---@field catcherThrownBall boolean ---@field catcherThrownBall boolean
---@field offenseState OffenseState ---@field offenseState OffenseState
---@field inning number ---@field inning number
---@field stats Statistics
---@field batBase XyPair ---@field batBase XyPair
---@field batTip XyPair ---@field batTip XyPair
---@field batAngleDeg number ---@field batAngleDeg number
@ -104,8 +107,6 @@ Game = {}
---@param state MutableState | nil ---@param state MutableState | nil
---@return Game ---@return Game
function Game.new(settings, announcer, fielding, baserunning, npc, state) function Game.new(settings, announcer, fielding, baserunning, npc, state)
teams.away.score = 0
teams.home.score = 0
announcer = announcer or Announcer.new() announcer = announcer or Announcer.new()
fielding = fielding or Fielding.new() fielding = fielding or Fielding.new()
settings.userTeam = nil -- "away" settings.userTeam = nil -- "away"
@ -137,6 +138,7 @@ function Game.new(settings, announcer, fielding, baserunning, npc, state)
battingTeamSprites = settings.awayTeamSprites, battingTeamSprites = settings.awayTeamSprites,
fieldingTeamSprites = settings.homeTeamSprites, fieldingTeamSprites = settings.homeTeamSprites,
runnerBlipper = runnerBlipper, runnerBlipper = runnerBlipper,
stats = Statistics.new(),
}, },
}, { __index = Game }) }, { __index = Game })
@ -210,10 +212,6 @@ local function getOppositeTeamId(teamId)
end end
end end
function Game:getBattingTeam()
return teams[self.state.battingTeam] or error("Unknown battingTeam: " .. (self.state.battingTeam or "nil"))
end
function Game:getFieldingTeam() function Game:getFieldingTeam()
return teams[getOppositeTeamId(self.state.battingTeam)] return teams[getOppositeTeamId(self.state.battingTeam)]
end end
@ -265,24 +263,33 @@ end
function Game:nextHalfInning() function Game:nextHalfInning()
pitchTracker:reset() pitchTracker:reset()
local gameOver = self.state.inning == self.settings.finalInning and teams.away.score ~= teams.home.score local homeScore, awayScore = utils.totalScores(self.state.stats)
if not gameOver then -- TODO end the game if away team just batted and home team is winning
self.fielding:celebrate() local gameOver = self.state.battingTeam == "home"
self.state.secondsSinceLastRunnerMove = -7 and self.state.inning == self.settings.finalInning
self.fielding:benchTo(self:getFieldingTeam().benchPosition) and awayScore ~= homeScore
self.announcer:say("SWITCHING SIDES...")
end
if gameOver then if gameOver then
self.announcer:say("AND THAT'S THE BALL GAME!") self.announcer:say("AND THAT'S THE BALL GAME!")
else playdate.timer.new(3000, function()
transitionTo(BoxScore.new(self.state.stats))
end)
return
end
Fielding.celebrate()
self.state.secondsSinceLastRunnerMove = -7
self.fielding:benchTo(self:getFieldingTeam().benchPosition)
self.announcer:say("SWITCHING SIDES...")
self.fielding:resetFielderPositions() self.fielding:resetFielderPositions()
if self.state.battingTeam == teams.home then if self.state.battingTeam == "home" then
self.state.inning = self.state.inning + 1 self.state.inning = self.state.inning + 1
self.state.stats:pushInning()
end end
self.state.battingTeam = getOppositeTeamId(self.state.battingTeam) self.state.battingTeam = getOppositeTeamId(self.state.battingTeam)
playdate.timer.new(2000, function() playdate.timer.new(2000, function()
if self.state.battingTeam == teams.home then if self.state.battingTeam == "home" then
self.state.battingTeamSprites = self.settings.homeTeamSprites self.state.battingTeamSprites = self.settings.homeTeamSprites
self.state.runnerBlipper = self.homeTeamBlipper self.state.runnerBlipper = self.homeTeamBlipper
self.state.fieldingTeamSprites = self.settings.awayTeamSprites self.state.fieldingTeamSprites = self.settings.awayTeamSprites
@ -293,12 +300,21 @@ function Game:nextHalfInning()
end end
end) end)
end end
---@return TeamInningData
function Game:battingTeamCurrentInning()
return self.state.stats.innings[self.state.inning][self.state.battingTeam]
end
---@return TeamInningData
function Game:fieldingTeamCurrentInning()
return self.state.stats.innings[self.state.inning][getOppositeTeamId(self.state.battingTeam)]
end end
---@param scoredRunCount number ---@param scoredRunCount number
function Game:score(scoredRunCount) function Game:score(scoredRunCount)
local batting = self:getBattingTeam() self:battingTeamCurrentInning().score = self:battingTeamCurrentInning().score + scoredRunCount
batting.score = batting.score + scoredRunCount
self.announcer:say("SCORE!") self.announcer:say("SCORE!")
end end
@ -394,6 +410,8 @@ function Game:updateBatting(batDeg, batSpeed)
local hitBallScaler = gfx.animator.new(2000, 9 + (mult * mult * 0.5), C.SmallestBallRadius, utils.easingHill) local hitBallScaler = gfx.animator.new(2000, 9 + (mult * mult * 0.5), C.SmallestBallRadius, utils.easingHill)
self.state.ball:launch(ballDestX, ballDestY, playdate.easingFunctions.outQuint, 2000, nil, hitBallScaler) self.state.ball:launch(ballDestX, ballDestY, playdate.easingFunctions.outQuint, 2000, nil, hitBallScaler)
-- 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()
battingTeamStats.hits[#battingTeamStats.hits + 1] = utils.xy(ballDestX, ballDestY)
if utils.isFoulBall(ballDestX, ballDestY) then if utils.isFoulBall(ballDestX, ballDestY) then
self.announcer:say("Foul ball!") self.announcer:say("Foul ball!")
@ -401,7 +419,6 @@ function Game:updateBatting(batDeg, batSpeed)
-- TODO: Have a fielder chase for the fly-out -- TODO: Have a fielder chase for the fly-out
return return
end end
self.baserunning:convertBatterToRunner() self.baserunning:convertBatterToRunner()
self.fielding:haveSomeoneChase(ballDestX, ballDestY) self.fielding:haveSomeoneChase(ballDestX, ballDestY)
@ -465,6 +482,13 @@ function Game:updateGameState()
if self.state.secondsSincePitchAllowed > C.ReturnToPitcherAfterSeconds and not self.state.catcherThrownBall then if self.state.secondsSincePitchAllowed > C.ReturnToPitcherAfterSeconds and not self.state.catcherThrownBall then
local outcome = pitchTracker:updatePitchCounts() local outcome = pitchTracker:updatePitchCounts()
local currentPitchingStats = self:fieldingTeamCurrentInning().pitching
if outcome == PitchOutcomes.Strike or outcome == PitchOutcomes.StrikeOut then
currentPitchingStats.strikes = currentPitchingStats.strikes + 1
end
if outcome == PitchOutcomes.Ball or outcome == PitchOutcomes.Walk then
currentPitchingStats.balls = currentPitchingStats.balls + 1
end
if outcome == PitchOutcomes.StrikeOut then if outcome == PitchOutcomes.StrikeOut then
self:strikeOut() self:strikeOut()
elseif outcome == PitchOutcomes.Walk then elseif outcome == PitchOutcomes.Walk then
@ -488,7 +512,6 @@ function Game:updateGameState()
self:updateBatting(self.state.batAngleDeg, batSpeed) self:updateBatting(self.state.batAngleDeg, batSpeed)
-- Walk batter to the plate -- Walk batter to the plate
-- TODO: Ensure batter can't be nil, here
self.baserunning:updateRunner(self.baserunning.batter, nil, crankLimited, self.state.deltaSeconds) self.baserunning:updateRunner(self.baserunning.batter, nil, crankLimited, self.state.deltaSeconds)
if self.state.secondsSincePitchAllowed > C.PitchAfterSeconds then if self.state.secondsSincePitchAllowed > C.PitchAfterSeconds then
@ -513,7 +536,8 @@ function Game:updateGameState()
if self.state.secondsSinceLastRunnerMove > C.ResetFieldersAfterSeconds then if self.state.secondsSinceLastRunnerMove > C.ResetFieldersAfterSeconds then
-- End of play. Throw the ball back to the pitcher -- End of play. Throw the ball back to the pitcher
self.state.ball:launch(C.PitchStartX, C.PitchStartY, playdate.easingFunctions.linear, nil, true) self.state.ball:launch(C.PitchStartX, C.PitchStartY, playdate.easingFunctions.linear, nil, true)
self.fielding:markAllIneligible() -- This is ugly, and ideally would not be necessary if Fielding handled the return throw directly. -- This is ugly, and ideally would not be necessary if Fielding handled the return throw directly.
self.fielding:markAllIneligible()
self.fielding:resetFielderPositions() self.fielding:resetFielderPositions()
self.state.offenseState = C.Offense.batting self.state.offenseState = C.Offense.batting
-- TODO: Remove, or replace with nextBatter() -- TODO: Remove, or replace with nextBatter()
@ -547,8 +571,6 @@ function Game:updateGameState()
actionQueue:runWaiting(self.state.deltaSeconds) actionQueue:runWaiting(self.state.deltaSeconds)
end end
-- TODO: Swappable update() for main menu, etc.
function Game:update() function Game:update()
playdate.timer.updateTimers() playdate.timer.updateTimers()
gfx.animation.blinker.updateAll() gfx.animation.blinker.updateAll()
@ -629,11 +651,21 @@ function Game:update()
if math.abs(offsetX) > 10 or math.abs(offsetY) > 10 then if math.abs(offsetX) > 10 or math.abs(offsetY) > 10 then
drawMinimap(self.baserunning.runners, self.fielding.fielders) drawMinimap(self.baserunning.runners, self.fielding.fielders)
end end
drawScoreboard(0, C.Screen.H * 0.77, teams, self.baserunning.outs, self:getBattingTeam(), self.state.inning) local homeScore, awayScore = utils.totalScores(self.state.stats)
drawScoreboard(
0,
C.Screen.H * 0.77,
homeScore,
awayScore,
self.baserunning.outs,
self.state.battingTeam,
self.state.inning
)
drawBallsAndStrikes(290, C.Screen.H - 20, pitchTracker.balls, pitchTracker.strikes) drawBallsAndStrikes(290, C.Screen.H - 20, pitchTracker.balls, pitchTracker.strikes)
self.announcer:draw(C.Center.x, 10) self.announcer:draw(C.Center.x, 10)
if playdate.isCrankDocked() then if playdate.isCrankDocked() then
-- luacheck: ignore
playdate.ui.crankIndicator:draw() playdate.ui.crankIndicator:draw()
end end
end end

View File

@ -22,6 +22,7 @@ end
---@param catcherThrownBall boolean ---@param catcherThrownBall boolean
---@param deltaSec number ---@param deltaSec number
---@return number ---@return number
-- luacheck: no unused
function Npc:updateBatAngle(ball, catcherThrownBall, deltaSec) function Npc:updateBatAngle(ball, catcherThrownBall, deltaSec)
if not catcherThrownBall and ball.y > 200 and ball.y < 230 and (ball.x < C.Center.x + 15) then if not catcherThrownBall and ball.y > 200 and ball.y < 230 and (ball.x < C.Center.x + 15) then
npcBatDeg = npcBatDeg + (deltaSec * npcBatSpeed) npcBatDeg = npcBatDeg + (deltaSec * npcBatSpeed)

View File

@ -232,8 +232,23 @@ function utils.getNearestOf(array, x, y, extraCondition)
return nearest, nearestDistance return nearest, nearestDistance
end end
---@param stats Statistics
---@return number homeScore, number awayScore
function utils.totalScores(stats)
local homeScore = 0
local awayScore = 0
for _, inning in pairs(stats.innings) do
homeScore = homeScore + inning.home.score
awayScore = awayScore + inning.away.score
end
return homeScore, awayScore
end
PitchOutcomes = { PitchOutcomes = {
Strike = {},
StrikeOut = {}, StrikeOut = {},
Ball = {},
Walk = {}, Walk = {},
} }