Add .luarc.json for library reading.

Move more stuff to utils.
More type hints.
*Some* fleshing-out of getNextThrowTarget() etc.
Closer to proper multi-baserunner support.
This commit is contained in:
Sage Vaillancourt 2025-01-30 00:18:02 -05:00
parent 4093f9705a
commit 533c625d47
3 changed files with 241 additions and 133 deletions

8
.luarc.json Normal file
View File

@ -0,0 +1,8 @@
{
"Lua.runtime.version": "Lua 5.4",
"Lua.diagnostics.disable": ["undefined-global", "lowercase-global"],
"Lua.diagnostics.globals": ["playdate", "import"],
"Lua.runtime.nonstandardSymbol": ["+=", "-=", "*=", "/="],
"Lua.workspace.library": ["/home/sage/Downloads/PlaydateSDK-2.6.2/CoreLibs"],
"Lua.workspace.preloadFileSize": 1000
}

View File

@ -12,14 +12,14 @@ import 'utils.lua'
--- @alias Runner { x: number, y: number, nextBase: Base, prevBase: Base, forcedTo: Base } --- @alias Runner { x: number, y: number, nextBase: Base, prevBase: Base, forcedTo: Base }
--- @alias Fielder { onArrive: fun() | nil, x: number | nil, y: number | nil, targetX: number | nil, targetY: number | nil } --- @alias Fielder { onArrive: fun() | nil, x: number | nil, y: number | nil, target: Position | nil, speed: number }
local gfx = playdate.graphics local gfx = playdate.graphics
playdate.display.setRefreshRate(50) playdate.display.setRefreshRate(50)
gfx.setBackgroundColor(gfx.kColorWhite) gfx.setBackgroundColor(gfx.kColorWhite)
playdate.setMenuImage(gfx.image.new("images/game/menu-image.png")) playdate.setMenuImage(gfx.image.new("images/game/menu-image.png"))
local grassBackground = gfx.image.new("images/game/grass.png") local grassBackground = gfx.image.new("images/game/grass.png") or {}
local playerImageBlipper = blipper.new( local playerImageBlipper = blipper.new(
100, 100,
@ -27,6 +27,7 @@ local playerImageBlipper = blipper.new(
"images/game/player-lowhat.png" "images/game/player-lowhat.png"
) )
---@type Position
local backgroundPan = { local backgroundPan = {
x = 0, x = 0,
y = 0, y = 0,
@ -48,7 +49,7 @@ local batLength = 45
local tagDistance = 20 local tagDistance = 20
local ballY = ballStartY local ballY = 999 -- ballStartY
local ballX = 200 local ballX = 200
local ballSize = 6 local ballSize = 6
@ -83,54 +84,63 @@ local nextBaseMap = {
[bases.third] = bases.home [bases.third] = bases.home
} }
local fielderSpeed = 40
---@type table<string, Fielder> ---@type table<string, Fielder>
local fielders = { local fielders = {
first = { first = {
x = nil, speed = 40,
y = nil,
}, },
second = {}, second = {
shortstop = {}, speed = 40,
third = {}, },
pitcher = {}, shortstop = {
left = {}, speed = 40,
center = {}, },
right = {} third = {
speed = 40,
},
pitcher = {
speed = 30,
},
catcher = {
speed = 20,
},
left = {
speed = 40,
},
center = {
speed = 40,
},
right = {
speed = 40,
}
} }
function resetFielderPositions() --- Resets the target positions of all fielders to their defaults (at their field positions).
fielders.first.x = screenW - 65 ---@param fromOffTheField boolean If provided, also sets all runners' current position to one centralized location.
fielders.first.y = screenH * 0.48 function resetFielderPositions(fromOffTheField)
if fromOffTheField then
for _,fielder in pairs(fielders) do
fielder.x = centerX
fielder.y = screenH
end
end
fielders.second.x = screenW * 0.70 fielders.first.target = { x = screenW - 65, y = screenH * 0.48 }
fielders.second.y = screenH * 0.30 fielders.second.target = { x = screenW * 0.70, y = screenH * 0.30 }
fielders.shortstop.target = { x = screenW * 0.30, y = screenH * 0.30 }
fielders.shortstop.x = screenW * 0.30 fielders.third.target = { x = screenW * 0.1, y = screenH * 0.48 }
fielders.shortstop.y = screenH * 0.30 fielders.pitcher.target = { x = screenW * 0.48, y = screenH * 0.40 }
fielders.catcher.target = { x = screenW * 0.475, y = screenH * 0.92 }
fielders.third.x = screenW * 0.1 fielders.left.target = { x = screenW * -1, y = screenH * -0.2 }
fielders.third.y = screenH * 0.48 fielders.center.target = { x = centerX, y = screenH * -0.4 }
fielders.right.target = { x = screenW * 2, y = screenH * fielders.left.target.y }
fielders.pitcher.x = screenW * 0.48
fielders.pitcher.y = screenH * 0.40
fielders.left.x = screenW * -1
fielders.left.y = screenH * -0.2
fielders.center.x = fielders.second.x
fielders.center.y = screenH * -0.4
fielders.right.x = screenW * 2
fielders.right.y = screenH * fielders.left.y
end end
resetFielderPositions() resetFielderPositions(true)
local playerStartingX = bases.home.x - 40 local playerStartingX = bases.home.x - 40
local playerStartingY = bases.home.y - 3 local playerStartingY = bases.home.y - 3
--- @type table<Runner, Runner> --- @type Runner[]
local runners = { } local runners = { }
---@return Runner ---@return Runner
@ -145,6 +155,7 @@ function newRunner()
return new return new
end end
---@type Runner | nil
local batter = newRunner() local batter = newRunner()
function throwBall(destX, destY, easingFunc, flyTimeMs) function throwBall(destX, destY, easingFunc, flyTimeMs)
@ -158,18 +169,12 @@ end
function pitch() function pitch()
pitchAnimator:reset() pitchAnimator:reset()
resetFielderPositions() ballY = ballStartY
ballX = 200 ballX = 200
currentMode = MODES.batting currentMode = MODES.batting
backgroundPan.y = 0 backgroundPan.y = 0
backgroundPan.x = 0 backgroundPan.x = 0
-- TODO: Add new runners, instead
--runners = {}
--batter.x = playerStartingX
--batter.y = playerStartingY
batter = newRunner()
end end
function playdate.AButtonDown() function playdate.AButtonDown()
@ -177,19 +182,19 @@ function playdate.AButtonDown()
end end
function playdate.upButtonDown() function playdate.upButtonDown()
batBaseY -= 1 batBaseY = batBaseY - 1
end end
function playdate.downButtonDown() function playdate.downButtonDown()
batBaseY += 1 batBaseY = batBaseY + 1
end end
function playdate.rightButtonDown() function playdate.rightButtonDown()
batBaseX += 1 batBaseX = batBaseX + 1
end end
function playdate.leftButtonDown() function playdate.leftButtonDown()
batBaseX -= 1 batBaseX = batBaseX - 1
end end
local pitchClockSec = 99 local pitchClockSec = 99
@ -211,8 +216,6 @@ end
local ballCatchHitbox = 3 local ballCatchHitbox = 3
function isTouchingBall(x, y) function isTouchingBall(x, y)
local ballDistance = distanceBetween(x, y, ballX, ballY) local ballDistance = distanceBetween(x, y, ballX, ballY)
-- print("ballDistance:")
-- print(ballDistance)
return ballDistance < ballCatchHitbox return ballDistance < ballCatchHitbox
end end
@ -224,31 +227,34 @@ function outRunner(runnerIndex)
updateForcedTos() updateForcedTos()
end end
function updateInfield() function updateFielders()
if ballDestX == nil or ballDestY == nil then local touchingBaseCache = buildCache(
return function(runner)
return isTouchingBase(runner.x, runner.y)
end end
)
for _,fielder in pairs(fielders) do for _,fielder in pairs(fielders) do
if fielder.targetX ~= nil and fielder.targetY ~= nil then -- TODO: Target unforced runners (or their target bases) for tagging
local x, y, distance = normalizeVector(fielder.x, fielder.y, fielder.targetX, fielder.targetY) -- With new Position-based scheme, fielders are now able to set `fielder.target = runner` to track directly
if fielder.target ~= nil then
local x, y, distance = normalizeVector(fielder.x, fielder.y, fielder.target.x, fielder.target.y)
if distance > 1 then if distance > 1 then
fielder.x -= x * fielderSpeed * deltaTime fielder.x = fielder.x - (x * fielder.speed * deltaTime)
fielder.y -= y * fielderSpeed * deltaTime fielder.y = fielder.y - (y * fielder.speed * deltaTime)
else else
if fielder.onArrive then if fielder.onArrive then
fielder.onArrive() fielder.onArrive()
end end
fielder.targetX = nil fielder.target = nil
fielder.targetY = nil
end end
end end
if isTouchingBall(fielder.x, fielder.y) then if isTouchingBall(fielder.x, fielder.y) then
local touchedBase = isTouchingBase(fielder.x, fielder.y) local touchedBase = isTouchingBase(fielder.x, fielder.y)
for i,runner in pairs(runners) do for i,runner in pairs(runners) do
local runnerOnBase = isTouchingBase(runner.x, runner.y) local runnerOnBase = touchingBaseCache.get(runner)
if touchedBase and runner.forcedTo == touchedBase and touchedBase ~= runnerOnBase then if touchedBase and runner.forcedTo == touchedBase and touchedBase ~= runnerOnBase then
outRunner(i) outRunner(i)
elseif not runnerOnBase then elseif not runnerOnBase then
@ -262,37 +268,18 @@ function updateInfield()
end end
end end
--- Returns the nearest base, as well as the distance from that base --- Returns true if at least one runner is still moving
---@generic T : {x: number, y: number} ---@return boolean
---@param array T[]
---@param x: number
---@param y: number
---@return T,number
function getNearestOf(array, x, y)
local nearest, nearestDistance = nil, nil
for _, element in pairs(array) do
if nearest == nil then
nearest = element
nearestDistance = distanceBetween(element.x, element.y, x, y)
else
local distance = distanceBetween(element.x, element.y, x, y)
if distance < nearestDistance then
nearest = element
nearestDistance = distance
end
end
end
return nearest, nearestDistance
end
function updateRunners() function updateRunners()
local runnerSpeed = 20 local autoRunSpeed = 20
--autoRunSpeed = 140
local nonPlayerRunners = filter(runners, function (runner) local nonPlayerRunners = filter(runners, function (runner)
return runner ~= batter return runner ~= batter
end) end)
local runnerMoved = false
for _,runner in pairs(nonPlayerRunners) do for _,runner in pairs(nonPlayerRunners) do
local nearestBase, nearestBaseDistance = getNearestOf(bases, runner.x, runner.y) local _, nearestBaseDistance = getNearestOf(bases, runner.x, runner.y)
if runner.nextBase then if runner.nextBase then
local nb = runner.nextBase local nb = runner.nextBase
local x, y, distance = normalizeVector(runner.x, runner.y, nb.x, nb.y) local x, y, distance = normalizeVector(runner.x, runner.y, nb.x, nb.y)
@ -302,17 +289,21 @@ function updateRunners()
if crankChange < 0 then if crankChange < 0 then
mult = -1 mult = -1
end end
local prevX, prevY = runner.x, runner.y
-- TODO: Drift toward nearest base? -- TODO: Drift toward nearest base?
local autoRun = nearestBaseDistance > 5 and mult * runnerSpeed * deltaTime or 0 local autoRun = nearestBaseDistance > 5 and mult * autoRunSpeed * deltaTime or 0
mult = autoRun + (crankChange / 20) mult = autoRun + (crankChange / 20)
runner.x -= x * mult runner.x = runner.x - (x * mult)
runner.y -= y * mult runner.y = runner.y - (y * mult)
runnerMoved = runnerMoved or prevX ~= runner.x or prevY ~= runner.y
else else
runner.nextBase = nextBaseMap[runner.nextBase] runner.nextBase = nextBaseMap[runner.nextBase]
runner.forcedTo = nil runner.forcedTo = nil
end end
end end
end end
return runnerMoved
end end
---@return boolean ---@return boolean
@ -325,32 +316,82 @@ function throwArrivedBeforeRunner()
return false return false
end end
function getRunnerTargeting(base)
for _,runner in pairs(runners) do
if runner.nextBase == base then
return runner
end
end
return nil
end
---@return Base[] ---@return Base[]
function getForcedOutTargets() function getForcedOutTargets()
return { bases.first } local targets = {}
for _,base in pairs(bases) do
local runnerTargetingBase = getRunnerTargeting(base)
if runnerTargetingBase then
targets[#targets+1] = base
else
return targets
end
end
return targets
-- return { bases.first }
end end
---@return number,number --- Returns the position,distance of the basest closest to the runner furthest from a base
function getNextThrowTarget() function getBaseOfStrandedRunner()
local targets = getForcedOutTargets() local farRunnersBase, farDistance
return targets[1].x, targets[1].y for _,runner in pairs(runners) do
local base, distance = getNearestOf(bases, runner.x, runner.y, function(base)
return runner.nextBase == base
end)
if farRunnersBase == nil then
farRunnersBase = base
farDistance = distance
elseif farDistance < distance then
farRunnersBase = base
farDistance = distance
end
end
return farRunnersBase, farDistance
end end
--- Returns x,y of the throw target
---@return number|nil, number|nil
function getNextThrowTarget()
-- TODO: Handle missed throws, check for fielders at target, etc.
local targets = getForcedOutTargets()
print("forcedOutTargets:")
printTable(targets)
if #targets ~= 0 then
return targets[1].x, targets[1].y
end
-- local baseCloseToStrandedRunner = getBaseOfStrandedRunner()
-- return baseCloseToStrandedRunner.x, baseCloseToStrandedRunner.y
end
local resetFieldersAfterSeconds = 5
local secondsSinceLastRunnerMove = 0
function updateGameState() function updateGameState()
deltaTime = playdate.getElapsedTime() deltaTime = playdate.getElapsedTime() or 0
playdate.resetElapsedTime() playdate.resetElapsedTime()
elapsedTime = elapsedTime + deltaTime elapsedTime = elapsedTime + deltaTime
if elapsedTime > pitchClockSec then -- if elapsedTime > pitchClockSec then
elapsedTime = 0 -- elapsedTime = 0
pitch() -- pitch()
end -- end
if currentMode == MODES.running then if currentMode == MODES.running then
ballX = hitAnimatorX:currentValue() ballX = hitAnimatorX:currentValue()
ballY = hitAnimatorY:currentValue() ballY = hitAnimatorY:currentValue()
ballSize = ballSizeAnimator:currentValue() ballSize = ballSizeAnimator:currentValue()
else elseif ballY < 999 then
ballY = pitchAnimator:currentValue() ballY = pitchAnimator:currentValue()
ballSize = 6 ballSize = 6
end end
@ -375,25 +416,36 @@ function updateGameState()
ballDestX = ballX + (ballVelX * hitMult) ballDestX = ballX + (ballVelX * hitMult)
ballDestY = ballY + (ballVelY * hitMult) ballDestY = ballY + (ballVelY * hitMult)
throwBall(ballDestX, ballDestY, playdate.easingFunctions.outQuint, 2000) throwBall(ballDestX, ballDestY, playdate.easingFunctions.outQuint, 2000)
local chasingFielder = getNearestOf(fielders, ballDestX, ballDestY)
chasingFielder.targetX = ballDestX fielders.first.target = bases.first
chasingFielder.targetY = ballDestY
chasingFielder.onArrive = function()
local targetX, targetY = getNextThrowTarget()
throwBall(targetX, targetY, playdate.easingFunctions.linear)
chasingFielder.onArrive = nil
end
fielders.first.targetX = bases.first.x
fielders.first.targetY = bases.first.y
batter.nextBase = bases.first batter.nextBase = bases.first
batter.forcedTo = bases.first batter.forcedTo = bases.first
batter = nil -- Demote batter to a mere runner batter = nil -- Demote batter to a mere runner
local chasingFielder = getNearestOf(fielders, ballDestX, ballDestY)
chasingFielder.target = { x = ballDestX, y = ballDestY }
chasingFielder.onArrive = function()
local targetX, targetY = getNextThrowTarget()
if targetX ~= nil then
throwBall(targetX, targetY, playdate.easingFunctions.linear)
chasingFielder.onArrive = nil
end
end
end end
if currentMode == MODES.running then if currentMode == MODES.running then
updateRunners() if updateRunners() then
updateInfield() secondsSinceLastRunnerMove = 0
else
secondsSinceLastRunnerMove = secondsSinceLastRunnerMove + deltaTime
if secondsSinceLastRunnerMove > resetFieldersAfterSeconds then
resetFielderPositions(false)
currentMode = MODES.batting
batter = newRunner()
end end
end
end
updateFielders()
end end
function playdate.update() function playdate.update()
@ -443,6 +495,7 @@ function playdate.update()
-- TODO? Change blip speed depending on runner speed? -- TODO? Change blip speed depending on runner speed?
for _,runner in pairs(runners) do for _,runner in pairs(runners) do
-- TODO? Scale sprites down as y increases
playerImageBlipper:draw( playerImageBlipper:draw(
false, false,
runner.x + backgroundPan.x, runner.x + backgroundPan.x,

View File

@ -25,7 +25,7 @@ end
---@generic TIn ---@generic TIn
---@generic TOut ---@generic TOut
---@param array TIn[] ---@param array TIn[]
---@param condition fun(TIn): TOut ---@param mapper fun(TIn): TOut
---@return TOut[] ---@return TOut[]
function map(array, mapper) function map(array, mapper)
local newArray = {} local newArray = {}
@ -42,6 +42,8 @@ function distanceBetween(x1, y1, x2, y2)
return math.sqrt((a*a) + (b*b)), a, b return math.sqrt((a*a) + (b*b)), a, b
end end
--- Returns true only if the point is below the given line, within the x bounds of said line, and above the bottomBound
--- @return boolean
function pointDirectlyUnderLine(pointX, pointY, lineX1, lineY1, lineX2, lineY2, bottomBound) function pointDirectlyUnderLine(pointX, pointY, lineX1, lineY1, lineX2, lineY2, bottomBound)
-- This check currently assumes right-handedness. -- This check currently assumes right-handedness.
-- I.e. it assumes the ball is to the right of batBaseX -- I.e. it assumes the ball is to the right of batBaseX
@ -61,10 +63,56 @@ function pointDirectlyUnderLine(pointX, pointY, lineX1, lineY1, lineX2, lineY2,
return yDelta <= 0 return yDelta <= 0
end end
blipper = { --- Returns the nearest position object from the given point, as well as its distance from that point
--- Build an object that simply "blips" between the given images at the given interval. ---@generic T : {x: number, y: number | nil}
--- Expects `playdate.graphics.animation.blinker.updateAll()` to be called on every update. ---@param array T[]
new = function(msInterval, imagePath1, imagePath2) ---@param x number
---@param y number
---@return T,number|nil
function getNearestOf(array, x, y, extraCondition)
local nearest, nearestDistance = nil, nil
for _, element in pairs(array) do
if not extraCondition or extraCondition(element) then
if nearest == nil then
nearest = element
nearestDistance = distanceBetween(element.x, element.y, x, y)
else
local distance = distanceBetween(element.x, element.y, x, y)
if distance < nearestDistance then
nearest = element
nearestDistance = distance
end
end
end
end
return nearest, nearestDistance
end
local NO_VALUE = {}
function buildCache(fetcher)
local cacheData = {}
return {
cacheDate = cacheData,
get = function(key)
if cacheData[key] == NO_VALUE then
return nil
end
if cacheData[key] ~= nil then
return cacheData[key]
end
cacheData[key] = fetcher(key) or NO_VALUE
return cacheData[key]
end
}
end
blipper = {}
--- Build an object that simply "blips" between the given images at the given interval.
--- Expects `playdate.graphics.animation.blinker.updateAll()` to be called on every update.
function blipper.new(msInterval, imagePath1, imagePath2)
local blinker = playdate.graphics.animation.blinker.new(msInterval, msInterval, true) local blinker = playdate.graphics.animation.blinker.new(msInterval, msInterval, true)
blinker:start() blinker:start()
return { return {
@ -76,5 +124,4 @@ blipper = {
currentImage:draw(x, y) currentImage:draw(x, y)
end end
} }
end end
}