Try to cluster global state together.

Start peeling out fielder functions into a new file.
A bit more constant use.
In Makefile, parse main.lua imports for source files.
This commit is contained in:
Sage Vaillancourt 2025-02-08 11:52:39 -05:00
parent 324673ea98
commit 66bd97499a
7 changed files with 190 additions and 178 deletions

View File

@ -1,4 +1,4 @@
SOURCE_FILES := src/utils.lua src/constants.lua src/assets.lua src/draw/* src/dbg.lua src/npc.lua src/announcer.lua src/graphics.lua src/main.lua
SOURCE_FILES := $(shell grep "import '" src/main.lua | grep -v CoreLibs | sed "s/.*'\(.*\)'.*/\1/") main.lua
GENERATED_FILES := src/assets.lua
all:

View File

@ -98,3 +98,5 @@ C.CrankPower = 10
--- How fast baserunners move after a walk
C.WalkedRunnerSpeed = 10
C.ResetFieldersAfterSeconds = 3

View File

@ -21,9 +21,9 @@ end
-- Only works if called with the bases empty (i.e. the only runner should be the batter.
-- selene: allow(unused_variable)
function dbg.loadTheBases(runners)
newRunner()
newRunner()
newRunner()
utils.newRunner()
utils.newRunner()
utils.newRunner()
runners[2].x = C.Bases[C.First].x
runners[2].y = C.Bases[C.First].y
runners[2].nextBase = C.Bases[C.Second]

56
src/field.lua Normal file
View File

@ -0,0 +1,56 @@
---@param name string
---@param speed number
---@return Fielder
function newFielder(name, speed)
return {
name = name,
speed = speed,
}
end
-- selene: allow(unscoped_variables)
Field = {
fielders = {
first = newFielder("First", 40),
second = newFielder("Second", 40),
shortstop = newFielder("Shortstop", 40),
third = newFielder("Third", 40),
pitcher = newFielder("Pitcher", 30),
catcher = newFielder("Catcher", 35),
left = newFielder("Left", 40),
center = newFielder("C.Center", 40),
right = newFielder("Right", 40),
},
}
--- Actually only benches the infield, because outfielders are far away!
---@param position XYPair
function Field.benchTo(self, position)
self.fielders.first.target = position
self.fielders.second.target = position
self.fielders.shortstop.target = position
self.fielders.third.target = position
self.fielders.pitcher.target = position
self.fielders.catcher.target = position
end
--- Resets the target positions of all fielders to their defaults (at their field positions).
---@param fromOffTheField XYPair | nil If provided, also sets all runners' current position to one centralized location.
function Field.resetFielderPositions(self, fromOffTheField)
if fromOffTheField then
for _, fielder in pairs(self.fielders) do
fielder.x = fromOffTheField.x
fielder.y = fromOffTheField.y
end
end
self.fielders.first.target = utils.xy(C.Screen.W - 65, C.Screen.H * 0.48)
self.fielders.second.target = utils.xy(C.Screen.W * 0.70, C.Screen.H * 0.30)
self.fielders.shortstop.target = utils.xy(C.Screen.W * 0.30, C.Screen.H * 0.30)
self.fielders.third.target = utils.xy(C.Screen.W * 0.1, C.Screen.H * 0.48)
self.fielders.pitcher.target = utils.xy(C.PitcherStartPos.x, C.PitcherStartPos.y)
self.fielders.catcher.target = utils.xy(C.Screen.W * 0.475, C.Screen.H * 0.92)
self.fielders.left.target = utils.xy(C.Screen.W * -1, C.Screen.H * -0.2)
self.fielders.center.target = utils.xy(C.Center.x, C.Screen.H * -0.4)
self.fielders.right.target = utils.xy(C.Screen.W * 2, self.fielders.left.target.y)
end

View File

@ -2,19 +2,19 @@
--- XXX
--- XOX
--- Where each character is the size of the screen, and 'O' is the default view.
function getDrawOffset(screenW, screenH, ballX, ballY)
function getDrawOffset(ballX, ballY)
local offsetX, offsetY
if ballY > screenH then
if ballY > C.Screen.H or ballX >= C.BallOffscreen then
return 0, 0
end
offsetY = math.max(0, -1 * ballY)
if ballX > 0 and ballX < screenW then
if ballX > 0 and ballX < C.Screen.W then
offsetX = 0
elseif ballX < 0 then
offsetX = math.max(-1 * screenW, ballX * -1)
elseif ballX > screenW then
offsetX = math.min(screenW * 2, (ballX * -1) + screenW)
offsetX = math.max(-1 * C.Screen.W, ballX * -1)
elseif ballX > C.Screen.W then
offsetX = math.min(C.Screen.W * 2, (ballX * -1) + C.Screen.W)
end
return offsetX * 1.3, offsetY * 1.5

View File

@ -30,10 +30,11 @@ import 'assets.lua'
import 'announcer.lua'
import 'dbg.lua'
import 'field.lua'
import 'graphics.lua'
import 'npc.lua'
import 'draw/overlay'
import 'draw/fielder'
import 'draw/overlay.lua'
import 'draw/fielder.lua'
-- stylua: ignore end
-- selene: allow(shadowing)
@ -44,16 +45,72 @@ local PlayerImageBlipper <const> = blipper.new(100, Player, PlayerLowHat)
local FielderDanceAnimator <const> = gfx.animator.new(1, 10, 0, utils.easingHill)
FielderDanceAnimator.repeatCount = C.DanceBounceCount - 1
function fieldersDance()
FielderDanceAnimator:reset(C.DanceBounceMs)
end
local ballAnimatorY = gfx.animator.new(0, C.BallOffscreen, C.BallOffscreen, playdate.easingFunctions.linear)
local ballAnimatorX = gfx.animator.new(0, C.BallOffscreen, C.BallOffscreen, playdate.easingFunctions.linear)
---@alias PseudoAnimator { currentValue: fun(self): number; reset: fun(self, durationMs: number | nil) }
---@alias Pitch { x: PseudoAnimator, y: PseudoAnimator, z: PseudoAnimator | nil }
local ballAnimatorX = utils.staticAnimator(C.BallOffscreen)
local ballAnimatorY = utils.staticAnimator(C.BallOffscreen)
local ballSizeAnimator = utils.staticAnimator(C.SmallestBallRadius)
-- TODO? Replace this AND ballSizeAnimator with a ballAnimatorZ?
-- ...that might lose some of the magic of both. Compromise available? idk
local ballFloatAnimator = gfx.animator.new(2000, -60, 0, utils.easingHill)
local deltaSeconds = 0
local BatterHandPos <const> = utils.xy(10, 25)
local batBase <const> = utils.xy(C.Center.x - 34, 215)
local batTip <const> = utils.xy(0, 0)
local ball <const> = {
x = C.Center.x --[[@as number]],
y = C.Center.y --[[@as number]],
z = 0,
size = C.SmallestBallRadius,
heldBy = nil --[[@type Runner | nil]],
}
---@alias Team { score: number, benchPosition: XYPair }
---@type table<string, Team>
local teams <const> = {
home = {
score = 0,
benchPosition = utils.xy(C.Screen.W + 10, C.Center.y),
},
away = {
score = 0,
benchPosition = utils.xy(-10, C.Center.y),
},
}
local PlayerTeam <const> = teams.away
local battingTeam = teams.away
local outs = 0
local inning = 1
local offenseMode = C.Offense.batting
--- @type Runner[]
local runners <const> = {}
--- @type Runner[]
local outRunners <const> = {}
---@type Runner | nil
local batter = utils.newRunner(runners)
local throwMeter = 0
-- TODO: Replace with a timer, repeatedly reset, instead of setting to 0
local secondsSinceLastRunnerMove = 0
-- TODO: Replace with a timer, repeatedly reset instead of setting to 0
local secondsSincePitchAllowed = -5
local catcherThrownBall = false
local batAngleDeg = C.CrankOffsetDeg
---@type Pitch[]
local Pitches <const> = {
-- Fastball
@ -83,39 +140,6 @@ local Pitches <const> = {
},
}
local BatterHandPos <const> = utils.xy(10, 25)
local batBase <const> = utils.xy(C.Center.x - 34, 215)
local batTip <const> = utils.xy(0, 0)
local ball <const> = {
x = C.Center.x --[[@as number]],
y = C.Center.y --[[@as number]],
z = 0,
size = C.SmallestBallRadius,
heldBy = nil --[[@type Runner | nil]],
}
---@alias Team { score: number, benchPosition: XYPair }
---@type table<string, Team>
local teams <const> = {
home = {
score = 0,
benchPosition = utils.xy(C.Screen.W + 10, C.Center.y),
},
away = {
score = 0,
benchPosition = utils.xy(-10, C.Center.y),
},
}
local PlayerTeam <const> = teams.home
local battingTeam = teams.away
local outs = 0
local inning = 1
local offenseMode = C.Offense.batting
---@return boolean playerIsOnSide, boolean playerIsOnOtherSide
function playerIsOn(side)
local ret
@ -127,93 +151,6 @@ function playerIsOn(side)
return ret, not ret
end
-- TODO? Replace this AND ballSizeAnimator with a ballHeightAnimator
-- ...that might lose some of the magic of both. Compromise available? idk
local ballFloatAnimator = gfx.animator.new(2000, -60, 0, utils.easingHill)
local BallSizeMs = 2000
local ballSizeAnimator = gfx.animator.new(BallSizeMs, 9, C.SmallestBallRadius, utils.easingHill)
local deltaSeconds = 0
---@param name string
---@param speed number
---@return Fielder
function newFielder(name, speed)
return {
name = name,
speed = speed,
}
end
---@type table<string, Fielder>
local fielders <const> = {
first = newFielder("First", 40),
second = newFielder("Second", 40),
shortstop = newFielder("Shortstop", 40),
third = newFielder("Third", 40),
pitcher = newFielder("Pitcher", 30),
catcher = newFielder("Catcher", 35),
left = newFielder("Left", 40),
center = newFielder("C.Center", 40),
right = newFielder("Right", 40),
}
--- Actually only benches the infield, because outfielders are far away!
---@param position XYPair
function benchAllFielders(position)
fielders.first.target = position
fielders.second.target = position
fielders.shortstop.target = position
fielders.third.target = position
fielders.pitcher.target = position
fielders.catcher.target = position
end
--- Resets the target positions of all fielders to their defaults (at their field positions).
---@param fromOffTheField XYPair | nil If provided, also sets all runners' current position to one centralized location.
function resetFielderPositions(fromOffTheField)
if fromOffTheField then
for _, fielder in pairs(fielders) do
fielder.x = fromOffTheField.x
fielder.y = fromOffTheField.y
end
end
fielders.first.target = utils.xy(C.Screen.W - 65, C.Screen.H * 0.48)
fielders.second.target = utils.xy(C.Screen.W * 0.70, C.Screen.H * 0.30)
fielders.shortstop.target = utils.xy(C.Screen.W * 0.30, C.Screen.H * 0.30)
fielders.third.target = utils.xy(C.Screen.W * 0.1, C.Screen.H * 0.48)
fielders.pitcher.target = utils.xy(C.PitcherStartPos.x, C.PitcherStartPos.y)
fielders.catcher.target = utils.xy(C.Screen.W * 0.475, C.Screen.H * 0.92)
fielders.left.target = utils.xy(C.Screen.W * -1, C.Screen.H * -0.2)
fielders.center.target = utils.xy(C.Center.x, C.Screen.H * -0.4)
fielders.right.target = utils.xy(C.Screen.W * 2, fielders.left.target.y)
end
--- @type Runner[]
local runners <const> = {}
--- @type Runner[]
local outRunners <const> = {}
---@return Runner
function newRunner()
local new = {
x = C.RightHandedBattersBox.x - 60,
y = C.RightHandedBattersBox.y + 60,
nextBase = C.RightHandedBattersBox,
prevBase = nil,
forcedTo = C.Bases[C.First],
}
runners[#runners + 1] = new
return new
end
---@type Runner | nil
local batter = newRunner()
local throwMeter = 0
--- "Throws" the ball from its current position to the given destination.
---@param destX number
---@param destY number
@ -242,10 +179,6 @@ function throwBall(destX, destY, easingFunc, flyTimeMs, floaty, customBallScaler
end
end
-- TODO: Replace with a timer, repeatedly reset instead of setting to 0
local secondsSincePitchAllowed = -5
local catcherThrownBall = false
---@param pitchFlyTimeMs number | nil
---@param pitchTypeIndex number | nil
function pitch(pitchFlyTimeMs, pitchTypeIndex)
@ -316,10 +249,6 @@ function updateForcedRunners()
end
end
local ResetFieldersAfterSeconds <const> = 5
-- TODO: Replace with a timer, repeatedly reset, instead of setting to 0
local secondsSinceLastRunnerMove = 0
---@param runner integer | Runner
function outRunner(runner, message)
if type(runner) ~= "number" then
@ -346,9 +275,9 @@ function outRunner(runner, message)
local currentlyFieldingTeam = battingTeam == teams.home and teams.away or teams.home
local gameOver = inning == 9 and teams.away.score ~= teams.home.score
if not gameOver then
fieldersDance()
FielderDanceAnimator:reset(C.DanceBounceMs)
secondsSinceLastRunnerMove = -7
benchAllFielders(currentlyFieldingTeam.benchPosition)
Field:benchTo(currentlyFieldingTeam.benchPosition)
announcer:say("SWITCHING SIDES...")
end
while #runners > 0 do
@ -361,7 +290,7 @@ function outRunner(runner, message)
if gameOver then
announcer:say("AND THAT'S THE BALL GAME!")
else
resetFielderPositions()
Field:resetFielderPositions()
if battingTeam == teams.home then
inning = inning + 1
end
@ -427,7 +356,7 @@ end
function tryToMakeAnOut(fielder)
local targetX, targetY = getNextOutTarget()
if targetX ~= nil and targetY ~= nil then
local nearestFielder = utils.getNearestOf(fielders, targetX, targetY)
local nearestFielder = utils.getNearestOf(Field.fielders, targetX, targetY)
nearestFielder.target = utils.xy(targetX, targetY)
if nearestFielder == fielder then
ball.heldBy = fielder
@ -461,7 +390,7 @@ function buttonControlledThrow(thrower, throwFlyMs, forbidThrowHome)
return false
end
local closestFielder = utils.getNearestOf(fielders, targetBase.x, targetBase.y, function(fielder)
local closestFielder = utils.getNearestOf(Field.fielders, targetBase.x, targetBase.y, function(fielder)
return fielder ~= thrower
end)
@ -524,7 +453,7 @@ function updateFielder(fielder)
if playerIsOn(C.Sides.defense) then
local throwFly = readThrow()
if throwFly then
buttonControlledThrow(fielders.pitcher, throwFly)
buttonControlledThrow(Field.fielders.pitcher, throwFly)
end
else
updateNpcFielder(fielder, outedSomeRunner)
@ -586,22 +515,19 @@ function updateRunner(runner, runnerIndex, appliedSpeed)
return prevX ~= runner.x or prevY ~= runner.y
end
---@type number
local batAngleDeg
function nextBatter()
batter = nil
playdate.timer.new(2000, function()
pitchTracker:reset()
if not batter then
batter = newRunner()
batter = utils.newRunner(runners)
end
end)
end
function walk()
announcer:say("Walk!")
fielders.first.target = C.Bases[C.First]
Field.fielders.first.target = C.Bases[C.First]
batter.nextBase = C.Bases[C.First]
batter.prevBase = C.Bases[C.Home]
offenseMode = C.Offense.walking
@ -653,14 +579,14 @@ function updateBatting(batDeg, batSpeed)
local hitBallScaler = gfx.animator.new(2000, 9 + (mult * mult * 0.5), C.SmallestBallRadius, utils.easingHill)
throwBall(ballDestX, ballDestY, playdate.easingFunctions.outQuint, 2000, nil, hitBallScaler)
fielders.first.target = C.Bases[C.First]
Field.fielders.first.target = C.Bases[C.First]
batter.nextBase = C.Bases[C.First]
batter.prevBase = C.Bases[C.Home]
updateForcedRunners()
batter.forcedTo = C.Bases[C.First]
batter = nil -- Demote batter to a mere runner
local chasingFielder = utils.getNearestOf(fielders, ballDestX, ballDestY)
local chasingFielder = utils.getNearestOf(Field.fielders, ballDestX, ballDestY)
chasingFielder.target = { x = ballDestX, y = ballDestY }
end
end
@ -721,10 +647,12 @@ function updateGameState()
if ball.heldBy then
ball.x = ball.heldBy.x
ball.y = ball.heldBy.y
ball.size = C.SmallestBallRadius
else
ball.x = ballAnimatorX:currentValue()
ball.z = ballFloatAnimator:currentValue()
ball.y = ballAnimatorY:currentValue() + ball.z
ball.size = ballSizeAnimator:currentValue()
end
local playerOnOffense, playerOnDefense = playerIsOn(C.Sides.offense)
@ -741,7 +669,7 @@ function updateGameState()
pitchTracker.recordedPitchX = ball.x
end
local pitcher = fielders.pitcher
local pitcher = Field.fielders.pitcher
if utils.distanceBetween(pitcher.x, pitcher.y, C.PitchStartX, C.PitchStartY) < C.BaseHitbox then
secondsSincePitchAllowed = secondsSincePitchAllowed + deltaSeconds
end
@ -787,12 +715,12 @@ function updateGameState()
secondsSinceLastRunnerMove = 0
else
secondsSinceLastRunnerMove = secondsSinceLastRunnerMove + deltaSeconds
if secondsSinceLastRunnerMove > ResetFieldersAfterSeconds then
if secondsSinceLastRunnerMove > C.ResetFieldersAfterSeconds then
throwBall(C.PitchStartX, C.PitchStartY, playdate.easingFunctions.linear, nil, true)
resetFielderPositions()
Field:resetFielderPositions()
offenseMode = C.Offense.batting
if not batter then
batter = newRunner()
batter = utils.newRunner(runners)
end
end
end
@ -803,7 +731,7 @@ function updateGameState()
end
end
for _, fielder in pairs(fielders) do
for _, fielder in pairs(Field.fielders) do
updateFielder(fielder)
end
walkAwayOutRunners()
@ -817,17 +745,14 @@ function playdate.update()
gfx.clear()
gfx.setColor(gfx.kColorBlack)
local offsetX, offsetY = 0, 0
if ball.x < C.BallOffscreen then
offsetX, offsetY = getDrawOffset(C.Screen.W, C.Screen.H, ball.x, ball.y)
gfx.setDrawOffset(offsetX, offsetY)
end
local offsetX, offsetY = getDrawOffset(ball.x, ball.y)
gfx.setDrawOffset(offsetX, offsetY)
GrassBackground:draw(-400, -240)
local fielderDanceHeight = FielderDanceAnimator:currentValue()
local ballIsHeld = false
for _, fielder in pairs(fielders) do
for _, fielder in pairs(Field.fielders) do
ballIsHeld = drawFielder(ball, fielder.x, fielder.y + fielderDanceHeight) or ballIsHeld
end
@ -853,6 +778,7 @@ function playdate.update()
PlayerImageBlipper:draw(false, runner.x, runner.y)
end
end
for _, runner in pairs(outRunners) do
PlayerFrown:draw(runner.x, runner.y)
end
@ -880,7 +806,7 @@ function init()
playdate.display.setRefreshRate(50)
gfx.setBackgroundColor(gfx.kColorWhite)
playdate.setMenuImage(gfx.image.new("images/game/menu-image.png"))
resetFielderPositions(teams.home.benchPosition)
Field:resetFielderPositions(teams.home.benchPosition)
playdate.getSystemMenu():addMenuItem("Restart game", function() end) -- TODO?
playdate.timer.new(2000, function()

View File

@ -1,6 +1,13 @@
-- selene: allow(unscoped_variables)
utils = {}
--- @alias XYPair {
--- x: number,
--- y: number,
--- }
local sqrt <const> = math.sqrt
function utils.easingHill(t, b, c, d)
c = c + 0.0 -- convert to float to prevent integer overflow
t = t / d
@ -9,10 +16,18 @@ function utils.easingHill(t, b, c, d)
return (c * t) + b
end
--- @alias XYPair {
--- x: number,
--- y: number,
--- }
--- Build an "animator" whose `:currentValue()` always returns the given value.
--- Essentially an "empty object" pattern for initial object positions.
---@param value number
---@return PseudoAnimator
function utils.staticAnimator(value)
return {
currentValue = function(_)
return value
end,
reset = function(_) end
}
end
---@param x number
---@param y number
@ -84,7 +99,7 @@ end
function utils.distanceBetween(x1, y1, x2, y2)
local x = x1 - x2
local y = y1 - y2
return math.sqrt((x * x) + (y * y)), x, y
return sqrt((x * x) + (y * y)), x, y
end
---@return number distance, number x, number y, number z
@ -92,7 +107,20 @@ function utils.distanceBetweenZ(x1, y1, z1, x2, y2, z2)
local x = x1 - x2
local y = y1 - y2
local z = z1 - z2
return math.sqrt((x * x) + (y * y) + (z * z)), x, y, z
return sqrt((x * x) + (y * y) + (z * z)), x, y, z
end
---@return Runner
function utils.newRunner(runners)
local new = {
x = C.RightHandedBattersBox.x - 60,
y = C.RightHandedBattersBox.y + 60,
nextBase = C.RightHandedBattersBox,
prevBase = nil,
forcedTo = C.Bases[C.First],
}
runners[#runners + 1] = new
return new
end
--- Returns true only if the point is below the given line, within the x bounds of said line, and above the bottomBound