Add perfect-power indicator to throwMeter

Some other tightening-up in there.
E.g. clears the lastReadThrow when on a new fielder.
Add type annotations to assets files.
This commit is contained in:
Sage Vaillancourt 2025-02-20 13:56:57 -05:00
parent 35c7754207
commit 56c0c27d75
8 changed files with 105 additions and 34 deletions

View File

@ -2,66 +2,109 @@
-- Instead, edit the source file directly: assets.lua2p. -- Instead, edit the source file directly: assets.lua2p.
-- luacheck: ignore -- luacheck: ignore
---@type pd_image
DarkPlayerBack = playdate.graphics.image.new("images/game/DarkPlayerBack.png") DarkPlayerBack = playdate.graphics.image.new("images/game/DarkPlayerBack.png")
-- luacheck: ignore -- luacheck: ignore
---@type pd_image
Glove = playdate.graphics.image.new("images/game/Glove.png") Glove = playdate.graphics.image.new("images/game/Glove.png")
-- luacheck: ignore -- luacheck: ignore
---@type pd_image
PerfectPowerFlickerRight = playdate.graphics.image.new("images/game/PerfectPowerFlickerRight.png")
-- luacheck: ignore
---@type pd_image
DarkSkinFan = playdate.graphics.image.new("images/game/DarkSkinFan.png")
-- luacheck: ignore
---@type pd_image
PlayerFrown = playdate.graphics.image.new("images/game/PlayerFrown.png") PlayerFrown = playdate.graphics.image.new("images/game/PlayerFrown.png")
-- luacheck: ignore -- luacheck: ignore
---@type pd_image
PerfectPowerFlickerLeft = playdate.graphics.image.new("images/game/PerfectPowerFlickerLeft.png")
-- luacheck: ignore
---@type pd_image
BigBat = playdate.graphics.image.new("images/game/BigBat.png") BigBat = playdate.graphics.image.new("images/game/BigBat.png")
-- luacheck: ignore -- luacheck: ignore
---@type pd_image
LightSkinFan = playdate.graphics.image.new("images/game/LightSkinFan.png")
-- luacheck: ignore
---@type pd_image
GloveHoldingBall = playdate.graphics.image.new("images/game/GloveHoldingBall.png") GloveHoldingBall = playdate.graphics.image.new("images/game/GloveHoldingBall.png")
-- luacheck: ignore -- luacheck: ignore
---@type pd_image
GameLogo = playdate.graphics.image.new("images/game/GameLogo.png") GameLogo = playdate.graphics.image.new("images/game/GameLogo.png")
-- luacheck: ignore -- luacheck: ignore
---@type pd_image
Hat = playdate.graphics.image.new("images/game/Hat.png") Hat = playdate.graphics.image.new("images/game/Hat.png")
-- luacheck: ignore -- luacheck: ignore
---@type pd_image
DarkPlayerBase = playdate.graphics.image.new("images/game/DarkPlayerBase.png") DarkPlayerBase = playdate.graphics.image.new("images/game/DarkPlayerBase.png")
-- luacheck: ignore -- luacheck: ignore
---@type pd_image
MenuImage = playdate.graphics.image.new("images/game/MenuImage.png") MenuImage = playdate.graphics.image.new("images/game/MenuImage.png")
-- luacheck: ignore -- luacheck: ignore
---@type pd_image
PlayerSmile = playdate.graphics.image.new("images/game/PlayerSmile.png") PlayerSmile = playdate.graphics.image.new("images/game/PlayerSmile.png")
-- luacheck: ignore -- luacheck: ignore
---@type pd_image
Minimap = playdate.graphics.image.new("images/game/Minimap.png") Minimap = playdate.graphics.image.new("images/game/Minimap.png")
-- luacheck: ignore -- luacheck: ignore
---@type pd_image
GrassBackground = playdate.graphics.image.new("images/game/GrassBackground.png") GrassBackground = playdate.graphics.image.new("images/game/GrassBackground.png")
-- luacheck: ignore -- luacheck: ignore
---@type pd_image
GrassBackgroundSmall = playdate.graphics.image.new("images/game/GrassBackgroundSmall.png") GrassBackgroundSmall = playdate.graphics.image.new("images/game/GrassBackgroundSmall.png")
-- luacheck: ignore -- luacheck: ignore
---@type pd_image
LightPlayerBase = playdate.graphics.image.new("images/game/LightPlayerBase.png") LightPlayerBase = playdate.graphics.image.new("images/game/LightPlayerBase.png")
-- luacheck: ignore -- luacheck: ignore
---@type pd_image
PerfectPowerBg = playdate.graphics.image.new("images/game/PerfectPowerBg.png")
-- luacheck: ignore
---@type pd_image
LightPlayerBack = playdate.graphics.image.new("images/game/LightPlayerBack.png") LightPlayerBack = playdate.graphics.image.new("images/game/LightPlayerBack.png")
-- luacheck: ignore -- luacheck: ignore
---@type pd_sampleplayer
BatCrackReverb = playdate.sound.sampleplayer.new("sounds/BatCrackReverb.wav") BatCrackReverb = playdate.sound.sampleplayer.new("sounds/BatCrackReverb.wav")
-- luacheck: ignore -- luacheck: ignore
---@type pd_sampleplayer
BootTune = playdate.sound.sampleplayer.new("music/BootTune.wav") BootTune = playdate.sound.sampleplayer.new("music/BootTune.wav")
-- luacheck: ignore -- luacheck: ignore
---@type pd_sampleplayer
BootTuneOrgany = playdate.sound.sampleplayer.new("music/BootTuneOrgany.wav") BootTuneOrgany = playdate.sound.sampleplayer.new("music/BootTuneOrgany.wav")
-- luacheck: ignore -- luacheck: ignore
---@type pd_sampleplayer
TinnyBackground = playdate.sound.sampleplayer.new("music/TinnyBackground.wav") TinnyBackground = playdate.sound.sampleplayer.new("music/TinnyBackground.wav")
-- luacheck: ignore -- luacheck: ignore
---@type pd_sampleplayer
MenuMusic = playdate.sound.sampleplayer.new("music/MenuMusic.wav") MenuMusic = playdate.sound.sampleplayer.new("music/MenuMusic.wav")
Logos = { Logos = {
{ name = "Base", image = playdate.graphics.image.new("images/game/logos/Base.png") }, { name = "Base", image = playdate.graphics.image.new("images/game/logos/Base.png") },
-- luacheck: ignore -- luacheck: ignore
---@type pd_image
{ name = "Cats", image = playdate.graphics.image.new("images/game/logos/Cats.png") }, { name = "Cats", image = playdate.graphics.image.new("images/game/logos/Cats.png") },
-- luacheck: ignore -- luacheck: ignore
---@type pd_image
{ name = "Hearts", image = playdate.graphics.image.new("images/game/logos/Hearts.png") }, { name = "Hearts", image = playdate.graphics.image.new("images/game/logos/Hearts.png") },
-- luacheck: ignore -- luacheck: ignore
---@type pd_image
{ name = "Checkmarks", image = playdate.graphics.image.new("images/game/logos/Checkmarks.png") }, { name = "Checkmarks", image = playdate.graphics.image.new("images/game/logos/Checkmarks.png") },
-- luacheck: ignore -- luacheck: ignore
---@type pd_image
{ name = "Smiles", image = playdate.graphics.image.new("images/game/logos/Smiles.png") }, { name = "Smiles", image = playdate.graphics.image.new("images/game/logos/Smiles.png") },
-- luacheck: ignore -- luacheck: ignore
---@type pd_image
{ name = "FingerGuns", image = playdate.graphics.image.new("images/game/logos/FingerGuns.png") }, { name = "FingerGuns", image = playdate.graphics.image.new("images/game/logos/FingerGuns.png") },
-- luacheck: ignore -- luacheck: ignore
---@type pd_image
{ name = "Frown", image = playdate.graphics.image.new("images/game/logos/Frown.png") }, { name = "Frown", image = playdate.graphics.image.new("images/game/logos/Frown.png") },
-- luacheck: ignore -- luacheck: ignore
---@type pd_image
{ name = "Arrows", image = playdate.graphics.image.new("images/game/logos/Arrows.png") }, { name = "Arrows", image = playdate.graphics.image.new("images/game/logos/Arrows.png") },
-- luacheck: ignore -- luacheck: ignore
---@type pd_image
{ name = "Turds", image = playdate.graphics.image.new("images/game/logos/Turds.png") }, { name = "Turds", image = playdate.graphics.image.new("images/game/logos/Turds.png") },
} }

