Committing my "realer" physics changes as they stand
Which is to say, *very, very, unplayable*
This commit is contained in:
parent
db1409d94d
commit
3e256d95c9
209
src/ball.lua
209
src/ball.lua
|
@ -1,14 +1,18 @@
|
|||
---@class Ball
|
||||
---@field x number
|
||||
---@field y number
|
||||
---@field xVelocity number
|
||||
---@field yVelocity number
|
||||
---@field z number
|
||||
---@field flyBall boolean
|
||||
---@field size number
|
||||
---@field heldBy Fielder | nil
|
||||
---@field xAnimator SimpleAnimator
|
||||
---@field yAnimator SimpleAnimator
|
||||
---@field sizeAnimator SimpleAnimator
|
||||
---@field floatAnimator SimpleAnimator
|
||||
---@field zAnimator SimpleAnimator
|
||||
---@field private animatorLib pd_animator_lib
|
||||
---@field private bounce thread Requires deltaSeconds on resume, returns ball height.
|
||||
---@field private flyTimeMs number
|
||||
Ball = {}
|
||||
|
||||
|
@ -17,9 +21,10 @@ Ball = {}
|
|||
function Ball.new(animatorLib)
|
||||
return setmetatable({
|
||||
animatorLib = animatorLib,
|
||||
x = C.Center.x --[[@as number]],
|
||||
y = C.Center.y --[[@as number]],
|
||||
x = 400 --[[@as number]],
|
||||
y = 300 --[[@as number]],
|
||||
z = 0,
|
||||
flyBall = false,
|
||||
size = C.SmallestBallRadius,
|
||||
heldBy = nil --[[@type Runner | nil]],
|
||||
|
||||
|
@ -29,52 +34,184 @@ function Ball.new(animatorLib)
|
|||
-- TODO? Replace these with a ballAnimatorZ?
|
||||
-- ...that might lose some of the magic of both. Compromise available? idk
|
||||
sizeAnimator = utils.staticAnimator(C.SmallestBallRadius),
|
||||
floatAnimator = animatorLib.new(2000, -60, 0, utils.easingHill),
|
||||
zAnimator = animatorLib.new(2000, -60, 0, utils.easingHill),
|
||||
}, { __index = Ball })
|
||||
end
|
||||
|
||||
function Ball:updatePosition()
|
||||
if self.heldBy then
|
||||
self.x = self.heldBy.x
|
||||
self.y = self.heldBy.y
|
||||
self.z = C.GloveZ
|
||||
self.size = C.SmallestBallRadius
|
||||
else
|
||||
self.x = self.xAnimator:currentValue()
|
||||
local z = self.floatAnimator:currentValue()
|
||||
-- TODO: This `+ z` is more graphics logic than physics logic
|
||||
self.y = self.yAnimator:currentValue() + z
|
||||
self.z = z
|
||||
self.size = self.sizeAnimator:currentValue()
|
||||
---@param fielder Fielder
|
||||
function Ball:caughtBy(fielder)
|
||||
self.heldBy = fielder
|
||||
-- if self.flyBall then
|
||||
-- TODO: Signal an out if the ball hasn't touched the ground.
|
||||
-- end
|
||||
end
|
||||
|
||||
function timeToFirstBounce(v)
|
||||
local h0 = 0.1 -- m/s
|
||||
-- local v = 10 -- m/s, current velocity
|
||||
local g = 10 -- m/s/s
|
||||
local t = 0 -- starting time
|
||||
local rho = 0.75 -- coefficient of restitution
|
||||
local tau = 0.10 -- contact time for bounce
|
||||
local hmax = h0 -- keep track of the maximum height
|
||||
local h = h0
|
||||
local hstop = 0.01 -- stop when bounce is less than 1 cm
|
||||
local freefall = true -- state: freefall or in contact
|
||||
local vmax = math.sqrt(2 * hmax * g)
|
||||
local dt = 0.1
|
||||
|
||||
local ret = 0
|
||||
|
||||
while hmax > hstop do
|
||||
if freefall then
|
||||
local hnew = h + v * dt - 0.5 * g * dt * dt
|
||||
if hnew < 0 then
|
||||
return ret
|
||||
else
|
||||
t = t + dt
|
||||
v = v - g * dt
|
||||
h = hnew
|
||||
end
|
||||
else
|
||||
t = t + tau
|
||||
vmax = vmax * rho
|
||||
v = vmax
|
||||
freefall = true
|
||||
h = 0
|
||||
end
|
||||
hmax = 0.5 * vmax * vmax / g
|
||||
ret = ret + dt
|
||||
end
|
||||
return 0
|
||||
end
|
||||
|
||||
function bouncer(v, ball)
|
||||
printTable(ball)
|
||||
local startMs = playdate.getCurrentTimeMilliseconds()
|
||||
return function()
|
||||
local h0 = 0.1 -- m/s
|
||||
-- local v = 10 -- m/s, current velocity
|
||||
local g = 10 -- m/s/s
|
||||
local t = 0 -- starting time
|
||||
local rho = 0.60 -- coefficient of restitution
|
||||
local tau = 0.10 -- contact time for bounce
|
||||
local hmax = h0 -- keep track of the maximum height
|
||||
local h = h0
|
||||
local hstop = 0.01 -- stop when bounce is less than 1 cm
|
||||
local freefall = true -- state: freefall or in contact
|
||||
local t_last = -math.sqrt(2 * h0 / g) -- time we would have launched to get to h0 at t=0
|
||||
local vmax = math.sqrt(v * g)
|
||||
|
||||
while hmax > hstop do
|
||||
local dt = coroutine.yield(h, not freefall)
|
||||
if freefall then
|
||||
local hnew = h + v * dt - 0.5 * g * dt * dt
|
||||
if hnew < 0 then
|
||||
-- Bounced!
|
||||
t = t_last + 2 * math.sqrt(2 * hmax / g)
|
||||
freefall = false
|
||||
t_last = t + tau
|
||||
h = 0
|
||||
else
|
||||
t = t + dt
|
||||
v = v - g * dt
|
||||
h = hnew
|
||||
end
|
||||
else
|
||||
t = t + tau
|
||||
vmax = vmax * rho
|
||||
v = vmax
|
||||
freefall = true
|
||||
h = 0
|
||||
end
|
||||
hmax = 0.5 * vmax * vmax / g
|
||||
end
|
||||
return 0
|
||||
end
|
||||
end
|
||||
|
||||
---@param deltaSeconds number
|
||||
function Ball:updatePosition(deltaSeconds)
|
||||
-- printTable({ x = self.x, y = self.y, z = self.z })
|
||||
if self.heldBy then
|
||||
utils.moveAtSpeedZ(self, deltaSeconds * 10, { x = self.heldBy.x, y = self.heldBy.y, z = C.GloveZ })
|
||||
-- self.x = self.heldBy.x
|
||||
-- self.y = self.heldBy.y
|
||||
-- self.z = C.GloveZ
|
||||
-- self.size = C.SmallestBallRadius
|
||||
else
|
||||
-- self.x = self.x + self.xVelocity
|
||||
-- self.x = self.xAnimator:currentValue()
|
||||
-- self.y = self.yAnimator:currentValue()
|
||||
-- self.z = self.zAnimator:currentValue()
|
||||
if self.bounce then
|
||||
local alive, z, justBounced = coroutine.resume(self.bounce, deltaSeconds)
|
||||
if alive then
|
||||
local lostVelMult = justBounced and 0.8 or 0.99
|
||||
|
||||
self.xVelocity = self.xVelocity * (1 - (deltaSeconds * lostVelMult))
|
||||
self.yVelocity = self.yVelocity * (1 - (deltaSeconds * lostVelMult))
|
||||
|
||||
self.x = self.x + (self.xVelocity * deltaSeconds)
|
||||
self.y = self.y + (self.yVelocity * deltaSeconds)
|
||||
|
||||
self.z = z * 30
|
||||
else
|
||||
self.bounce = nil
|
||||
end
|
||||
end
|
||||
self.size = (math.max(0, 0.04 * (self.z - C.GloveZ))) + C.SmallestBallRadius
|
||||
-- print(self.size)
|
||||
end
|
||||
|
||||
if self.z < 1 then
|
||||
self.flyBall = false
|
||||
end
|
||||
end
|
||||
|
||||
---@alias DestinationAndFlightTime { destX: number, destY: number, flyTimeMs: number }
|
||||
|
||||
---@alias LaunchControls DestinationAndFlightTime
|
||||
|
||||
--- 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
|
||||
function Ball:launch(destX, destY, easingFunc, flyTimeMs, floaty, customBallScaler)
|
||||
---@param freshHit boolean | nil
|
||||
---@param flyTimeMs number | nil The angle away from parallel to the ground.
|
||||
--- 0 is straight forward, 90 is straight up, 180 is straight behind.
|
||||
function Ball:launch(destX, destY, _, freshHit, _, power)
|
||||
if freshHit then
|
||||
self.flyBall = true
|
||||
end
|
||||
throwMeter:reset()
|
||||
self.heldBy = nil
|
||||
|
||||
if not flyTimeMs then
|
||||
flyTimeMs = utils.distanceBetween(self.x, self.y, destX, destY) * C.DefaultLaunchPower
|
||||
end
|
||||
local flightDistance, x, y = utils.distanceBetween(self.x, self.y, destX, destY)
|
||||
local timeToBounce = timeToFirstBounce(10)
|
||||
|
||||
self.flyTimeMs = flyTimeMs
|
||||
-- if not flyTimeMs then
|
||||
-- flyTimeMs = flightDistance * C.DefaultLaunchPower
|
||||
-- end
|
||||
|
||||
if customBallScaler then
|
||||
self.sizeAnimator = customBallScaler
|
||||
else
|
||||
-- TODO? Scale based on distance?
|
||||
self.sizeAnimator = self.animatorLib.new(flyTimeMs, 9, C.SmallestBallRadius, utils.easingHill)
|
||||
end
|
||||
self.yAnimator = self.animatorLib.new(flyTimeMs, self.y, destY, easingFunc)
|
||||
self.xAnimator = self.animatorLib.new(flyTimeMs, self.x, destX, easingFunc)
|
||||
if floaty then
|
||||
self.floatAnimator:reset(flyTimeMs)
|
||||
end
|
||||
-- TODO? set a maxThrowDistance to limit throws by, instead
|
||||
power = power or 5
|
||||
self.xVelocity = -1.1 * x -- (x / -flightDistance) * power
|
||||
self.yVelocity = -1.1 * y -- (y / -flightDistance) * power
|
||||
printTable({ x = x, y = y })
|
||||
printTable({
|
||||
destX = destX,
|
||||
destY = destY,
|
||||
x = x,
|
||||
y = y,
|
||||
-- xVelocity = self.xVelocity,
|
||||
-- yVelocity = self.yVelocity,
|
||||
-- timeToBounce = timeToBounce,
|
||||
})
|
||||
-- -- TODO? Scale based on distance?
|
||||
-- self.yAnimator = self.animatorLib.new(flyTimeMs, self.y, destY, easingFunc)
|
||||
-- self.xAnimator = self.animatorLib.new(flyTimeMs, self.x, destX, easingFunc)
|
||||
-- self.zAnimator = self.animatorLib.new(flyTimeMs, flightDistance, C.GloveZ, utils.easingHill)
|
||||
|
||||
self.bounce = coroutine.create(bouncer(10))
|
||||
end
|
||||
|
|
|
@ -81,6 +81,7 @@ function Baserunning:outEligibleRunners(fielder)
|
|||
|
||||
for i, runner in pairs(self.runners) do
|
||||
local runnerOnBase = utils.isTouchingBase(runner.x, runner.y)
|
||||
-- TODO: Tag-outs when two baserunners are on the same base.
|
||||
if -- Force out
|
||||
touchedBase
|
||||
and runner.prevBase -- Make sure the runner is not standing at home
|
||||
|
|
|
@ -67,7 +67,7 @@ C.ScoreboardDelayMs = 2000
|
|||
--- generally as a check for whether or not it's in play.
|
||||
C.BallOffscreen = 999
|
||||
|
||||
C.PitchAfterSeconds = 6
|
||||
C.PitchAfterSeconds = 8
|
||||
C.ReturnToPitcherAfterSeconds = 2.4
|
||||
C.PitchFlyMs = 1050
|
||||
C.PitchStartX = 195
|
||||
|
@ -78,14 +78,14 @@ C.DefaultLaunchPower = 4
|
|||
--- The max distance at which a fielder can tag out a runner.
|
||||
C.TagDistance = 15
|
||||
|
||||
C.BallCatchHitbox = 3
|
||||
C.BallCatchHitbox = 15
|
||||
|
||||
--- The max distance at which a runner can be considered on base.
|
||||
C.BaseHitbox = 10
|
||||
|
||||
C.BattingPower = 25
|
||||
C.BatterHandPos = utils.xy(25, 15)
|
||||
C.GloveZ = 0 -- 10
|
||||
C.GloveZ = 10
|
||||
|
||||
C.SmallestBallRadius = 6
|
||||
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
-- selene: allow(shadowing)
|
||||
local gfx = playdate.graphics
|
||||
|
||||
local ScoreFont <const> = playdate.graphics.font.new("fonts/font-full-circle.pft")
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
-- selene: allow(shadowing)
|
||||
local gfx = playdate.graphics
|
||||
|
||||
---@alias SpriteCollection { smiling: pd_image, lowHat: pd_image, frowning: pd_image, back: pd_image }
|
||||
|
@ -52,11 +51,9 @@ function buildCollection(base, back, logo, isDark)
|
|||
}
|
||||
end
|
||||
|
||||
--selene: allow(unscoped_variables)
|
||||
---@type SpriteCollection
|
||||
AwayTeamSprites = nil
|
||||
|
||||
--selene: allow(unscoped_variables)
|
||||
---@type SpriteCollection
|
||||
HomeTeamSprites = nil
|
||||
|
||||
|
|
|
@ -1,15 +1,22 @@
|
|||
--- @class Fielder {
|
||||
--- @class Glove
|
||||
--- @field z number
|
||||
|
||||
--- @class Fielder
|
||||
--- @field glove Glove
|
||||
--- @field catchEligible boolean
|
||||
--- @field name string
|
||||
--- @field x number
|
||||
--- @field y number
|
||||
--- @field target XyPair | nil
|
||||
--- @field speed number
|
||||
--- @field armStrength number
|
||||
|
||||
-- TODO: Run down baserunners in a pickle.
|
||||
|
||||
---@class Fielding
|
||||
---@field fielders table<string, Fielder>
|
||||
---@field fielderHoldingBall Fielder | nil
|
||||
---@field private onFlyOut fun()
|
||||
Fielding = {}
|
||||
|
||||
FielderDanceAnimator = playdate.graphics.animator.new(1, 10, 0, utils.easingHill)
|
||||
|
@ -23,10 +30,14 @@ local function newFielder(name, speed)
|
|||
name = name,
|
||||
speed = speed * C.FielderRunMult,
|
||||
catchEligible = true,
|
||||
armStrength = 10,
|
||||
glove = {
|
||||
z = C.GloveZ
|
||||
},
|
||||
}
|
||||
end
|
||||
|
||||
function Fielding.new()
|
||||
function Fielding.new(onFlyOut)
|
||||
return setmetatable({
|
||||
fielders = {
|
||||
first = newFielder("First", 40),
|
||||
|
@ -39,6 +50,7 @@ function Fielding.new()
|
|||
center = newFielder("Center", 50),
|
||||
right = newFielder("Right", 50),
|
||||
},
|
||||
onFlyOut = onFlyOut,
|
||||
---@type Fielder | nil
|
||||
fielderHoldingBall = nil,
|
||||
}, { __index = Fielding })
|
||||
|
@ -78,26 +90,52 @@ end
|
|||
|
||||
---@param deltaSeconds number
|
||||
---@param fielder Fielder
|
||||
---@param ballPos XyPair
|
||||
---@param ballPos Point3d
|
||||
---@return boolean inCatchingRange
|
||||
local function updateFielderPosition(deltaSeconds, fielder, ballPos)
|
||||
if fielder.target ~= nil then
|
||||
if not utils.moveAtSpeed(fielder, fielder.speed * deltaSeconds, fielder.target) then
|
||||
fielder.target = nil
|
||||
-- if fielder.name == "Left" then
|
||||
-- printTable({ target = fielder.target })
|
||||
-- end
|
||||
if fielder.target.z then
|
||||
if not utils.moveAtSpeedZ(fielder, fielder.speed * deltaSeconds, fielder.target) then
|
||||
if fielder.name == "Left" then
|
||||
print("CLEAR LEFT'S 3D TARGET")
|
||||
end
|
||||
fielder.target = nil
|
||||
end
|
||||
else
|
||||
if not utils.moveAtSpeed(fielder, fielder.speed * deltaSeconds, fielder.target) then
|
||||
if fielder.name == "Left" then
|
||||
print("CLEAR LEFT'S 2D TARGET")
|
||||
end
|
||||
fielder.target = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return utils.distanceBetweenPoints(fielder, ballPos) < C.BallCatchHitbox
|
||||
--local distance = utils.distanceBetweenZ(fielder.x, fielder.y, C.GloveZ, ballPos.x, ballPos.y, ballPos.z)
|
||||
if ballPos.z > C.GloveZ * 2 then
|
||||
return false
|
||||
end
|
||||
local distance = utils.distanceBetween(fielder.x, fielder.y, ballPos.x, ballPos.y)
|
||||
return distance < C.BallCatchHitbox
|
||||
end
|
||||
|
||||
--- Selects the nearest fielder to move toward the given coordinates.
|
||||
--- Other fielders should attempt to cover their bases
|
||||
---@param self table
|
||||
---@param ballDestX number
|
||||
---@param ballDestY number
|
||||
function Fielding:haveSomeoneChase(ballDestX, ballDestY)
|
||||
---@param ball Ball
|
||||
function Fielding:haveSomeoneChase(ballDestX, ballDestY, ball)
|
||||
local chasingFielder = utils.getNearestOf(self.fielders, ballDestX, ballDestY)
|
||||
chasingFielder.target = { x = ballDestX, y = ballDestY }
|
||||
chasingFielder.target = ball
|
||||
-- local timer = playdate.timer.new(1000)
|
||||
-- timer.updateCallback = function()
|
||||
-- printTable(chasingFielder.target)
|
||||
-- end
|
||||
print("chasingFielder: " .. chasingFielder.name)
|
||||
printTable(ball)
|
||||
|
||||
for _, base in ipairs(C.Bases) do
|
||||
local nearest = utils.getNearestOf(self.fielders, base.x, base.y, function(fielder)
|
||||
|
@ -112,13 +150,24 @@ end
|
|||
---@param deltaSeconds number
|
||||
---@return Fielder | nil fielderHoldingBall nil if no fielder is currently touching the ball
|
||||
function Fielding:updateFielderPositions(ball, deltaSeconds)
|
||||
local fielderHoldingBall = nil
|
||||
local fielderHoldingBall
|
||||
for _, fielder in pairs(self.fielders) do
|
||||
local inCatchingRange = updateFielderPosition(deltaSeconds, fielder, ball)
|
||||
-- if inCatchingRange then
|
||||
-- printTable({
|
||||
-- inCatchingRange = inCatchingRange,
|
||||
-- catchEligible = fielder.catchEligible,
|
||||
-- fielderName = fielder.name,
|
||||
-- })
|
||||
-- end
|
||||
if inCatchingRange and fielder.catchEligible then
|
||||
-- TODO: Base this catch on fielder skill?
|
||||
fielderHoldingBall = fielder
|
||||
ball.heldBy = fielder -- How much havoc will this wreak?
|
||||
if ball.flyBall then
|
||||
self.onFlyOut()
|
||||
ball.flyBall = false
|
||||
end
|
||||
ball:caughtBy(fielder)
|
||||
end
|
||||
end
|
||||
-- TODO: The need is growing for a distinction between touching the ball and holding the ball.
|
||||
|
@ -153,21 +202,17 @@ end
|
|||
---@param ball { launch: LaunchBall }
|
||||
---@param throwFlyMs number
|
||||
local function userThrowToCoroutine(field, targetBase, ball, throwFlyMs)
|
||||
while true do
|
||||
if field.fielderHoldingBall == nil then
|
||||
coroutine.yield()
|
||||
else
|
||||
local closestFielder = utils.getNearestOf(field.fielders, targetBase.x, targetBase.y, function(fielder)
|
||||
return fielder ~= field.fielderHoldingBall -- presumably, this is who will be doing the throwing
|
||||
end)
|
||||
|
||||
closestFielder.target = targetBase
|
||||
ball:launch(targetBase.x, targetBase.y, playdate.easingFunctions.linear, throwFlyMs)
|
||||
Fielding.markIneligible(field.fielderHoldingBall)
|
||||
|
||||
return
|
||||
end
|
||||
while field.fielderHoldingBall == nil do
|
||||
coroutine.yield()
|
||||
end
|
||||
|
||||
local closestFielder = utils.getNearestOf(field.fielders, targetBase.x, targetBase.y, function(fielder)
|
||||
return fielder ~= field.fielderHoldingBall -- presumably, this is who will be doing the throwing
|
||||
end)
|
||||
|
||||
closestFielder.target = targetBase
|
||||
ball:launch(targetBase.x, targetBase.y, playdate.easingFunctions.linear, false, throwFlyMs)
|
||||
Fielding.markIneligible(field.fielderHoldingBall)
|
||||
end
|
||||
|
||||
--- Buffer in a fielder throw action.
|
||||
|
@ -182,7 +227,7 @@ function Fielding:userThrowTo(targetBase, ball, throwFlyMs)
|
|||
end)
|
||||
end
|
||||
|
||||
function Fielding:celebrate()
|
||||
function Fielding.celebrate()
|
||||
FielderDanceAnimator:reset(C.DanceBounceMs)
|
||||
end
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ function getDrawOffset(ballX, ballY)
|
|||
if ballY > C.Screen.H or ballX >= C.BallOffscreen then
|
||||
return 0, 0
|
||||
end
|
||||
offsetY = math.max(0, -1 * ballY)
|
||||
offsetY = math.max(0, -1.4 * ballY)
|
||||
|
||||
if ballX > 0 and ballX < C.Screen.W then
|
||||
offsetX = 0
|
||||
|
@ -17,7 +17,7 @@ function getDrawOffset(ballX, ballY)
|
|||
offsetX = math.min(C.Screen.W * 2, (ballX * -1) + C.Screen.W)
|
||||
end
|
||||
|
||||
return offsetX * 1.3, offsetY * 1.5
|
||||
return offsetX * 1.7, offsetY
|
||||
end
|
||||
|
||||
---@class Blipper
|
||||
|
|
|
@ -85,6 +85,7 @@ function MainMenu.update()
|
|||
if playdate.buttonJustPressed(playdate.kButtonA) then
|
||||
startGame()
|
||||
end
|
||||
startGame()
|
||||
if playdate.buttonJustPressed(playdate.kButtonUp) then
|
||||
inningCountSelection = inningCountSelection + 1
|
||||
end
|
||||
|
|
112
src/main.lua
112
src/main.lua
|
@ -15,9 +15,8 @@ import 'CoreLibs/ui.lua'
|
|||
--- destX: number,
|
||||
--- destY: number,
|
||||
--- easingFunc: EasingFunc,
|
||||
--- freshHit: boolean | nil,
|
||||
--- flyTimeMs: number | nil,
|
||||
--- floaty: boolean | nil,
|
||||
--- customBallScaler: pd_animator | nil,
|
||||
--- )
|
||||
|
||||
-- stylua: ignore start
|
||||
|
@ -107,8 +106,7 @@ function Game.new(settings, announcer, fielding, baserunning, npc, state)
|
|||
teams.away.score = 0
|
||||
teams.home.score = 0
|
||||
announcer = announcer or Announcer.new()
|
||||
fielding = fielding or Fielding.new()
|
||||
settings.userTeam = nil -- "away"
|
||||
settings.userTeam = "home" -- "away"
|
||||
|
||||
local homeTeamBlipper = blipper.new(100, settings.homeTeamSprites.smiling, settings.homeTeamSprites.lowHat)
|
||||
local awayTeamBlipper = blipper.new(100, settings.awayTeamSprites.smiling, settings.awayTeamSprites.lowHat)
|
||||
|
@ -119,7 +117,6 @@ function Game.new(settings, announcer, fielding, baserunning, npc, state)
|
|||
local o = setmetatable({
|
||||
settings = settings,
|
||||
announcer = announcer,
|
||||
fielding = fielding,
|
||||
homeTeamBlipper = homeTeamBlipper,
|
||||
awayTeamBlipper = awayTeamBlipper,
|
||||
state = state or {
|
||||
|
@ -131,7 +128,7 @@ function Game.new(settings, announcer, fielding, baserunning, npc, state)
|
|||
battingTeam = battingTeam,
|
||||
offenseState = C.Offense.batting,
|
||||
inning = 1,
|
||||
catcherThrownBall = false,
|
||||
catcherThrownBall = true,
|
||||
secondsSinceLastRunnerMove = 0,
|
||||
secondsSincePitchAllowed = 0,
|
||||
battingTeamSprites = settings.awayTeamSprites,
|
||||
|
@ -140,14 +137,18 @@ function Game.new(settings, announcer, fielding, baserunning, npc, state)
|
|||
},
|
||||
}, { __index = Game })
|
||||
|
||||
o.fielding = fielding or Fielding.new(function()
|
||||
print("Fly out!")
|
||||
end)
|
||||
o.baserunning = baserunning or Baserunning.new(announcer, function()
|
||||
o:nextHalfInning()
|
||||
end)
|
||||
o.npc = npc or Npc.new(o.baserunning.runners, o.fielding.fielders)
|
||||
|
||||
o.fielding:resetFielderPositions(teams.home.benchPosition)
|
||||
playdate.timer.new(2000, function()
|
||||
ball:launch(C.PitchStartX, C.PitchStartY, playdate.easingFunctions.linear, nil, false)
|
||||
playdate.timer.new(3500, function()
|
||||
print("Start pitcher with ball")
|
||||
ball:launch(C.PitcherStartPos.x, C.PitcherStartPos.y, playdate.easingFunctions.linear, false, nil, 6)
|
||||
end)
|
||||
|
||||
BootTune:play()
|
||||
|
@ -238,28 +239,29 @@ end
|
|||
---@param pitchTypeIndex number | nil
|
||||
function Game:pitch(pitchFlyTimeMs, pitchTypeIndex)
|
||||
Fielding.markIneligible(self.fielding.fielders.pitcher)
|
||||
self.state.ball:launch(C.PitchStartX, C.PitchEndY, nil, false, nil, 2000 / pitchFlyTimeMs)
|
||||
self.state.ball.heldBy = nil
|
||||
self.state.catcherThrownBall = false
|
||||
self.state.offenseState = C.Offense.batting
|
||||
|
||||
local current = Pitches[pitchTypeIndex](self.state.ball)
|
||||
self.state.ball.xAnimator = current.x
|
||||
self.state.ball.yAnimator = current.y or Pitches[1](self.state.ball).y
|
||||
-- local current = Pitches[pitchTypeIndex](self.state.ball)
|
||||
-- self.state.ball.xAnimator = current.x
|
||||
-- self.state.ball.yAnimator = current.y or Pitches[1](self.state.ball).y
|
||||
|
||||
-- TODO: This would need to be sanely replaced in ball:launch() etc.
|
||||
-- if current.z then
|
||||
-- ball.floatAnimator = current.z
|
||||
-- ball.floatAnimator:reset()
|
||||
-- -- TODO: This would need to be sanely replaced in ball:launch() etc.
|
||||
-- -- if current.z then
|
||||
-- -- ball.zAnimator = current.z
|
||||
-- -- ball.zAnimator:reset()
|
||||
-- -- end
|
||||
|
||||
-- if pitchFlyTimeMs then
|
||||
-- self.state.ball.xAnimator:reset(pitchFlyTimeMs)
|
||||
-- self.state.ball.yAnimator:reset(pitchFlyTimeMs)
|
||||
-- else
|
||||
-- self.state.ball.xAnimator:reset()
|
||||
-- self.state.ball.yAnimator:reset()
|
||||
-- end
|
||||
|
||||
if pitchFlyTimeMs then
|
||||
self.state.ball.xAnimator:reset(pitchFlyTimeMs)
|
||||
self.state.ball.yAnimator:reset(pitchFlyTimeMs)
|
||||
else
|
||||
self.state.ball.xAnimator:reset()
|
||||
self.state.ball.yAnimator:reset()
|
||||
end
|
||||
|
||||
self.state.secondsSincePitchAllowed = 0
|
||||
end
|
||||
|
||||
|
@ -267,7 +269,7 @@ function Game:nextHalfInning()
|
|||
pitchTracker:reset()
|
||||
local gameOver = self.state.inning == self.settings.finalInning and teams.away.score ~= teams.home.score
|
||||
if not gameOver then
|
||||
self.fielding:celebrate()
|
||||
Fielding.celebrate()
|
||||
self.state.secondsSinceLastRunnerMove = -7
|
||||
self.fielding:benchTo(self:getFieldingTeam().benchPosition)
|
||||
self.announcer:say("SWITCHING SIDES...")
|
||||
|
@ -379,20 +381,20 @@ function Game:updateBatting(batDeg, batSpeed)
|
|||
-- Hit!
|
||||
BatCrackReverb:play()
|
||||
self.state.offenseState = C.Offense.running
|
||||
local ballAngle = batAngle + math.rad(90)
|
||||
-- local ballAngle = batAngle + math.rad(90)
|
||||
|
||||
local mult = math.abs(batSpeed / 15)
|
||||
local ballVelX = mult * 10 * math.sin(ballAngle)
|
||||
local ballVelY = mult * 5 * math.cos(ballAngle)
|
||||
if ballVelY > 0 then
|
||||
ballVelX = ballVelX * -1
|
||||
ballVelY = ballVelY * -1
|
||||
end
|
||||
local ballDestX = self.state.ball.x + (ballVelX * C.BattingPower)
|
||||
local ballDestY = self.state.ball.y + (ballVelY * C.BattingPower)
|
||||
-- local mult = math.abs(batSpeed / 15)
|
||||
-- local ballVelX = mult * 10 * math.sin(ballAngle)
|
||||
-- local ballVelY = mult * 5 * math.cos(ballAngle)
|
||||
-- if ballVelY > 0 then
|
||||
-- ballVelX = ballVelX * -1
|
||||
-- ballVelY = ballVelY * -1
|
||||
-- end
|
||||
local ballDestX = self.fielding.fielders.left.x -- self.state.ball.x + (ballVelX * C.BattingPower)
|
||||
local ballDestY = self.fielding.fielders.left.y -- self.state.ball.y + (ballVelY * C.BattingPower)
|
||||
pitchTracker:reset()
|
||||
local hitBallScaler = gfx.animator.new(2000, 9 + (mult * mult * 0.5), C.SmallestBallRadius, utils.easingHill)
|
||||
self.state.ball:launch(ballDestX, ballDestY, playdate.easingFunctions.outQuint, 2000, nil, hitBallScaler)
|
||||
print("Hit ball!")
|
||||
self.state.ball:launch(ballDestX, ballDestY, playdate.easingFunctions.outQuint, true, 3000, batSpeed / 3)
|
||||
-- TODO? A dramatic eye-level view on a home-run could be sick.
|
||||
|
||||
if utils.isFoulBall(ballDestX, ballDestY) then
|
||||
|
@ -404,7 +406,7 @@ function Game:updateBatting(batDeg, batSpeed)
|
|||
|
||||
self.baserunning:convertBatterToRunner()
|
||||
|
||||
self.fielding:haveSomeoneChase(ballDestX, ballDestY)
|
||||
self.fielding:haveSomeoneChase(ballDestX, ballDestY, self.state.ball)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -443,7 +445,7 @@ function Game:updateGameState()
|
|||
crankLimited = crankLimited * -1
|
||||
end
|
||||
|
||||
self.state.ball:updatePosition()
|
||||
self.state.ball:updatePosition(self.state.deltaSeconds)
|
||||
|
||||
local userOnOffense, userOnDefense = self:userIsOn("offense")
|
||||
|
||||
|
@ -471,7 +473,9 @@ function Game:updateGameState()
|
|||
self:walk()
|
||||
end
|
||||
-- Catcher has the ball. Throw it back to the pitcher
|
||||
self.state.ball:launch(C.PitchStartX, C.PitchStartY, playdate.easingFunctions.linear, nil, true)
|
||||
print("Catcher return ball to pitcher")
|
||||
self.state.ball:launch(C.PitcherStartPos.x, C.PitcherStartPos.y, playdate.easingFunctions.linear, false, 20)
|
||||
self.fielding:markAllIneligible()
|
||||
self.state.catcherThrownBall = true
|
||||
end
|
||||
|
||||
|
@ -512,8 +516,10 @@ function Game:updateGameState()
|
|||
self.state.secondsSinceLastRunnerMove = self.state.secondsSinceLastRunnerMove + self.state.deltaSeconds
|
||||
if self.state.secondsSinceLastRunnerMove > C.ResetFieldersAfterSeconds then
|
||||
-- End of play. Throw the ball back to the pitcher
|
||||
self.state.ball:launch(C.PitchStartX, C.PitchStartY, playdate.easingFunctions.linear, nil, true)
|
||||
self.fielding:markAllIneligible() -- This is ugly, and ideally would not be necessary if Fielding handled the return throw directly.
|
||||
print("Return ball to pitcher")
|
||||
self.state.ball:launch(C.PitcherStartPos.x, C.PitcherStartPos.y, playdate.easingFunctions.linear, false, 20)
|
||||
-- This is ugly. Maybe Fielding should handle the return throw directly.
|
||||
self.fielding:markAllIneligible()
|
||||
self.fielding:resetFielderPositions()
|
||||
self.state.offenseState = C.Offense.batting
|
||||
-- TODO: Remove, or replace with nextBatter()
|
||||
|
@ -558,7 +564,8 @@ function Game:update()
|
|||
gfx.clear()
|
||||
gfx.setColor(gfx.kColorBlack)
|
||||
|
||||
local offsetX, offsetY = getDrawOffset(ball.x, ball.y)
|
||||
local ballY = ball.y - (ball.z * 0.2)
|
||||
local offsetX, offsetY = getDrawOffset(ball.x, ballY)
|
||||
gfx.setDrawOffset(offsetX, offsetY)
|
||||
|
||||
GrassBackground:draw(-400, -240)
|
||||
|
@ -619,10 +626,10 @@ function Game:update()
|
|||
gfx.setLineWidth(2)
|
||||
|
||||
gfx.setColor(gfx.kColorWhite)
|
||||
gfx.fillCircleAtPoint(ball.x, ball.y, ball.size)
|
||||
gfx.fillCircleAtPoint(ball.x, ballY, ball.size)
|
||||
|
||||
gfx.setColor(gfx.kColorBlack)
|
||||
gfx.drawCircleAtPoint(ball.x, ball.y, ball.size)
|
||||
gfx.drawCircleAtPoint(ball.x, ballY, ball.size)
|
||||
end
|
||||
|
||||
gfx.setDrawOffset(0, 0)
|
||||
|
@ -634,6 +641,7 @@ function Game:update()
|
|||
self.announcer:draw(C.Center.x, 10)
|
||||
|
||||
if playdate.isCrankDocked() then
|
||||
-- luacheck: ignore
|
||||
playdate.ui.crankIndicator:draw()
|
||||
end
|
||||
end
|
||||
|
@ -644,3 +652,19 @@ playdate.setMenuImage(gfx.image.new("images/game/menu-image.png"))
|
|||
playdate.getSystemMenu():addMenuItem("Restart game", function() end) -- TODO?
|
||||
|
||||
MainMenu.start(Game)
|
||||
|
||||
-- local b = coroutine.create(bouncer(10))
|
||||
-- local x = 0
|
||||
--
|
||||
-- function playdate.update()
|
||||
-- -- print(playdate.getFPS())
|
||||
-- local deltaSeconds = playdate.getElapsedTime() or 0
|
||||
-- playdate.resetElapsedTime()
|
||||
-- local alive, z = coroutine.resume(b, deltaSeconds)
|
||||
-- if alive then
|
||||
-- z = z * 10
|
||||
-- x = x + (deltaSeconds * 40)
|
||||
-- gfx.setColor(gfx.kColorBlack)
|
||||
-- gfx.drawCircleAtPoint(x, 240 - z, 5)
|
||||
-- end
|
||||
-- end
|
||||
|
|
15
src/npc.lua
15
src/npc.lua
|
@ -5,7 +5,6 @@ local npcBatSpeed = 1500
|
|||
---@class Npc
|
||||
---@field runners Runner[]
|
||||
---@field fielders Fielder[]
|
||||
-- selene: allow(unscoped_variables)
|
||||
Npc = {}
|
||||
|
||||
---@param runners Runner[]
|
||||
|
@ -22,7 +21,7 @@ end
|
|||
---@param catcherThrownBall boolean
|
||||
---@param deltaSec number
|
||||
---@return number
|
||||
function Npc:updateBatAngle(ball, catcherThrownBall, deltaSec)
|
||||
function Npc:updateBatAngle(ball, catcherThrownBall, deltaSec) -- luacheck: no unused args
|
||||
if not catcherThrownBall and ball.y > 200 and ball.y < 230 and (ball.x < C.Center.x + 15) then
|
||||
npcBatDeg = npcBatDeg + (deltaSec * npcBatSpeed)
|
||||
else
|
||||
|
@ -32,7 +31,7 @@ function Npc:updateBatAngle(ball, catcherThrownBall, deltaSec)
|
|||
return npcBatDeg
|
||||
end
|
||||
|
||||
function Npc:batSpeed()
|
||||
function Npc:batSpeed() -- luacheck: no unused args
|
||||
return npcBatSpeed / 1.5
|
||||
end
|
||||
|
||||
|
@ -122,6 +121,7 @@ end
|
|||
---@param ball { x: number, y: number, heldBy: Fielder | nil, launch: LaunchBall }
|
||||
local function tryToMakeAPlay(fielders, fielder, runners, ball)
|
||||
local targetX, targetY = getNextOutTarget(runners)
|
||||
printTable({ targetX = targetX, targetY = targetY })
|
||||
if targetX ~= nil and targetY ~= nil then
|
||||
local nearestFielder = utils.getNearestOf(fielders, targetX, targetY, function(grabCandidate)
|
||||
return grabCandidate.catchEligible
|
||||
|
@ -130,8 +130,11 @@ local function tryToMakeAPlay(fielders, fielder, runners, ball)
|
|||
if nearestFielder == fielder then
|
||||
ball.heldBy = fielder
|
||||
else
|
||||
ball:launch(targetX, targetY, playdate.easingFunctions.linear, nil, true)
|
||||
Fielding.markIneligible(nearestFielder)
|
||||
playdate.timer.new(500, function()
|
||||
print("Try to make a play")
|
||||
ball:launch(targetX, targetY, playdate.easingFunctions.linear, false, nearestFielder.armStrength)
|
||||
Fielding.markIneligible(nearestFielder)
|
||||
end)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -151,7 +154,7 @@ function Npc:fielderAction(fielder, outedSomeRunner, ball)
|
|||
end
|
||||
|
||||
---@return number
|
||||
function Npc:pitchSpeed()
|
||||
function Npc:pitchSpeed() -- luacheck: no unused args
|
||||
return 2
|
||||
end
|
||||
|
||||
|
|
|
@ -24,6 +24,9 @@ function fakeBall(x, y, z)
|
|||
y = y,
|
||||
z = z or 0,
|
||||
heldBy = nil,
|
||||
caughtBy = function(self, fielder)
|
||||
self.heldBy = fielder
|
||||
end,
|
||||
}
|
||||
end
|
||||
|
||||
|
@ -40,6 +43,8 @@ function testBallPickedUpByNearbyFielders()
|
|||
local secondBaseman = fielding.fielders.second
|
||||
ball.x = secondBaseman.x
|
||||
ball.y = secondBaseman.y
|
||||
ball.z = C.GloveZ
|
||||
|
||||
fielding:updateFielderPositions(ball, 0.01)
|
||||
luaunit.assertIs(secondBaseman, ball.heldBy, "Ball should be held by the nearest fielder")
|
||||
end
|
||||
|
|
|
@ -64,6 +64,16 @@ function utils.normalizeVector(x1, y1, x2, y2)
|
|||
local distance, x, y = utils.distanceBetween(x1, y1, x2, y2)
|
||||
return x / distance, y / distance, distance
|
||||
end
|
||||
--- Returns the normalized vector as two values, plus the distance between the given points.
|
||||
---@param x1 number
|
||||
---@param y1 number
|
||||
---@param x2 number
|
||||
---@param y2 number
|
||||
---@return number x, number y, number z, number distance
|
||||
function utils.normalize3dVector(x1, y1, z1, x2, y2, z2)
|
||||
local distance, x, y, z = utils.distanceBetweenZ(x1, y1, z1, x2, y2, z2)
|
||||
return x / distance, y / distance, z / distance, distance
|
||||
end
|
||||
|
||||
--- Push the given obect at the given speed toward a target. Speed should be pre-multiplied by the frame's delta time.
|
||||
--- Stops when within 1. Returns true only if the object did actually move.
|
||||
|
@ -83,6 +93,25 @@ function utils.moveAtSpeed(mover, speed, target)
|
|||
return false
|
||||
end
|
||||
|
||||
--- Same as moveAtSpeed(), but takes into account any Z-distance.
|
||||
--- Does NOT shift the z of the given mover.
|
||||
---@param mover { x: number, y: number, z: number }
|
||||
---@param speed number
|
||||
---@param target { x: number, y: number, z: number }
|
||||
---@return boolean
|
||||
function utils.moveAtSpeedZ(mover, speed, target)
|
||||
local z = mover.z or C.GloveZ
|
||||
local x, y, _, distance = utils.normalize3dVector(mover.x, mover.y, z, target.x, target.y, target.z)
|
||||
|
||||
if distance > 1 then
|
||||
mover.x = mover.x - (x * speed)
|
||||
mover.y = mover.y - (y * speed)
|
||||
return true
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
---@generic T
|
||||
---@param array T[]
|
||||
---@param condition fun(T): boolean
|
||||
|
|
Loading…
Reference in New Issue