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.
-- luacheck: ignore
---@type pd_image
DarkPlayerBack = playdate.graphics.image.new("images/game/DarkPlayerBack.png")
-- luacheck: ignore
---@type pd_image
Glove = playdate.graphics.image.new("images/game/Glove.png")
-- 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")
-- 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")
-- 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")
-- luacheck: ignore
---@type pd_image
GameLogo = playdate.graphics.image.new("images/game/GameLogo.png")
-- luacheck: ignore
---@type pd_image
Hat = playdate.graphics.image.new("images/game/Hat.png")
-- luacheck: ignore
---@type pd_image
DarkPlayerBase = playdate.graphics.image.new("images/game/DarkPlayerBase.png")
-- luacheck: ignore
---@type pd_image
MenuImage = playdate.graphics.image.new("images/game/MenuImage.png")
-- luacheck: ignore
---@type pd_image
PlayerSmile = playdate.graphics.image.new("images/game/PlayerSmile.png")
-- luacheck: ignore
---@type pd_image
Minimap = playdate.graphics.image.new("images/game/Minimap.png")
-- luacheck: ignore
---@type pd_image
GrassBackground = playdate.graphics.image.new("images/game/GrassBackground.png")
-- luacheck: ignore
---@type pd_image
GrassBackgroundSmall = playdate.graphics.image.new("images/game/GrassBackgroundSmall.png")
-- luacheck: ignore
---@type pd_image
LightPlayerBase = playdate.graphics.image.new("images/game/LightPlayerBase.png")
-- 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")
-- luacheck: ignore
---@type pd_sampleplayer
BatCrackReverb = playdate.sound.sampleplayer.new("sounds/BatCrackReverb.wav")
-- luacheck: ignore
---@type pd_sampleplayer
BootTune = playdate.sound.sampleplayer.new("music/BootTune.wav")
-- luacheck: ignore
---@type pd_sampleplayer
BootTuneOrgany = playdate.sound.sampleplayer.new("music/BootTuneOrgany.wav")
-- luacheck: ignore
---@type pd_sampleplayer
TinnyBackground = playdate.sound.sampleplayer.new("music/TinnyBackground.wav")
-- luacheck: ignore
---@type pd_sampleplayer
MenuMusic = playdate.sound.sampleplayer.new("music/MenuMusic.wav")
Logos = {
{ name = "Base", image = playdate.graphics.image.new("images/game/logos/Base.png") },
-- luacheck: ignore
---@type pd_image
{ name = "Cats", image = playdate.graphics.image.new("images/game/logos/Cats.png") },
-- luacheck: ignore
---@type pd_image
{ name = "Hearts", image = playdate.graphics.image.new("images/game/logos/Hearts.png") },
-- luacheck: ignore
---@type pd_image
{ name = "Checkmarks", image = playdate.graphics.image.new("images/game/logos/Checkmarks.png") },
-- luacheck: ignore
---@type pd_image
{ name = "Smiles", image = playdate.graphics.image.new("images/game/logos/Smiles.png") },
-- luacheck: ignore
---@type pd_image
{ name = "FingerGuns", image = playdate.graphics.image.new("images/game/logos/FingerGuns.png") },
-- luacheck: ignore
---@type pd_image
{ name = "Frown", image = playdate.graphics.image.new("images/game/logos/Frown.png") },
-- luacheck: ignore
---@type pd_image
{ name = "Arrows", image = playdate.graphics.image.new("images/game/logos/Arrows.png") },
-- luacheck: ignore
---@type pd_image
{ 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"
handle = handle ~= nil and handle or function(varName, value)
return varName .. ' = ' .. value
@ -13,6 +13,7 @@
local varName = file:gsub(".*/(.*)." .. extension, "%1")
file = file:gsub("src/", "")
assetCode = assetCode .. '-- luacheck: ignore\n'
assetCode = assetCode .. '---@type ' .. type ..'\n'
assetCode = assetCode .. handle(varName, newFunc .. '("' .. file .. '")') .. sep
end
end
@ -23,13 +24,13 @@ function generatedFileWarning()
return "-- GENERATED FILE - DO NOT EDIT\n-- Instead, edit the source file directly: assets.lua2p."
end)!!(generatedFileWarning())
!!(dirLookup('images/game', 'png', 'playdate.graphics.image.new'))
!!(dirLookup('sounds', 'wav', 'playdate.sound.sampleplayer.new'))
!!(dirLookup('music', 'wav', 'playdate.sound.sampleplayer.new'))
!!(dirLookup('images/game', 'png', 'playdate.graphics.image.new', 'pd_image'))
!!(dirLookup('sounds', 'wav', 'playdate.sound.sampleplayer.new', 'pd_sampleplayer'))
!!(dirLookup('music', 'wav', 'playdate.sound.sampleplayer.new', 'pd_sampleplayer'))
Logos = {
{ 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 .. ' }'
end))
}

