Much more fleshed-out constants.lua

* Move scoreboard.lua to draw/overlay.lua
* Move minimap drawing into overlay.lua
* Remove playdate imports from utils.lua
This commit is contained in:
Sage Vaillancourt 2025-02-08 00:49:03 -05:00
parent f07530623f
commit 4a4049996f
7 changed files with 234 additions and 223 deletions

View File

@ -1,4 +1,4 @@
SOURCE_FILES := src/constants.lua src/draw/* src/utils.lua src/dbg.lua src/npc.lua src/announcer.lua src/graphics.lua src/scoreboard.lua src/main.lua SOURCE_FILES := src/utils.lua src/constants.lua src/draw/* src/dbg.lua src/npc.lua src/announcer.lua src/graphics.lua src/main.lua
all: all:
pdc src BatterUp.pdx pdc src BatterUp.pdx

View File

@ -11,3 +11,88 @@ C.Center = utils.xy(C.Screen.W / 2, C.Screen.H / 2)
C.StrikeZoneStartX = C.Center.x - 16 C.StrikeZoneStartX = C.Center.x - 16
C.StrikeZoneEndX = C.StrikeZoneStartX + 24 C.StrikeZoneEndX = C.StrikeZoneStartX + 24
C.StrikeZoneStartY = C.Screen.H - 35 C.StrikeZoneStartY = C.Screen.H - 35
--- @alias Base {
--- x: number,
--- y: number,
--- }
---@type Base[]
C.Bases = {
utils.xy(C.Screen.W * 0.93, C.Screen.H * 0.52),
utils.xy(C.Screen.W * 0.47, C.Screen.H * 0.19),
utils.xy(C.Screen.W * 0.03, C.Screen.H * 0.52),
utils.xy(C.Screen.W * 0.474, C.Screen.H * 0.79),
}
C.First, C.Second, C.Third, C.Home = 1, 2, 3, 4
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 - 35,
y = C.Bases[C.Home].y,
}
---@type table<Base, Base | nil>
C.NextBaseMap = {
[C.RightHandedBattersBox] = nil, -- Runner should not escape the box before a hit!
[C.Bases[C.First]] = C.Bases[C.Second],
[C.Bases[C.Second]] = C.Bases[C.Third],
[C.Bases[C.Third]] = C.Bases[C.Home],
}
--- Angle to align the bat to
C.CrankOffsetDeg = 90
C.DanceBounceMs = 500
C.DanceBounceCount = 4
--- Used to draw the ball well out of bounds, and
--- generally as a check for whether or not it's in play.
C.BallOffscreen = 999
C.PitchAfterSeconds = 7
C.PitchFlyMs = 1050
C.PitchStartX = 195
C.PitchStartY, C.PitchEndY = 105, 240
--- The max distance at which a fielder can tag out a runner.
C.TagDistance = 15
--- The max distance at which a runner can be considered on base.
C.BaseHitbox = 10
C.BattingPower = 20
C.SmallestBallRadius = 6
C.BatLength = 50
--- An enum for what state the offense is in
C.Offense = {
batting = {},
running = {},
walking = {},
}
--- An enum for which side (offense or defense) a team is on.
C.Sides = {
offense = {},
defense = {},
}
C.PitcherStartPos = {
x = C.Screen.W * 0.48,
y = C.Screen.H * 0.40,
}
C.ThrowMeterMax = 15
C.ThrowMeterDrainPerSec = 150
--- Controls how hard the ball can be hit, and
--- how fast the ball can be thrown.
C.CrankPower = 10
--- How fast baserunners move after a walk
C.WalkedRunnerSpeed = 10

View File

@ -20,21 +20,21 @@ 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.
-- selene: allow(unused_variable) -- selene: allow(unused_variable)
function dbg.loadTheBases(baseConstants, runners) function dbg.loadTheBases(runners)
newRunner() newRunner()
newRunner() newRunner()
newRunner() newRunner()
runners[2].x = baseConstants[1].x runners[2].x = C.Bases[C.First].x
runners[2].y = baseConstants[1].y runners[2].y = C.Bases[C.First].y
runners[2].nextBase = baseConstants[2] runners[2].nextBase = C.Bases[C.Second]
runners[3].x = baseConstants[2].x runners[3].x = C.Bases[C.Second].x
runners[3].y = baseConstants[2].y runners[3].y = C.Bases[C.Second].y
runners[3].nextBase = baseConstants[3] runners[3].nextBase = C.Bases[C.Third]
runners[4].x = baseConstants[3].x runners[4].x = C.Bases[C.Third].x
runners[4].y = baseConstants[3].y runners[4].y = C.Bases[C.Third].y
runners[4].nextBase = baseConstants[4] runners[4].nextBase = C.Bases[C.Home]
end end
if not playdate then if not playdate then

View File

@ -2,27 +2,31 @@
local gfx = playdate.graphics local gfx = playdate.graphics
local ScoreFont <const> = playdate.graphics.font.new("fonts/font-full-circle.pft") local ScoreFont <const> = playdate.graphics.font.new("fonts/font-full-circle.pft")
local OutBubbleRadius <const> = 5
local ScoreboardMarginX <const> = 6 local MinimapBackground <const> = gfx.image.new("images/game/minimap.png") --[[@as pd_image]]
local ScoreboardMarginRight <const> = 4
local ScoreboardHeight <const> = 55 local MinimapSizeX, MinimapSizeY <const> = MinimapBackground:getSize()
local Indicator = "> " local MinimapPosX, MinimapPosY = C.Screen.W - MinimapSizeX, C.Screen.H - MinimapSizeY
local IndicatorWidth <const> = ScoreFont:getTextWidth(Indicator)
local MinimapMultX <const> = 0.75 * MinimapSizeX / C.Screen.W
local MinimapOffsetX <const> = MinimapPosX + 5
local MinimapMultY <const> = 0.70 * MinimapSizeY / C.FieldHeight
local MinimapOffsetY <const> = MinimapPosY - 15
function drawMinimap(runners)
MinimapBackground:draw(MinimapPosX, MinimapPosY)
gfx.setColor(gfx.kColorBlack)
for _, runner in pairs(runners) do
local x = (MinimapMultX * runner.x) + MinimapOffsetX
local y = (MinimapMultY * runner.y) + MinimapOffsetY
gfx.fillRect(x, y, 8, 8)
end
end
local BallStrikeMarginY <const> = 4 local BallStrikeMarginY <const> = 4
local BallStrikeWidth <const> = 60 local BallStrikeWidth <const> = 60
local BallStrikeHeight <const> = (BallStrikeMarginY * 2) + ScoreFont:getHeight() local BallStrikeHeight <const> = (BallStrikeMarginY * 2) + ScoreFont:getHeight()
---@param teams any
---@param battingTeam any
---@return string, number, string, number
function getIndicators(teams, battingTeam)
if teams.home == battingTeam then
return Indicator, 0, "", IndicatorWidth
end
return "", IndicatorWidth, Indicator, 0
end
function drawBallsAndStrikes(x, y, balls, strikes) function drawBallsAndStrikes(x, y, balls, strikes)
if balls == 0 and strikes == 0 then if balls == 0 and strikes == 0 then
return return
@ -40,6 +44,23 @@ function drawBallsAndStrikes(x, y, balls, strikes)
gfx.setImageDrawMode(originalDrawMode) gfx.setImageDrawMode(originalDrawMode)
end end
local OutBubbleRadius <const> = 5
local ScoreboardMarginX <const> = 6
local ScoreboardMarginRight <const> = 4
local ScoreboardHeight <const> = 55
local Indicator = "> "
local IndicatorWidth <const> = ScoreFont:getTextWidth(Indicator)
---@param teams any
---@param battingTeam any
---@return string, number, string, number
function getIndicators(teams, battingTeam)
if teams.home == battingTeam then
return Indicator, 0, "", IndicatorWidth
end
return "", IndicatorWidth, Indicator, 0
end
function drawScoreboard(x, y, teams, outs, battingTeam, inning) function drawScoreboard(x, y, teams, outs, battingTeam, inning)
local homeScore = teams.home.score local homeScore = teams.home.score
local awayScore = teams.away.score local awayScore = teams.away.score

View File

@ -7,16 +7,6 @@ import 'CoreLibs/object.lua'
import 'CoreLibs/timer.lua' import 'CoreLibs/timer.lua'
import 'CoreLibs/ui.lua' import 'CoreLibs/ui.lua'
--- @alias XYPair {
--- x: number,
--- y: number,
--- }
--- @alias Base {
--- x: number,
--- y: number,
--- }
--- @alias Runner { --- @alias Runner {
--- x: number, --- x: number,
--- y: number, --- y: number,
@ -41,44 +31,37 @@ import 'announcer.lua'
import 'dbg.lua' import 'dbg.lua'
import 'graphics.lua' import 'graphics.lua'
import 'npc.lua' import 'npc.lua'
import 'scoreboard.lua' import 'draw/overlay'
import 'draw/fielder' import 'draw/fielder'
-- stylua: ignore end -- stylua: ignore end
-- selene: allow(shadowing) -- selene: allow(shadowing)
local gfx <const> = playdate.graphics local gfx <const> = playdate.graphics
-- selene: allow(shadowing)
local C <const> = C
local BootTune <const> = playdate.sound.sampleplayer.new("sounds/boot-tune.wav") local BootTune <const> = playdate.sound.sampleplayer.new("sounds/boot-tune.wav")
-- local BootTune <const> = playdate.sound.sampleplayer.new("sounds/boot-tune-organy.wav") -- local BootTune <const> = playdate.sound.sampleplayer.new("sounds/boot-tune-organy.wav")
local TinnyBackground <const> = playdate.sound.sampleplayer.new("sounds/tinny-background.wav") local TinnyBackground <const> = playdate.sound.sampleplayer.new("sounds/tinny-background.wav")
local BatCrackSound <const> = playdate.sound.sampleplayer.new("sounds/bat-crack-reverb.wav") local BatCrackSound <const> = playdate.sound.sampleplayer.new("sounds/bat-crack-reverb.wav")
local GrassBackground <const> = gfx.image.new("images/game/grass.png") --[[@as pd_image]] local GrassBackground <const> = gfx.image.new("images/game/grass.png") --[[@as pd_image]]
local Minimap <const> = gfx.image.new("images/game/minimap.png") --[[@as pd_image]]
local PlayerFrown <const> = gfx.image.new("images/game/player-frown.png") --[[@as pd_image]] local PlayerFrown <const> = gfx.image.new("images/game/player-frown.png") --[[@as pd_image]]
local PlayerSmile <const> = gfx.image.new("images/game/player.png") --[[@as pd_image]] local PlayerSmile <const> = gfx.image.new("images/game/player.png") --[[@as pd_image]]
local PlayerBack <const> = gfx.image.new("images/game/player-back.png") --[[@as pd_image]] local PlayerBack <const> = gfx.image.new("images/game/player-back.png") --[[@as pd_image]]
local PlayerImageBlipper <const> = blipper.new(100, "images/game/player.png", "images/game/player-lowhat.png") local PlayerImageBlipper <const> = blipper.new(100, "images/game/player.png", "images/game/player-lowhat.png")
local DanceBounceMs <const> = 500
local DanceBounceCount <const> = 4
local FielderDanceAnimator <const> = gfx.animator.new(1, 10, 0, utils.easingHill) local FielderDanceAnimator <const> = gfx.animator.new(1, 10, 0, utils.easingHill)
FielderDanceAnimator.repeatCount = DanceBounceCount - 1 FielderDanceAnimator.repeatCount = C.DanceBounceCount - 1
-- selene: allow(unused_variable) -- selene: allow(unused_variable)
function fieldersDance() function fieldersDance()
FielderDanceAnimator:reset(DanceBounceMs) FielderDanceAnimator:reset(C.DanceBounceMs)
end end
local BallOffscreen <const> = 999 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)
local PitchFlyMs <const> = 1050
local PitchStartX <const> = 195
local PitchStartY <const>, PitchEndY <const> = 105, 240
local ballAnimatorY = gfx.animator.new(0, BallOffscreen, BallOffscreen, playdate.easingFunctions.linear)
local ballAnimatorX = gfx.animator.new(0, BallOffscreen, BallOffscreen, playdate.easingFunctions.linear)
---@alias PseudoAnimator { currentValue: fun(self): number; reset: fun(self, durationMs: number | nil) } ---@alias PseudoAnimator { currentValue: fun(self): number; reset: fun(self, durationMs: number | nil) }
---@alias Pitch { x: PseudoAnimator, y: PseudoAnimator, z: PseudoAnimator | nil } ---@alias Pitch { x: PseudoAnimator, y: PseudoAnimator, z: PseudoAnimator | nil }
@ -87,64 +70,44 @@ local ballAnimatorX = gfx.animator.new(0, BallOffscreen, BallOffscreen, playdate
local Pitches <const> = { local Pitches <const> = {
-- Fastball -- Fastball
{ {
x = gfx.animator.new(0, PitchStartX, PitchStartX, playdate.easingFunctions.linear), x = gfx.animator.new(0, C.PitchStartX, C.PitchStartX, playdate.easingFunctions.linear),
y = gfx.animator.new(PitchFlyMs / 1.3, PitchStartY, PitchEndY, playdate.easingFunctions.linear), y = gfx.animator.new(C.PitchFlyMs / 1.3, C.PitchStartY, C.PitchEndY, playdate.easingFunctions.linear),
}, },
-- Curve ball -- Curve ball
{ {
x = gfx.animator.new(PitchFlyMs, PitchStartX + 20, PitchStartX, utils.easingHill), x = gfx.animator.new(C.PitchFlyMs, C.PitchStartX + 20, C.PitchStartX, utils.easingHill),
y = gfx.animator.new(PitchFlyMs, PitchStartY, PitchEndY, playdate.easingFunctions.linear), y = gfx.animator.new(C.PitchFlyMs, C.PitchStartY, C.PitchEndY, playdate.easingFunctions.linear),
}, },
-- Slider -- Slider
{ {
x = gfx.animator.new(PitchFlyMs, PitchStartX - 20, PitchStartX, utils.easingHill), x = gfx.animator.new(C.PitchFlyMs, C.PitchStartX - 20, C.PitchStartX, utils.easingHill),
y = gfx.animator.new(PitchFlyMs, PitchStartY, PitchEndY, playdate.easingFunctions.linear), y = gfx.animator.new(C.PitchFlyMs, C.PitchStartY, C.PitchEndY, playdate.easingFunctions.linear),
}, },
-- Wobbbleball -- Wobbbleball
{ {
x = { x = {
currentValue = function() currentValue = function()
return PitchStartX + (10 * math.sin((ballAnimatorY:currentValue() - PitchStartY) / 10)) return C.PitchStartX + (10 * math.sin((ballAnimatorY:currentValue() - C.PitchStartY) / 10))
end, end,
reset = function() end, reset = function() end,
}, },
y = gfx.animator.new(PitchFlyMs * 1.3, PitchStartY, PitchEndY, playdate.easingFunctions.linear), y = gfx.animator.new(C.PitchFlyMs * 1.3, C.PitchStartY, C.PitchEndY, playdate.easingFunctions.linear),
}, },
} }
local CrankOffsetDeg <const> = 90 local BatterHandPos <const> = utils.xy(10, 25)
local BatOffset <const> = utils.xy(10, 25)
local batBase <const> = utils.xy(C.Center.x - 34, 215) local batBase <const> = utils.xy(C.Center.x - 34, 215)
local batTip <const> = utils.xy(0, 0) local batTip <const> = utils.xy(0, 0)
local TagDistance <const> = 15
local SmallestBallRadius <const> = 6
local ball <const> = { local ball <const> = {
x = C.Center.x --[[@as number]], x = C.Center.x --[[@as number]],
y = C.Center.y --[[@as number]], y = C.Center.y --[[@as number]],
z = 0, z = 0,
size = SmallestBallRadius, size = C.SmallestBallRadius,
heldBy = nil --[[@type Runner | nil]], heldBy = nil --[[@type Runner | nil]],
} }
local BatLength <const> = 50
local Offense <const> = {
batting = {},
running = {},
walking = {},
}
local Sides <const> = {
offense = {},
defense = {},
}
local offenseMode = Offense.batting
---@alias Team { score: number, benchPosition: XYPair } ---@alias Team { score: number, benchPosition: XYPair }
---@type table<string, Team> ---@type table<string, Team>
@ -163,14 +126,15 @@ local PlayerTeam <const> = teams.home
local battingTeam = teams.away local battingTeam = teams.away
local outs = 0 local outs = 0
local inning = 1 local inning = 1
local offenseMode = C.Offense.batting
---@return boolean playerIsOnSide, boolean playerIsOnOtherSide ---@return boolean playerIsOnSide, boolean playerIsOnOtherSide
function playerIsOn(side) function playerIsOn(side)
local ret local ret
if PlayerTeam == battingTeam then if PlayerTeam == battingTeam then
ret = side == Sides.offense ret = side == C.Sides.offense
else else
ret = side == Sides.defense ret = side == C.Sides.defense
end end
return ret, not ret return ret, not ret
end end
@ -179,33 +143,10 @@ end
-- ...that might lose some of the magic of both. Compromise available? idk -- ...that might lose some of the magic of both. Compromise available? idk
local ballFloatAnimator = gfx.animator.new(2000, -60, 0, utils.easingHill) local ballFloatAnimator = gfx.animator.new(2000, -60, 0, utils.easingHill)
local BallSizeMs = 2000 local BallSizeMs = 2000
local ballSizeAnimator = gfx.animator.new(BallSizeMs, 9, SmallestBallRadius, utils.easingHill) local ballSizeAnimator = gfx.animator.new(BallSizeMs, 9, C.SmallestBallRadius, utils.easingHill)
local HitMult = 20
local deltaSeconds = 0 local deltaSeconds = 0
local First <const>, Second <const>, Third <const>, Home <const> = 1, 2, 3, 4
---@type Base[]
local Bases = {
utils.xy(C.Screen.W * 0.93, C.Screen.H * 0.52),
utils.xy(C.Screen.W * 0.47, C.Screen.H * 0.19),
utils.xy(C.Screen.W * 0.03, C.Screen.H * 0.52),
utils.xy(C.Screen.W * 0.474, C.Screen.H * 0.79),
}
-- Pseudo-base for batter to target
local RightHandedBattersBox <const> = utils.xy(Bases[Home].x - 35, Bases[Home].y)
---@type table<Base, Base | nil>
local NextBaseMap <const> = {
[RightHandedBattersBox] = nil, -- Runner should not escape the box before a hit!
[Bases[First]] = Bases[Second],
[Bases[Second]] = Bases[Third],
[Bases[Third]] = Bases[Home],
}
---@param name string ---@param name string
---@param speed number ---@param speed number
---@return Fielder ---@return Fielder
@ -229,11 +170,6 @@ local fielders <const> = {
right = newFielder("Right", 40), right = newFielder("Right", 40),
} }
local PitcherStartPos <const> = {
x = C.Screen.W * 0.48,
y = C.Screen.H * 0.40,
}
--- Actually only benches the infield, because outfielders are far away! --- Actually only benches the infield, because outfielders are far away!
---@param position XYPair ---@param position XYPair
function benchAllFielders(position) function benchAllFielders(position)
@ -259,16 +195,13 @@ function resetFielderPositions(fromOffTheField)
fielders.second.target = utils.xy(C.Screen.W * 0.70, C.Screen.H * 0.30) 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.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.third.target = utils.xy(C.Screen.W * 0.1, C.Screen.H * 0.48)
fielders.pitcher.target = utils.xy(PitcherStartPos.x, PitcherStartPos.y) 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.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.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.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) fielders.right.target = utils.xy(C.Screen.W * 2, fielders.left.target.y)
end end
local BatterStartingX <const> = Bases[Home].x - 40
local BatterStartingY <const> = Bases[Home].y - 3
--- @type Runner[] --- @type Runner[]
local runners <const> = {} local runners <const> = {}
@ -278,11 +211,11 @@ local outRunners <const> = {}
---@return Runner ---@return Runner
function newRunner() function newRunner()
local new = { local new = {
x = BatterStartingX - 60, x = C.RightHandedBattersBox.x - 60,
y = BatterStartingY + 60, y = C.RightHandedBattersBox.y + 60,
nextBase = RightHandedBattersBox, nextBase = C.RightHandedBattersBox,
prevBase = nil, prevBase = nil,
forcedTo = Bases[First], forcedTo = C.Bases[C.First],
} }
runners[#runners + 1] = new runners[#runners + 1] = new
return new return new
@ -292,7 +225,6 @@ end
local batter = newRunner() local batter = newRunner()
local throwMeter = 0 local throwMeter = 0
local PitchMeterLimit = 15
--- "Throws" the ball from its current position to the given destination. --- "Throws" the ball from its current position to the given destination.
---@param destX number ---@param destX number
@ -302,35 +234,35 @@ local PitchMeterLimit = 15
---@param floaty boolean | nil ---@param floaty boolean | nil
---@param customBallScaler pd_animator | nil ---@param customBallScaler pd_animator | nil
function throwBall(destX, destY, easingFunc, flyTimeMs, floaty, customBallScaler) function throwBall(destX, destY, easingFunc, flyTimeMs, floaty, customBallScaler)
ball.heldBy = nil
throwMeter = 0
if not flyTimeMs then if not flyTimeMs then
flyTimeMs = utils.distanceBetween(ball.x, ball.y, destX, destY) * 5 flyTimeMs = utils.distanceBetween(ball.x, ball.y, destX, destY) * 5
end end
ball.heldBy = nil
if customBallScaler then if customBallScaler then
ballSizeAnimator = customBallScaler ballSizeAnimator = customBallScaler
else else
-- TODO? Scale based on distance? -- TODO? Scale based on distance?
ballSizeAnimator = gfx.animator.new(flyTimeMs, 9, SmallestBallRadius, utils.easingHill) ballSizeAnimator = gfx.animator.new(flyTimeMs, 9, C.SmallestBallRadius, utils.easingHill)
end end
ballAnimatorY = gfx.animator.new(flyTimeMs, ball.y, destY, easingFunc) ballAnimatorY = gfx.animator.new(flyTimeMs, ball.y, destY, easingFunc)
ballAnimatorX = gfx.animator.new(flyTimeMs, ball.x, destX, easingFunc) ballAnimatorX = gfx.animator.new(flyTimeMs, ball.x, destX, easingFunc)
if floaty then if floaty then
ballFloatAnimator:reset(flyTimeMs) ballFloatAnimator:reset(flyTimeMs)
end end
throwMeter = 0
end end
local PitchAfterSeconds = 7
-- TODO: Replace with a timer, repeatedly reset instead of setting to 0 -- TODO: Replace with a timer, repeatedly reset instead of setting to 0
local secondsSincePitchAllowed = -5 local secondsSincePitchAllowed = -5
local catcherThrownBall = false local catcherThrownBall = false
---@param pitchFlyTimeMs number | nil ---@param pitchFlyTimeMs number | nil
---@param pitchTypeIndex number | nil ---@param pitchTypeIndex number | nil
function pitch(pitchFlyTimeMs, pitchTypeIndex) function pitch(pitchFlyTimeMs, pitchTypeIndex)
catcherThrownBall = false catcherThrownBall = false
offenseMode = Offense.batting offenseMode = C.Offense.batting
local current = Pitches[pitchTypeIndex] local current = Pitches[pitchTypeIndex]
ballAnimatorX = current.x ballAnimatorX = current.x
@ -353,15 +285,13 @@ function pitch(pitchFlyTimeMs, pitchTypeIndex)
secondsSincePitchAllowed = 0 secondsSincePitchAllowed = 0
end end
local BaseHitbox = 10
--- Returns the base being touched by the player at (x,y), or nil, if no base is being touched --- Returns the base being touched by the player at (x,y), or nil, if no base is being touched
---@param x number ---@param x number
---@param y number ---@param y number
---@return Base | nil ---@return Base | nil
function isTouchingBase(x, y) function isTouchingBase(x, y)
return utils.first(Bases, function(base) return utils.first(C.Bases, function(base)
return utils.distanceBetween(x, y, base.x, base.y) < BaseHitbox return utils.distanceBetween(x, y, base.x, base.y) < C.BaseHitbox
end) end)
end end
@ -386,7 +316,7 @@ end
function updateForcedRunners() function updateForcedRunners()
local stillForced = true local stillForced = true
for _, base in ipairs(Bases) do for _, base in ipairs(C.Bases) do
local runnerTargetingBase = getRunnerWithNextBase(base) local runnerTargetingBase = getRunnerWithNextBase(base)
if runnerTargetingBase then if runnerTargetingBase then
if stillForced then if stillForced then
@ -462,7 +392,7 @@ end
---@return Base[] ---@return Base[]
function getForcedOutTargets() function getForcedOutTargets()
local targets = {} local targets = {}
for _, base in ipairs(Bases) do for _, base in ipairs(C.Bases) do
local runnerTargetingBase = getRunnerWithNextBase(base) local runnerTargetingBase = getRunnerWithNextBase(base)
if runnerTargetingBase then if runnerTargetingBase then
targets[#targets + 1] = base targets[#targets + 1] = base
@ -479,7 +409,7 @@ function getBaseOfStrandedRunner()
local farRunnersBase, farDistance local farRunnersBase, farDistance
for _, runner in pairs(runners) do for _, runner in pairs(runners) do
if runner ~= batter then if runner ~= batter then
local nearestBase, distance = utils.getNearestOf(Bases, runner.x, runner.y) local nearestBase, distance = utils.getNearestOf(C.Bases, runner.x, runner.y)
if farRunnersBase == nil or farDistance < distance then if farRunnersBase == nil or farDistance < distance then
farRunnersBase = nearestBase farRunnersBase = nearestBase
farDistance = distance farDistance = distance
@ -520,8 +450,8 @@ function tryToMakeAnOut(fielder)
end end
function readThrow() function readThrow()
if throwMeter > PitchMeterLimit then if throwMeter > C.ThrowMeterMax then
return (PitchFlyMs / (throwMeter / PitchMeterLimit)) return (C.PitchFlyMs / (throwMeter / C.ThrowMeterMax))
end end
return nil return nil
end end
@ -532,13 +462,13 @@ end
function buttonControlledThrow(thrower, throwFlyMs, forbidThrowHome) function buttonControlledThrow(thrower, throwFlyMs, forbidThrowHome)
local targetBase local targetBase
if playdate.buttonIsPressed(playdate.kButtonLeft) then if playdate.buttonIsPressed(playdate.kButtonLeft) then
targetBase = Bases[Third] targetBase = C.Bases[C.Third]
elseif playdate.buttonIsPressed(playdate.kButtonUp) then elseif playdate.buttonIsPressed(playdate.kButtonUp) then
targetBase = Bases[Second] targetBase = C.Bases[C.Second]
elseif playdate.buttonIsPressed(playdate.kButtonRight) then elseif playdate.buttonIsPressed(playdate.kButtonRight) then
targetBase = Bases[First] targetBase = C.Bases[C.First]
elseif not forbidThrowHome and playdate.buttonIsPressed(playdate.kButtonDown) then elseif not forbidThrowHome and playdate.buttonIsPressed(playdate.kButtonDown) then
targetBase = Bases[Home] targetBase = C.Bases[C.Home]
else else
return false return false
end end
@ -550,7 +480,7 @@ function buttonControlledThrow(thrower, throwFlyMs, forbidThrowHome)
throwBall(targetBase.x, targetBase.y, playdate.easingFunctions.linear, throwFlyMs) throwBall(targetBase.x, targetBase.y, playdate.easingFunctions.linear, throwFlyMs)
closestFielder.target = targetBase closestFielder.target = targetBase
secondsSinceLastRunnerMove = 0 secondsSinceLastRunnerMove = 0
offenseMode = Offense.running offenseMode = C.Offense.running
return true return true
end end
@ -566,7 +496,7 @@ function outEligibleRunners(fielder)
and runner.forcedTo == touchedBase and runner.forcedTo == touchedBase
and touchedBase ~= runnerOnBase and touchedBase ~= runnerOnBase
-- Tag out -- Tag out
or not runnerOnBase and utils.distanceBetween(runner.x, runner.y, fielder.x, fielder.y) < TagDistance or not runnerOnBase and utils.distanceBetween(runner.x, runner.y, fielder.x, fielder.y) < C.TagDistance
then then
outRunner(i) outRunner(i)
didOutRunner = true didOutRunner = true
@ -577,7 +507,7 @@ function outEligibleRunners(fielder)
end end
function updateNpcFielder(fielder, outedSomeRunner) function updateNpcFielder(fielder, outedSomeRunner)
if offenseMode ~= Offense.running then if offenseMode ~= C.Offense.running then
return return
end end
if outedSomeRunner then if outedSomeRunner then
@ -603,7 +533,7 @@ function updateFielder(fielder)
local outedSomeRunner = outEligibleRunners(fielder) local outedSomeRunner = outEligibleRunners(fielder)
if playerIsOn(Sides.defense) then if playerIsOn(C.Sides.defense) then
local throwFly = readThrow() local throwFly = readThrow()
if throwFly then if throwFly then
buttonControlledThrow(fielders.pitcher, throwFly) buttonControlledThrow(fielders.pitcher, throwFly)
@ -626,14 +556,14 @@ function updateRunner(runner, runnerIndex, appliedSpeed)
return false return false
end end
local nearestBase, nearestBaseDistance = utils.getNearestOf(Bases, runner.x, runner.y) local nearestBase, nearestBaseDistance = utils.getNearestOf(C.Bases, runner.x, runner.y)
if if
nearestBaseDistance < 5 nearestBaseDistance < 5
and runnerIndex ~= nil and runnerIndex ~= nil
and runner ~= batter --runner.prevBase and runner ~= batter --runner.prevBase
and runner.nextBase == Bases[Home] and runner.nextBase == C.Bases[C.Home]
and nearestBase == Bases[Home] and nearestBase == C.Bases[C.Home]
then then
score(runnerIndex) score(runnerIndex)
end end
@ -642,7 +572,7 @@ function updateRunner(runner, runnerIndex, appliedSpeed)
local x, y, distance = utils.normalizeVector(runner.x, runner.y, nb.x, nb.y) local x, y, distance = utils.normalizeVector(runner.x, runner.y, nb.x, nb.y)
if distance < 2 then if distance < 2 then
runner.nextBase = NextBaseMap[runner.nextBase] runner.nextBase = C.NextBaseMap[runner.nextBase]
runner.forcedTo = nil runner.forcedTo = nil
return false return false
end end
@ -683,10 +613,10 @@ end
function walk() function walk()
announcer:say("Walk!") announcer:say("Walk!")
fielders.first.target = Bases[First] fielders.first.target = C.Bases[C.First]
batter.nextBase = Bases[First] batter.nextBase = C.Bases[C.First]
batter.prevBase = Bases[Home] batter.prevBase = C.Bases[C.Home]
offenseMode = Offense.walking offenseMode = C.Offense.walking
batter = nil batter = nil
updateForcedRunners() updateForcedRunners()
nextBatter() nextBatter()
@ -701,25 +631,25 @@ end
---@param batDeg number ---@param batDeg number
function updateBatting(batDeg, batSpeed) function updateBatting(batDeg, batSpeed)
if ball.y < BallOffscreen then if ball.y < C.BallOffscreen then
ball.y = ballAnimatorY:currentValue() + ballFloatAnimator:currentValue() ball.y = ballAnimatorY:currentValue() + ballFloatAnimator:currentValue()
ball.size = SmallestBallRadius -- ballFloatAnimator:currentValue() ball.size = C.SmallestBallRadius -- ballFloatAnimator:currentValue()
end end
local batAngle = math.rad(batDeg) local batAngle = math.rad(batDeg)
-- TODO: animate bat-flip or something -- TODO: animate bat-flip or something
batBase.x = batter and (batter.x + BatOffset.x) or 0 batBase.x = batter and (batter.x + BatterHandPos.x) or 0
batBase.y = batter and (batter.y + BatOffset.y) or 0 batBase.y = batter and (batter.y + BatterHandPos.y) or 0
batTip.x = batBase.x + (BatLength * math.sin(batAngle)) batTip.x = batBase.x + (C.BatLength * math.sin(batAngle))
batTip.y = batBase.y + (BatLength * math.cos(batAngle)) batTip.y = batBase.y + (C.BatLength * math.cos(batAngle))
if if
batSpeed > 0 batSpeed > 0
and utils.pointDirectlyUnderLine(ball.x, ball.y, batBase.x, batBase.y, batTip.x, batTip.y, C.Screen.H) and utils.pointDirectlyUnderLine(ball.x, ball.y, batBase.x, batBase.y, batTip.x, batTip.y, C.Screen.H)
and ball.y < 232 --not isTouchingBall(fielders.catcher.x, fielders.catcher.y) and ball.y < 232
then then
BatCrackSound:play() BatCrackSound:play()
offenseMode = Offense.running offenseMode = C.Offense.running
local ballAngle = batAngle + math.rad(90) local ballAngle = batAngle + math.rad(90)
local mult = math.abs(batSpeed / 15) local mult = math.abs(batSpeed / 15)
@ -729,23 +659,17 @@ function updateBatting(batDeg, batSpeed)
ballVelX = ballVelX * -1 ballVelX = ballVelX * -1
ballVelY = ballVelY * -1 ballVelY = ballVelY * -1
end end
local ballDestX = ball.x + (ballVelX * HitMult) local ballDestX = ball.x + (ballVelX * C.BattingPower)
local ballDestY = ball.y + (ballVelY * HitMult) local ballDestY = ball.y + (ballVelY * C.BattingPower)
-- Hit! -- Hit!
throwBall( local hitBallScaler = gfx.animator.new(2000, 9 + (mult * mult * 0.5), C.SmallestBallRadius, utils.easingHill)
ballDestX, throwBall(ballDestX, ballDestY, playdate.easingFunctions.outQuint, 2000, nil, hitBallScaler)
ballDestY,
playdate.easingFunctions.outQuint,
2000,
nil,
gfx.animator.new(2000, 9 + (mult * mult * 0.5), SmallestBallRadius, utils.easingHill)
)
fielders.first.target = Bases[First] fielders.first.target = C.Bases[C.First]
batter.nextBase = Bases[First] batter.nextBase = C.Bases[C.First]
batter.prevBase = Bases[Home] batter.prevBase = C.Bases[C.Home]
updateForcedRunners() updateForcedRunners()
batter.forcedTo = Bases[First] batter.forcedTo = C.Bases[C.First]
batter = nil -- Demote batter to a mere runner batter = nil -- Demote batter to a mere runner
local chasingFielder = utils.getNearestOf(fielders, ballDestX, ballDestY) local chasingFielder = utils.getNearestOf(fielders, ballDestX, ballDestY)
@ -801,7 +725,7 @@ function updateGameState()
deltaSeconds = playdate.getElapsedTime() or 0 deltaSeconds = playdate.getElapsedTime() or 0
playdate.resetElapsedTime() playdate.resetElapsedTime()
local crankChange = playdate.getCrankChange() --[[@as number]] local crankChange = playdate.getCrankChange() --[[@as number]]
local crankLimited = crankChange == 0 and 0 or (math.log(math.abs(crankChange)) * 10) local crankLimited = crankChange == 0 and 0 or (math.log(math.abs(crankChange)) * C.CrankPower)
if crankChange < 0 then if crankChange < 0 then
crankLimited = crankLimited * -1 crankLimited = crankLimited * -1
end end
@ -815,21 +739,22 @@ function updateGameState()
ball.y = ballAnimatorY:currentValue() + ball.z ball.y = ballAnimatorY:currentValue() + ball.z
end end
local playerOnOffense, playerOnDefense = playerIsOn(Sides.offense) local playerOnOffense, playerOnDefense = playerIsOn(C.Sides.offense)
if playerOnDefense then if playerOnDefense then
throwMeter = math.max(0, throwMeter - (deltaSeconds * 150)) throwMeter = math.max(0, throwMeter - (deltaSeconds * C.ThrowMeterDrainPerSec))
throwMeter = throwMeter + math.abs(crankLimited) throwMeter = throwMeter + math.abs(crankLimited)
end end
if offenseMode == Offense.batting then if offenseMode == C.Offense.batting then
if ball.y < C.StrikeZoneStartY then if ball.y < C.StrikeZoneStartY then
pitchTracker.recordedPitchX = nil pitchTracker.recordedPitchX = nil
elseif not pitchTracker.recordedPitchX then elseif not pitchTracker.recordedPitchX then
pitchTracker.recordedPitchX = ball.x pitchTracker.recordedPitchX = ball.x
end end
if utils.distanceBetween(fielders.pitcher.x, fielders.pitcher.y, PitchStartX, PitchStartY) < BaseHitbox then local pitcher = fielders.pitcher
if utils.distanceBetween(pitcher.x, pitcher.y, C.PitchStartX, C.PitchStartY) < C.BaseHitbox then
secondsSincePitchAllowed = secondsSincePitchAllowed + deltaSeconds secondsSincePitchAllowed = secondsSincePitchAllowed + deltaSeconds
end end
@ -840,13 +765,13 @@ function updateGameState()
elseif outcome == PitchOutcomes.Walk then elseif outcome == PitchOutcomes.Walk then
walk() walk()
end end
throwBall(PitchStartX, PitchStartY, playdate.easingFunctions.linear, nil, true) throwBall(C.PitchStartX, C.PitchStartY, playdate.easingFunctions.linear, nil, true)
catcherThrownBall = true catcherThrownBall = true
end end
local batSpeed local batSpeed
if playerOnOffense then if playerOnOffense then
batAngleDeg = (playdate.getCrankPosition() + CrankOffsetDeg) % 360 batAngleDeg = (playdate.getCrankPosition() + C.CrankOffsetDeg) % 360
batSpeed = crankLimited batSpeed = crankLimited
else else
batAngleDeg = npc.updateBatAngle(ball, catcherThrownBall, deltaSeconds) batAngleDeg = npc.updateBatAngle(ball, catcherThrownBall, deltaSeconds)
@ -858,35 +783,35 @@ function updateGameState()
-- TODO: Ensure batter can't be nil, here -- TODO: Ensure batter can't be nil, here
updateRunner(batter, nil, crankLimited) updateRunner(batter, nil, crankLimited)
if secondsSincePitchAllowed > PitchAfterSeconds then if secondsSincePitchAllowed > C.PitchAfterSeconds then
if playerOnDefense then if playerOnDefense then
local throwFly = readThrow() local throwFly = readThrow()
if throwFly and not buttonControlledThrow(fielders.pitcher, throwFly, true) then if throwFly and not buttonControlledThrow(pitcher, throwFly, true) then
playerPitch(throwFly) playerPitch(throwFly)
end end
else else
pitch(PitchFlyMs, math.random(#Pitches)) pitch(C.PitchFlyMs, math.random(#Pitches))
end end
end end
elseif offenseMode == Offense.running then elseif offenseMode == C.Offense.running then
local appliedSpeed = playerOnOffense and crankLimited or npc.runningSpeed(Bases, runners) local appliedSpeed = playerOnOffense and crankLimited or npc.runningSpeed(runners)
if updateRunning(appliedSpeed) then if updateRunning(appliedSpeed) then
secondsSinceLastRunnerMove = 0 secondsSinceLastRunnerMove = 0
else else
secondsSinceLastRunnerMove = secondsSinceLastRunnerMove + deltaSeconds secondsSinceLastRunnerMove = secondsSinceLastRunnerMove + deltaSeconds
if secondsSinceLastRunnerMove > ResetFieldersAfterSeconds then if secondsSinceLastRunnerMove > ResetFieldersAfterSeconds then
throwBall(PitchStartX, PitchStartY, playdate.easingFunctions.linear, nil, true) throwBall(C.PitchStartX, C.PitchStartY, playdate.easingFunctions.linear, nil, true)
resetFielderPositions() resetFielderPositions()
offenseMode = Offense.batting offenseMode = C.Offense.batting
if not batter then if not batter then
batter = newRunner() batter = newRunner()
end end
end end
end end
elseif offenseMode == Offense.walking then elseif offenseMode == C.Offense.walking then
updateForcedRunners() updateForcedRunners()
if not updateRunning(10, true) then if not updateRunning(C.WalkedRunnerSpeed, true) then
offenseMode = Offense.batting offenseMode = C.Offense.batting
end end
end end
@ -896,26 +821,6 @@ function updateGameState()
walkAwayOutRunners() walkAwayOutRunners()
end end
local MinimapSizeX, MinimapSizeY <const> = Minimap:getSize()
local MinimapPosX, MinimapPosY = C.Screen.W - MinimapSizeX, C.Screen.H - MinimapSizeY
local FieldHeight <const> = Bases[Home].y - Bases[Second].y
local MinimapMultX <const> = 0.75 * MinimapSizeX / C.Screen.W
local MinimapOffsetX <const> = MinimapPosX + 5
local MinimapMultY <const> = 0.70 * MinimapSizeY / FieldHeight
local MinimapOffsetY <const> = MinimapPosY - 15
function drawMinimap()
Minimap:draw(MinimapPosX, MinimapPosY)
gfx.setColor(gfx.kColorBlack)
for _, runner in pairs(runners) do
local x = (MinimapMultX * runner.x) + MinimapOffsetX
local y = (MinimapMultY * runner.y) + MinimapOffsetY
gfx.fillRect(x, y, 8, 8)
end
end
function playdate.update() function playdate.update()
playdate.timer.updateTimers() playdate.timer.updateTimers()
gfx.animation.blinker.updateAll() gfx.animation.blinker.updateAll()
@ -925,7 +830,7 @@ function playdate.update()
gfx.setColor(gfx.kColorBlack) gfx.setColor(gfx.kColorBlack)
local offsetX, offsetY = 0, 0 local offsetX, offsetY = 0, 0
if ball.x < BallOffscreen then if ball.x < C.BallOffscreen then
offsetX, offsetY = getDrawOffset(C.Screen.W, C.Screen.H, ball.x, ball.y) offsetX, offsetY = getDrawOffset(C.Screen.W, C.Screen.H, ball.x, ball.y)
gfx.setDrawOffset(offsetX, offsetY) gfx.setDrawOffset(offsetX, offsetY)
end end
@ -938,7 +843,7 @@ function playdate.update()
ballIsHeld = drawFielder(ball, fielder.x, fielder.y + fielderDanceHeight) or ballIsHeld ballIsHeld = drawFielder(ball, fielder.x, fielder.y + fielderDanceHeight) or ballIsHeld
end end
if offenseMode == Offense.batting then if offenseMode == C.Offense.batting then
gfx.setLineWidth(5) gfx.setLineWidth(5)
gfx.drawLine(batBase.x, batBase.y, batTip.x, batTip.y) gfx.drawLine(batBase.x, batBase.y, batTip.x, batTip.y)
end end
@ -976,7 +881,7 @@ function playdate.update()
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) > 10 then
drawMinimap() drawMinimap(runners)
end end
drawScoreboard(0, C.Screen.H * 0.77, teams, outs, battingTeam, inning) drawScoreboard(0, C.Screen.H * 0.77, teams, outs, battingTeam, inning)
drawBallsAndStrikes(290, C.Screen.H - 20, pitchTracker.balls, pitchTracker.strikes) drawBallsAndStrikes(290, C.Screen.H - 20, pitchTracker.balls, pitchTracker.strikes)
@ -991,7 +896,7 @@ function init()
playdate.getSystemMenu():addMenuItem("Restart game", function() end) -- TODO? playdate.getSystemMenu():addMenuItem("Restart game", function() end) -- TODO?
playdate.timer.new(2000, function() playdate.timer.new(2000, function()
throwBall(PitchStartX, PitchStartY, playdate.easingFunctions.linear, nil, false) throwBall(C.PitchStartX, C.PitchStartY, playdate.easingFunctions.linear, nil, false)
end) end)
BootTune:play() BootTune:play()
BootTune:setFinishCallback(function() BootTune:setFinishCallback(function()

View File

@ -19,12 +19,12 @@ function npc.batSpeed()
return npcBatSpeed return npcBatSpeed
end end
function npc.runningSpeed(baseConstants, runners) function npc.runningSpeed(runners)
if #runners == 0 then if #runners == 0 then
return 0 return 0
end end
local touchedBase = isTouchingBase(runners[1].x, runners[1].y) local touchedBase = isTouchingBase(runners[1].x, runners[1].y)
if not touchedBase or touchedBase == baseConstants[4] then if not touchedBase or touchedBase == C.Bases[C.Home] then
return 10 return 10
end end
return 0 return 0

View File

@ -1,8 +1,3 @@
-- stylua: ignore start
import 'CoreLibs/animation.lua'
import 'CoreLibs/graphics.lua'
-- stylua: ignore end
-- selene: allow(unscoped_variables) -- selene: allow(unscoped_variables)
utils = {} utils = {}
@ -14,6 +9,11 @@ function utils.easingHill(t, b, c, d)
return (c * t) + b return (c * t) + b
end end
--- @alias XYPair {
--- x: number,
--- y: number,
--- }
---@param x number ---@param x number
---@param y number ---@param y number
---@return XYPair ---@return XYPair