import 'CoreLibs/animation.lua' import 'CoreLibs/animator.lua' import 'CoreLibs/easing.lua' import 'CoreLibs/graphics.lua' 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) playdate.setMenuImage(gfx.image.new("images/game/menu-image.png")) local grassBackground = gfx.image.new("images/game/grass.png") local playerImageBlipper = blipper.new( 100, "images/game/player.png", "images/game/player-lowhat.png" ) local backgroundPan = { x = 0, y = 0, } local pitchFlyTimeMs = 2500 local ballStartY, endY = 90, 250 local easingFunction = playdate.easingFunctions.outQuint local pitchAnimator = gfx.animator.new(pitchFlyTimeMs, ballStartY, endY, easingFunction) local ballSizeMs = 2000 local ballSizeAnimator = gfx.animator.new(ballSizeMs, 9, 6, playdate.easingFunctions.outBounce) local screenW, screenH = 400, 240 local centerX, centerY = screenW / 2, screenH / 2 local batBaseX, batBaseY = centerX - 34, 215 local batLength = 45 local tagDistance = 20 local ballY = ballStartY local ballX = 200 local ballSize = 6 local batTipX = 0 local batTipY = 0 local MODES = { batting = {}, running = {} } local currentMode = MODES.batting local hitAnimatorY local hitAnimatorX 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 }, third = { x = screenW * 0.03, y = screenH * 0.52 }, 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, }, second = {}, shortstop = {}, third = {}, pitcher = {}, left = {}, center = {}, right = {} } function resetFielderPositions() fielders.first.x = screenW - 65 fielders.first.y = screenH * 0.48 fielders.second.x = screenW * 0.70 fielders.second.y = screenH * 0.30 fielders.shortstop.x = screenW * 0.30 fielders.shortstop.y = screenH * 0.30 fielders.third.x = screenW * 0.1 fielders.third.y = screenH * 0.48 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 resetFielderPositions() local playerStartingX = bases.home.x - 40 local playerStartingY = bases.home.y - 3 --- @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 end ballSizeAnimator:reset(flyTimeMs) hitAnimatorY = gfx.animator.new(flyTimeMs, ballY, destY, easingFunc) hitAnimatorX = gfx.animator.new(flyTimeMs, ballX, destX, easingFunc) end function pitch() pitchAnimator:reset() resetFielderPositions() ballX = 200 currentMode = MODES.batting backgroundPan.y = 0 backgroundPan.x = 0 -- TODO: Add new runners, instead --runners = {} --batter.x = playerStartingX --batter.y = playerStartingY batter = newRunner() end function playdate.AButtonDown() pitch() end function playdate.upButtonDown() batBaseY -= 1 end function playdate.downButtonDown() batBaseY += 1 end function playdate.rightButtonDown() batBaseX += 1 end function playdate.leftButtonDown() batBaseX -= 1 end 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 end for _,fielder in pairs(fielders) do if fielder.targetX ~= nil and fielder.targetY ~= nil then local x, y, distance = normalizeVector(fielder.x, fielder.y, fielder.targetX, fielder.targetY) if distance > 1 then fielder.x -= x * fielderSpeed * deltaTime fielder.y -= y * fielderSpeed * deltaTime else if fielder.onArrive then fielder.onArrive() end fielder.targetX = nil fielder.targetY = nil end end 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 end end end end --- 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(element.x, element.y, x, y) if distance < nearestDistance then nearest = element nearestDistance = distance end end end 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 end function updateGameState() deltaTime = playdate.getElapsedTime() playdate.resetElapsedTime() elapsedTime = elapsedTime + deltaTime if elapsedTime > pitchClockSec then elapsedTime = 0 pitch() end if currentMode == MODES.running then ballX = hitAnimatorX:currentValue() ballY = hitAnimatorY:currentValue() ballSize = ballSizeAnimator:currentValue() else ballY = pitchAnimator:currentValue() ballSize = 6 end local batAngle = math.rad(playdate.getCrankPosition() + 90) batTipX = batBaseX + (batLength * math.sin(batAngle)) batTipY = batBaseY + (batLength * math.cos(batAngle)) crankChange, acceleratedChange = playdate.getCrankChange() if currentMode == MODES.batting and acceleratedChange >= 0 and pointDirectlyUnderLine(ballX, ballY, batBaseX, batBaseY, batTipX, batTipY, screenH) then currentMode = MODES.running ballAngle = batAngle + math.rad(90) 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 end ballDestX = ballX + (ballVelX * hitMult) ballDestY = ballY + (ballVelY * hitMult) throwBall(ballDestX, ballDestY, playdate.easingFunctions.outQuint, 2000) local chasingFielder = getNearestOf(fielders, ballDestX, ballDestY) chasingFielder.targetX = ballDestX 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.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 end if ballX < ballBuffer then backgroundPan.x = math.max(-400, -1 * (ballX - ballBuffer)) elseif ballX > (screenW - ballBuffer) then backgroundPan.x = math.min(800, -1 * (ballX - ballBuffer)) end if ballX > 0 and ballX < (screenW - ballBuffer) then backgroundPan.x = 0 end gfx.clear() grassBackground:draw(backgroundPan.x - 400, backgroundPan.y - 240) gfx.setColor(gfx.kColorBlack) gfx.setLineWidth(2) gfx.drawCircleAtPoint(ballX + backgroundPan.x, ballY + backgroundPan.y, ballSize) for _,fielder in pairs(fielders) do gfx.fillRect(fielder.x + backgroundPan.x, fielder.y + backgroundPan.y, 14, 25) end gfx.setLineWidth(5) if currentMode == MODES.batting then gfx.drawLine( batBaseX + backgroundPan.x, batBaseY + backgroundPan.y, batTipX + backgroundPan.x, batTipY + backgroundPan.y ) end if playdate.isCrankDocked() or (crankChange < 2 and currentMode == MODES.running) then playdate.ui.crankIndicator:draw() end -- TODO? Change blip speed depending on runner speed? for _,runner in pairs(runners) do playerImageBlipper:draw( false, runner.x + backgroundPan.x, runner.y + backgroundPan.y ) end end