454 lines
12 KiB
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
|