Player pitching, npc batting, and gloves

Or at least a basic implementation thereof
This commit is contained in:
Sage Vaillancourt 2025-02-05 18:25:34 -05:00
parent 779b13d56b
commit 57625a9b80
4 changed files with 124 additions and 30 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

BIN
src/images/game/glove.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -58,6 +58,11 @@ local PlayerFrown <const> = gfx.image.new("images/game/player-frown.png") --[[@a
local PlayerSmile <const> = gfx.image.new("images/game/player.png") --[[@as pd_image]] local PlayerSmile <const> = gfx.image.new("images/game/player.png") --[[@as pd_image]]
local PlayerBack <const> = gfx.image.new("images/game/player-back.png") --[[@as pd_image]] local PlayerBack <const> = gfx.image.new("images/game/player-back.png") --[[@as pd_image]]
local Glove <const> = gfx.image.new("images/game/glove.png") --[[@as pd_image]]
local GloveHoldingBall <const> = gfx.image.new("images/game/glove-holding-ball.png") --[[@as pd_image]]
local GloveSizeX, GloveSizeY <const> = Glove:getSize()
local GloveOffX, GloveOffY <const> = GloveSizeX / 2, GloveSizeY / 2
local PlayerImageBlipper <const> = blipper.new(100, "images/game/player.png", "images/game/player-lowhat.png") local PlayerImageBlipper <const> = blipper.new(100, "images/game/player.png", "images/game/player-lowhat.png")
local DanceBounceMs <const> = 500 local DanceBounceMs <const> = 500
@ -79,7 +84,7 @@ local PitchStartY <const>, PitchEndY <const> = 105, 240
local ballAnimatorY = gfx.animator.new(0, BallOffscreen, BallOffscreen, playdate.easingFunctions.linear) local ballAnimatorY = gfx.animator.new(0, BallOffscreen, BallOffscreen, playdate.easingFunctions.linear)
local ballAnimatorX = gfx.animator.new(0, BallOffscreen, BallOffscreen, playdate.easingFunctions.linear) local ballAnimatorX = gfx.animator.new(0, BallOffscreen, BallOffscreen, playdate.easingFunctions.linear)
---@alias PseudoAnimator { currentValue: fun(self): number; reset: fun(self) } ---@alias PseudoAnimator { currentValue: fun(self): number; reset: fun(self, durationMs: number | nil) }
---@alias Pitch { x: PseudoAnimator, y: PseudoAnimator, z: PseudoAnimator | nil } ---@alias Pitch { x: PseudoAnimator, y: PseudoAnimator, z: PseudoAnimator | nil }
---@type Pitch[] ---@type Pitch[]
@ -156,7 +161,7 @@ local teams <const> = {
}, },
} }
local PlayerTeam <const> = teams.away local PlayerTeam <const> = teams.home
local battingTeam = teams.away local battingTeam = teams.away
local outs = 0 local outs = 0
local inning = 1 local inning = 1
@ -217,7 +222,7 @@ local fielders <const> = {
shortstop = newFielder("Shortstop", 40), shortstop = newFielder("Shortstop", 40),
third = newFielder("Third", 40), third = newFielder("Third", 40),
pitcher = newFielder("Pitcher", 30), pitcher = newFielder("Pitcher", 30),
catcher = newFielder("Catcher", 20), catcher = newFielder("Catcher", 35),
left = newFielder("Left", 40), left = newFielder("Left", 40),
center = newFielder("Center", 40), center = newFielder("Center", 40),
right = newFielder("Right", 40), right = newFielder("Right", 40),
@ -316,7 +321,8 @@ local secondsSincePitchAllowed = -5
local catcherThrownBall = false local catcherThrownBall = false
function pitch() ---@param pitchFlyTimeMs number | nil
function pitch(pitchFlyTimeMs)
catcherThrownBall = false catcherThrownBall = false
offenseMode = Offense.batting offenseMode = Offense.batting
@ -330,15 +336,17 @@ function pitch()
-- ballFloatAnimator:reset() -- ballFloatAnimator:reset()
-- end -- end
ballAnimatorX:reset() if pitchFlyTimeMs then
ballAnimatorY:reset() ballAnimatorX:reset(pitchFlyTimeMs)
ballAnimatorY:reset(pitchFlyTimeMs)
else
ballAnimatorX:reset()
ballAnimatorY:reset()
end
secondsSincePitchAllowed = 0 secondsSincePitchAllowed = 0
end end
local crankChange = 0
local acceleratedChange = 0
local BaseHitbox = 10 local BaseHitbox = 10
--- Returns the base being touched by the player at (x,y), or nil, if no base is being touched --- Returns the base being touched by the player at (x,y), or nil, if no base is being touched
@ -360,10 +368,10 @@ local BallCatchHitbox = 3
--- Returns true only if the given point is touching the ball at its current position --- Returns true only if the given point is touching the ball at its current position
---@param x number ---@param x number
---@param y number ---@param y number
---@return boolean ---@return boolean, number
function isTouchingBall(x, y) function isTouchingBall(x, y)
local ballDistance = utils.distanceBetween(x, y, ball.x, ball.y) local ballDistance = utils.distanceBetween(x, y, ball.x, ball.y)
return ballDistance < BallCatchHitbox return ballDistance < BallCatchHitbox, ballDistance
end end
---@param base Base ---@param base Base
@ -626,14 +634,14 @@ end
---@type number ---@type number
local batAngleDeg local batAngleDeg
function updateBatting() ---@param batDeg number
function updateBatting(batDeg, batSpeed)
if ball.y < BallOffscreen then if ball.y < BallOffscreen then
ball.y = ballAnimatorY:currentValue() + ballFloatAnimator:currentValue() ball.y = ballAnimatorY:currentValue() + ballFloatAnimator:currentValue()
ball.size = SmallestBallRadius -- ballFloatAnimator:currentValue() ball.size = SmallestBallRadius -- ballFloatAnimator:currentValue()
end end
batAngleDeg = (playdate.getCrankPosition() + CrankOffsetDeg) % 360 local batAngle = math.rad(batDeg)
local batAngle = math.rad(batAngleDeg)
-- TODO: animate bat-flip or something -- TODO: animate bat-flip or something
batBase.x = batter and (batter.x + BatOffset.x) or 0 batBase.x = batter and (batter.x + BatOffset.x) or 0
batBase.y = batter and (batter.y + BatOffset.y) or 0 batBase.y = batter and (batter.y + BatOffset.y) or 0
@ -641,7 +649,7 @@ function updateBatting()
batTip.y = batBase.y + (BatLength * math.cos(batAngle)) batTip.y = batBase.y + (BatLength * math.cos(batAngle))
if if
acceleratedChange >= 0 -- > 0 batSpeed >= 0 -- > 0
and utils.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
@ -649,7 +657,7 @@ function updateBatting()
offenseMode = Offense.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(batSpeed / 15)
local ballVelX = mult * 10 * math.sin(ballAngle) local ballVelX = mult * 10 * math.sin(ballAngle)
local ballVelY = mult * 5 * math.cos(ballAngle) local ballVelY = mult * 5 * math.cos(ballAngle)
if ballVelY > 0 then if ballVelY > 0 then
@ -712,10 +720,47 @@ function walkAwayOutRunners()
end end
end end
local npcBatDeg = 0
local NpcBatSpeed <const> = 1200
function npcBatAngle()
if not catcherThrownBall and ball.y > 190 and ball.y < 230 and (ball.x < Center.x + 15) then
npcBatDeg = npcBatDeg + (deltaSeconds * NpcBatSpeed)
else
npcBatDeg = 200
end
return npcBatDeg
end
function npcBatChange()
return deltaSeconds * NpcBatSpeed
end
function npcRunningSpeed()
if #runners == 0 then
return 0
end
local touchedBase = isTouchingBase(runners[1].x, runners[1].y)
if not touchedBase or touchedBase == Bases[Home] then
return 35
end
return 0
end
local pitchMeter = 0
local PitchMeterLimit = 25
function readPitch()
if pitchMeter > PitchMeterLimit then
return (PitchFlyMs / (pitchMeter / PitchMeterLimit))
end
return nil
end
function updateGameState() function updateGameState()
deltaSeconds = playdate.getElapsedTime() or 0 deltaSeconds = playdate.getElapsedTime() or 0
playdate.resetElapsedTime() playdate.resetElapsedTime()
crankChange, acceleratedChange = playdate.getCrankChange() --[[@as number, number]] local crankChange = playdate.getCrankChange() --[[@as number, number]]
if ball.heldBy then if ball.heldBy then
ball.x = ball.heldBy.x ball.x = ball.heldBy.x
@ -726,22 +771,42 @@ function updateGameState()
end end
if offenseMode == Offense.batting then if offenseMode == Offense.batting then
local playerOnOffense = playerIsOn(Sides.offense)
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)
catcherThrownBall = true catcherThrownBall = true
else
pitchMeter = 0
end end
if not playerOnOffense then
pitchMeter = math.max(0, pitchMeter - (deltaSeconds * 150))
pitchMeter = pitchMeter + crankChange
if pitchMeter > PitchMeterLimit then
printTable({ pitchMeter = pitchMeter })
end
end
if secondsSincePitchAllowed > PitchAfterSeconds then if secondsSincePitchAllowed > PitchAfterSeconds then
pitch() if playerOnOffense then
pitch(PitchFlyMs)
else
local pitchFly = readPitch()
if pitchFly then
pitch(pitchFly)
end
end
end end
updateBatting() batAngleDeg = playerOnOffense and ((playdate.getCrankPosition() + CrankOffsetDeg) % 360) or npcBatAngle()
local batSpeed = playerOnOffense and crankChange or npcBatChange()
updateBatting(batAngleDeg, batSpeed)
-- TODO: Ensure batter can't be nil, here -- TODO: Ensure batter can't be nil, here
updateRunner(batter, nil, crankChange) updateRunner(batter, nil, crankChange)
elseif offenseMode == Offense.running then elseif offenseMode == Offense.running then
if playerIsOn(Sides.defense) then local appliedSpeed = playerIsOn(Sides.offense) and crankChange or npcRunningSpeed()
updateRunning(999) if updateRunning(appliedSpeed) then
end
if updateRunning(crankChange) then
secondsSinceLastRunnerMove = 0 secondsSinceLastRunnerMove = 0
else else
secondsSinceLastRunnerMove = secondsSinceLastRunnerMove + deltaSeconds secondsSinceLastRunnerMove = secondsSinceLastRunnerMove + deltaSeconds
@ -761,6 +826,22 @@ end
-- TODO -- TODO
function drawMinimap() end function drawMinimap() end
---@param fielder Fielder
---@return boolean isHoldingBall
function drawFielderGlove(fielder)
printTable({ ballFloatValue = ballFloatAnimator:currentValue() })
local distanceFromBall =
utils.distanceBetweenZ(fielder.x, fielder.y, 0, ball.x, ball.y, ballFloatAnimator:currentValue())
local shoulderX, shoulderY = fielder.x + 10, fielder.y + FielderDanceAnimator:currentValue() + 5
if distanceFromBall > 30 then
Glove:draw(shoulderX, shoulderY)
return false
else
GloveHoldingBall:draw(ball.x - GloveOffX, ball.y - GloveOffY)
return true
end
end
function playdate.update() function playdate.update()
playdate.timer.updateTimers() playdate.timer.updateTimers()
@ -779,8 +860,11 @@ function playdate.update()
GrassBackground:draw(-400, -240) GrassBackground:draw(-400, -240)
local fielderDanceHeight = FielderDanceAnimator:currentValue() local fielderDanceHeight = FielderDanceAnimator:currentValue()
local ballIsHeld = false
for _, fielder in pairs(fielders) do for _, fielder in pairs(fielders) do
gfx.fillRect(fielder.x, fielder.y - fielderDanceHeight, 14, 25) local fielderY = fielder.y + fielderDanceHeight
gfx.fillRect(fielder.x, fielderY, 14, 25)
ballIsHeld = drawFielderGlove(fielder) or ballIsHeld
end end
if offenseMode == Offense.batting then if offenseMode == Offense.batting then
@ -809,13 +893,15 @@ function playdate.update()
PlayerFrown:draw(runner.x, runner.y) PlayerFrown:draw(runner.x, runner.y)
end end
gfx.setLineWidth(2) if not ballIsHeld then
gfx.setLineWidth(2)
gfx.setColor(gfx.kColorWhite) gfx.setColor(gfx.kColorWhite)
gfx.fillCircleAtPoint(ball.x, ball.y, ball.size) gfx.fillCircleAtPoint(ball.x, ball.y, ball.size)
gfx.setColor(gfx.kColorBlack) gfx.setColor(gfx.kColorBlack)
gfx.drawCircleAtPoint(ball.x, ball.y, ball.size) gfx.drawCircleAtPoint(ball.x, ball.y, ball.size)
end
gfx.setDrawOffset(0, 0) gfx.setDrawOffset(0, 0)
if math.abs(offsetX) > 10 or math.abs(offsetY) > 10 then if math.abs(offsetX) > 10 or math.abs(offsetY) > 10 then

View File

@ -61,13 +61,21 @@ function utils.filter(array, condition)
return newArray return newArray
end end
---@return number, number, number ---@return number distance, number x, number y
function utils.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
end end
---@return number distance, number x, number y
function utils.distanceBetweenZ(x1, y1, z1, x2, y2, z2)
local a = x1 - x2
local b = y1 - y2
local c = z1 - z2
return math.sqrt((a * a) + (b * b) + (c * c)), 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 --- 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 utils.pointDirectlyUnderLine(pointX, pointY, lineX1, lineY1, lineX2, lineY2, bottomBound) function utils.pointDirectlyUnderLine(pointX, pointY, lineX1, lineY1, lineX2, lineY2, bottomBound)