BatterUp/src/main.lua

454 lines
12 KiB
Lua

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<string, Base>
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<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,
},
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<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 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