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
if pitchFlyTimeMs then
ballAnimatorX:reset(pitchFlyTimeMs)
ballAnimatorY:reset(pitchFlyTimeMs)
else
ballAnimatorX:reset() ballAnimatorX:reset()
ballAnimatorY: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
updateBatting() end
end
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,6 +893,7 @@ function playdate.update()
PlayerFrown:draw(runner.x, runner.y) PlayerFrown:draw(runner.x, runner.y)
end end
if not ballIsHeld then
gfx.setLineWidth(2) gfx.setLineWidth(2)
gfx.setColor(gfx.kColorWhite) gfx.setColor(gfx.kColorWhite)
@ -816,6 +901,7 @@ function playdate.update()
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)