From 57625a9b807c41b357ba4ffa370e9963d98bdbaf Mon Sep 17 00:00:00 2001 From: Sage Vaillancourt Date: Wed, 5 Feb 2025 18:25:34 -0500 Subject: [PATCH] Player pitching, npc batting, and gloves Or at least a basic implementation thereof --- src/images/game/glove-holding-ball.png | Bin 0 -> 4568 bytes src/images/game/glove.png | Bin 0 -> 4324 bytes src/main.lua | 144 ++++++++++++++++++++----- src/utils.lua | 10 +- 4 files changed, 124 insertions(+), 30 deletions(-) create mode 100644 src/images/game/glove-holding-ball.png create mode 100644 src/images/game/glove.png diff --git a/src/images/game/glove-holding-ball.png b/src/images/game/glove-holding-ball.png new file mode 100644 index 0000000000000000000000000000000000000000..de2e43c5272141bfda93ec56b5f29bd20f15cdbf GIT binary patch literal 4568 zcmeHKe{2)i9Y2R330e8EXzjKZ)XQZJ&~iV0_Sv_X1Y(nz#%YM%EM26P^W8gh5c?e8 zIdRf;WYACw6N7}UqJxetYUxTfjkFaVbOmVx(MtV+)>2jmi;OmnE$gzW6;er-eediz ziB@TfHvY?5&+q%*`+h&~`~AG{``Y(ZQ)AuY(z{DB3|kzm_cueo6xy`tON$2>6ni>K(h24d3hPgMUHvcJm_~pD+AfmvD)2^1$O^EknhRM2SHwy z*RgC^M-jFd`oqvTkiP`|i_o+ub9=o3vh|eMpRv^#)&`@=zAhMQ!a0g%D5nSP8O|j# zoXEIv)*~{4$ar9X$I&T|ETq19+S}mYPOY}KDOg*JCsVqrwIdA69De+Oum0CH%g;R; zelT$Vl7(OFxX^TD`TXY9>+_* z)BD;N8uw7m*@^!3d%v=D->rXqeC_(jD{%gShLb(n-o#~9JhXr4mk+NVD|M9LQC_l1 zYKb@gFg&{A(VHcspHk;v9(#A(``N3npX=@QFORu?TX_wC_IIyrJACu0Kua^9HL{Ba zsQR%B=?|_8?*yJ+@a*bm4t-K}qJGVtr(6FPZQ;^cE&Iar_Z*}Z-zAUUap{5dr8ib? zm%gUIw8l8{r&ax}zbRAdn>N*b_0G$?FK*o*Ef3vVbnnFOzdrrubN$Ef*syOweWn zOuRi|h#4PYtGD&{sH(F$o)tEUjoQ#{3xfsEG45o=5Fs+6LN&HfE}2pIW@ zEvA_iX}YVci|TSxdMZY)TOgehgngi&EZyVMl2`0G}}OEC3&LMoQ8w#nACMJ>9}E1Dybpcj$91j7WD9(ap%v(;|W0S+5gk=NyNld8Zo0KvLR2|3-Qf*7{|;Jh4V zr$IcF(jY6P_UT?(PzpfF66aQUFH0hiTOoOuS0$sW*Fy@7>QdaSM-^nwhEim4gPw{@ zaGY9PiXl3gh}jDk;o`ccppW1vW@@IXT{2ZL@DUAKBAuC9h-h&XHYJNqR&aU*uiL|U z-Mrv(d#03HP|AQzv@q?`X|GtvB0?}gtYqaW0NC>oi&&dNlBuU6y58<13PrcD9D?hl zNT%eMOa!2XG8~yK!z2TRIp@9w7t$3??f!4t*5$#gKp&30UNd0-ZhIzxP2)^NzR2}NS#Qr{RFJslDtJq#1K@Ee7a7~YoC(}9Pjk1P9%_=D;gzv0WqYc zx_QzSW!z5WL6VorkyJo8bk*#VQfOTacm%E>J?$STe3gArt7rPswFOzH0ECeY6WbXNFis zTV-+bP^;+wIQE>vv^omZqpA5{qr&WP`CJt%7ahb92nbOzmYAf@f>()^PawNKe`Kbt`tsf><=#Q zxwhPY!>IV|N=37G-HJ@v?!?f*+aDDL{U^_a-!Hm2GOEAzqj*bG-N+}=QtiZEq~iTHyth8esbuEgL?7r_tNODi214IF%}Fo`hUKD^NxQ5(X$l3 literal 0 HcmV?d00001 diff --git a/src/images/game/glove.png b/src/images/game/glove.png new file mode 100644 index 0000000000000000000000000000000000000000..c0374e6efe30abb0e067a9b5c99068709ddbf92a GIT binary patch literal 4324 zcmeHKYj6|S6<+Z)=Iu0TLqb^=0(M}ZT1l%N3mYt9D_E{$E3OlMq*uFl#bQe6%8kK(g&zIe7udQ(RMtsnG9)Rs^zbW4A@F=}!MJ$T#KW{U8_TY-}Ii zHHDZ9{Q+nK$UlJo*U(FRKHd2FiA`S6v*DES&!xa@|Ds5sO8Z7b;TL%X~C?@A3S$G_Ds(NKv$WuL)R3ro zw0>~$-B$_*FEQ_&7&*?$a-W>P*e#JTRsn1R}9k}vfsG&|AFb8JsW2%Nu zw|;!y_d)2f=}(nB`NTg;k5{d}{#4^%Vhuv;fIe{anVa^rYT&G7w47bldiM2|_o3y+ zbF0mRUleyWzFnkN)!trt;z z+qrS1Jqwny+M(>8lZmaiExor1>U4rb)<29M@>}eXj1$tRko(EG$R8*0$Yg8B$|_^ zoC#14t_-rRW+~D!v6=!@BcxGSyoDB z+4lB!rrpgL={U>#{eG4cSV5q{f;P7#EtH{?<}w>1ixI@8lGam}ZX`(?6UB^HD?m{& zPmaWwNJS!f_@tR*0r+4uD8=#&$0ic&SP#<*wE;*jpzriBqgzs#t;43#npSYA4JWN- zVSHMC72MwA$f?g8|B3User8S7k?ZtC&|kJfgim%}a~CPopKQ3N)v= z-BOH~kQl>bpu$PhLP-VNPyo*8fFojrc+HP!gpfds9!{c>-wzPH*B|pKNc8yJV<3K# z)*&lV^Vq0tC>5X-BzRTP&(m1)ssQEJ=$Pi0XdkC}R4*@SK1Fb#R7GBIq!S2^Q%|5c zW>d+yGhq`hudNLSD1qTdHMPyi(!e1=RqM&t%;-c^PvCkB*=+JYx8(D?MXxCM+=3vD z8a3dw37KePI;GQ@v5!TD%>c2;&Qkzz^spDXB8`z{q@#w>9H8<=H$NSL>!czJ1(AgT zG+&0JBW0N8pfG2{x8Wj&s%u;Rm$iL)$WpL}Bd^j;SbvM7%HF7Y+>#w-ht0Zkl}OUL z6f#n>TQE@@R-HHjD?6oZLdiIW>XA#=5xxE$sUV1Mzvjk1TJXeTwCKYejWn-Fdt#i| zjU|lyT$ZFfx@l-uJ4)lVao`cSg7kELpvYq9pi0Ka(!L4XrvQY}93+@$gfKBj7@ONL zYuAjC9hb8I#Yw4SFs8_WUseaj3u+;oFNQhJAl~oulRJy=a|x21Tx3H0PSQ0=*Mu0D zknv=8P0}?X1}0=YnO*-kx(Y{Mr*IPf1+~M=QjgTM2VS&h#5Pn0iEH+A=(T6}fo4{! z>Q0j&m}PczWsa1!fpVr5j)Z1@J!Se0v!_*`*m@LH+rq)M(c&`)yVe$Uas^Yb`}W$G zmkvq08aA5WluvPO?%TTD)wR6$#`cwGi4tkwz=e_JxRaVQ=?>e_gzP?#wflMJvnRk|7!*9I6REzwT$-{sUIi&vyU- literal 0 HcmV?d00001 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)