View File

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

View File

@ -4,20 +4,33 @@ local gfx <const> = playdate.graphics
local ThrowMeterHeight <const> = 50
local ThrowMeterLingerSec <const> = 1.5
local flickerTimer = gfx.animation.blinker.new(50, 50, true)
flickerTimer:start()
---@param x number
---@param y number
function throwMeter:draw(x, y)
gfx.setLineWidth(1)
gfx.drawRect(x, y, 14, ThrowMeterHeight)
if self.lastReadThrow then
-- TODO: If ratio is "perfect", show some additional effect
-- 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
gfx.fillRect(x + 2, y + ThrowMeterHeight - height, 10, height)
-- TODO: Dither or bend if the user throws 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
-- TODO: Dither or bend if the user throws too hard
-- Or maybe dither if it's too soft - bend if it's too hard
end
function throwMeter:drawNearFielder(fielder)
@ -25,6 +38,9 @@ function throwMeter:drawNearFielder(fielder)
return
end
if fielder then
if fielder ~= self.lastThrower then
self.lastReadThrow = nil
end
self.lastThrower = fielder
actionQueue:upsert("throwMeterLinger", 200 + ThrowMeterLingerSec * 1000, function()
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()
return getPitchMissBy(accuracy) + C.PitchStart.x + (10 * math.sin((ball.yAnimator:currentValue() - C.PitchStart.y) / 10))
end,
reset = function() end,
reset = function()
end,
},
y = gfx.animator.new(C.PitchFlyMs * 1.3, C.PitchStart.y, C.PitchEndY, playdate.easingFunctions.linear),
}
@ -116,54 +117,66 @@ end
throwMeter = {
MinCharge = 25,
value = 0,
idealPower = 50,
--- Used at draw-time only.
---@type number
lastReadThrow = nil,
--- Used at draw-time only.
---@type Fielder | nil
lastThrower = nil,
--- Used at draw-time only.
---@type boolean
wasPerfect = false,
}
function throwMeter:reset()
self.value = 0
end
local crankQueue = {}
local MaxPowerRatio <const> = 1.5
--- Returns nil when a throw is NOT requested.
---@param chargeAmount number
---@return number | nil powerRatio, number | nil accuracy, boolean isPerfect
function throwMeter:readThrow(chargeAmount)
local ret = self:applyCharge(chargeAmount)
if ret 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
local power = self:readCharge(chargeAmount)
if not power then
return nil, nil, false
end
return nil, nil, false
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.
---@param chargeAmount number
function throwMeter:applyCharge(chargeAmount)
---@return number | nil
function throwMeter:readCharge(chargeAmount)
if chargeAmount == 0 then
return
return nil
end
local currentTimeMs = playdate.getCurrentTimeMilliseconds()
local removedOne = false
while #crankQueue ~= 0 and (currentTimeMs - crankQueue[1].time) > 0.33 do
local minTimeHasPassed = false
while #crankQueue ~= 0 and (currentTimeMs - crankQueue[1].time) > CrankRecordSec do
table.remove(crankQueue, 1)
removedOne = true -- At least 1/3 second has passed
minTimeHasPassed = true
end
crankQueue[#crankQueue + 1] = { time = currentTimeMs, chargeAmount = math.abs(chargeAmount) }
if not removedOne then
if not minTimeHasPassed then
return nil
end
@ -174,7 +187,6 @@ function throwMeter:applyCharge(chargeAmount)
if currentCharge > throwMeter.MinCharge then
self.lastReadThrow = currentCharge
print(tostring(currentCharge) .. " from " .. #crankQueue)
crankQueue = {}
return currentCharge
else