NPCs can play each other

Some timing tweaks and TODOs.
Cluster global state tighter together.
This commit is contained in:
Sage Vaillancourt 2025-02-11 13:50:03 -05:00
parent b9d25e18d8
commit 1926960c86
5 changed files with 59 additions and 42 deletions

View File

@ -23,7 +23,7 @@ function Announcer.new()
}, { __index = Announcer })
end
local DurationMs <const> = 3000
local DurationMs <const> = 2000
function Announcer:popIn()
self.animatorY = AnnouncerAnimatorInY

View File

@ -17,6 +17,8 @@
---@field onThirdOut fun()
Baserunning = {}
-- TODO: Implement slides. Would require making fielders' gloves "real objects" whose state is tracked.
---@param announcer any
---@return Baserunning
function Baserunning.new(announcer, onThirdOut)
@ -196,6 +198,9 @@ 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)

View File

@ -5,6 +5,8 @@
--- speed: number,
--- }
-- TODO: Run down baserunners in a pickle.
-- selene: allow(unscoped_variables)
---@class Fielding
---@field fielders table<string, Fielder>
@ -117,6 +119,9 @@ function Fielding:updateFielderPositions(ball, deltaSeconds)
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.fielderTouchingBall = fielderTouchingBall
return fielderTouchingBall
end

View File

@ -42,15 +42,23 @@ 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.
---@alias SimpleAnimator { currentValue: fun(self): number; reset: fun(self, durationMs: number | nil) }
------------------
-- GLOBAL STATE --
------------------
local deltaSeconds = 0
local ball = Ball.new(gfx.animator)
---@alias Team { score: number, benchPosition: XyPair }
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 = {
@ -63,25 +71,28 @@ local teams <const> = {
},
}
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 battingTeam = teams.away
local offenseState = C.Offense.batting
-- TODO: Replace with timers, repeatedly reset, instead of constantly setting to 0
local secondsSinceLastRunnerMove = 0
local secondsSincePitchAllowed = -5
local secondsSincePitchAllowed = 0
local catcherThrownBall = false
-- 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 batBase <const> = utils.xy(C.Center.x - 34, 215)
local batTip <const> = utils.xy(0, 0)
-------------------------
-- END OF GLOBAL STATE --
-------------------------
local batAngleDeg = C.CrankOffsetDeg
local UserTeam <const> = teams.away
---@alias SimpleAnimator { currentValue: fun(self): number; reset: fun(self, durationMs: number | nil) }
---@alias Pitch { x: SimpleAnimator, y: SimpleAnimator, z: SimpleAnimator | nil }
---@type Pitch[]
@ -113,8 +124,12 @@ local Pitches <const> = {
},
}
---@return boolean userIsOnSide, boolean playerIsOnOtherSide
---@return boolean userIsOnSide, boolean userIsOnOtherSide
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
@ -124,13 +139,7 @@ local function userIsOn(side)
return ret, not ret
end
--- 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
---@type LaunchBall
local function launchBall(destX, destY, easingFunc, flyTimeMs, floaty, customBallScaler)
throwMeter:reset()
ball:launch(destX, destY, easingFunc, flyTimeMs, floaty, customBallScaler)
@ -174,10 +183,15 @@ local function nextHalfInning()
announcer:say("SWITCHING SIDES...")
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()
if gameOver then
announcer:say("AND THAT'S THE BALL GAME!")
else
fielding:resetFielderPositions()
if battingTeam == teams.home then
inning = inning + 1
end
battingTeam = currentlyFieldingTeam
playdate.timer.new(2000, function()
if battingTeam == teams.home then
battingTeamSprites = HomeTeamSprites
runnerBlipper = HomeTeamBlipper
@ -187,16 +201,9 @@ local function nextHalfInning()
fieldingTeamSprites = HomeTeamSprites
runnerBlipper = AwayTeamBlipper
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
end
local baserunning = Baserunning.new(announcer, nextHalfInning)
local npc = Npc.new(baserunning.runners, fielding.fielders)
@ -443,7 +450,7 @@ local function updateGameState()
end
if fielderHoldingBall then
local outedSomeRunner = baserunning:outEligibleRunners(fielderHoldingBall)
if userOnOffense then
if not userOnDefense then
npc:fielderAction(offenseState, fielderHoldingBall, outedSomeRunner, ball, launchBall)
end
end

View File

@ -48,7 +48,7 @@ function Npc:runningSpeed(ball)
local runner1 = self.runners[1]
local ballIsFar = utils.distanceBetweenZ(ball.x, ball.y, ball.z, runner1.x, runner1.y, 0) > 250
local ballIsFar = utils.distanceBetweenZ(ball.x, ball.y, ball.z, runner1.x, runner1.y, 0) > 300
if ballIsFar or runner1.forcedTo then
return baseRunningSpeed