Add .luarc.json for library reading.
Move more stuff to utils. More type hints. *Some* fleshing-out of getNextThrowTarget() etc. Closer to proper multi-baserunner support.
This commit is contained in:
parent
4093f9705a
commit
533c625d47
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"Lua.runtime.version": "Lua 5.4",
|
||||
"Lua.diagnostics.disable": ["undefined-global", "lowercase-global"],
|
||||
"Lua.diagnostics.globals": ["playdate", "import"],
|
||||
"Lua.runtime.nonstandardSymbol": ["+=", "-=", "*=", "/="],
|
||||
"Lua.workspace.library": ["/home/sage/Downloads/PlaydateSDK-2.6.2/CoreLibs"],
|
||||
"Lua.workspace.preloadFileSize": 1000
|
||||
}
|
285
src/main.lua
285
src/main.lua
|
@ -12,21 +12,22 @@ import 'utils.lua'
|
|||
|
||||
--- @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 }
|
||||
--- @alias Fielder { onArrive: fun() | nil, x: number | nil, y: number | nil, target: Position | nil, speed: number }
|
||||
|
||||
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 grassBackground = gfx.image.new("images/game/grass.png") or {}
|
||||
|
||||
local playerImageBlipper = blipper.new(
|
||||
100,
|
||||
100,
|
||||
"images/game/player.png",
|
||||
"images/game/player-lowhat.png"
|
||||
)
|
||||
|
||||
---@type Position
|
||||
local backgroundPan = {
|
||||
x = 0,
|
||||
y = 0,
|
||||
|
@ -48,7 +49,7 @@ local batLength = 45
|
|||
|
||||
local tagDistance = 20
|
||||
|
||||
local ballY = ballStartY
|
||||
local ballY = 999 -- ballStartY
|
||||
local ballX = 200
|
||||
local ballSize = 6
|
||||
|
||||
|
@ -83,54 +84,63 @@ local nextBaseMap = {
|
|||
[bases.third] = bases.home
|
||||
}
|
||||
|
||||
local fielderSpeed = 40
|
||||
|
||||
---@type table<string, Fielder>
|
||||
local fielders = {
|
||||
first = {
|
||||
x = nil,
|
||||
y = nil,
|
||||
speed = 40,
|
||||
},
|
||||
second = {},
|
||||
shortstop = {},
|
||||
third = {},
|
||||
pitcher = {},
|
||||
left = {},
|
||||
center = {},
|
||||
right = {}
|
||||
second = {
|
||||
speed = 40,
|
||||
},
|
||||
shortstop = {
|
||||
speed = 40,
|
||||
},
|
||||
third = {
|
||||
speed = 40,
|
||||
},
|
||||
pitcher = {
|
||||
speed = 30,
|
||||
},
|
||||
catcher = {
|
||||
speed = 20,
|
||||
},
|
||||
left = {
|
||||
speed = 40,
|
||||
},
|
||||
center = {
|
||||
speed = 40,
|
||||
},
|
||||
right = {
|
||||
speed = 40,
|
||||
}
|
||||
}
|
||||
|
||||
function resetFielderPositions()
|
||||
fielders.first.x = screenW - 65
|
||||
fielders.first.y = screenH * 0.48
|
||||
--- Resets the target positions of all fielders to their defaults (at their field positions).
|
||||
---@param fromOffTheField boolean If provided, also sets all runners' current position to one centralized location.
|
||||
function resetFielderPositions(fromOffTheField)
|
||||
if fromOffTheField then
|
||||
for _,fielder in pairs(fielders) do
|
||||
fielder.x = centerX
|
||||
fielder.y = screenH
|
||||
end
|
||||
end
|
||||
|
||||
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
|
||||
fielders.first.target = { x = screenW - 65, y = screenH * 0.48 }
|
||||
fielders.second.target = { x = screenW * 0.70, y = screenH * 0.30 }
|
||||
fielders.shortstop.target = { x = screenW * 0.30, y = screenH * 0.30 }
|
||||
fielders.third.target = { x = screenW * 0.1, y = screenH * 0.48 }
|
||||
fielders.pitcher.target = { x = screenW * 0.48, y = screenH * 0.40 }
|
||||
fielders.catcher.target = { x = screenW * 0.475, y = screenH * 0.92 }
|
||||
fielders.left.target = { x = screenW * -1, y = screenH * -0.2 }
|
||||
fielders.center.target = { x = centerX, y = screenH * -0.4 }
|
||||
fielders.right.target = { x = screenW * 2, y = screenH * fielders.left.target.y }
|
||||
end
|
||||
resetFielderPositions()
|
||||
resetFielderPositions(true)
|
||||
|
||||
local playerStartingX = bases.home.x - 40
|
||||
local playerStartingY = bases.home.y - 3
|
||||
|
||||
--- @type table<Runner, Runner>
|
||||
--- @type Runner[]
|
||||
local runners = { }
|
||||
|
||||
---@return Runner
|
||||
|
@ -145,6 +155,7 @@ function newRunner()
|
|||
return new
|
||||
end
|
||||
|
||||
---@type Runner | nil
|
||||
local batter = newRunner()
|
||||
|
||||
function throwBall(destX, destY, easingFunc, flyTimeMs)
|
||||
|
@ -158,18 +169,12 @@ end
|
|||
|
||||
function pitch()
|
||||
pitchAnimator:reset()
|
||||
resetFielderPositions()
|
||||
ballY = ballStartY
|
||||
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()
|
||||
|
@ -177,19 +182,19 @@ function playdate.AButtonDown()
|
|||
end
|
||||
|
||||
function playdate.upButtonDown()
|
||||
batBaseY -= 1
|
||||
batBaseY = batBaseY - 1
|
||||
end
|
||||
|
||||
function playdate.downButtonDown()
|
||||
batBaseY += 1
|
||||
batBaseY = batBaseY + 1
|
||||
end
|
||||
|
||||
function playdate.rightButtonDown()
|
||||
batBaseX += 1
|
||||
batBaseX = batBaseX + 1
|
||||
end
|
||||
|
||||
function playdate.leftButtonDown()
|
||||
batBaseX -= 1
|
||||
batBaseX = batBaseX - 1
|
||||
end
|
||||
|
||||
local pitchClockSec = 99
|
||||
|
@ -211,8 +216,6 @@ end
|
|||
local ballCatchHitbox = 3
|
||||
function isTouchingBall(x, y)
|
||||
local ballDistance = distanceBetween(x, y, ballX, ballY)
|
||||
-- print("ballDistance:")
|
||||
-- print(ballDistance)
|
||||
return ballDistance < ballCatchHitbox
|
||||
end
|
||||
|
||||
|
@ -224,31 +227,34 @@ function outRunner(runnerIndex)
|
|||
updateForcedTos()
|
||||
end
|
||||
|
||||
function updateInfield()
|
||||
if ballDestX == nil or ballDestY == nil then
|
||||
return
|
||||
end
|
||||
function updateFielders()
|
||||
local touchingBaseCache = buildCache(
|
||||
function(runner)
|
||||
return isTouchingBase(runner.x, runner.y)
|
||||
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)
|
||||
-- TODO: Target unforced runners (or their target bases) for tagging
|
||||
-- With new Position-based scheme, fielders are now able to set `fielder.target = runner` to track directly
|
||||
if fielder.target ~= nil then
|
||||
local x, y, distance = normalizeVector(fielder.x, fielder.y, fielder.target.x, fielder.target.y)
|
||||
|
||||
if distance > 1 then
|
||||
fielder.x -= x * fielderSpeed * deltaTime
|
||||
fielder.y -= y * fielderSpeed * deltaTime
|
||||
fielder.x = fielder.x - (x * fielder.speed * deltaTime)
|
||||
fielder.y = fielder.y - (y * fielder.speed * deltaTime)
|
||||
else
|
||||
if fielder.onArrive then
|
||||
fielder.onArrive()
|
||||
end
|
||||
fielder.targetX = nil
|
||||
fielder.targetY = nil
|
||||
fielder.target = 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)
|
||||
local runnerOnBase = touchingBaseCache.get(runner)
|
||||
if touchedBase and runner.forcedTo == touchedBase and touchedBase ~= runnerOnBase then
|
||||
outRunner(i)
|
||||
elseif not runnerOnBase then
|
||||
|
@ -262,37 +268,18 @@ function updateInfield()
|
|||
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
|
||||
|
||||
--- Returns true if at least one runner is still moving
|
||||
---@return boolean
|
||||
function updateRunners()
|
||||
local runnerSpeed = 20
|
||||
local autoRunSpeed = 20
|
||||
--autoRunSpeed = 140
|
||||
local nonPlayerRunners = filter(runners, function (runner)
|
||||
return runner ~= batter
|
||||
end)
|
||||
|
||||
local runnerMoved = false
|
||||
for _,runner in pairs(nonPlayerRunners) do
|
||||
local nearestBase, nearestBaseDistance = getNearestOf(bases, runner.x, runner.y)
|
||||
local _, 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)
|
||||
|
@ -302,17 +289,21 @@ function updateRunners()
|
|||
if crankChange < 0 then
|
||||
mult = -1
|
||||
end
|
||||
local prevX, prevY = runner.x, runner.y
|
||||
-- TODO: Drift toward nearest base?
|
||||
local autoRun = nearestBaseDistance > 5 and mult * runnerSpeed * deltaTime or 0
|
||||
local autoRun = nearestBaseDistance > 5 and mult * autoRunSpeed * deltaTime or 0
|
||||
mult = autoRun + (crankChange / 20)
|
||||
runner.x -= x * mult
|
||||
runner.y -= y * mult
|
||||
runner.x = runner.x - (x * mult)
|
||||
runner.y = runner.y - (y * mult)
|
||||
runnerMoved = runnerMoved or prevX ~= runner.x or prevY ~= runner.y
|
||||
else
|
||||
runner.nextBase = nextBaseMap[runner.nextBase]
|
||||
runner.forcedTo = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return runnerMoved
|
||||
end
|
||||
|
||||
---@return boolean
|
||||
|
@ -325,32 +316,82 @@ function throwArrivedBeforeRunner()
|
|||
return false
|
||||
end
|
||||
|
||||
function getRunnerTargeting(base)
|
||||
for _,runner in pairs(runners) do
|
||||
if runner.nextBase == base then
|
||||
return runner
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
---@return Base[]
|
||||
function getForcedOutTargets()
|
||||
return { bases.first }
|
||||
local targets = {}
|
||||
for _,base in pairs(bases) do
|
||||
local runnerTargetingBase = getRunnerTargeting(base)
|
||||
if runnerTargetingBase then
|
||||
targets[#targets+1] = base
|
||||
else
|
||||
return targets
|
||||
end
|
||||
end
|
||||
return targets
|
||||
-- return { bases.first }
|
||||
end
|
||||
|
||||
---@return number,number
|
||||
function getNextThrowTarget()
|
||||
local targets = getForcedOutTargets()
|
||||
return targets[1].x, targets[1].y
|
||||
--- Returns the position,distance of the basest closest to the runner furthest from a base
|
||||
function getBaseOfStrandedRunner()
|
||||
local farRunnersBase, farDistance
|
||||
for _,runner in pairs(runners) do
|
||||
local base, distance = getNearestOf(bases, runner.x, runner.y, function(base)
|
||||
return runner.nextBase == base
|
||||
end)
|
||||
if farRunnersBase == nil then
|
||||
farRunnersBase = base
|
||||
farDistance = distance
|
||||
elseif farDistance < distance then
|
||||
farRunnersBase = base
|
||||
farDistance = distance
|
||||
end
|
||||
end
|
||||
|
||||
return farRunnersBase, farDistance
|
||||
end
|
||||
|
||||
--- Returns x,y of the throw target
|
||||
---@return number|nil, number|nil
|
||||
function getNextThrowTarget()
|
||||
-- TODO: Handle missed throws, check for fielders at target, etc.
|
||||
local targets = getForcedOutTargets()
|
||||
print("forcedOutTargets:")
|
||||
printTable(targets)
|
||||
if #targets ~= 0 then
|
||||
return targets[1].x, targets[1].y
|
||||
end
|
||||
|
||||
-- local baseCloseToStrandedRunner = getBaseOfStrandedRunner()
|
||||
-- return baseCloseToStrandedRunner.x, baseCloseToStrandedRunner.y
|
||||
end
|
||||
|
||||
local resetFieldersAfterSeconds = 5
|
||||
local secondsSinceLastRunnerMove = 0
|
||||
|
||||
function updateGameState()
|
||||
deltaTime = playdate.getElapsedTime()
|
||||
deltaTime = playdate.getElapsedTime() or 0
|
||||
playdate.resetElapsedTime()
|
||||
elapsedTime = elapsedTime + deltaTime
|
||||
|
||||
if elapsedTime > pitchClockSec then
|
||||
elapsedTime = 0
|
||||
pitch()
|
||||
end
|
||||
-- if elapsedTime > pitchClockSec then
|
||||
-- elapsedTime = 0
|
||||
-- pitch()
|
||||
-- end
|
||||
|
||||
if currentMode == MODES.running then
|
||||
ballX = hitAnimatorX:currentValue()
|
||||
ballY = hitAnimatorY:currentValue()
|
||||
ballSize = ballSizeAnimator:currentValue()
|
||||
else
|
||||
elseif ballY < 999 then
|
||||
ballY = pitchAnimator:currentValue()
|
||||
ballSize = 6
|
||||
end
|
||||
|
@ -375,25 +416,36 @@ function updateGameState()
|
|||
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
|
||||
|
||||
fielders.first.target = bases.first
|
||||
batter.nextBase = bases.first
|
||||
batter.forcedTo = bases.first
|
||||
batter = nil -- Demote batter to a mere runner
|
||||
|
||||
local chasingFielder = getNearestOf(fielders, ballDestX, ballDestY)
|
||||
chasingFielder.target = { x = ballDestX, y = ballDestY }
|
||||
chasingFielder.onArrive = function()
|
||||
local targetX, targetY = getNextThrowTarget()
|
||||
if targetX ~= nil then
|
||||
throwBall(targetX, targetY, playdate.easingFunctions.linear)
|
||||
chasingFielder.onArrive = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if currentMode == MODES.running then
|
||||
updateRunners()
|
||||
updateInfield()
|
||||
if updateRunners() then
|
||||
secondsSinceLastRunnerMove = 0
|
||||
else
|
||||
secondsSinceLastRunnerMove = secondsSinceLastRunnerMove + deltaTime
|
||||
if secondsSinceLastRunnerMove > resetFieldersAfterSeconds then
|
||||
resetFielderPositions(false)
|
||||
currentMode = MODES.batting
|
||||
batter = newRunner()
|
||||
end
|
||||
end
|
||||
end
|
||||
updateFielders()
|
||||
end
|
||||
|
||||
function playdate.update()
|
||||
|
@ -443,6 +495,7 @@ function playdate.update()
|
|||
|
||||
-- TODO? Change blip speed depending on runner speed?
|
||||
for _,runner in pairs(runners) do
|
||||
-- TODO? Scale sprites down as y increases
|
||||
playerImageBlipper:draw(
|
||||
false,
|
||||
runner.x + backgroundPan.x,
|
||||
|
|
|
@ -25,7 +25,7 @@ end
|
|||
---@generic TIn
|
||||
---@generic TOut
|
||||
---@param array TIn[]
|
||||
---@param condition fun(TIn): TOut
|
||||
---@param mapper fun(TIn): TOut
|
||||
---@return TOut[]
|
||||
function map(array, mapper)
|
||||
local newArray = {}
|
||||
|
@ -42,6 +42,8 @@ function distanceBetween(x1, y1, x2, y2)
|
|||
return math.sqrt((a*a) + (b*b)), a, b
|
||||
end
|
||||
|
||||
--- Returns true only if the point is below the given line, within the x bounds of said line, and above the bottomBound
|
||||
--- @return boolean
|
||||
function pointDirectlyUnderLine(pointX, pointY, lineX1, lineY1, lineX2, lineY2, bottomBound)
|
||||
-- This check currently assumes right-handedness.
|
||||
-- I.e. it assumes the ball is to the right of batBaseX
|
||||
|
@ -61,20 +63,65 @@ function pointDirectlyUnderLine(pointX, pointY, lineX1, lineY1, lineX2, lineY2,
|
|||
return yDelta <= 0
|
||||
end
|
||||
|
||||
blipper = {
|
||||
--- Build an object that simply "blips" between the given images at the given interval.
|
||||
--- Expects `playdate.graphics.animation.blinker.updateAll()` to be called on every update.
|
||||
new = function(msInterval, imagePath1, imagePath2)
|
||||
local blinker = playdate.graphics.animation.blinker.new(msInterval, msInterval, true)
|
||||
blinker:start()
|
||||
return {
|
||||
blinker = blinker,
|
||||
image1 = playdate.graphics.image.new(imagePath1),
|
||||
image2 = playdate.graphics.image.new(imagePath2),
|
||||
draw = function(self, disableBlipping, x, y)
|
||||
local currentImage = (disableBlipping or self.blinker.on) and self.image2 or self.image1
|
||||
currentImage:draw(x, y)
|
||||
end
|
||||
}
|
||||
--- Returns the nearest position object from the given point, as well as its distance from that point
|
||||
---@generic T : {x: number, y: number | nil}
|
||||
---@param array T[]
|
||||
---@param x number
|
||||
---@param y number
|
||||
---@return T,number|nil
|
||||
function getNearestOf(array, x, y, extraCondition)
|
||||
local nearest, nearestDistance = nil, nil
|
||||
for _, element in pairs(array) do
|
||||
if not extraCondition or extraCondition(element) then
|
||||
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
|
||||
end
|
||||
}
|
||||
|
||||
return nearest, nearestDistance
|
||||
end
|
||||
|
||||
local NO_VALUE = {}
|
||||
|
||||
function buildCache(fetcher)
|
||||
local cacheData = {}
|
||||
return {
|
||||
cacheDate = cacheData,
|
||||
get = function(key)
|
||||
if cacheData[key] == NO_VALUE then
|
||||
return nil
|
||||
end
|
||||
if cacheData[key] ~= nil then
|
||||
return cacheData[key]
|
||||
end
|
||||
cacheData[key] = fetcher(key) or NO_VALUE
|
||||
return cacheData[key]
|
||||
end
|
||||
}
|
||||
end
|
||||
|
||||
blipper = {}
|
||||
|
||||
--- Build an object that simply "blips" between the given images at the given interval.
|
||||
--- Expects `playdate.graphics.animation.blinker.updateAll()` to be called on every update.
|
||||
function blipper.new(msInterval, imagePath1, imagePath2)
|
||||
local blinker = playdate.graphics.animation.blinker.new(msInterval, msInterval, true)
|
||||
blinker:start()
|
||||
return {
|
||||
blinker = blinker,
|
||||
image1 = playdate.graphics.image.new(imagePath1),
|
||||
image2 = playdate.graphics.image.new(imagePath2),
|
||||
draw = function(self, disableBlipping, x, y)
|
||||
local currentImage = (disableBlipping or self.blinker.on) and self.image2 or self.image1
|
||||
currentImage:draw(x, y)
|
||||
end
|
||||
}
|
||||
end
|
Loading…
Reference in New Issue