Sort runners/fielders by `y` for draw order

Pulled Fielding:drawFielders() back out into main.lua for this.
Also switching some table-keyed enums to string literal types.
This commit is contained in:
Sage Vaillancourt 2025-02-15 22:13:37 -05:00
parent 51855e13cf
commit bbaaca4a2d
4 changed files with 93 additions and 59 deletions

View File

@ -31,7 +31,7 @@ C.FieldHeight = C.Bases[C.Home].y - C.Bases[C.Second].y
-- Pseudo-base for batter to target
C.RightHandedBattersBox = {
x = C.Bases[C.Home].x - 30,
y = C.Bases[C.Home].y,
y = C.Bases[C.Home].y + 10,
}
---@type table<Base, Base | nil>
@ -94,22 +94,16 @@ C.BatLength = 35
-- TODO: enums implemented this way are probably going to be difficult to serialize!
---@alias OffenseState table
---@alias OffenseState "batting" | "running" | "walking"
--- An enum for what state the offense is in
---@type table<string, OffenseState>
C.Offense = {
batting = {},
running = {},
walking = {},
batting = "batting",
running = "running",
walking = "walking",
}
---@alias Side table
--- An enum for which side (offense or defense) a team is on.
---@type table<string, Side>
C.Sides = {
offense = {},
defense = {},
}
---@alias Side "offense" | "defense"
C.PitcherStartPos = {
x = C.Screen.W * 0.48,

View File

@ -13,7 +13,8 @@
---@field fielderHoldingBall Fielder | nil
Fielding = {}
local FielderDanceAnimator <const> = playdate.graphics.animator.new(1, 10, 0, utils.easingHill)
-- selene: allow(unscoped_variables)
FielderDanceAnimator = playdate.graphics.animator.new(1, 10, 0, utils.easingHill)
FielderDanceAnimator.repeatCount = C.DanceBounceCount - 1
---@param name string
@ -187,18 +188,6 @@ function Fielding:celebrate()
FielderDanceAnimator:reset(C.DanceBounceMs)
end
---@param fielderSprites SpriteCollection
---@param ball Point3d
---@return boolean ballIsHeldByAFielder
function Fielding:drawFielders(fielderSprites, ball)
local ballIsHeld = false
local danceOffset = FielderDanceAnimator:currentValue()
for _, fielder in pairs(self.fielders) do
ballIsHeld = drawFielder(fielderSprites, ball, fielder.x, fielder.y + danceOffset) or ballIsHeld
end
return ballIsHeld
end
if not playdate or playdate.TEST_MODE then
return { Fielding, newFielder }
end

View File

@ -46,10 +46,10 @@ import 'npc.lua'
local gfx <const>, C <const> = playdate.graphics, C
---@alias Team { score: number, benchPosition: XyPair }
---@type table<string, Team>
---@type table<TeamId, Team>
local teams <const> = {
home = {
score = 0,
score = 0, -- TODO: Extract this last bit of global mutable state.
benchPosition = utils.xy(C.Screen.W + 10, C.Center.y),
},
away = {
@ -58,17 +58,19 @@ local teams <const> = {
},
}
---@alias TeamId 'home' | 'away'
--- Well, maybe not "Settings", but passive state that probably won't change much, if at all, during a game.
---@class Settings
---@field finalInning number
---@field userTeam Team | nil
---@field userTeam TeamId | nil
---@field awayTeamSprites SpriteCollection
---@field homeTeamSprites SpriteCollection
---@class MutableState
---@field deltaSeconds number
---@field ball Ball
---@field battingTeam Team
---@field battingTeam TeamId
---@field catcherThrownBall boolean
---@field offenseState OffenseState
---@field inning number
@ -104,20 +106,24 @@ Game = {}
---@param state MutableState | nil
---@return Game
function Game.new(settings, announcer, fielding, baserunning, npc, state)
teams.away.score = 0
teams.home.score = 0
announcer = announcer or Announcer.new()
fielding = fielding or Fielding.new()
settings.userTeam = teams.away
settings.userTeam = nil -- "away"
local homeTeamBlipper = blipper.new(100, settings.homeTeamSprites.smiling, settings.homeTeamSprites.lowHat)
local awayTeamBlipper = blipper.new(100, settings.awayTeamSprites.smiling, settings.awayTeamSprites.lowHat)
local battingTeam = teams.away
local runnerBlipper = battingTeam == teams.away and awayTeamBlipper or homeTeamBlipper
local battingTeam = "away"
local runnerBlipper = battingTeam == "away" and awayTeamBlipper or homeTeamBlipper
local ball = Ball.new(gfx.animator)
local o = setmetatable({
settings = settings,
announcer = announcer,
fielding = fielding,
homeTeamBlipper = homeTeamBlipper,
awayTeamBlipper = awayTeamBlipper,
state = state or {
batBase = utils.xy(C.Center.x - 34, 215),
batTip = utils.xy(0, 0),
@ -132,8 +138,6 @@ function Game.new(settings, announcer, fielding, baserunning, npc, state)
secondsSincePitchAllowed = 0,
battingTeamSprites = settings.awayTeamSprites,
fieldingTeamSprites = settings.homeTeamSprites,
homeTeamBlipper = homeTeamBlipper,
awayTeamBlipper = awayTeamBlipper,
runnerBlipper = runnerBlipper,
},
}, { __index = Game })
@ -196,6 +200,27 @@ local Pitches <const> = {
end,
}
---@param teamId TeamId
---@return TeamId
local function getOppositeTeamId(teamId)
if teamId == "home" then
return "away"
elseif teamId == "away" then
return "home"
else
error("Unknown TeamId: " .. (teamId or "nil"))
end
end
function Game:getBattingTeam()
return teams[self.state.battingTeam] or error("Unknown battingTeam: " .. (self.state.battingTeam or "nil"))
end
function Game:getFieldingTeam()
return teams[getOppositeTeamId(self.state.battingTeam)]
end
---@param side Side
---@return boolean userIsOnSide, boolean userIsOnOtherSide
function Game:userIsOn(side)
if self.settings.userTeam == nil then
@ -204,9 +229,9 @@ function Game:userIsOn(side)
end
local ret
if self.settings.userTeam == self.state.battingTeam then
ret = side == C.Sides.offense
ret = side == "offense"
else
ret = side == C.Sides.defense
ret = side == "defense"
end
return ret, not ret
end
@ -242,12 +267,11 @@ end
function Game:nextHalfInning()
pitchTracker:reset()
local currentlyFieldingTeam = self.state.battingTeam == teams.home and teams.away or teams.home
local gameOver = self.state.inning == self.settings.finalInning and teams.away.score ~= teams.home.score
if not gameOver then
self.fielding:celebrate()
self.state.secondsSinceLastRunnerMove = -7
self.fielding:benchTo(currentlyFieldingTeam.benchPosition)
self.fielding:benchTo(self:getFieldingTeam().benchPosition)
self.announcer:say("SWITCHING SIDES...")
end
@ -258,7 +282,7 @@ function Game:nextHalfInning()
if self.state.battingTeam == teams.home then
self.state.inning = self.state.inning + 1
end
self.state.battingTeam = currentlyFieldingTeam
self.state.battingTeam = getOppositeTeamId(self.state.battingTeam)
playdate.timer.new(2000, function()
if self.state.battingTeam == teams.home then
self.state.battingTeamSprites = self.settings.homeTeamSprites
@ -275,7 +299,8 @@ end
---@param scoredRunCount number
function Game:score(scoredRunCount)
self.state.battingTeam.score = self.state.battingTeam.score + scoredRunCount
local batting = self:getBattingTeam()
batting.score = batting.score + scoredRunCount
self.announcer:say("SCORE!")
end
@ -422,7 +447,7 @@ function Game:updateGameState()
self.state.ball:updatePosition()
local userOnOffense, userOnDefense = self:userIsOn(C.Sides.offense)
local userOnOffense, userOnDefense = self:userIsOn("offense")
if userOnDefense then
throwMeter:applyCharge(self.state.deltaSeconds, crankLimited)
@ -540,28 +565,54 @@ function Game:update()
GrassBackground:draw(-400, -240)
local ballIsHeld = self.fielding:drawFielders(self.state.fieldingTeamSprites, ball)
---@type { y: number, drawAction: fun() }[]
local characterDraws = {}
function addDraw(y, drawAction)
characterDraws[#characterDraws + 1] = { y = y, drawAction = drawAction }
end
local danceOffset = FielderDanceAnimator:currentValue()
local ballIsHeld = false
for _, fielder in pairs(self.fielding.fielders) do
addDraw(fielder.y + danceOffset, function()
ballIsHeld = drawFielder(
self.state.fieldingTeamSprites,
self.state.ball,
fielder.x,
fielder.y + danceOffset
) or ballIsHeld
end)
end
local playerHeightOffset = 20
-- TODO? Scale sprites down as y increases
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.back:draw(runner.x, runner.y - playerHeightOffset)
else
self.state.battingTeamSprites.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)
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(5)
gfx.drawLine(self.state.batBase.x, self.state.batBase.y, self.state.batTip.x, self.state.batTip.y)
end
local playerHeightOffset = 10
-- TODO? Scale sprites down as y increases
for _, runner in pairs(self.baserunning.runners) do
if runner == self.baserunning.batter then
if self.state.batAngleDeg > 50 and self.state.batAngleDeg < 200 then
self.state.battingTeamSprites.back:draw(runner.x, runner.y - playerHeightOffset)
else
self.state.battingTeamSprites.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)
end
end
for _, runner in pairs(self.baserunning.outRunners) do
self.state.battingTeamSprites.frowning:draw(runner.x, runner.y - playerHeightOffset)
end
@ -580,7 +631,7 @@ function Game:update()
if math.abs(offsetX) > 10 or math.abs(offsetY) > 10 then
drawMinimap(self.baserunning.runners, self.fielding.fielders)
end
drawScoreboard(0, C.Screen.H * 0.77, teams, self.baserunning.outs, self.state.battingTeam, self.state.inning)
drawScoreboard(0, C.Screen.H * 0.77, teams, self.baserunning.outs, self:getBattingTeam(), self.state.inning)
drawBallsAndStrikes(290, C.Screen.H - 20, pitchTracker.balls, pitchTracker.strikes)
self.announcer:draw(C.Center.x, 10)

View File

@ -27,7 +27,7 @@ function Npc:updateBatAngle(ball, catcherThrownBall, deltaSec)
npcBatDeg = npcBatDeg + (deltaSec * npcBatSpeed)
else
npcBatSpeed = (1 + math.random()) * BaseNpcBatSpeed
npcBatDeg = 200
npcBatDeg = 230
end
return npcBatDeg
end