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