diff --git a/src/main.lua b/src/main.lua index 5462775..2fcd395 100644 --- a/src/main.lua +++ b/src/main.lua @@ -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 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 +local nextBaseMap = { + [bases.first] = bases.second, + [bases.second] = bases.third, + [bases.third] = bases.home +} + local fielderSpeed = 40 + +---@type table 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,15 +129,24 @@ resetFielderPositions() local playerStartingX = bases.home.x - 40 local playerStartingY = bases.home.y - 3 -local player = { - x = playerStartingX, - y = playerStartingY, - nextBase = nil, - prevBase = nil, -} +--- @type table local runners = { } +---@return Runner +function newRunner() + local new = { + x = playerStartingX, + y = playerStartingY, + nextBase = nil, + prevBase = nil, + } + runners[#runners + 1] = new + return new +end + +local batter = newRunner() + function throwBall(destX, destY, easingFunc, flyTimeMs) if not flyTimeMs then flyTimeMs = distanceBetween(ballX, ballY, destX, destY) * 5 @@ -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 - 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 + if isTouchingBall(fielder.x, fielder.y) then + local touchedBase = isTouchingBase(fielder.x, fielder.y) + for i,runner in pairs(runners) do + 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? - playerImageBlipper:draw( - currentMode ~= MODES.running, - player.x + backgroundPan.x, - player.y + backgroundPan.y - ) + for _,runner in pairs(runners) do + playerImageBlipper:draw( + false, + runner.x + backgroundPan.x, + runner.y + backgroundPan.y + ) + end - -- for i,runner in pairs(runners) do - -- playerImage:draw(runner.x, runner.y) - -- end end diff --git a/src/utils.lua b/src/utils.lua index 86820e8..b7069c9 100644 --- a/src/utils.lua +++ b/src/utils.lua @@ -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