358 lines
9.6 KiB
Lua
358 lines
9.6 KiB
Lua
import 'CoreLibs/animator.lua'
|
|
import 'CoreLibs/easing.lua'
|
|
import 'CoreLibs/graphics.lua'
|
|
import 'CoreLibs/object.lua'
|
|
import 'CoreLibs/ui.lua'
|
|
|
|
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 playerHighHat = gfx.image.new("images/game/player.png")
|
|
local playerLowHat = gfx.image.new("images/game/player-lowhat.png")
|
|
local playerImage = playerHighHat
|
|
local secPerFrame = 0.1
|
|
local playerFrameElapsed = 0
|
|
|
|
local backgroundPan = -240
|
|
|
|
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 ballY = ballStartY
|
|
local ballX = 200
|
|
local ballVelX, ballVelY = 0, 0
|
|
local ballSize = 6
|
|
|
|
local batTipX = 0
|
|
local batTipY = 0
|
|
|
|
local MODES = {
|
|
batting = 0,
|
|
running = 1
|
|
}
|
|
local currentMode = MODES.batting
|
|
|
|
local hitAnimatorY
|
|
local hitAnimatorX
|
|
|
|
local hitMult = 10
|
|
|
|
local deltaTime = 0
|
|
|
|
local basePositions = {
|
|
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 }
|
|
}
|
|
|
|
local fielders = {
|
|
first = {},
|
|
second = {},
|
|
shortstop = {},
|
|
third = {},
|
|
pitcher = {}
|
|
}
|
|
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
|
|
end
|
|
resetFielderPositions()
|
|
|
|
local playerStartingX = basePositions.home.x - 40
|
|
local playerStartingY = basePositions.home.y - 3
|
|
local player = {
|
|
x = playerStartingX,
|
|
y = playerStartingY,
|
|
nextBase = nil,
|
|
prevBase = nil,
|
|
}
|
|
|
|
local runners = { }
|
|
|
|
function hitBall(destX, destY, hitFlyTime)
|
|
ballSizeAnimator:reset(ballSizeMs)
|
|
hitAnimatorY = gfx.animator.new(hitFlyTime, ballY, destY, playdate.easingFunctions.outQuint)
|
|
hitAnimatorX = gfx.animator.new(hitFlyTime, ballX, destX, playdate.easingFunctions.outQuint)
|
|
end
|
|
|
|
function throwBall(destX, destY)
|
|
local throwFlyTime = distanceBetween(ballX, ballY, destX, destY) * 5
|
|
ballSizeAnimator:reset(throwFlyTime)
|
|
hitAnimatorY = gfx.animator.new(throwFlyTime, ballY, destY, playdate.easingFunctions.linear)
|
|
hitAnimatorX = gfx.animator.new(throwFlyTime, ballX, destX, playdate.easingFunctions.linear)
|
|
end
|
|
|
|
function getNextThrowTarget()
|
|
return basePositions.first.x, basePositions.first.y
|
|
end
|
|
|
|
function pitch()
|
|
pitchAnimator:reset()
|
|
resetFielderPositions()
|
|
ballVelX = 0
|
|
ballVelY = 0
|
|
ballX = 200
|
|
currentMode = MODES.batting
|
|
|
|
-- TODO: Add new runners, instead
|
|
runners = {}
|
|
player.x = playerStartingX
|
|
player.y = playerStartingY
|
|
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
|
|
|
|
function ballPassedThruBat(ballX, ballY, batBaseX, batBaseY, batTipX, batTipY)
|
|
-- This check currently assumes right-handedness.
|
|
-- I.e. it assumes the ball is to the right of batBaseX
|
|
if ballX < batBaseX or ballX > batTipX or ballY > screenH then
|
|
return false
|
|
end
|
|
|
|
local m = (batTipY - batBaseY) / (batTipX - batBaseX)
|
|
|
|
-- y = mx + b
|
|
-- b = y1 - (m * x1)
|
|
local b = batBaseY - (m * batBaseX)
|
|
local yOnLine = (m * ballX) + b
|
|
local yP = ballY
|
|
local yDelta = yOnLine - yP
|
|
|
|
return yDelta <= 0
|
|
end
|
|
|
|
function updateInfield()
|
|
if ballDestX == nil or ballDestY == nil then
|
|
return
|
|
end
|
|
|
|
local fielderSpeed = 40
|
|
for title,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
|
|
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)
|
|
if distance > 1 then
|
|
local mult = 1
|
|
if crankChange < 0 then
|
|
mult = -1
|
|
end
|
|
mult = (mult * runnerSpeed * deltaTime) + (crankChange / 20)
|
|
runner.x -= x * mult
|
|
runner.y -= y * mult
|
|
else
|
|
if runner.onArrive then
|
|
runner.onArrive()
|
|
end
|
|
runner.targetX = nil
|
|
runner.targetY = nil
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
function normalizeVector(x1, y1, x2, y2)
|
|
local distance, a, b = distanceBetween(x1, y1, x2, y2)
|
|
return a / distance, b / distance, distance
|
|
end
|
|
|
|
function distanceBetween(x1, y1, x2, y2)
|
|
local a = x1 - x2
|
|
local b = y1 - y2
|
|
return math.sqrt((a*a) + (b*b)), a, b
|
|
end
|
|
|
|
function getNearestFielder(x, y)
|
|
local nearestFielder, nearestDistance = nil, nil
|
|
for title,fielder in pairs(fielders) do
|
|
if nearestFielder == nil then
|
|
nearestFielder = fielder
|
|
nearestDistance = distanceBetween(fielder.x, fielder.y, x, y)
|
|
else
|
|
local distance = distanceBetween(fielder.x, fielder.y, x, y)
|
|
if distance < nearestDistance then
|
|
nearestFielder = fielder
|
|
nearestDistance = distance
|
|
end
|
|
end
|
|
end
|
|
|
|
return nearestFielder
|
|
end
|
|
|
|
function ballIsBeingThrown()
|
|
return false
|
|
end
|
|
|
|
function throwArrivedBeforeRunner()
|
|
return false
|
|
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
|
|
|
|
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
|
|
ballPassedThruBat(ballX, ballY, batBaseX, batBaseY, batTipX, batTipY) then
|
|
ballAngle = batAngle + math.rad(90)
|
|
|
|
mult = math.abs(acceleratedChange / 15)
|
|
ballVelX = mult * 10 * math.sin(ballAngle)
|
|
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)
|
|
hitBall(ballDestX, ballDestY, 2000)
|
|
local chasingFielder = getNearestFielder(ballDestX, ballDestY)
|
|
chasingFielder.targetX = ballDestX
|
|
chasingFielder.targetY = ballDestY
|
|
chasingFielder.onArrive = function()
|
|
throwBall(getNextThrowTarget())
|
|
chasingFielder.onArrive = nil
|
|
end
|
|
fielders.first.targetX = basePositions.first.x
|
|
fielders.first.targetY = basePositions.first.y
|
|
currentMode = MODES.running
|
|
player.nextBase = basePositions.first
|
|
runners[#runners+1] = player
|
|
end
|
|
|
|
if currentMode == MODES.running then
|
|
updateRunners()
|
|
updateInfield()
|
|
end
|
|
end
|
|
|
|
function playdate.update()
|
|
updateGameState()
|
|
|
|
grassBackground:draw(0, backgroundPan)
|
|
|
|
gfx.setColor(gfx.kColorBlack)
|
|
gfx.setLineWidth(2)
|
|
gfx.drawCircleAtPoint(ballX, ballY, ballSize)
|
|
|
|
for title,fielder in pairs(fielders) do
|
|
gfx.fillRect(fielder.x, fielder.y, 14, 25)
|
|
end
|
|
|
|
gfx.setLineWidth(5)
|
|
|
|
if currentMode == MODES.batting then
|
|
gfx.drawLine(batBaseX, batBaseY, batTipX, batTipY)
|
|
end
|
|
|
|
if playdate.isCrankDocked() then
|
|
playdate.ui.crankIndicator:draw()
|
|
end
|
|
|
|
playerImage:draw(player.x, player.y)
|
|
|
|
-- TODO: Use gfx.animation.blinker instead
|
|
if currentMode == MODES.running and playerFrameElapsed > secPerFrame then
|
|
playerFrameElapsed = 0
|
|
if playerImage == playerHighHat then
|
|
playerImage = playerLowHat
|
|
else
|
|
playerImage = playerHighHat
|
|
end
|
|
else
|
|
playerFrameElapsed += deltaTime
|
|
end
|
|
|
|
-- for i,runner in pairs(runners) do
|
|
-- playerImage:draw(runner.x, runner.y)
|
|
-- end
|
|
end
|