Refactoring, typehints, better baserunning

This commit is contained in:
Sage Vaillancourt 2025-01-29 00:36:55 -05:00
parent db52923b32
commit 4093f9705a
2 changed files with 178 additions and 66 deletions

View File

@ -6,6 +6,14 @@ import 'CoreLibs/object.lua'
import 'CoreLibs/ui.lua' import 'CoreLibs/ui.lua'
import 'utils.lua' import 'utils.lua'
--- @alias Position { x: number, y: number }
--- @alias Base { x: number, y: number }
--- @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 }
local gfx = playdate.graphics local gfx = playdate.graphics
playdate.display.setRefreshRate(50) playdate.display.setRefreshRate(50)
gfx.setBackgroundColor(gfx.kColorWhite) gfx.setBackgroundColor(gfx.kColorWhite)
@ -38,9 +46,10 @@ local batBaseX, batBaseY = centerX - 34, 215
local batLength = 45 local batLength = 45
local tagDistance = 20
local ballY = ballStartY local ballY = ballStartY
local ballX = 200 local ballX = 200
local ballVelX, ballVelY = 0, 0
local ballSize = 6 local ballSize = 6
local batTipX = 0 local batTipX = 0
@ -59,6 +68,7 @@ local hitMult = 10
local deltaTime = 0 local deltaTime = 0
---@type table<string, Base>
local bases = { local bases = {
first = { x = screenW * 0.93, y = screenH * 0.52 }, first = { x = screenW * 0.93, y = screenH * 0.52 },
second = { x = screenW * 0.47, y = screenH * 0.19 }, second = { x = screenW * 0.47, y = screenH * 0.19 },
@ -66,12 +76,20 @@ local bases = {
home = { x = screenW * 0.474, y = screenH * 0.79 } home = { x = screenW * 0.474, y = screenH * 0.79 }
} }
---@type table<Base, Base>
local nextBaseMap = {
[bases.first] = bases.second,
[bases.second] = bases.third,
[bases.third] = bases.home
}
local fielderSpeed = 40 local fielderSpeed = 40
---@type table<string, Fielder>
local fielders = { local fielders = {
first = { first = {
x = nil, x = nil,
y = nil, y = nil,
covering = nil -- TODO?
}, },
second = {}, second = {},
shortstop = {}, shortstop = {},
@ -81,6 +99,7 @@ local fielders = {
center = {}, center = {},
right = {} right = {}
} }
function resetFielderPositions() function resetFielderPositions()
fielders.first.x = screenW - 65 fielders.first.x = screenW - 65
fielders.first.y = screenH * 0.48 fielders.first.y = screenH * 0.48
@ -110,14 +129,23 @@ resetFielderPositions()
local playerStartingX = bases.home.x - 40 local playerStartingX = bases.home.x - 40
local playerStartingY = bases.home.y - 3 local playerStartingY = bases.home.y - 3
local player = {
--- @type table<Runner, Runner>
local runners = { }
---@return Runner
function newRunner()
local new = {
x = playerStartingX, x = playerStartingX,
y = playerStartingY, y = playerStartingY,
nextBase = nil, nextBase = nil,
prevBase = nil, prevBase = nil,
} }
runners[#runners + 1] = new
return new
end
local runners = { } local batter = newRunner()
function throwBall(destX, destY, easingFunc, flyTimeMs) function throwBall(destX, destY, easingFunc, flyTimeMs)
if not flyTimeMs then if not flyTimeMs then
@ -131,8 +159,6 @@ end
function pitch() function pitch()
pitchAnimator:reset() pitchAnimator:reset()
resetFielderPositions() resetFielderPositions()
ballVelX = 0
ballVelY = 0
ballX = 200 ballX = 200
currentMode = MODES.batting currentMode = MODES.batting
@ -140,9 +166,10 @@ function pitch()
backgroundPan.x = 0 backgroundPan.x = 0
-- TODO: Add new runners, instead -- TODO: Add new runners, instead
runners = {} --runners = {}
player.x = playerStartingX --batter.x = playerStartingX
player.y = playerStartingY --batter.y = playerStartingY
batter = newRunner()
end end
function playdate.AButtonDown() function playdate.AButtonDown()
@ -169,6 +196,34 @@ local pitchClockSec = 99
local elapsedTime = 0 local elapsedTime = 0
local crankChange local crankChange
local baseHitbox = 13
--- Returns the base being touched by the runner at (x,y), or nil, if no base is being touched
function isTouchingBase(x, y)
for _,base in pairs(bases) do
if distanceBetween(x, y, base.x, base.y) < baseHitbox then
return base
end
end
return nil
end
local ballCatchHitbox = 3
function isTouchingBall(x, y)
local ballDistance = distanceBetween(x, y, ballX, ballY)
-- print("ballDistance:")
-- print(ballDistance)
return ballDistance < ballCatchHitbox
end
function updateForcedTos()
end
function outRunner(runnerIndex)
table.remove(runners, runnerIndex)
updateForcedTos()
end
function updateInfield() function updateInfield()
if ballDestX == nil or ballDestY == nil then if ballDestX == nil or ballDestY == nil then
return return
@ -189,66 +244,93 @@ function updateInfield()
fielder.targetY = nil fielder.targetY = nil
end end
end end
end
end
function updateRunners() if isTouchingBall(fielder.x, fielder.y) then
local runnerSpeed = 20 local touchedBase = isTouchingBase(fielder.x, fielder.y)
for i,runner in pairs(runners) do for i,runner in pairs(runners) do
if runner.nextBase then local runnerOnBase = isTouchingBase(runner.x, runner.y)
local nb = runner.nextBase if touchedBase and runner.forcedTo == touchedBase and touchedBase ~= runnerOnBase then
local x, y, distance = normalizeVector(runner.x, runner.y, nb.x, nb.y) outRunner(i)
runner.hasArrived = distance <= 1 elseif not runnerOnBase then
local fielderDistance = distanceBetween(runner.x, runner.y, fielder.x, fielder.y)
if runner.hasArrived then if fielderDistance < tagDistance then
-- if runner.onArrive then outRunner(i)
-- runner.onArrive() end
-- end
runner.targetX = nil
runner.targetY = nil
else
local mult = 1
if crankChange < 0 then
mult = -1
end end
mult = (mult * runnerSpeed * deltaTime) + (crankChange / 20)
runner.x -= x * mult
runner.y -= y * mult
end end
end end
end end
end end
function getNearestFielder(x, y) --- Returns the nearest base, as well as the distance from that base
local nearestFielder, nearestDistance = nil, nil ---@generic T : {x: number, y: number}
for _, fielder in pairs(fielders) do ---@param array T[]
if nearestFielder == nil then ---@param x: number
nearestFielder = fielder ---@param y: number
nearestDistance = distanceBetween(fielder.x, fielder.y, x, y) ---@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 else
local distance = distanceBetween(fielder.x, fielder.y, x, y) local distance = distanceBetween(element.x, element.y, x, y)
if distance < nearestDistance then if distance < nearestDistance then
nearestFielder = fielder nearest = element
nearestDistance = distance nearestDistance = distance
end end
end end
end end
return nearestFielder return nearest, nearestDistance
end end
function updateRunners()
local runnerSpeed = 20
local nonPlayerRunners = filter(runners, function (runner)
return runner ~= batter
end)
for _,runner in pairs(nonPlayerRunners) do
local nearestBase, nearestBaseDistance = getNearestOf(bases, runner.x, runner.y)
if runner.nextBase then
local nb = runner.nextBase
local x, y, distance = normalizeVector(runner.x, runner.y, nb.x, nb.y)
if distance > 1 then
local mult = 1
if crankChange < 0 then
mult = -1
end
-- TODO: Drift toward nearest base?
local autoRun = nearestBaseDistance > 5 and mult * runnerSpeed * deltaTime or 0
mult = autoRun + (crankChange / 20)
runner.x -= x * mult
runner.y -= y * mult
else
runner.nextBase = nextBaseMap[runner.nextBase]
runner.forcedTo = nil
end
end
end
end
---@return boolean
function ballIsBeingThrown() function ballIsBeingThrown()
return false return false
end end
---@return boolean
function throwArrivedBeforeRunner() function throwArrivedBeforeRunner()
return false return false
end end
---@return Base[]
function getForcedOutTargets() function getForcedOutTargets()
return { bases.first } return { bases.first }
end end
---@return number,number
function getNextThrowTarget() function getNextThrowTarget()
local targets = getForcedOutTargets() local targets = getForcedOutTargets()
return targets[1].x, targets[1].y return targets[1].x, targets[1].y
@ -273,7 +355,7 @@ function updateGameState()
ballSize = 6 ballSize = 6
end end
batAngle = math.rad(playdate.getCrankPosition() + 90) local batAngle = math.rad(playdate.getCrankPosition() + 90)
batTipX = batBaseX + (batLength * math.sin(batAngle)) batTipX = batBaseX + (batLength * math.sin(batAngle))
batTipY = batBaseY + (batLength * math.cos(batAngle)) batTipY = batBaseY + (batLength * math.cos(batAngle))
@ -283,9 +365,9 @@ function updateGameState()
currentMode = MODES.running currentMode = MODES.running
ballAngle = batAngle + math.rad(90) ballAngle = batAngle + math.rad(90)
mult = math.abs(acceleratedChange / 15) local mult = math.abs(acceleratedChange / 15)
ballVelX = mult * 10 * math.sin(ballAngle) local ballVelX = mult * 10 * math.sin(ballAngle)
ballVelY = mult * 5 * math.cos(ballAngle) local ballVelY = mult * 5 * math.cos(ballAngle)
if ballVelY > 0 then if ballVelY > 0 then
ballVelX = ballVelX * -1 ballVelX = ballVelX * -1
ballVelY = ballVelY * -1 ballVelY = ballVelY * -1
@ -293,7 +375,7 @@ 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 = getNearestFielder(ballDestX, ballDestY) local chasingFielder = getNearestOf(fielders, ballDestX, ballDestY)
chasingFielder.targetX = ballDestX chasingFielder.targetX = ballDestX
chasingFielder.targetY = ballDestY chasingFielder.targetY = ballDestY
chasingFielder.onArrive = function() chasingFielder.onArrive = function()
@ -303,20 +385,27 @@ function updateGameState()
end end
fielders.first.targetX = bases.first.x fielders.first.targetX = bases.first.x
fielders.first.targetY = bases.first.y fielders.first.targetY = bases.first.y
player.nextBase = bases.first batter.nextBase = bases.first
runners[#runners+1] = player batter.forcedTo = bases.first
batter = nil -- Demote batter to a mere runner
end end
if currentMode == MODES.running then if currentMode == MODES.running then
updateRunners() updateRunners()
updateInfield() updateInfield()
end end
end
function playdate.update()
updateGameState()
playdate.graphics.animation.blinker.updateAll()
-- TODO: Show baserunning minimap when panning? -- TODO: Show baserunning minimap when panning?
local ballBuffer = 5 local ballBuffer = 5
if ballY < ballBuffer then if ballY < ballBuffer then
backgroundPan.y = math.max(ballBuffer, -1 * (ballY - ballBuffer)) backgroundPan.y = math.max(ballBuffer, -1 * (ballY - ballBuffer))
else backgroundPan.y = 0 else
backgroundPan.y = 0
end end
if ballX < ballBuffer then if ballX < ballBuffer then
backgroundPan.x = math.max(-400, -1 * (ballX - ballBuffer)) backgroundPan.x = math.max(-400, -1 * (ballX - ballBuffer))
@ -327,11 +416,6 @@ function updateGameState()
if ballX > 0 and ballX < (screenW - ballBuffer) then if ballX > 0 and ballX < (screenW - ballBuffer) then
backgroundPan.x = 0 backgroundPan.x = 0
end end
end
function playdate.update()
updateGameState()
playdate.graphics.animation.blinker.updateAll()
gfx.clear() gfx.clear()
grassBackground:draw(backgroundPan.x - 400, backgroundPan.y - 240) grassBackground:draw(backgroundPan.x - 400, backgroundPan.y - 240)
@ -358,13 +442,12 @@ function playdate.update()
end end
-- TODO? Change blip speed depending on runner speed? -- TODO? Change blip speed depending on runner speed?
for _,runner in pairs(runners) do
playerImageBlipper:draw( playerImageBlipper:draw(
currentMode ~= MODES.running, false,
player.x + backgroundPan.x, runner.x + backgroundPan.x,
player.y + backgroundPan.y runner.y + backgroundPan.y
) )
end
-- for i,runner in pairs(runners) do
-- playerImage:draw(runner.x, runner.y)
-- end
end end

View File

@ -2,11 +2,40 @@ import 'CoreLibs/animation.lua'
import 'CoreLibs/graphics.lua' import 'CoreLibs/graphics.lua'
--- Returns the normalized vector as two values, plus the distance between the given points. --- Returns the normalized vector as two values, plus the distance between the given points.
---@return number, number, number
function normalizeVector(x1, y1, x2, y2) function normalizeVector(x1, y1, x2, y2)
local distance, a, b = distanceBetween(x1, y1, x2, y2) local distance, a, b = distanceBetween(x1, y1, x2, y2)
return a / distance, b / distance, distance return a / distance, b / distance, distance
end end
---@generic T
---@param array T[]
---@param condition fun(T): boolean
---@return T[]
function filter(array, condition)
local newArray = {}
for _,element in pairs(array) do
if condition(element) then
newArray[#newArray + 1] = element
end
end
return newArray
end
---@generic TIn
---@generic TOut
---@param array TIn[]
---@param condition fun(TIn): TOut
---@return TOut[]
function map(array, mapper)
local newArray = {}
for _,element in pairs(array) do
newArray[#newArray + 1] = mapper(element)
end
return newArray
end
---@return number, number, number
function distanceBetween(x1, y1, x2, y2) function distanceBetween(x1, y1, x2, y2)
local a = x1 - x2 local a = x1 - x2
local b = y1 - y2 local b = y1 - y2