Add basic testing. Requires luaunit.
Put utils functions into a module. Of sorts, anyway. Add benchAllFielders() Add playerIsOn() and associated "sides". Pass appliedSpeed through into runner funcs. Should make it easier to put under NPC control.
This commit is contained in:
parent
45a20cbb70
commit
779b13d56b
3
Makefile
3
Makefile
|
@ -7,5 +7,8 @@ check:
|
||||||
stylua -c --indent-type Spaces src/
|
stylua -c --indent-type Spaces src/
|
||||||
cat __stub.ext.lua <(sed 's/^function/-- selene: allow(unused_variable)\nfunction/' ${PLAYDATE_SDK_PATH}/CoreLibs/__types.lua) ${SOURCE_FILES} | grep -v '^import' | sed 's/<const>//g' | selene -
|
cat __stub.ext.lua <(sed 's/^function/-- selene: allow(unused_variable)\nfunction/' ${PLAYDATE_SDK_PATH}/CoreLibs/__types.lua) ${SOURCE_FILES} | grep -v '^import' | sed 's/<const>//g' | selene -
|
||||||
|
|
||||||
|
test: check
|
||||||
|
(cd src; find ./test -name '*lua' | xargs -L1 lua)
|
||||||
|
|
||||||
lint:
|
lint:
|
||||||
stylua --indent-type Spaces src/
|
stylua --indent-type Spaces src/
|
||||||
|
|
234
src/main.lua
234
src/main.lua
|
@ -47,7 +47,7 @@ local Screen <const> = {
|
||||||
H = playdate.display.getHeight(),
|
H = playdate.display.getHeight(),
|
||||||
}
|
}
|
||||||
|
|
||||||
local Center <const> = xy(Screen.W / 2, Screen.H / 2)
|
local Center <const> = utils.xy(Screen.W / 2, Screen.H / 2)
|
||||||
|
|
||||||
local BootTune <const> = playdate.sound.sampleplayer.new("sounds/boot-tune.wav")
|
local BootTune <const> = playdate.sound.sampleplayer.new("sounds/boot-tune.wav")
|
||||||
-- local BootTune <const> = playdate.sound.sampleplayer.new("sounds/boot-tune-organy.wav")
|
-- local BootTune <const> = playdate.sound.sampleplayer.new("sounds/boot-tune-organy.wav")
|
||||||
|
@ -62,7 +62,7 @@ local PlayerImageBlipper <const> = blipper.new(100, "images/game/player.png", "i
|
||||||
|
|
||||||
local DanceBounceMs <const> = 500
|
local DanceBounceMs <const> = 500
|
||||||
local DanceBounceCount <const> = 4
|
local DanceBounceCount <const> = 4
|
||||||
local FielderDanceAnimator <const> = gfx.animator.new(1, 10, 0, easingHill)
|
local FielderDanceAnimator <const> = gfx.animator.new(1, 10, 0, utils.easingHill)
|
||||||
FielderDanceAnimator.repeatCount = DanceBounceCount - 1
|
FielderDanceAnimator.repeatCount = DanceBounceCount - 1
|
||||||
|
|
||||||
-- selene: allow(unused_variable)
|
-- selene: allow(unused_variable)
|
||||||
|
@ -91,12 +91,12 @@ local Pitches <const> = {
|
||||||
},
|
},
|
||||||
-- Slider
|
-- Slider
|
||||||
{
|
{
|
||||||
x = gfx.animator.new(PitchFlyMs, PitchStartX - 20, PitchStartX, easingHill),
|
x = gfx.animator.new(PitchFlyMs, PitchStartX - 20, PitchStartX, utils.easingHill),
|
||||||
y = gfx.animator.new(PitchFlyMs, PitchStartY, PitchEndY, playdate.easingFunctions.linear),
|
y = gfx.animator.new(PitchFlyMs, PitchStartY, PitchEndY, playdate.easingFunctions.linear),
|
||||||
},
|
},
|
||||||
-- Curve ball
|
-- Curve ball
|
||||||
{
|
{
|
||||||
x = gfx.animator.new(PitchFlyMs, PitchStartX + 20, PitchStartX, easingHill),
|
x = gfx.animator.new(PitchFlyMs, PitchStartX + 20, PitchStartX, utils.easingHill),
|
||||||
y = gfx.animator.new(PitchFlyMs, PitchStartY, PitchEndY, playdate.easingFunctions.linear),
|
y = gfx.animator.new(PitchFlyMs, PitchStartY, PitchEndY, playdate.easingFunctions.linear),
|
||||||
},
|
},
|
||||||
-- Wobbbleball
|
-- Wobbbleball
|
||||||
|
@ -112,12 +112,12 @@ local Pitches <const> = {
|
||||||
}
|
}
|
||||||
|
|
||||||
local CrankOffsetDeg <const> = 90
|
local CrankOffsetDeg <const> = 90
|
||||||
local BatOffset <const> = xy(10, 25)
|
local BatOffset <const> = utils.xy(10, 25)
|
||||||
|
|
||||||
local batBase <const> = xy(Center.x - 34, 215)
|
local batBase <const> = utils.xy(Center.x - 34, 215)
|
||||||
local batTip <const> = xy(0, 0)
|
local batTip <const> = utils.xy(0, 0)
|
||||||
|
|
||||||
local TagDistance <const> = 20
|
local TagDistance <const> = 15
|
||||||
|
|
||||||
local SmallestBallRadius <const> = 6
|
local SmallestBallRadius <const> = 6
|
||||||
|
|
||||||
|
@ -130,18 +130,50 @@ local ball <const> = {
|
||||||
|
|
||||||
local BatLength <const> = 50 --45
|
local BatLength <const> = 50 --45
|
||||||
|
|
||||||
local Modes <const> = {
|
local Offense <const> = {
|
||||||
batting = {},
|
batting = {},
|
||||||
running = {},
|
running = {},
|
||||||
}
|
}
|
||||||
|
|
||||||
local currentMode = Modes.batting
|
local Sides <const> = {
|
||||||
|
offense = {},
|
||||||
|
defense = {},
|
||||||
|
}
|
||||||
|
|
||||||
|
local offenseMode = Offense.batting
|
||||||
|
|
||||||
|
---@alias Team { score: number, benchPosition: XYPair }
|
||||||
|
|
||||||
|
---@type table<string, Team>
|
||||||
|
local teams <const> = {
|
||||||
|
home = {
|
||||||
|
score = 0,
|
||||||
|
benchPosition = utils.xy(Screen.W + 10, Center.y),
|
||||||
|
},
|
||||||
|
away = {
|
||||||
|
score = 0,
|
||||||
|
benchPosition = utils.xy(-10, Center.y),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
local PlayerTeam <const> = teams.away
|
||||||
|
local battingTeam = teams.away
|
||||||
|
local outs = 0
|
||||||
|
local inning = 1
|
||||||
|
|
||||||
|
function playerIsOn(side)
|
||||||
|
if PlayerTeam == battingTeam then
|
||||||
|
return side == Sides.offense
|
||||||
|
else
|
||||||
|
return side == Sides.defense
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
-- TODO? Replace this AND ballSizeAnimator with a ballHeightAnimator
|
-- TODO? Replace this AND ballSizeAnimator with a ballHeightAnimator
|
||||||
-- ...that might lose some of the magic of both. Compromise available? idk
|
-- ...that might lose some of the magic of both. Compromise available? idk
|
||||||
local ballFloatAnimator = gfx.animator.new(2000, -60, 0, easingHill)
|
local ballFloatAnimator = gfx.animator.new(2000, -60, 0, utils.easingHill)
|
||||||
local BallSizeMs = 2000
|
local BallSizeMs = 2000
|
||||||
local ballSizeAnimator = gfx.animator.new(BallSizeMs, 9, SmallestBallRadius, easingHill)
|
local ballSizeAnimator = gfx.animator.new(BallSizeMs, 9, SmallestBallRadius, utils.easingHill)
|
||||||
|
|
||||||
local HitMult = 20
|
local HitMult = 20
|
||||||
|
|
||||||
|
@ -151,14 +183,14 @@ local First <const>, Second <const>, Third <const>, Home <const> = 1, 2, 3, 4
|
||||||
|
|
||||||
---@type Base[]
|
---@type Base[]
|
||||||
local Bases = {
|
local Bases = {
|
||||||
xy(Screen.W * 0.93, Screen.H * 0.52),
|
utils.xy(Screen.W * 0.93, Screen.H * 0.52),
|
||||||
xy(Screen.W * 0.47, Screen.H * 0.19),
|
utils.xy(Screen.W * 0.47, Screen.H * 0.19),
|
||||||
xy(Screen.W * 0.03, Screen.H * 0.52),
|
utils.xy(Screen.W * 0.03, Screen.H * 0.52),
|
||||||
xy(Screen.W * 0.474, Screen.H * 0.79),
|
utils.xy(Screen.W * 0.474, Screen.H * 0.79),
|
||||||
}
|
}
|
||||||
|
|
||||||
-- Pseudo-base for batter to target
|
-- Pseudo-base for batter to target
|
||||||
local RightHandedBattersBox <const> = xy(Bases[Home].x - 35, Bases[Home].y)
|
local RightHandedBattersBox <const> = utils.xy(Bases[Home].x - 35, Bases[Home].y)
|
||||||
|
|
||||||
---@type table<Base, Base | nil>
|
---@type table<Base, Base | nil>
|
||||||
local NextBaseMap <const> = {
|
local NextBaseMap <const> = {
|
||||||
|
@ -196,25 +228,36 @@ local PitcherStartPos <const> = {
|
||||||
y = Screen.H * 0.40,
|
y = Screen.H * 0.40,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
--- Actually only benches the infield, because outfielders are far away!
|
||||||
|
---@param position XYPair
|
||||||
|
function benchAllFielders(position)
|
||||||
|
fielders.first.target = position
|
||||||
|
fielders.second.target = position
|
||||||
|
fielders.shortstop.target = position
|
||||||
|
fielders.third.target = position
|
||||||
|
fielders.pitcher.target = position
|
||||||
|
fielders.catcher.target = position
|
||||||
|
end
|
||||||
|
|
||||||
--- Resets the target positions of all fielders to their defaults (at their field positions).
|
--- 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.
|
---@param fromOffTheField XYPair | nil If provided, also sets all runners' current position to one centralized location.
|
||||||
function resetFielderPositions(fromOffTheField)
|
function resetFielderPositions(fromOffTheField)
|
||||||
if fromOffTheField then
|
if fromOffTheField then
|
||||||
for _, fielder in pairs(fielders) do
|
for _, fielder in pairs(fielders) do
|
||||||
fielder.x = Center.x
|
fielder.x = fromOffTheField.x
|
||||||
fielder.y = Screen.H
|
fielder.y = fromOffTheField.y
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
fielders.first.target = xy(Screen.W - 65, Screen.H * 0.48)
|
fielders.first.target = utils.xy(Screen.W - 65, Screen.H * 0.48)
|
||||||
fielders.second.target = xy(Screen.W * 0.70, Screen.H * 0.30)
|
fielders.second.target = utils.xy(Screen.W * 0.70, Screen.H * 0.30)
|
||||||
fielders.shortstop.target = xy(Screen.W * 0.30, Screen.H * 0.30)
|
fielders.shortstop.target = utils.xy(Screen.W * 0.30, Screen.H * 0.30)
|
||||||
fielders.third.target = xy(Screen.W * 0.1, Screen.H * 0.48)
|
fielders.third.target = utils.xy(Screen.W * 0.1, Screen.H * 0.48)
|
||||||
fielders.pitcher.target = xy(PitcherStartPos.x, PitcherStartPos.y)
|
fielders.pitcher.target = utils.xy(PitcherStartPos.x, PitcherStartPos.y)
|
||||||
fielders.catcher.target = xy(Screen.W * 0.475, Screen.H * 0.92)
|
fielders.catcher.target = utils.xy(Screen.W * 0.475, Screen.H * 0.92)
|
||||||
fielders.left.target = xy(Screen.W * -1, Screen.H * -0.2)
|
fielders.left.target = utils.xy(Screen.W * -1, Screen.H * -0.2)
|
||||||
fielders.center.target = xy(Center.x, Screen.H * -0.4)
|
fielders.center.target = utils.xy(Center.x, Screen.H * -0.4)
|
||||||
fielders.right.target = xy(Screen.W * 2, fielders.left.target.y)
|
fielders.right.target = utils.xy(Screen.W * 2, fielders.left.target.y)
|
||||||
end
|
end
|
||||||
|
|
||||||
local BatterStartingX <const> = Bases[Home].x - 40
|
local BatterStartingX <const> = Bases[Home].x - 40
|
||||||
|
@ -251,14 +294,14 @@ local batter = newRunner()
|
||||||
---@param customBallScaler pd_animator | nil
|
---@param customBallScaler pd_animator | nil
|
||||||
function throwBall(destX, destY, easingFunc, flyTimeMs, floaty, customBallScaler)
|
function throwBall(destX, destY, easingFunc, flyTimeMs, floaty, customBallScaler)
|
||||||
if not flyTimeMs then
|
if not flyTimeMs then
|
||||||
flyTimeMs = distanceBetween(ball.x, ball.y, destX, destY) * 5
|
flyTimeMs = utils.distanceBetween(ball.x, ball.y, destX, destY) * 5
|
||||||
end
|
end
|
||||||
ball.heldBy = nil
|
ball.heldBy = nil
|
||||||
if customBallScaler then
|
if customBallScaler then
|
||||||
ballSizeAnimator = customBallScaler
|
ballSizeAnimator = customBallScaler
|
||||||
else
|
else
|
||||||
-- TODO? Scale based on distance?
|
-- TODO? Scale based on distance?
|
||||||
ballSizeAnimator = gfx.animator.new(flyTimeMs, 9, SmallestBallRadius, easingHill)
|
ballSizeAnimator = gfx.animator.new(flyTimeMs, 9, SmallestBallRadius, utils.easingHill)
|
||||||
end
|
end
|
||||||
ballAnimatorY = gfx.animator.new(flyTimeMs, ball.y, destY, easingFunc)
|
ballAnimatorY = gfx.animator.new(flyTimeMs, ball.y, destY, easingFunc)
|
||||||
ballAnimatorX = gfx.animator.new(flyTimeMs, ball.x, destX, easingFunc)
|
ballAnimatorX = gfx.animator.new(flyTimeMs, ball.x, destX, easingFunc)
|
||||||
|
@ -275,7 +318,7 @@ local catcherThrownBall = false
|
||||||
|
|
||||||
function pitch()
|
function pitch()
|
||||||
catcherThrownBall = false
|
catcherThrownBall = false
|
||||||
currentMode = Modes.batting
|
offenseMode = Offense.batting
|
||||||
|
|
||||||
local current = Pitches[math.random(#Pitches)]
|
local current = Pitches[math.random(#Pitches)]
|
||||||
ballAnimatorX = current.x
|
ballAnimatorX = current.x
|
||||||
|
@ -293,9 +336,8 @@ function pitch()
|
||||||
secondsSincePitchAllowed = 0
|
secondsSincePitchAllowed = 0
|
||||||
end
|
end
|
||||||
|
|
||||||
local elapsedSec = 0
|
|
||||||
local crankChange = 0
|
local crankChange = 0
|
||||||
local acceleratedChange
|
local acceleratedChange = 0
|
||||||
|
|
||||||
local BaseHitbox = 10
|
local BaseHitbox = 10
|
||||||
|
|
||||||
|
@ -305,7 +347,7 @@ local BaseHitbox = 10
|
||||||
---@return Base | nil
|
---@return Base | nil
|
||||||
function isTouchingBase(x, y)
|
function isTouchingBase(x, y)
|
||||||
for _, base in ipairs(Bases) do
|
for _, base in ipairs(Bases) do
|
||||||
if distanceBetween(x, y, base.x, base.y) < BaseHitbox then
|
if utils.distanceBetween(x, y, base.x, base.y) < BaseHitbox then
|
||||||
return base
|
return base
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -320,26 +362,10 @@ local BallCatchHitbox = 3
|
||||||
---@param y number
|
---@param y number
|
||||||
---@return boolean
|
---@return boolean
|
||||||
function isTouchingBall(x, y)
|
function isTouchingBall(x, y)
|
||||||
local ballDistance = distanceBetween(x, y, ball.x, ball.y)
|
local ballDistance = utils.distanceBetween(x, y, ball.x, ball.y)
|
||||||
return ballDistance < BallCatchHitbox
|
return ballDistance < BallCatchHitbox
|
||||||
end
|
end
|
||||||
|
|
||||||
---@alias Team { score: number }
|
|
||||||
|
|
||||||
---@type table<string, Team>
|
|
||||||
local teams <const> = {
|
|
||||||
home = {
|
|
||||||
score = 0,
|
|
||||||
},
|
|
||||||
away = {
|
|
||||||
score = 0,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
local battingTeam = teams.away
|
|
||||||
local outs = 0
|
|
||||||
local inning = 1
|
|
||||||
|
|
||||||
---@param base Base
|
---@param base Base
|
||||||
---@return Runner | nil
|
---@return Runner | nil
|
||||||
function getRunnerTargeting(base)
|
function getRunnerTargeting(base)
|
||||||
|
@ -367,6 +393,10 @@ function updateForcedRunners()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local ResetFieldersAfterSeconds <const> = 5
|
||||||
|
-- TODO: Replace with a timer, repeatedly reset, instead of setting to 0
|
||||||
|
local secondsSinceLastRunnerMove = 0
|
||||||
|
|
||||||
---@param runnerIndex integer
|
---@param runnerIndex integer
|
||||||
function outRunner(runnerIndex)
|
function outRunner(runnerIndex)
|
||||||
outs = outs + 1
|
outs = outs + 1
|
||||||
|
@ -376,9 +406,12 @@ function outRunner(runnerIndex)
|
||||||
|
|
||||||
announcer:say("YOU'RE OUT!")
|
announcer:say("YOU'RE OUT!")
|
||||||
if outs == 3 then
|
if outs == 3 then
|
||||||
|
local currentlyFieldingTeam = battingTeam == teams.home and teams.away or teams.home
|
||||||
local gameOver = inning == 9 and teams.away.score ~= teams.home.score
|
local gameOver = inning == 9 and teams.away.score ~= teams.home.score
|
||||||
if not gameOver then
|
if not gameOver then
|
||||||
fieldersDance()
|
fieldersDance()
|
||||||
|
secondsSinceLastRunnerMove = -7
|
||||||
|
benchAllFielders(currentlyFieldingTeam.benchPosition)
|
||||||
announcer:say("SWITCHING SIDES...")
|
announcer:say("SWITCHING SIDES...")
|
||||||
end
|
end
|
||||||
while #runners > 0 do
|
while #runners > 0 do
|
||||||
|
@ -387,14 +420,13 @@ function outRunner(runnerIndex)
|
||||||
-- Delay to keep end-of-inning on the scoreboard for a few seconds
|
-- Delay to keep end-of-inning on the scoreboard for a few seconds
|
||||||
playdate.timer.new(3000, function()
|
playdate.timer.new(3000, function()
|
||||||
outs = 0
|
outs = 0
|
||||||
if battingTeam == teams.home then
|
battingTeam = currentlyFieldingTeam
|
||||||
battingTeam = teams.away
|
|
||||||
inning = inning + 1
|
|
||||||
else
|
|
||||||
battingTeam = teams.home
|
|
||||||
end
|
|
||||||
if gameOver then
|
if gameOver then
|
||||||
announcer:say("AND THAT'S THE BALL GAME!")
|
announcer:say("AND THAT'S THE BALL GAME!")
|
||||||
|
else
|
||||||
|
if battingTeam == teams.home then
|
||||||
|
inning = inning + 1
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
@ -427,9 +459,7 @@ end
|
||||||
function getBaseOfStrandedRunner()
|
function getBaseOfStrandedRunner()
|
||||||
local farRunnersBase, farDistance
|
local farRunnersBase, farDistance
|
||||||
for _, runner in pairs(runners) do
|
for _, runner in pairs(runners) do
|
||||||
local nearestBase, distance = getNearestOf(Bases, runner.x, runner.y, function(base)
|
local nearestBase, distance = utils.getNearestOf(Bases, runner.x, runner.y)
|
||||||
return runner.nextBase == base
|
|
||||||
end)
|
|
||||||
if farRunnersBase == nil or farDistance < distance then
|
if farRunnersBase == nil or farDistance < distance then
|
||||||
farRunnersBase = nearestBase
|
farRunnersBase = nearestBase
|
||||||
farDistance = distance
|
farDistance = distance
|
||||||
|
@ -459,8 +489,8 @@ end
|
||||||
function tryToMakeAnOut(fielder)
|
function tryToMakeAnOut(fielder)
|
||||||
local targetX, targetY = getNextOutTarget()
|
local targetX, targetY = getNextOutTarget()
|
||||||
if targetX ~= nil and targetY ~= nil then
|
if targetX ~= nil and targetY ~= nil then
|
||||||
local nearestFielder = getNearestOf(fielders, targetX, targetY)
|
local nearestFielder = utils.getNearestOf(fielders, targetX, targetY)
|
||||||
nearestFielder.target = xy(targetX, targetY)
|
nearestFielder.target = utils.xy(targetX, targetY)
|
||||||
if nearestFielder == fielder then
|
if nearestFielder == fielder then
|
||||||
ball.heldBy = fielder
|
ball.heldBy = fielder
|
||||||
else
|
else
|
||||||
|
@ -476,7 +506,7 @@ end
|
||||||
---@param target { x: number, y: number }
|
---@param target { x: number, y: number }
|
||||||
---@return boolean
|
---@return boolean
|
||||||
function moveAtSpeed(mover, speed, target)
|
function moveAtSpeed(mover, speed, target)
|
||||||
local x, y, distance = normalizeVector(mover.x, mover.y, target.x, target.y)
|
local x, y, distance = utils.normalizeVector(mover.x, mover.y, target.x, target.y)
|
||||||
|
|
||||||
if distance > 1 then
|
if distance > 1 then
|
||||||
mover.x = mover.x - (x * speed)
|
mover.x = mover.x - (x * speed)
|
||||||
|
@ -488,35 +518,30 @@ function moveAtSpeed(mover, speed, target)
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param fielder Fielder
|
---@param fielder Fielder
|
||||||
---@param runnerBaseCache Cache<Runner, Base>
|
function updateFielder(fielder)
|
||||||
function updateFielder(fielder, runnerBaseCache)
|
|
||||||
-- 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
|
if fielder.target ~= nil then
|
||||||
if not moveAtSpeed(fielder, fielder.speed * deltaSeconds, fielder.target) then
|
if not moveAtSpeed(fielder, fielder.speed * deltaSeconds, fielder.target) then
|
||||||
fielder.target = nil
|
fielder.target = nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
if currentMode ~= Modes.running or not isTouchingBall(fielder.x, fielder.y) then
|
if offenseMode ~= Offense.running or not isTouchingBall(fielder.x, fielder.y) then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
-- TODO: Check for double-plays or other available outs.
|
-- TODO: Check for double-plays or other available outs.
|
||||||
local touchedBase = isTouchingBase(fielder.x, fielder.y)
|
local touchedBase = isTouchingBase(fielder.x, fielder.y)
|
||||||
for i, runner in pairs(runners) do
|
for i, runner in pairs(runners) do
|
||||||
|
local runnerOnBase = isTouchingBase(runner.x, runner.y)
|
||||||
if
|
if
|
||||||
( -- Force out
|
( -- Force out
|
||||||
touchedBase
|
touchedBase
|
||||||
-- and runner.prevBase -- Make sure the runner is not standing at home
|
-- and runner.prevBase -- Make sure the runner is not standing at home
|
||||||
and runner.forcedTo == touchedBase
|
and runner.forcedTo == touchedBase
|
||||||
and touchedBase ~= runnerBaseCache.get(runner)
|
and touchedBase ~= runnerOnBase
|
||||||
)
|
|
||||||
or ( -- Tag out
|
|
||||||
not runnerBaseCache.get(runner)
|
|
||||||
and distanceBetween(runner.x, runner.y, fielder.x, fielder.y) < TagDistance
|
|
||||||
)
|
)
|
||||||
|
-- Tag out
|
||||||
|
or (not runnerOnBase and utils.distanceBetween(runner.x, runner.y, fielder.x, fielder.y) < TagDistance)
|
||||||
then
|
then
|
||||||
outRunner(i)
|
outRunner(i)
|
||||||
playdate.timer.new(750, function()
|
playdate.timer.new(750, function()
|
||||||
|
@ -529,15 +554,11 @@ function updateFielder(fielder, runnerBaseCache)
|
||||||
end
|
end
|
||||||
|
|
||||||
function updateFielders()
|
function updateFielders()
|
||||||
local runnerBaseCache = buildCache(function(runner)
|
|
||||||
return isTouchingBase(runner.x, runner.y)
|
|
||||||
end)
|
|
||||||
|
|
||||||
for _, fielder in pairs(fielders) do
|
for _, fielder in pairs(fielders) do
|
||||||
updateFielder(fielder, runnerBaseCache)
|
updateFielder(fielder)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- if currentMode == Modes.batting then
|
-- if offenseMode == Offense.batting then
|
||||||
-- moveAtSpeed(
|
-- moveAtSpeed(
|
||||||
-- fielders.catcher,
|
-- fielders.catcher,
|
||||||
-- fielders.catcher.speed * 2 * deltaSeconds,
|
-- fielders.catcher.speed * 2 * deltaSeconds,
|
||||||
|
@ -549,8 +570,9 @@ end
|
||||||
--- Returns true only if the given runner moved during this update.
|
--- Returns true only if the given runner moved during this update.
|
||||||
---@param runner Runner | nil
|
---@param runner Runner | nil
|
||||||
---@param runnerIndex integer | nil May only be nil if runner == batter
|
---@param runnerIndex integer | nil May only be nil if runner == batter
|
||||||
|
---@param appliedSpeed number
|
||||||
---@return boolean
|
---@return boolean
|
||||||
function updateRunner(runner, runnerIndex)
|
function updateRunner(runner, runnerIndex, appliedSpeed)
|
||||||
local autoRunSpeed = 20 * deltaSeconds
|
local autoRunSpeed = 20 * deltaSeconds
|
||||||
--autoRunSpeed = 140
|
--autoRunSpeed = 140
|
||||||
|
|
||||||
|
@ -558,8 +580,7 @@ function updateRunner(runner, runnerIndex)
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
local appliedSpeed = crankChange -- TODO: Allow for individual runner control via buttons
|
local nearestBase, nearestBaseDistance = utils.getNearestOf(Bases, runner.x, runner.y)
|
||||||
local nearestBase, nearestBaseDistance = getNearestOf(Bases, runner.x, runner.y)
|
|
||||||
|
|
||||||
if
|
if
|
||||||
nearestBaseDistance < 5
|
nearestBaseDistance < 5
|
||||||
|
@ -572,9 +593,9 @@ function updateRunner(runner, runnerIndex)
|
||||||
end
|
end
|
||||||
|
|
||||||
local nb = runner.nextBase
|
local nb = runner.nextBase
|
||||||
local x, y, distance = normalizeVector(runner.x, runner.y, nb.x, nb.y)
|
local x, y, distance = utils.normalizeVector(runner.x, runner.y, nb.x, nb.y)
|
||||||
|
|
||||||
if distance < 1 then
|
if distance < 2 then
|
||||||
runner.nextBase = NextBaseMap[runner.nextBase]
|
runner.nextBase = NextBaseMap[runner.nextBase]
|
||||||
runner.forcedTo = nil
|
runner.forcedTo = nil
|
||||||
return false
|
return false
|
||||||
|
@ -602,10 +623,6 @@ function updateRunner(runner, runnerIndex)
|
||||||
return prevX ~= runner.x or prevY ~= runner.y
|
return prevX ~= runner.x or prevY ~= runner.y
|
||||||
end
|
end
|
||||||
|
|
||||||
local ResetFieldersAfterSeconds <const> = 5
|
|
||||||
-- TODO: Replace with a timer, repeatedly reset, instead of setting to 0
|
|
||||||
local secondsSinceLastRunnerMove = 0
|
|
||||||
|
|
||||||
---@type number
|
---@type number
|
||||||
local batAngleDeg
|
local batAngleDeg
|
||||||
|
|
||||||
|
@ -624,12 +641,12 @@ function updateBatting()
|
||||||
batTip.y = batBase.y + (BatLength * math.cos(batAngle))
|
batTip.y = batBase.y + (BatLength * math.cos(batAngle))
|
||||||
|
|
||||||
if
|
if
|
||||||
acceleratedChange > 0
|
acceleratedChange >= 0 -- > 0
|
||||||
and pointDirectlyUnderLine(ball.x, ball.y, batBase.x, batBase.y, batTip.x, batTip.y, Screen.H)
|
and utils.pointDirectlyUnderLine(ball.x, ball.y, batBase.x, batBase.y, batTip.x, batTip.y, Screen.H)
|
||||||
and ball.y < 232 --not isTouchingBall(fielders.catcher.x, fielders.catcher.y)
|
and ball.y < 232 --not isTouchingBall(fielders.catcher.x, fielders.catcher.y)
|
||||||
then
|
then
|
||||||
BatCrackSound:play()
|
BatCrackSound:play()
|
||||||
currentMode = Modes.running
|
offenseMode = Offense.running
|
||||||
local ballAngle = batAngle + math.rad(90)
|
local ballAngle = batAngle + math.rad(90)
|
||||||
|
|
||||||
local mult = math.abs(crankChange / 15)
|
local mult = math.abs(crankChange / 15)
|
||||||
|
@ -648,7 +665,7 @@ function updateBatting()
|
||||||
playdate.easingFunctions.outQuint,
|
playdate.easingFunctions.outQuint,
|
||||||
2000,
|
2000,
|
||||||
nil,
|
nil,
|
||||||
gfx.animator.new(2000, 9 + (mult * mult * 0.5), SmallestBallRadius, easingHill)
|
gfx.animator.new(2000, 9 + (mult * mult * 0.5), SmallestBallRadius, utils.easingHill)
|
||||||
)
|
)
|
||||||
|
|
||||||
fielders.first.target = Bases[First]
|
fielders.first.target = Bases[First]
|
||||||
|
@ -658,18 +675,19 @@ function updateBatting()
|
||||||
batter.forcedTo = Bases[First]
|
batter.forcedTo = Bases[First]
|
||||||
batter = nil -- Demote batter to a mere runner
|
batter = nil -- Demote batter to a mere runner
|
||||||
|
|
||||||
local chasingFielder = getNearestOf(fielders, ballDestX, ballDestY)
|
local chasingFielder = utils.getNearestOf(fielders, ballDestX, ballDestY)
|
||||||
chasingFielder.target = { x = ballDestX, y = ballDestY }
|
chasingFielder.target = { x = ballDestX, y = ballDestY }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Update non-batter runners.
|
--- Update non-batter runners.
|
||||||
--- Returns true only if at least one of the given runners moved during this update
|
--- Returns true only if at least one of the given runners moved during this update
|
||||||
|
---@param appliedSpeed number
|
||||||
---@return boolean
|
---@return boolean
|
||||||
function updateRunning()
|
function updateRunning(appliedSpeed)
|
||||||
ball.size = ballSizeAnimator:currentValue()
|
ball.size = ballSizeAnimator:currentValue()
|
||||||
|
|
||||||
local nonBatterRunners = filter(runners, function(runner)
|
local nonBatterRunners = utils.filter(runners, function(runner)
|
||||||
return runner ~= batter
|
return runner ~= batter
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
@ -677,7 +695,7 @@ function updateRunning()
|
||||||
|
|
||||||
-- TODO: Filter for the runner closest to the currently-held direction button
|
-- TODO: Filter for the runner closest to the currently-held direction button
|
||||||
for runnerIndex, runner in ipairs(nonBatterRunners) do
|
for runnerIndex, runner in ipairs(nonBatterRunners) do
|
||||||
runnerMoved = updateRunner(runner, runnerIndex) or runnerMoved
|
runnerMoved = updateRunner(runner, runnerIndex, appliedSpeed) or runnerMoved
|
||||||
end
|
end
|
||||||
|
|
||||||
return runnerMoved
|
return runnerMoved
|
||||||
|
@ -697,7 +715,6 @@ end
|
||||||
function updateGameState()
|
function updateGameState()
|
||||||
deltaSeconds = playdate.getElapsedTime() or 0
|
deltaSeconds = playdate.getElapsedTime() or 0
|
||||||
playdate.resetElapsedTime()
|
playdate.resetElapsedTime()
|
||||||
elapsedSec = elapsedSec + deltaSeconds
|
|
||||||
crankChange, acceleratedChange = playdate.getCrankChange() --[[@as number, number]]
|
crankChange, acceleratedChange = playdate.getCrankChange() --[[@as number, number]]
|
||||||
|
|
||||||
if ball.heldBy then
|
if ball.heldBy then
|
||||||
|
@ -708,7 +725,7 @@ function updateGameState()
|
||||||
ball.y = ballAnimatorY:currentValue() + ballFloatAnimator:currentValue()
|
ball.y = ballAnimatorY:currentValue() + ballFloatAnimator:currentValue()
|
||||||
end
|
end
|
||||||
|
|
||||||
if currentMode == Modes.batting then
|
if offenseMode == Offense.batting then
|
||||||
secondsSincePitchAllowed = secondsSincePitchAllowed + deltaSeconds
|
secondsSincePitchAllowed = secondsSincePitchAllowed + deltaSeconds
|
||||||
if secondsSincePitchAllowed > 3.5 and not catcherThrownBall then
|
if secondsSincePitchAllowed > 3.5 and not catcherThrownBall then
|
||||||
throwBall(PitchStartX, PitchStartY, playdate.easingFunctions.linear, nil, true)
|
throwBall(PitchStartX, PitchStartY, playdate.easingFunctions.linear, nil, true)
|
||||||
|
@ -719,16 +736,19 @@ function updateGameState()
|
||||||
end
|
end
|
||||||
updateBatting()
|
updateBatting()
|
||||||
-- TODO: Ensure batter can't be nil, here
|
-- TODO: Ensure batter can't be nil, here
|
||||||
updateRunner(batter)
|
updateRunner(batter, nil, crankChange)
|
||||||
elseif currentMode == Modes.running then
|
elseif offenseMode == Offense.running then
|
||||||
if updateRunning() then
|
if playerIsOn(Sides.defense) then
|
||||||
|
updateRunning(999)
|
||||||
|
end
|
||||||
|
if updateRunning(crankChange) then
|
||||||
secondsSinceLastRunnerMove = 0
|
secondsSinceLastRunnerMove = 0
|
||||||
else
|
else
|
||||||
secondsSinceLastRunnerMove = secondsSinceLastRunnerMove + deltaSeconds
|
secondsSinceLastRunnerMove = secondsSinceLastRunnerMove + deltaSeconds
|
||||||
if secondsSinceLastRunnerMove > ResetFieldersAfterSeconds then
|
if secondsSinceLastRunnerMove > ResetFieldersAfterSeconds then
|
||||||
throwBall(PitchStartX, PitchStartY, playdate.easingFunctions.linear, nil, true)
|
throwBall(PitchStartX, PitchStartY, playdate.easingFunctions.linear, nil, true)
|
||||||
resetFielderPositions(false)
|
resetFielderPositions()
|
||||||
currentMode = Modes.batting
|
offenseMode = Offense.batting
|
||||||
batter = newRunner()
|
batter = newRunner()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -763,7 +783,7 @@ function playdate.update()
|
||||||
gfx.fillRect(fielder.x, fielder.y - fielderDanceHeight, 14, 25)
|
gfx.fillRect(fielder.x, fielder.y - fielderDanceHeight, 14, 25)
|
||||||
end
|
end
|
||||||
|
|
||||||
if currentMode == Modes.batting then
|
if offenseMode == Offense.batting then
|
||||||
gfx.setLineWidth(5)
|
gfx.setLineWidth(5)
|
||||||
gfx.drawLine(batBase.x, batBase.y, batTip.x, batTip.y)
|
gfx.drawLine(batBase.x, batBase.y, batTip.x, batTip.y)
|
||||||
end
|
end
|
||||||
|
@ -809,7 +829,7 @@ function init()
|
||||||
playdate.display.setRefreshRate(50)
|
playdate.display.setRefreshRate(50)
|
||||||
gfx.setBackgroundColor(gfx.kColorWhite)
|
gfx.setBackgroundColor(gfx.kColorWhite)
|
||||||
playdate.setMenuImage(gfx.image.new("images/game/menu-image.png"))
|
playdate.setMenuImage(gfx.image.new("images/game/menu-image.png"))
|
||||||
resetFielderPositions(true)
|
resetFielderPositions(teams.home.benchPosition)
|
||||||
playdate.getSystemMenu():addMenuItem("Restart game", function() end) -- TODO?
|
playdate.getSystemMenu():addMenuItem("Restart game", function() end) -- TODO?
|
||||||
|
|
||||||
playdate.timer.new(2000, function()
|
playdate.timer.new(2000, function()
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
import = function() end
|
||||||
|
luaunit = require("luaunit")
|
||||||
|
luaunit.ORDER_ACTUAL_EXPECTED = false
|
||||||
|
|
||||||
|
utils = require("utils")
|
||||||
|
|
||||||
|
function testFilter()
|
||||||
|
local numArr = { 5, 10, 15, 20 }
|
||||||
|
local greaterThanTen = function(n)
|
||||||
|
return n > 10
|
||||||
|
end
|
||||||
|
luaunit.assertEquals({ 15, 20 }, utils.filter(numArr, greaterThanTen))
|
||||||
|
end
|
||||||
|
|
||||||
|
os.exit(luaunit.LuaUnit.run())
|
|
@ -3,7 +3,10 @@ import 'CoreLibs/animation.lua'
|
||||||
import 'CoreLibs/graphics.lua'
|
import 'CoreLibs/graphics.lua'
|
||||||
-- stylua: ignore end
|
-- stylua: ignore end
|
||||||
|
|
||||||
function easingHill(t, b, c, d)
|
-- selene: allow(unscoped_variables)
|
||||||
|
utils = {}
|
||||||
|
|
||||||
|
function utils.easingHill(t, b, c, d)
|
||||||
c = c + 0.0 -- convert to float to prevent integer overflow
|
c = c + 0.0 -- convert to float to prevent integer overflow
|
||||||
t = t / d
|
t = t / d
|
||||||
t = ((t * 2) - 1)
|
t = ((t * 2) - 1)
|
||||||
|
@ -13,7 +16,7 @@ end
|
||||||
|
|
||||||
-- Useful for quick print-the-value-in-place debugging.
|
-- Useful for quick print-the-value-in-place debugging.
|
||||||
-- selene: allow(unused_variable)
|
-- selene: allow(unused_variable)
|
||||||
function label(value, name)
|
function utils.label(value, name)
|
||||||
if type(value) == "table" then
|
if type(value) == "table" then
|
||||||
print(name .. ":")
|
print(name .. ":")
|
||||||
printTable(value)
|
printTable(value)
|
||||||
|
@ -26,7 +29,7 @@ end
|
||||||
---@param x number
|
---@param x number
|
||||||
---@param y number
|
---@param y number
|
||||||
---@return XYPair
|
---@return XYPair
|
||||||
function xy(x, y)
|
function utils.xy(x, y)
|
||||||
return {
|
return {
|
||||||
x = x,
|
x = x,
|
||||||
y = y,
|
y = y,
|
||||||
|
@ -39,8 +42,8 @@ end
|
||||||
---@param x2 number
|
---@param x2 number
|
||||||
---@param y2 number
|
---@param y2 number
|
||||||
---@return number x, number y, number distance
|
---@return number x, number y, number distance
|
||||||
function normalizeVector(x1, y1, x2, y2)
|
function utils.normalizeVector(x1, y1, x2, y2)
|
||||||
local distance, a, b = distanceBetween(x1, y1, x2, y2)
|
local distance, a, b = utils.distanceBetween(x1, y1, x2, y2)
|
||||||
return a / distance, b / distance, distance
|
return a / distance, b / distance, distance
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -48,7 +51,7 @@ end
|
||||||
---@param array T[]
|
---@param array T[]
|
||||||
---@param condition fun(T): boolean
|
---@param condition fun(T): boolean
|
||||||
---@return T[]
|
---@return T[]
|
||||||
function filter(array, condition)
|
function utils.filter(array, condition)
|
||||||
local newArray = {}
|
local newArray = {}
|
||||||
for _, element in pairs(array) do
|
for _, element in pairs(array) do
|
||||||
if condition(element) then
|
if condition(element) then
|
||||||
|
@ -59,7 +62,7 @@ function filter(array, condition)
|
||||||
end
|
end
|
||||||
|
|
||||||
---@return number, number, number
|
---@return number, number, number
|
||||||
function distanceBetween(x1, y1, x2, y2)
|
function utils.distanceBetween(x1, y1, x2, y2)
|
||||||
local a = x1 - x2
|
local a = x1 - x2
|
||||||
local b = y1 - y2
|
local b = y1 - y2
|
||||||
return math.sqrt((a * a) + (b * b)), a, b
|
return math.sqrt((a * a) + (b * b)), a, b
|
||||||
|
@ -67,7 +70,7 @@ end
|
||||||
|
|
||||||
--- Returns true only if the point is below the given line, within the x bounds of said line, and above the bottomBound
|
--- Returns true only if the point is below the given line, within the x bounds of said line, and above the bottomBound
|
||||||
--- @return boolean
|
--- @return boolean
|
||||||
function pointDirectlyUnderLine(pointX, pointY, lineX1, lineY1, lineX2, lineY2, bottomBound)
|
function utils.pointDirectlyUnderLine(pointX, pointY, lineX1, lineY1, lineX2, lineY2, bottomBound)
|
||||||
-- This check currently assumes right-handedness.
|
-- This check currently assumes right-handedness.
|
||||||
-- I.e. it assumes the ball is to the right of batBaseX
|
-- I.e. it assumes the ball is to the right of batBaseX
|
||||||
if pointX < lineX1 or pointX > lineX2 or pointY > bottomBound then
|
if pointX < lineX1 or pointX > lineX2 or pointY > bottomBound then
|
||||||
|
@ -92,15 +95,15 @@ end
|
||||||
---@param x number
|
---@param x number
|
||||||
---@param y number
|
---@param y number
|
||||||
---@return T,number|nil
|
---@return T,number|nil
|
||||||
function getNearestOf(array, x, y, extraCondition)
|
function utils.getNearestOf(array, x, y, extraCondition)
|
||||||
local nearest, nearestDistance = nil, nil
|
local nearest, nearestDistance = nil, nil
|
||||||
for _, element in pairs(array) do
|
for _, element in pairs(array) do
|
||||||
if not extraCondition or extraCondition(element) then
|
if not extraCondition or extraCondition(element) then
|
||||||
if nearest == nil then
|
if nearest == nil then
|
||||||
nearest = element
|
nearest = element
|
||||||
nearestDistance = distanceBetween(element.x, element.y, x, y)
|
nearestDistance = utils.distanceBetween(element.x, element.y, x, y)
|
||||||
else
|
else
|
||||||
local distance = distanceBetween(element.x, element.y, x, y)
|
local distance = utils.distanceBetween(element.x, element.y, x, y)
|
||||||
if distance < nearestDistance then
|
if distance < nearestDistance then
|
||||||
nearest = element
|
nearest = element
|
||||||
nearestDistance = distance
|
nearestDistance = distance
|
||||||
|
@ -114,32 +117,6 @@ end
|
||||||
|
|
||||||
---@alias Cache { get: fun(key: `Key`): `Value` }
|
---@alias Cache { get: fun(key: `Key`): `Value` }
|
||||||
|
|
||||||
--- Marker used by buildCache to indicate a cached `nil` value.
|
if not playdate then
|
||||||
local NoValue <const> = {}
|
return utils
|
||||||
|
|
||||||
--- Build a simple fetcher cache. On calling `get()`, if no value has already
|
|
||||||
--- been fetched, calls `fetcher(key)`, then caches and returns that value.
|
|
||||||
---
|
|
||||||
--- On reflection, it's probably pretty early for this optimization, and it's
|
|
||||||
--- optimizing in favor of CPU at the expense of memory, which is probably not
|
|
||||||
--- where the playdate's limitaitons lie. But it can stay for now.
|
|
||||||
---
|
|
||||||
---@generic Key
|
|
||||||
---@generic Value
|
|
||||||
---@return Cache<Key, Value>
|
|
||||||
function buildCache(fetcher)
|
|
||||||
local cacheData = {}
|
|
||||||
return {
|
|
||||||
get = function(key)
|
|
||||||
if cacheData[key] == NoValue then
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
if cacheData[key] ~= nil then
|
|
||||||
return cacheData[key]
|
|
||||||
end
|
|
||||||
local fetched = fetcher(key)
|
|
||||||
cacheData[key] = fetched ~= nil and fetched or NoValue
|
|
||||||
return cacheData[key]
|
|
||||||
end,
|
|
||||||
}
|
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in New Issue