View File

@ -1,4 +1,4 @@
!(function dirLookup(dir, extension, newFunc, sep, handle) !(function dirLookup(dir, extension, newFunc, type, sep, handle)
sep = sep or "\n" sep = sep or "\n"
handle = handle ~= nil and handle or function(varName, value) handle = handle ~= nil and handle or function(varName, value)
return varName .. ' = ' .. value return varName .. ' = ' .. value
@ -13,6 +13,7 @@
local varName = file:gsub(".*/(.*)." .. extension, "%1") local varName = file:gsub(".*/(.*)." .. extension, "%1")
file = file:gsub("src/", "") file = file:gsub("src/", "")
assetCode = assetCode .. '-- luacheck: ignore\n' assetCode = assetCode .. '-- luacheck: ignore\n'
assetCode = assetCode .. '---@type ' .. type ..'\n'
assetCode = assetCode .. handle(varName, newFunc .. '("' .. file .. '")') .. sep assetCode = assetCode .. handle(varName, newFunc .. '("' .. file .. '")') .. sep
end end
end end
@ -23,13 +24,13 @@ function generatedFileWarning()
return "-- GENERATED FILE - DO NOT EDIT\n-- Instead, edit the source file directly: assets.lua2p." return "-- GENERATED FILE - DO NOT EDIT\n-- Instead, edit the source file directly: assets.lua2p."
end)!!(generatedFileWarning()) end)!!(generatedFileWarning())
!!(dirLookup('images/game', 'png', 'playdate.graphics.image.new')) !!(dirLookup('images/game', 'png', 'playdate.graphics.image.new', 'pd_image'))
!!(dirLookup('sounds', 'wav', 'playdate.sound.sampleplayer.new')) !!(dirLookup('sounds', 'wav', 'playdate.sound.sampleplayer.new', 'pd_sampleplayer'))
!!(dirLookup('music', 'wav', 'playdate.sound.sampleplayer.new')) !!(dirLookup('music', 'wav', 'playdate.sound.sampleplayer.new', 'pd_sampleplayer'))
Logos = { Logos = {
{ name = "Base", image = playdate.graphics.image.new("images/game/logos/Base.png") }, { name = "Base", image = playdate.graphics.image.new("images/game/logos/Base.png") },
!!(dirLookup('images/game/logos -not -name "Base.png"', 'png', 'playdate.graphics.image.new', ",\n", function(varName, value) !!(dirLookup('images/game/logos -not -name "Base.png"', 'png', 'playdate.graphics.image.new', 'pd_image', ",\n", function(varName, value)
return '{ name = "' .. varName .. '", image = ' .. value .. ' }' return '{ name = "' .. varName .. '", image = ' .. value .. ' }'
end)) end))
} }

