Compare commits
No commits in common. "027bb31bffbfc3b35d528f511b4bca38a99c8e24" and "b9d25e18d810b9b9eae83e7c5041a93acd98ea46" have entirely different histories.
027bb31bff
...
b9d25e18d8
|
@ -23,7 +23,7 @@ function Announcer.new()
|
|||
}, { __index = Announcer })
|
||||
end
|
||||
|
||||
local DurationMs <const> = 2000
|
||||
local DurationMs <const> = 3000
|
||||
|
||||
function Announcer:popIn()
|
||||
self.animatorY = AnnouncerAnimatorInY
|
||||
|
|
35
src/ball.lua
35
src/ball.lua
|
@ -1,5 +1,6 @@
|
|||
-- selene: allow(unscoped_variables)
|
||||
---@class Ball
|
||||
---@field private animator pd_animator_lib
|
||||
---@field x number
|
||||
---@field y number
|
||||
---@field z number
|
||||
|
@ -9,15 +10,13 @@
|
|||
---@field yAnimator SimpleAnimator
|
||||
---@field sizeAnimator SimpleAnimator
|
||||
---@field floatAnimator SimpleAnimator
|
||||
---@field private animatorLib pd_animator_lib
|
||||
---@field private flyTimeMs number
|
||||
Ball = {}
|
||||
|
||||
---@param animatorLib pd_animator_lib
|
||||
---@param animator pd_animator_lib
|
||||
---@return Ball
|
||||
function Ball.new(animatorLib)
|
||||
function Ball.new(animator)
|
||||
return setmetatable({
|
||||
animatorLib = animatorLib,
|
||||
animator = animator,
|
||||
x = C.Center.x --[[@as number]],
|
||||
y = C.Center.y --[[@as number]],
|
||||
z = 0,
|
||||
|
@ -30,26 +29,10 @@ function Ball.new(animatorLib)
|
|||
-- TODO? Replace these with a ballAnimatorZ?
|
||||
-- ...that might lose some of the magic of both. Compromise available? idk
|
||||
sizeAnimator = utils.staticAnimator(C.SmallestBallRadius),
|
||||
floatAnimator = animatorLib.new(2000, -60, 0, utils.easingHill),
|
||||
floatAnimator = animator.new(2000, -60, 0, utils.easingHill),
|
||||
}, { __index = Ball })
|
||||
end
|
||||
|
||||
function Ball:updatePosition()
|
||||
if self.heldBy then
|
||||
self.x = self.heldBy.x
|
||||
self.y = self.heldBy.y
|
||||
self.z = C.GloveZ
|
||||
self.size = C.SmallestBallRadius
|
||||
else
|
||||
self.x = self.xAnimator:currentValue()
|
||||
local z = self.floatAnimator:currentValue()
|
||||
-- TODO: This `+ z` is more graphics logic than physics logic
|
||||
self.y = self.yAnimator:currentValue() + z
|
||||
self.z = z
|
||||
self.size = self.sizeAnimator:currentValue()
|
||||
end
|
||||
end
|
||||
|
||||
--- Launches the ball from its current position to the given destination.
|
||||
---@param destX number
|
||||
---@param destY number
|
||||
|
@ -64,16 +47,14 @@ function Ball:launch(destX, destY, easingFunc, flyTimeMs, floaty, customBallScal
|
|||
flyTimeMs = utils.distanceBetween(self.x, self.y, destX, destY) * C.DefaultLaunchPower
|
||||
end
|
||||
|
||||
self.flyTimeMs = flyTimeMs
|
||||
|
||||
if customBallScaler then
|
||||
self.sizeAnimator = customBallScaler
|
||||
else
|
||||
-- TODO? Scale based on distance?
|
||||
self.sizeAnimator = self.animatorLib.new(flyTimeMs, 9, C.SmallestBallRadius, utils.easingHill)
|
||||
self.sizeAnimator = self.animator.new(flyTimeMs, 9, C.SmallestBallRadius, utils.easingHill)
|
||||
end
|
||||
self.yAnimator = self.animatorLib.new(flyTimeMs, self.y, destY, easingFunc)
|
||||
self.xAnimator = self.animatorLib.new(flyTimeMs, self.x, destX, easingFunc)
|
||||
self.yAnimator = self.animator.new(flyTimeMs, self.y, destY, easingFunc)
|
||||
self.xAnimator = self.animator.new(flyTimeMs, self.x, destX, easingFunc)
|
||||
if floaty then
|
||||
self.floatAnimator:reset(flyTimeMs)
|
||||
end
|
||||
|
|
|
@ -17,9 +17,7 @@
|
|||
---@field onThirdOut fun()
|
||||
Baserunning = {}
|
||||
|
||||
-- TODO: Implement slides. Would require making fielders' gloves "real objects" whose state is tracked.
|
||||
|
||||
---@param announcer Announcer
|
||||
---@param announcer any
|
||||
---@return Baserunning
|
||||
function Baserunning.new(announcer, onThirdOut)
|
||||
local o = setmetatable({
|
||||
|
@ -34,7 +32,7 @@ function Baserunning.new(announcer, onThirdOut)
|
|||
onThirdOut = onThirdOut,
|
||||
}, { __index = Baserunning })
|
||||
|
||||
o:pushNewBatter()
|
||||
o.batter = o:newRunner()
|
||||
|
||||
return o
|
||||
end
|
||||
|
@ -79,7 +77,6 @@ end
|
|||
function Baserunning:outEligibleRunners(fielder)
|
||||
local touchedBase = utils.isTouchingBase(fielder.x, fielder.y)
|
||||
local didOutRunner = false
|
||||
|
||||
for i, runner in pairs(self.runners) do
|
||||
local runnerOnBase = utils.isTouchingBase(runner.x, runner.y)
|
||||
if -- Force out
|
||||
|
@ -117,14 +114,6 @@ function Baserunning:updateForcedRunners()
|
|||
end
|
||||
end
|
||||
|
||||
function Baserunning:convertBatterToRunner()
|
||||
self.batter.nextBase = C.Bases[C.First]
|
||||
self.batter.prevBase = C.Bases[C.Home]
|
||||
self:updateForcedRunners()
|
||||
self.batter.forcedTo = C.Bases[C.First]
|
||||
self.batter = nil -- Demote batter to a mere runner
|
||||
end
|
||||
|
||||
---@param deltaSeconds number
|
||||
function Baserunning:walkAwayOutRunners(deltaSeconds)
|
||||
for i, runner in ipairs(self.outRunners) do
|
||||
|
@ -138,7 +127,7 @@ function Baserunning:walkAwayOutRunners(deltaSeconds)
|
|||
end
|
||||
|
||||
---@return Runner
|
||||
function Baserunning:pushNewBatter()
|
||||
function Baserunning:newRunner()
|
||||
local new = {
|
||||
-- imageSet = math.random() < C.WokeMeter and FemmeSet or MascSet, -- TODO? lol.
|
||||
x = C.RightHandedBattersBox.x - 60,
|
||||
|
@ -148,7 +137,6 @@ function Baserunning:pushNewBatter()
|
|||
forcedTo = C.Bases[C.First],
|
||||
}
|
||||
self.runners[#self.runners + 1] = new
|
||||
self.batter = new
|
||||
return new
|
||||
end
|
||||
|
||||
|
@ -191,10 +179,8 @@ function Baserunning:updateRunner(runner, runnerIndex, appliedSpeed, deltaSecond
|
|||
|
||||
-- TODO: Do a better job drifting runners toward their bases when appliedSpeed is low/zero
|
||||
if distance < 2 then
|
||||
if runner.prevBase ~= nearestBase then
|
||||
runner.prevBase = runner.nextBase
|
||||
runner.nextBase = C.NextBaseMap[runner.nextBase]
|
||||
end
|
||||
runner.forcedTo = nil
|
||||
return false, false
|
||||
end
|
||||
|
@ -210,9 +196,6 @@ function Baserunning:updateRunner(runner, runnerIndex, appliedSpeed, deltaSecond
|
|||
end
|
||||
end
|
||||
|
||||
-- TODO: Make this less "sticky" for the user.
|
||||
-- Currently it can be a little hard to run *past* a base.
|
||||
|
||||
local autoRun = (nearestBaseDistance > 40 or runner.forcedTo) and mult * autoRunSpeed
|
||||
or nearestBaseDistance < 5 and 0
|
||||
or (nearestBase == runner.nextBase and autoRunSpeed or -1 * autoRunSpeed)
|
||||
|
@ -226,21 +209,16 @@ end
|
|||
|
||||
--- Update non-batter runners.
|
||||
--- Returns true only if at least one of the given runners moved during this update
|
||||
---@param appliedSpeed number | fun(runner: Runner): number
|
||||
---@param appliedSpeed number
|
||||
---@return boolean someRunnerMoved, number runnersScored
|
||||
function Baserunning:updateNonBatterRunners(appliedSpeed, forcedOnly, deltaSeconds)
|
||||
local someRunnerMoved = false
|
||||
local runnersScored = 0
|
||||
|
||||
local speedIsFunction = type(appliedSpeed) == "function"
|
||||
-- TODO: Filter for the runner closest to the currently-held direction button
|
||||
for runnerIndex, runner in ipairs(self.runners) do
|
||||
if runner ~= self.batter and (not forcedOnly or runner.forcedTo) then
|
||||
local speed = appliedSpeed
|
||||
if speedIsFunction then
|
||||
speed = appliedSpeed(runner)
|
||||
end
|
||||
local thisRunnerMoved, thisRunnerScored = self:updateRunner(runner, runnerIndex, speed, deltaSeconds)
|
||||
local thisRunnerMoved, thisRunnerScored = self:updateRunner(runner, runnerIndex, appliedSpeed, deltaSeconds)
|
||||
someRunnerMoved = someRunnerMoved or thisRunnerMoved
|
||||
if thisRunnerScored then
|
||||
runnersScored = runnersScored + 1
|
||||
|
@ -254,7 +232,3 @@ function Baserunning:updateNonBatterRunners(appliedSpeed, forcedOnly, deltaSecon
|
|||
|
||||
return someRunnerMoved, runnersScored
|
||||
end
|
||||
|
||||
if not playdate or playdate.TEST_MODE then
|
||||
return Baserunning
|
||||
end
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
C = {}
|
||||
|
||||
C.Screen = {
|
||||
W = playdate and playdate.display.getWidth() or 400,
|
||||
H = playdate and playdate.display.getHeight() or 240,
|
||||
W = playdate.display.getWidth(),
|
||||
H = playdate.display.getHeight(),
|
||||
}
|
||||
|
||||
C.Center = utils.xy(C.Screen.W / 2, C.Screen.H / 2)
|
||||
|
@ -86,7 +86,6 @@ C.BaseHitbox = 10
|
|||
|
||||
C.BattingPower = 25
|
||||
C.BatterHandPos = utils.xy(25, 15)
|
||||
C.GloveZ = 0 -- 10
|
||||
|
||||
C.SmallestBallRadius = 6
|
||||
|
||||
|
@ -131,7 +130,3 @@ C.UserThrowPower = 0.3
|
|||
C.WalkedRunnerSpeed = 10
|
||||
|
||||
C.ResetFieldersAfterSeconds = 2.5
|
||||
|
||||
if not playdate then
|
||||
return C
|
||||
end
|
||||
|
|
|
@ -21,9 +21,9 @@ end
|
|||
-- Only works if called with the bases empty (i.e. the only runner should be the batter.
|
||||
-- selene: allow(unused_variable)
|
||||
function dbg.loadTheBases(br)
|
||||
br:pushNewBatter()
|
||||
br:pushNewBatter()
|
||||
br:pushNewBatter()
|
||||
br:newRunner()
|
||||
br:newRunner()
|
||||
br:newRunner()
|
||||
|
||||
br.runners[2].x = C.Bases[C.First].x
|
||||
br.runners[2].y = C.Bases[C.First].y
|
||||
|
|
|
@ -1,16 +1,14 @@
|
|||
--- @class Fielder {
|
||||
--- @field catchEligible boolean
|
||||
--- @field x number
|
||||
--- @field y number
|
||||
--- @field target XyPair | nil
|
||||
--- @field speed number
|
||||
|
||||
-- TODO: Run down baserunners in a pickle.
|
||||
--- @alias Fielder {
|
||||
--- x: number,
|
||||
--- y: number,
|
||||
--- target: XyPair | nil,
|
||||
--- speed: number,
|
||||
--- }
|
||||
|
||||
-- selene: allow(unscoped_variables)
|
||||
---@class Fielding
|
||||
---@field fielders table<string, Fielder>
|
||||
---@field fielderHoldingBall Fielder | nil
|
||||
---@field fielderTouchingBall Fielder | nil
|
||||
Fielding = {}
|
||||
|
||||
local FielderDanceAnimator <const> = playdate.graphics.animator.new(1, 10, 0, utils.easingHill)
|
||||
|
@ -23,7 +21,6 @@ local function newFielder(name, speed)
|
|||
return {
|
||||
name = name,
|
||||
speed = speed * C.FielderRunMult,
|
||||
catchEligible = true,
|
||||
}
|
||||
end
|
||||
|
||||
|
@ -41,7 +38,7 @@ function Fielding.new()
|
|||
right = newFielder("Right", 50),
|
||||
},
|
||||
---@type Fielder | nil
|
||||
fielderHoldingBall = nil,
|
||||
fielderTouchingBall = nil,
|
||||
}, { __index = Fielding })
|
||||
end
|
||||
|
||||
|
@ -80,7 +77,7 @@ end
|
|||
---@param deltaSeconds number
|
||||
---@param fielder Fielder
|
||||
---@param ballPos XyPair
|
||||
---@return boolean inCatchingRange
|
||||
---@return boolean isTouchingBall
|
||||
local function updateFielderPosition(deltaSeconds, fielder, ballPos)
|
||||
if fielder.target ~= nil then
|
||||
if not utils.moveAtSpeed(fielder, fielder.speed * deltaSeconds, fielder.target) then
|
||||
|
@ -109,62 +106,37 @@ function Fielding:haveSomeoneChase(ballDestX, ballDestY)
|
|||
end
|
||||
end
|
||||
|
||||
---@param ball Ball
|
||||
---@param ball XyPair
|
||||
---@param deltaSeconds number
|
||||
---@return Fielder | nil fielderHoldingBall nil if no fielder is currently touching the ball
|
||||
---@return Fielder | nil fielderTouchingBall nil if no fielder is currently touching the ball
|
||||
function Fielding:updateFielderPositions(ball, deltaSeconds)
|
||||
local fielderHoldingBall = nil
|
||||
local fielderTouchingBall = nil
|
||||
for _, fielder in pairs(self.fielders) do
|
||||
local inCatchingRange = updateFielderPosition(deltaSeconds, fielder, ball)
|
||||
if inCatchingRange and fielder.catchEligible then
|
||||
-- TODO: Base this catch on fielder skill?
|
||||
fielderHoldingBall = fielder
|
||||
ball.heldBy = fielder -- How much havoc will this wreak?
|
||||
local isTouchingBall = updateFielderPosition(deltaSeconds, fielder, ball)
|
||||
if isTouchingBall then
|
||||
fielderTouchingBall = fielder
|
||||
end
|
||||
end
|
||||
-- TODO: The need is growing for a distinction between touching the ball and holding the ball.
|
||||
-- Or, at least, fielders need to start *stopping* the ball when they make contact with it.
|
||||
-- Right now, a line-drive *through* first will be counted as an out.
|
||||
self.fielderHoldingBall = fielderHoldingBall
|
||||
return fielderHoldingBall
|
||||
end
|
||||
|
||||
---@param fielder Fielder
|
||||
function Fielding.markIneligible(fielder)
|
||||
fielder.catchEligible = false
|
||||
playdate.timer.new(500, function()
|
||||
fielder.catchEligible = true
|
||||
end)
|
||||
end
|
||||
|
||||
function Fielding:markAllIneligible()
|
||||
for _, fielder in pairs(self.fielders) do
|
||||
fielder.catchEligible = false
|
||||
end
|
||||
playdate.timer.new(750, function()
|
||||
for _, fielder in pairs(self.fielders) do
|
||||
fielder.catchEligible = true
|
||||
end
|
||||
end)
|
||||
self.fielderTouchingBall = fielderTouchingBall
|
||||
return fielderTouchingBall
|
||||
end
|
||||
|
||||
-- TODO? Start moving target fielders close sooner?
|
||||
---@param field Fielding
|
||||
---@param field table
|
||||
---@param targetBase Base
|
||||
---@param launchBall LaunchBall
|
||||
---@param throwFlyMs number
|
||||
---@return ActionResult
|
||||
local function userThrowToImpl(field, targetBase, launchBall, throwFlyMs)
|
||||
if field.fielderHoldingBall == nil then
|
||||
if field.fielderTouchingBall == nil then
|
||||
return ActionResult.NeedsMoreTime
|
||||
end
|
||||
local closestFielder = utils.getNearestOf(field.fielders, targetBase.x, targetBase.y, function(fielder)
|
||||
return fielder ~= field.fielderHoldingBall -- presumably, this is who will be doing the throwing
|
||||
return fielder ~= field.fielderTouchingBall -- presumably, this is who will be doing the throwing
|
||||
end)
|
||||
|
||||
closestFielder.target = targetBase
|
||||
launchBall(targetBase.x, targetBase.y, playdate.easingFunctions.linear, throwFlyMs)
|
||||
Fielding.markIneligible(field.fielderHoldingBall)
|
||||
return ActionResult.Succeeded
|
||||
end
|
||||
|
||||
|
@ -195,7 +167,3 @@ function Fielding:drawFielders(fielderSprites, ball)
|
|||
end
|
||||
return ballIsHeld
|
||||
end
|
||||
|
||||
if not playdate or playdate.TEST_MODE then
|
||||
return { Fielding, newFielder }
|
||||
end
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 31 KiB |
109
src/main.lua
109
src/main.lua
|
@ -42,23 +42,15 @@ local gfx <const>, C <const> = playdate.graphics, C
|
|||
|
||||
local announcer = Announcer.new()
|
||||
local fielding = Fielding.new()
|
||||
-- TODO: Find a way to get baserunning and npc instantiated closer to the top, here.
|
||||
-- Currently difficult because they depend on nextHalfInning/each other.
|
||||
|
||||
------------------
|
||||
-- GLOBAL STATE --
|
||||
------------------
|
||||
---@alias SimpleAnimator { currentValue: fun(self): number; reset: fun(self, durationMs: number | nil) }
|
||||
|
||||
local deltaSeconds = 0
|
||||
|
||||
local ball = Ball.new(gfx.animator)
|
||||
|
||||
local batBase <const> = utils.xy(C.Center.x - 34, 215)
|
||||
local batTip <const> = utils.xy(0, 0)
|
||||
local batAngleDeg = C.CrankOffsetDeg
|
||||
|
||||
local catcherThrownBall = false
|
||||
|
||||
---@alias Team { score: number, benchPosition: XyPair }
|
||||
|
||||
---@type table<string, Team>
|
||||
local teams <const> = {
|
||||
home = {
|
||||
|
@ -71,28 +63,25 @@ local teams <const> = {
|
|||
},
|
||||
}
|
||||
|
||||
local inning = 1
|
||||
|
||||
local UserTeam <const> = teams.away
|
||||
local battingTeam = teams.away
|
||||
local battingTeamSprites = AwayTeamSprites
|
||||
local fieldingTeamSprites = HomeTeamSprites
|
||||
local runnerBlipper = battingTeam == teams.away and AwayTeamBlipper or HomeTeamBlipper
|
||||
local inning = 1
|
||||
local offenseState = C.Offense.batting
|
||||
|
||||
-- TODO: Replace with timers, repeatedly reset, instead of constantly setting to 0
|
||||
local secondsSinceLastRunnerMove = 0
|
||||
local secondsSincePitchAllowed = 0
|
||||
local secondsSincePitchAllowed = -5
|
||||
|
||||
-- These are only sort-of global state. They are purely graphical,
|
||||
-- but they need to be kept in sync with the rest of the globals.
|
||||
local runnerBlipper = battingTeam == teams.away and AwayTeamBlipper or HomeTeamBlipper
|
||||
local battingTeamSprites = AwayTeamSprites
|
||||
local fieldingTeamSprites = HomeTeamSprites
|
||||
local catcherThrownBall = false
|
||||
|
||||
-------------------------
|
||||
-- END OF GLOBAL STATE --
|
||||
-------------------------
|
||||
local batBase <const> = utils.xy(C.Center.x - 34, 215)
|
||||
local batTip <const> = utils.xy(0, 0)
|
||||
|
||||
local UserTeam <const> = teams.away
|
||||
local batAngleDeg = C.CrankOffsetDeg
|
||||
|
||||
---@alias SimpleAnimator { currentValue: fun(self): number; reset: fun(self, durationMs: number | nil) }
|
||||
---@alias Pitch { x: SimpleAnimator, y: SimpleAnimator, z: SimpleAnimator | nil }
|
||||
|
||||
---@type Pitch[]
|
||||
|
@ -124,12 +113,8 @@ local Pitches <const> = {
|
|||
},
|
||||
}
|
||||
|
||||
---@return boolean userIsOnSide, boolean userIsOnOtherSide
|
||||
---@return boolean userIsOnSide, boolean playerIsOnOtherSide
|
||||
local function userIsOn(side)
|
||||
if UserTeam == nil then
|
||||
-- Both teams are NPC-driven
|
||||
return false, false
|
||||
end
|
||||
local ret
|
||||
if UserTeam == battingTeam then
|
||||
ret = side == C.Sides.offense
|
||||
|
@ -139,7 +124,13 @@ local function userIsOn(side)
|
|||
return ret, not ret
|
||||
end
|
||||
|
||||
---@type LaunchBall
|
||||
--- Launches the ball from its current position to the given destination.
|
||||
---@param destX number
|
||||
---@param destY number
|
||||
---@param easingFunc EasingFunc
|
||||
---@param flyTimeMs number | nil
|
||||
---@param floaty boolean | nil
|
||||
---@param customBallScaler pd_animator | nil
|
||||
local function launchBall(destX, destY, easingFunc, flyTimeMs, floaty, customBallScaler)
|
||||
throwMeter:reset()
|
||||
ball:launch(destX, destY, easingFunc, flyTimeMs, floaty, customBallScaler)
|
||||
|
@ -148,8 +139,6 @@ end
|
|||
---@param pitchFlyTimeMs number | nil
|
||||
---@param pitchTypeIndex number | nil
|
||||
local function pitch(pitchFlyTimeMs, pitchTypeIndex)
|
||||
Fielding.markIneligible(fielding.fielders.pitcher)
|
||||
ball.heldBy = nil
|
||||
catcherThrownBall = false
|
||||
offenseState = C.Offense.batting
|
||||
|
||||
|
@ -185,15 +174,10 @@ local function nextHalfInning()
|
|||
announcer:say("SWITCHING SIDES...")
|
||||
end
|
||||
|
||||
if gameOver then
|
||||
announcer:say("AND THAT'S THE BALL GAME!")
|
||||
else
|
||||
fielding:resetFielderPositions()
|
||||
if battingTeam == teams.home then
|
||||
inning = inning + 1
|
||||
end
|
||||
-- TODO: Make the overlay handle its own dang delay.
|
||||
-- Delay to keep end-of-inning on the scoreboard for a few seconds
|
||||
playdate.timer.new(3000, function()
|
||||
battingTeam = currentlyFieldingTeam
|
||||
playdate.timer.new(2000, function()
|
||||
if battingTeam == teams.home then
|
||||
battingTeamSprites = HomeTeamSprites
|
||||
runnerBlipper = HomeTeamBlipper
|
||||
|
@ -203,9 +187,16 @@ local function nextHalfInning()
|
|||
fieldingTeamSprites = HomeTeamSprites
|
||||
runnerBlipper = AwayTeamBlipper
|
||||
end
|
||||
end)
|
||||
if gameOver then
|
||||
announcer:say("AND THAT'S THE BALL GAME!")
|
||||
else
|
||||
fielding:resetFielderPositions()
|
||||
if battingTeam == teams.home then
|
||||
inning = inning + 1
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
local baserunning = Baserunning.new(announcer, nextHalfInning)
|
||||
local npc = Npc.new(baserunning.runners, fielding.fielders)
|
||||
|
@ -248,14 +239,13 @@ local function nextBatter()
|
|||
playdate.timer.new(2000, function()
|
||||
pitchTracker:reset()
|
||||
if not baserunning.batter then
|
||||
baserunning:pushNewBatter()
|
||||
baserunning.batter = baserunning:newRunner()
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
local function walk()
|
||||
announcer:say("Walk!")
|
||||
-- TODO? Use baserunning:convertBatterToRunner()
|
||||
baserunning.batter.nextBase = C.Bases[C.First]
|
||||
baserunning.batter.prevBase = C.Bases[C.Home]
|
||||
offenseState = C.Offense.walking
|
||||
|
@ -311,13 +301,17 @@ local function updateBatting(batDeg, batSpeed)
|
|||
return
|
||||
end
|
||||
|
||||
baserunning:convertBatterToRunner()
|
||||
baserunning.batter.nextBase = C.Bases[C.First]
|
||||
baserunning.batter.prevBase = C.Bases[C.Home]
|
||||
baserunning:updateForcedRunners()
|
||||
baserunning.batter.forcedTo = C.Bases[C.First]
|
||||
baserunning.batter = nil -- Demote batter to a mere runner
|
||||
|
||||
fielding:haveSomeoneChase(ballDestX, ballDestY)
|
||||
end
|
||||
end
|
||||
|
||||
---@param appliedSpeed number | fun(runner: Runner): number
|
||||
---@param appliedSpeed number
|
||||
---@return boolean someRunnerMoved
|
||||
local function updateNonBatterRunners(appliedSpeed, forcedOnly)
|
||||
local runnerMoved, runnersScored = baserunning:updateNonBatterRunners(appliedSpeed, forcedOnly, deltaSeconds)
|
||||
|
@ -350,7 +344,16 @@ local function updateGameState()
|
|||
crankLimited = crankLimited * -1
|
||||
end
|
||||
|
||||
ball:updatePosition()
|
||||
if ball.heldBy then
|
||||
ball.x = ball.heldBy.x
|
||||
ball.y = ball.heldBy.y
|
||||
ball.size = C.SmallestBallRadius
|
||||
else
|
||||
ball.x = ball.xAnimator:currentValue()
|
||||
ball.z = ball.floatAnimator:currentValue()
|
||||
ball.y = ball.yAnimator:currentValue() + ball.z
|
||||
ball.size = ball.sizeAnimator:currentValue()
|
||||
end
|
||||
|
||||
local userOnOffense, userOnDefense = userIsOn(C.Sides.offense)
|
||||
|
||||
|
@ -408,10 +411,7 @@ local function updateGameState()
|
|||
end
|
||||
end
|
||||
elseif offenseState == C.Offense.running then
|
||||
local appliedSpeed = userOnOffense and crankLimited
|
||||
or function(runner)
|
||||
return npc:runningSpeed(runner, ball)
|
||||
end
|
||||
local appliedSpeed = userOnOffense and crankLimited or npc:runningSpeed(ball)
|
||||
if updateNonBatterRunners(appliedSpeed) then
|
||||
secondsSinceLastRunnerMove = 0
|
||||
else
|
||||
|
@ -419,12 +419,11 @@ local function updateGameState()
|
|||
if secondsSinceLastRunnerMove > C.ResetFieldersAfterSeconds then
|
||||
-- End of play. Throw the ball back to the pitcher
|
||||
launchBall(C.PitchStartX, C.PitchStartY, playdate.easingFunctions.linear, nil, true)
|
||||
fielding:markAllIneligible() -- This is ugly, and ideally would not be necessary if Fielding handled the return throw directly.
|
||||
fielding:resetFielderPositions()
|
||||
offenseState = C.Offense.batting
|
||||
-- TODO: Remove, or replace with nextBatter()
|
||||
if not baserunning.batter then
|
||||
baserunning:pushNewBatter()
|
||||
baserunning.batter = baserunning:newRunner()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -444,8 +443,8 @@ local function updateGameState()
|
|||
end
|
||||
if fielderHoldingBall then
|
||||
local outedSomeRunner = baserunning:outEligibleRunners(fielderHoldingBall)
|
||||
if not userOnDefense and offenseState == C.Offense.running then
|
||||
npc:fielderAction(fielderHoldingBall, outedSomeRunner, ball, launchBall)
|
||||
if userOnOffense then
|
||||
npc:fielderAction(offenseState, fielderHoldingBall, outedSomeRunner, ball, launchBall)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -453,8 +452,6 @@ local function updateGameState()
|
|||
actionQueue:runWaiting(deltaSeconds)
|
||||
end
|
||||
|
||||
-- TODO: Swappable update() for main menu, etc.
|
||||
|
||||
function playdate.update()
|
||||
playdate.timer.updateTimers()
|
||||
gfx.animation.blinker.updateAll()
|
||||
|
|
30
src/npc.lua
30
src/npc.lua
|
@ -39,25 +39,26 @@ end
|
|||
local baseRunningSpeed = 25
|
||||
|
||||
--- TODO: Individual runner control.
|
||||
---@param runner Runner
|
||||
---@param ball Point3d
|
||||
---@return number
|
||||
function Npc:runningSpeed(runner, ball)
|
||||
function Npc:runningSpeed(ball)
|
||||
if #self.runners == 0 then
|
||||
return 0
|
||||
end
|
||||
|
||||
local distanceFromBall = utils.distanceBetweenZ(ball.x, ball.y, ball.z, runner.x, runner.y, 0)
|
||||
local runner1 = self.runners[1]
|
||||
|
||||
if distanceFromBall > 400 or runner.forcedTo then
|
||||
local ballIsFar = utils.distanceBetweenZ(ball.x, ball.y, ball.z, runner1.x, runner1.y, 0) > 250
|
||||
|
||||
if ballIsFar or runner1.forcedTo then
|
||||
return baseRunningSpeed
|
||||
end
|
||||
|
||||
local touchedBase = utils.isTouchingBase(runner.x, runner.y)
|
||||
if not touchedBase and runner.nextBase then
|
||||
local distToNext = utils.distanceBetween(runner.x, runner.y, runner.nextBase.x, runner.nextBase.y)
|
||||
local distToPrev = utils.distanceBetween(runner.x, runner.y, runner.prevBase.x, runner.prevBase.y)
|
||||
if distToNext < distToPrev or distanceFromBall > 350 then
|
||||
local touchedBase = utils.isTouchingBase(runner1.x, runner1.y)
|
||||
if not touchedBase and runner1.nextBase then
|
||||
local distToNext = utils.distanceBetween(runner1.x, runner1.y, runner1.nextBase.x, runner1.nextBase.y)
|
||||
local distToPrev = utils.distanceBetween(runner1.x, runner1.y, runner1.prevBase.x, runner1.prevBase.y)
|
||||
if distToNext < distToPrev then
|
||||
return baseRunningSpeed
|
||||
else
|
||||
return -1 * baseRunningSpeed
|
||||
|
@ -123,24 +124,25 @@ end
|
|||
local function tryToMakeAPlay(fielders, fielder, runners, ball, launchBall)
|
||||
local targetX, targetY = getNextOutTarget(runners)
|
||||
if targetX ~= nil and targetY ~= nil then
|
||||
local nearestFielder = utils.getNearestOf(fielders, targetX, targetY, function(grabCandidate)
|
||||
return grabCandidate.catchEligible
|
||||
end)
|
||||
local nearestFielder = utils.getNearestOf(fielders, targetX, targetY)
|
||||
nearestFielder.target = utils.xy(targetX, targetY)
|
||||
if nearestFielder == fielder then
|
||||
ball.heldBy = fielder
|
||||
else
|
||||
launchBall(targetX, targetY, playdate.easingFunctions.linear, nil, true)
|
||||
Fielding.markIneligible(nearestFielder)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@param offenseState OffenseState
|
||||
---@param fielder Fielder
|
||||
---@param outedSomeRunner boolean
|
||||
---@param ball { x: number, y: number, heldBy: Fielder | nil }
|
||||
---@param launchBall LaunchBall
|
||||
function Npc:fielderAction(fielder, outedSomeRunner, ball, launchBall)
|
||||
function Npc:fielderAction(offenseState, fielder, outedSomeRunner, ball, launchBall)
|
||||
if offenseState ~= C.Offense.running then
|
||||
return
|
||||
end
|
||||
if outedSomeRunner then
|
||||
-- Delay a little before the next play
|
||||
playdate.timer.new(750, function()
|
||||
|
|
|
@ -1,21 +0,0 @@
|
|||
utils = require("utils")
|
||||
|
||||
local mockPlaydate = {
|
||||
TEST_MODE = true,
|
||||
graphics = {
|
||||
animator = {
|
||||
new = function()
|
||||
return utils.staticAnimator(0)
|
||||
end,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
---@type Announcer
|
||||
local mockAnnouncer = {
|
||||
say = function(self, message)
|
||||
self.lastMessage = message
|
||||
end,
|
||||
}
|
||||
|
||||
return { mockPlaydate, mockAnnouncer }
|
|
@ -1,170 +0,0 @@
|
|||
import = function() end
|
||||
luaunit = require("luaunit")
|
||||
luaunit.ORDER_ACTUAL_EXPECTED = false
|
||||
|
||||
utils = require("utils")
|
||||
C = require("constants")
|
||||
mocks = require("test/mocks")
|
||||
playdate, announcer = mocks[1], mocks[2]
|
||||
|
||||
Baserunning = require("baserunning")
|
||||
|
||||
local _f = require("fielding")
|
||||
Fielding, newFielder = _f[1], _f[2]
|
||||
|
||||
---@return Baserunning, { called: boolean }
|
||||
function buildBaserunning()
|
||||
local thirdOutCallbackData = { called = false }
|
||||
local baserunning = Baserunning.new(announcer, function()
|
||||
thirdOutCallbackData.called = true
|
||||
end)
|
||||
return baserunning, thirdOutCallbackData
|
||||
end
|
||||
|
||||
---@alias BaseIndexOrXyPair (integer | XyPair)
|
||||
|
||||
--- NOTE: in addition to the given runners, there is implicitly a batter running from first.
|
||||
---@param runnerLocations BaseIndexOrXyPair[]
|
||||
---@return Baserunning
|
||||
function buildRunnersOn(runnerLocations)
|
||||
local baserunning = buildBaserunning()
|
||||
baserunning:convertBatterToRunner()
|
||||
for _, location in ipairs(runnerLocations) do
|
||||
baserunning:pushNewBatter()
|
||||
local runner = baserunning.batter
|
||||
baserunning:convertBatterToRunner()
|
||||
if type(location) == "number" then
|
||||
-- Is a base index
|
||||
-- Push the runner *through* each base.
|
||||
for b = 1, location do
|
||||
runner.x = C.Bases[b].x
|
||||
runner.y = C.Bases[b].y
|
||||
baserunning:updateNonBatterRunners(0.001, false, 0.001)
|
||||
end
|
||||
else
|
||||
-- Is a raw XyPair
|
||||
runner.x = location.x
|
||||
runner.y = location.y
|
||||
end
|
||||
end
|
||||
return baserunning
|
||||
end
|
||||
|
||||
---@alias Condition { fielderWithBallAt: XyPair, outWhen: BaseIndexOrXyPair[][], safeWhen: BaseIndexOrXyPair[][] }
|
||||
|
||||
---@param expected boolean
|
||||
---@param fielderWithBallAt XyPair
|
||||
---@param when integer[][]
|
||||
function assertRunnerOutCondition(expected, when, fielderWithBallAt)
|
||||
local msg = expected and "out" or "safe"
|
||||
for _, runnersOn in ipairs(when) do
|
||||
local baserunning = buildRunnersOn(runnersOn)
|
||||
local outedSomeRunner = baserunning:outEligibleRunners(fielderWithBallAt)
|
||||
luaunit.failIf(outedSomeRunner ~= expected, "Runner should have been " .. msg .. ", but was not!")
|
||||
end
|
||||
end
|
||||
|
||||
---@param condition Condition
|
||||
function assertRunnerStatuses(condition)
|
||||
assertRunnerOutCondition(true, condition.outWhen, condition.fielderWithBallAt)
|
||||
assertRunnerOutCondition(false, condition.safeWhen, condition.fielderWithBallAt)
|
||||
end
|
||||
|
||||
function testForceOutsAtFirst()
|
||||
assertRunnerStatuses({
|
||||
fielderWithBallAt = C.Bases[C.First],
|
||||
outWhen = {
|
||||
{},
|
||||
{ 1 },
|
||||
{ 2 },
|
||||
{ 3 },
|
||||
{ 1, 2 },
|
||||
{ 1, 3 },
|
||||
{ 2, 3 },
|
||||
{ 1, 2, 3 },
|
||||
},
|
||||
safeWhen = {},
|
||||
})
|
||||
end
|
||||
|
||||
function testForceOutsAtSecond()
|
||||
assertRunnerStatuses({
|
||||
fielderWithBallAt = C.Bases[C.Second],
|
||||
outWhen = {
|
||||
{ 1 },
|
||||
{ 1, 2 },
|
||||
{ 1, 3 },
|
||||
{ 1, 2, 3 },
|
||||
},
|
||||
safeWhen = {
|
||||
{},
|
||||
{ 2 },
|
||||
{ 3 },
|
||||
{ 2, 3 },
|
||||
},
|
||||
})
|
||||
end
|
||||
|
||||
function testForceOutsAtThird()
|
||||
assertRunnerStatuses({
|
||||
fielderWithBallAt = C.Bases[C.Third],
|
||||
outWhen = {
|
||||
{ 1, 2 },
|
||||
{ 1, 2, 3 },
|
||||
},
|
||||
safeWhen = {
|
||||
{ 1 },
|
||||
{ 2 },
|
||||
{ 3 },
|
||||
{ 2, 3 },
|
||||
{ 1, 3 },
|
||||
},
|
||||
})
|
||||
end
|
||||
|
||||
function testForceOutsAtHome()
|
||||
assertRunnerStatuses({
|
||||
fielderWithBallAt = C.Bases[C.Home],
|
||||
outWhen = {
|
||||
{ 1, 2, 3 },
|
||||
},
|
||||
safeWhen = {
|
||||
{},
|
||||
{ 1 },
|
||||
{ 2 },
|
||||
{ 3 },
|
||||
{ 1, 2 },
|
||||
{ 1, 3 },
|
||||
{ 2, 3 },
|
||||
},
|
||||
})
|
||||
end
|
||||
|
||||
function testTagOutsShouldHappenOffBase()
|
||||
local fielderWithBallAt = utils.xy(10, 10) -- Some location not on a base.
|
||||
local farFromFielder = utils.xy(100, 100)
|
||||
assertRunnerStatuses({
|
||||
fielderWithBallAt = fielderWithBallAt,
|
||||
outWhen = {
|
||||
{ fielderWithBallAt },
|
||||
},
|
||||
safeWhen = {
|
||||
{ farFromFielder },
|
||||
},
|
||||
})
|
||||
end
|
||||
|
||||
function testTagOutsShouldNotHappenOnBase()
|
||||
assertRunnerStatuses({
|
||||
fielderWithBallAt = C.Bases[C.Third],
|
||||
outWhen = {},
|
||||
safeWhen = {
|
||||
{ 2 },
|
||||
{ 3 },
|
||||
{ 2, 3 },
|
||||
{ 2, 3, 4 },
|
||||
},
|
||||
})
|
||||
end
|
||||
|
||||
os.exit(luaunit.LuaUnit.run())
|
|
@ -1,57 +0,0 @@
|
|||
import = function() end
|
||||
luaunit = require("luaunit")
|
||||
luaunit.ORDER_ACTUAL_EXPECTED = false
|
||||
|
||||
utils = require("utils")
|
||||
C = require("constants")
|
||||
mocks = require("test/mocks")
|
||||
playdate = mocks[1]
|
||||
|
||||
_f = require("fielding")
|
||||
Fielding = _f[1]
|
||||
|
||||
---@return Fielding, number fielderCount
|
||||
local function fieldersAtDefaultPositions()
|
||||
local fielding = Fielding.new()
|
||||
fielding:resetFielderPositions()
|
||||
|
||||
local fielderCount = 0
|
||||
for _, fielder in pairs(fielding.fielders) do
|
||||
fielder.x = fielder.target.x
|
||||
fielder.y = fielder.target.y
|
||||
fielderCount = fielderCount + 1
|
||||
end
|
||||
|
||||
return fielding, fielderCount
|
||||
end
|
||||
|
||||
---@param x number
|
||||
---@param y number
|
||||
---@param z number | nil
|
||||
function fakeBall(x, y, z)
|
||||
return {
|
||||
x = x,
|
||||
y = y,
|
||||
z = z or 0,
|
||||
heldBy = nil,
|
||||
}
|
||||
end
|
||||
|
||||
function testBallPickedUpByNearbyFielders()
|
||||
local fielding, fielderCount = fieldersAtDefaultPositions()
|
||||
luaunit.assertIs("table", type(fielding))
|
||||
luaunit.assertIs("table", type(fielding.fielders))
|
||||
luaunit.assertEquals(9, fielderCount)
|
||||
|
||||
local ball = fakeBall(-100, -100, -100)
|
||||
fielding:updateFielderPositions(ball, 0.01)
|
||||
luaunit.assertIsNil(ball.heldBy, "Ball should not be held by a fielder yet")
|
||||
|
||||
local secondBaseman = fielding.fielders.second
|
||||
ball.x = secondBaseman.x
|
||||
ball.y = secondBaseman.y
|
||||
fielding:updateFielderPositions(ball, 0.01)
|
||||
luaunit.assertIs(secondBaseman, ball.heldBy, "Ball should be held by the nearest fielder")
|
||||
end
|
||||
|
||||
os.exit(luaunit.LuaUnit.run())
|
Loading…
Reference in New Issue