diff --git a/src/images/game/glove-holding-ball.png b/src/images/game/glove-holding-ball.png new file mode 100644 index 0000000..de2e43c Binary files /dev/null and b/src/images/game/glove-holding-ball.png differ diff --git a/src/images/game/glove.png b/src/images/game/glove.png new file mode 100644 index 0000000..c0374e6 Binary files /dev/null and b/src/images/game/glove.png differ diff --git a/src/main.lua b/src/main.lua index ac47f4c..1e356dc 100644 --- a/src/main.lua +++ b/src/main.lua @@ -58,6 +58,11 @@ local PlayerFrown = gfx.image.new("images/game/player-frown.png") --[[@a local PlayerSmile = gfx.image.new("images/game/player.png") --[[@as pd_image]] local PlayerBack = gfx.image.new("images/game/player-back.png") --[[@as pd_image]] +local Glove = gfx.image.new("images/game/glove.png") --[[@as pd_image]] +local GloveHoldingBall = gfx.image.new("images/game/glove-holding-ball.png") --[[@as pd_image]] +local GloveSizeX, GloveSizeY = Glove:getSize() +local GloveOffX, GloveOffY = GloveSizeX / 2, GloveSizeY / 2 + local PlayerImageBlipper = blipper.new(100, "images/game/player.png", "images/game/player-lowhat.png") local DanceBounceMs = 500 @@ -79,7 +84,7 @@ local PitchStartY , PitchEndY = 105, 240 local ballAnimatorY = 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 } ---@type Pitch[] @@ -156,7 +161,7 @@ local teams = { }, } -local PlayerTeam = teams.away +local PlayerTeam = teams.home local battingTeam = teams.away local outs = 0 local inning = 1 @@ -217,7 +222,7 @@ local fielders = { shortstop = newFielder("Shortstop", 40), third = newFielder("Third", 40), pitcher = newFielder("Pitcher", 30), - catcher = newFielder("Catcher", 20), + catcher = newFielder("Catcher", 35), left = newFielder("Left", 40), center = newFielder("Center", 40), right = newFielder("Right", 40), @@ -316,7 +321,8 @@ local secondsSincePitchAllowed = -5 local catcherThrownBall = false -function pitch() +---@param pitchFlyTimeMs number | nil +function pitch(pitchFlyTimeMs) catcherThrownBall = false offenseMode = Offense.batting @@ -330,15 +336,17 @@ function pitch() -- ballFloatAnimator:reset() -- end - ballAnimatorX:reset() - ballAnimatorY:reset() + if pitchFlyTimeMs then + ballAnimatorX:reset(pitchFlyTimeMs) + ballAnimatorY:reset(pitchFlyTimeMs) + else + ballAnimatorX:reset() + ballAnimatorY:reset() + end secondsSincePitchAllowed = 0 end -local crankChange = 0 -local acceleratedChange = 0 - local BaseHitbox = 10 --- 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 ---@param x number ---@param y number ----@return boolean +---@return boolean, number function isTouchingBall(x, y) local ballDistance = utils.distanceBetween(x, y, ball.x, ball.y) - return ballDistance < BallCatchHitbox + return ballDistance < BallCatchHitbox, ballDistance end ---@param base Base @@ -626,14 +634,14 @@ end ---@type number local batAngleDeg -function updateBatting() +---@param batDeg number +function updateBatting(batDeg, batSpeed) if ball.y < BallOffscreen then ball.y = ballAnimatorY:currentValue() + ballFloatAnimator:currentValue() ball.size = SmallestBallRadius -- ballFloatAnimator:currentValue() end - batAngleDeg = (playdate.getCrankPosition() + CrankOffsetDeg) % 360 - local batAngle = math.rad(batAngleDeg) + local batAngle = math.rad(batDeg) -- TODO: animate bat-flip or something batBase.x = batter and (batter.x + BatOffset.x) 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)) 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 ball.y < 232 --not isTouchingBall(fielders.catcher.x, fielders.catcher.y) then @@ -649,7 +657,7 @@ function updateBatting() offenseMode = Offense.running 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 ballVelY = mult * 5 * math.cos(ballAngle) if ballVelY > 0 then @@ -712,10 +720,47 @@ function walkAwayOutRunners() end end +local npcBatDeg = 0 +local NpcBatSpeed = 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() deltaSeconds = playdate.getElapsedTime() or 0 playdate.resetElapsedTime() - crankChange, acceleratedChange = playdate.getCrankChange() --[[@as number, number]] + local crankChange = playdate.getCrankChange() --[[@as number, number]] if ball.heldBy then ball.x = ball.heldBy.x @@ -726,22 +771,42 @@ function updateGameState() end if offenseMode == Offense.batting then + local playerOnOffense = playerIsOn(Sides.offense) secondsSincePitchAllowed = secondsSincePitchAllowed + deltaSeconds + if secondsSincePitchAllowed > 3.5 and not catcherThrownBall then throwBall(PitchStartX, PitchStartY, playdate.easingFunctions.linear, nil, true) catcherThrownBall = true + else + pitchMeter = 0 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 - pitch() + if playerOnOffense then + pitch(PitchFlyMs) + else + local pitchFly = readPitch() + if pitchFly then + pitch(pitchFly) + 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 updateRunner(batter, nil, crankChange) elseif offenseMode == Offense.running then - if playerIsOn(Sides.defense) then - updateRunning(999) - end - if updateRunning(crankChange) then + local appliedSpeed = playerIsOn(Sides.offense) and crankChange or npcRunningSpeed() + if updateRunning(appliedSpeed) then secondsSinceLastRunnerMove = 0 else secondsSinceLastRunnerMove = secondsSinceLastRunnerMove + deltaSeconds @@ -761,6 +826,22 @@ end -- TODO 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() playdate.timer.updateTimers() @@ -779,8 +860,11 @@ function playdate.update() GrassBackground:draw(-400, -240) local fielderDanceHeight = FielderDanceAnimator:currentValue() + local ballIsHeld = false 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 if offenseMode == Offense.batting then @@ -809,13 +893,15 @@ function playdate.update() PlayerFrown:draw(runner.x, runner.y) end - gfx.setLineWidth(2) + if not ballIsHeld then + gfx.setLineWidth(2) - gfx.setColor(gfx.kColorWhite) - gfx.fillCircleAtPoint(ball.x, ball.y, ball.size) + gfx.setColor(gfx.kColorWhite) + gfx.fillCircleAtPoint(ball.x, ball.y, ball.size) - gfx.setColor(gfx.kColorBlack) - gfx.drawCircleAtPoint(ball.x, ball.y, ball.size) + gfx.setColor(gfx.kColorBlack) + gfx.drawCircleAtPoint(ball.x, ball.y, ball.size) + end gfx.setDrawOffset(0, 0) if math.abs(offsetX) > 10 or math.abs(offsetY) > 10 then diff --git a/src/utils.lua b/src/utils.lua index d3a9211..0f72f17 100644 --- a/src/utils.lua +++ b/src/utils.lua @@ -61,13 +61,21 @@ function utils.filter(array, condition) return newArray end ----@return number, number, number +---@return number distance, number x, number y function utils.distanceBetween(x1, y1, x2, y2) local a = x1 - x2 local b = y1 - y2 return math.sqrt((a * a) + (b * b)), a, b 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 --- @return boolean function utils.pointDirectlyUnderLine(pointX, pointY, lineX1, lineY1, lineX2, lineY2, bottomBound)