View File

@ -65,7 +65,6 @@ end
---@param floaty boolean | nil ---@param floaty boolean | nil
---@param customBallScaler pd_animator | nil ---@param customBallScaler pd_animator | nil
function Ball:launch(destX, destY, easingFunc, flyTimeMs, floaty, customBallScaler) function Ball:launch(destX, destY, easingFunc, flyTimeMs, floaty, customBallScaler)
throwMeter:reset()
self.heldBy = nil self.heldBy = nil
-- Prevent silly insta-catches -- Prevent silly insta-catches

View File

@ -4,20 +4,33 @@ local gfx <const> = playdate.graphics
local ThrowMeterHeight <const> = 50 local ThrowMeterHeight <const> = 50
local ThrowMeterLingerSec <const> = 1.5 local ThrowMeterLingerSec <const> = 1.5
local flickerTimer = gfx.animation.blinker.new(50, 50, true)
flickerTimer:start()
---@param x number ---@param x number
---@param y number ---@param y number
function throwMeter:draw(x, y) function throwMeter:draw(x, y)
gfx.setLineWidth(1) gfx.setLineWidth(1)
gfx.drawRect(x, y, 14, ThrowMeterHeight) gfx.drawRect(x, y, 14, ThrowMeterHeight)
if self.lastReadThrow then if self.lastReadThrow then
-- TODO: If ratio is "perfect", show some additional effect
-- TODO: If meter has moved to a new fielder, empty it. -- TODO: If meter has moved to a new fielder, empty it.
local ratio = (self.lastReadThrow - throwMeter.MinCharge) / (self.idealPower - throwMeter.MinCharge) local ratio = 1
if not wasPerfect then
ratio = (self.lastReadThrow - throwMeter.MinCharge) / (self.idealPower - throwMeter.MinCharge)
end
local height = ThrowMeterHeight * ratio local height = ThrowMeterHeight * ratio
gfx.fillRect(x + 2, y + ThrowMeterHeight - height, 10, height) gfx.fillRect(x + 2, y + ThrowMeterHeight - height, 10, height)
end
-- TODO: Dither or bend if the user throws too hard -- TODO: Dither or bend if the user throws too hard
-- Or maybe dither if it's too soft - bend if it's too hard -- Or maybe dither if it's too soft - bend if it's too hard
if self.wasPerfect then
PerfectPowerBg:draw(x - 11, y - 9)
if flickerTimer.on then
PerfectPowerFlickerLeft:draw(x - 11, y - 9)
else
PerfectPowerFlickerRight:draw(x - 11, y - 9)
end
end
end
end end
function throwMeter:drawNearFielder(fielder) function throwMeter:drawNearFielder(fielder)
@ -25,6 +38,9 @@ function throwMeter:drawNearFielder(fielder)
return return
end end
if fielder then if fielder then
if fielder ~= self.lastThrower then
self.lastReadThrow = nil
end
self.lastThrower = fielder self.lastThrower = fielder
actionQueue:upsert("throwMeterLinger", 200 + ThrowMeterLingerSec * 1000, function() actionQueue:upsert("throwMeterLinger", 200 + ThrowMeterLingerSec * 1000, function()
local dt = 0 local dt = 0

Binary file not shown.

After

Width:  |  Height:  |  Size: 654 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 611 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 614 B

View File

@ -46,7 +46,8 @@ Pitches = {
currentValue = function() currentValue = function()
return getPitchMissBy(accuracy) + C.PitchStart.x + (10 * math.sin((ball.yAnimator:currentValue() - C.PitchStart.y) / 10)) return getPitchMissBy(accuracy) + C.PitchStart.x + (10 * math.sin((ball.yAnimator:currentValue() - C.PitchStart.y) / 10))
end, end,
reset = function() end, reset = function()
end,
}, },
y = gfx.animator.new(C.PitchFlyMs * 1.3, C.PitchStart.y, C.PitchEndY, playdate.easingFunctions.linear), y = gfx.animator.new(C.PitchFlyMs * 1.3, C.PitchStart.y, C.PitchEndY, playdate.easingFunctions.linear),
} }
@ -116,54 +117,66 @@ end
throwMeter = { throwMeter = {
MinCharge = 25, MinCharge = 25,
value = 0,
idealPower = 50, idealPower = 50,
--- Used at draw-time only.
---@type number
lastReadThrow = nil, lastReadThrow = nil,
--- Used at draw-time only. --- Used at draw-time only.
---@type Fielder | nil ---@type Fielder | nil
lastThrower = nil, lastThrower = nil,
--- Used at draw-time only.
---@type boolean
wasPerfect = false,
} }
function throwMeter:reset()
self.value = 0
end
local crankQueue = {} local crankQueue = {}
local MaxPowerRatio <const> = 1.5
--- Returns nil when a throw is NOT requested. --- Returns nil when a throw is NOT requested.
---@param chargeAmount number ---@param chargeAmount number
---@return number | nil powerRatio, number | nil accuracy, boolean isPerfect ---@return number | nil powerRatio, number | nil accuracy, boolean isPerfect
function throwMeter:readThrow(chargeAmount) function throwMeter:readThrow(chargeAmount)
local ret = self:applyCharge(chargeAmount) local power = self:readCharge(chargeAmount)
if ret then if not power then
local ratio = ret / self.idealPower
local accuracy
if ratio >= 1 then
accuracy = 1 / ratio / 2
else
accuracy = 1 -- Accuracy is perfect on slow throws
end
return ratio * 1.5, accuracy, math.abs(ratio - 1) < 0.05
end
return nil, nil, false return nil, nil, false
end end
local ratio = math.min(power / self.idealPower, MaxPowerRatio)
self.wasPerfect = math.abs(ratio - 1) < 0.05
local accuracy = 1
-- Only throw off accuracy on slow throws
if ratio >= 1 and not self.wasPerfect then
accuracy = 1 / ratio
end
return ratio * 1.5, accuracy, self.wasPerfect
end
local CrankRecordSec <const> = 0.33
--- If (within approx. a third of a second) the crank has moved more than 45 degrees, call that a throw. --- If (within approx. a third of a second) the crank has moved more than 45 degrees, call that a throw.
---@param chargeAmount number ---@param chargeAmount number
function throwMeter:applyCharge(chargeAmount) ---@return number | nil
function throwMeter:readCharge(chargeAmount)
if chargeAmount == 0 then if chargeAmount == 0 then
return return nil
end end
local currentTimeMs = playdate.getCurrentTimeMilliseconds() local currentTimeMs = playdate.getCurrentTimeMilliseconds()
local removedOne = false local minTimeHasPassed = false
while #crankQueue ~= 0 and (currentTimeMs - crankQueue[1].time) > 0.33 do while #crankQueue ~= 0 and (currentTimeMs - crankQueue[1].time) > CrankRecordSec do
table.remove(crankQueue, 1) table.remove(crankQueue, 1)
removedOne = true -- At least 1/3 second has passed minTimeHasPassed = true
end end
crankQueue[#crankQueue + 1] = { time = currentTimeMs, chargeAmount = math.abs(chargeAmount) } crankQueue[#crankQueue + 1] = { time = currentTimeMs, chargeAmount = math.abs(chargeAmount) }
if not removedOne then if not minTimeHasPassed then
return nil return nil
end end
@ -174,7 +187,6 @@ function throwMeter:applyCharge(chargeAmount)
if currentCharge > throwMeter.MinCharge then if currentCharge > throwMeter.MinCharge then
self.lastReadThrow = currentCharge self.lastReadThrow = currentCharge
print(tostring(currentCharge) .. " from " .. #crankQueue)
crankQueue = {} crankQueue = {}
return currentCharge return currentCharge
else else