Add a screen for showing the game's controls

Tweak MainMenu appearance to show this new option.
Simple new drawButton() graphics function.
Set a max value for transition delta, to keep from leaving gaps in the mask.
This commit is contained in:
Sage Vaillancourt 2025-02-23 13:10:09 -05:00
parent d82ab06534
commit aceefeb25c
8 changed files with 187 additions and 23 deletions

View File

@ -2,8 +2,7 @@
-- These warning-allieviators could also be injected directly into __types.lua
-- Base __types.lua can be found at https://github.com/balpha/playdate-types
-- selene: allow(unused_variable)
-- selene: allow(unscoped_variables)
---@type pd_playdate_lib
playdate = playdate
-- selene: allow(unscoped_variables)

View File

@ -1,6 +1,10 @@
-- GENERATED FILE - DO NOT EDIT
-- Instead, edit the source file directly: assets.lua2p.
-- luacheck: ignore
---@type pd_image
BallBackground = playdate.graphics.image.new("images/game/BallBackground.png")
-- luacheck: ignore
---@type pd_image
BigBat = playdate.graphics.image.new("images/game/BigBat.png")

99
src/control-screen.lua Normal file
View File

@ -0,0 +1,99 @@
local gfx = playdate.graphics
local HeaderFont <const> = playdate.graphics.font.new("fonts/Roobert-11-Medium.pft")
local DetailFont <const> = playdate.graphics.font.new("fonts/font-full-circle.pft")
---@alias TextObject { text: string, font: pd_font }
---@param texts TextObject[]
local function drawTexts(texts)
local xOffset = 10
local initialOffset <const> = -(HeaderFont:getHeight()) / 2
local yOffset = initialOffset
--- The text height plus a margin scaled to that height
function getOffsetOffset(textObject)
return (-4 + math.floor(textObject.font:getHeight() * 1.6)) / 2
end
-- Inverted buffer around text to separate it from the background
for _, textObject in ipairs(texts) do
local offsetOffset = getOffsetOffset(textObject)
yOffset = yOffset + offsetOffset
gfx.setImageDrawMode(gfx.kDrawModeInverted)
for x = xOffset - 6, xOffset + 6 do
for y = yOffset - 6, yOffset + 6 do
textObject.font:drawText(textObject.text, x, y)
end
end
yOffset = yOffset + offsetOffset
end
-- Drawing the actual text afterward (instead of inline) keeps the inverted buffer from drawing over it.
yOffset = initialOffset
gfx.setImageDrawMode(gfx.kDrawModeCopy)
for _, textObject in ipairs(texts) do
local offsetOffset = getOffsetOffset(textObject)
yOffset = yOffset + offsetOffset
textObject.font:drawText(textObject.text, xOffset, yOffset)
yOffset = yOffset + offsetOffset
end
end
---@param text string
---@return TextObject
local function header(text)
return { text = text, font = HeaderFont }
end
---@param text string
---@return TextObject
local function detail(text)
return { text = text, font = DetailFont }
end
---@class
---@field sceneToReturnTo Scene
---@field private renderedImage pd_image Static image doesn't need to be constantly re-rendered.
ControlScreen = {}
---@return pd_image
local function draw()
local image = gfx.image.new(C.Screen.W, C.Screen.H)
gfx.pushContext(image)
BallBackground:draw(0, 0)
drawTexts({
header("Batting:"),
detail("Swing the crank to swing your bat"),
detail("But watch out! Some pitches are tricky!"),
header("Pitching:"),
detail("Swing the crank to pitch the ball"),
detail("But be careful! Throw too hard and it might go wild!"),
detail("(shh: try holding A or B while you pitch)"),
header("Fielding:"),
detail("To throw, hold a direction button and crank!"),
detail("Right throws to 1st, Up goes to 2nd, etc."),
})
gfx.popContext()
return image
end
---@param sceneToReturnTo Scene
function ControlScreen.new(sceneToReturnTo)
return setmetatable({
sceneToReturnTo = sceneToReturnTo,
renderedImage = draw(),
}, { __index = ControlScreen })
end
function ControlScreen:update()
gfx.animation.blinker.updateAll()
gfx.clear()
self.renderedImage:draw(0, 0)
drawButton("B", 370, 210)
if playdate.buttonJustPressed(playdate.kButtonA) or playdate.buttonJustPressed(playdate.kButtonB) then
transitionBetween(self, self.sceneToReturnTo)
end
end

View File

@ -33,7 +33,8 @@ local function update()
while seamAngle > math.rad(90) do
local deltaSeconds = playdate.getElapsedTime()
playdate.resetElapsedTime()
seamAngle = seamAngle - (deltaSeconds * 3)
-- Setting a max value keeps from leaving unmasked areas
seamAngle = seamAngle - math.min(0.1, deltaSeconds * 3)
local seamAngleDeg = math.floor(math.deg(seamAngle))
seamAngleDeg = seamAngleDeg - (seamAngleDeg % degStep)
@ -68,25 +69,31 @@ function transitionTo(nextScene)
transitionBetween(previousScene, nextScene)
end
---@param scene Scene
---@return pd_image
local function getSceneRender(scene)
local image = gfx.image.new(C.Screen.W, C.Screen.H)
gfx.pushContext(image)
scene:update()
gfx.popContext()
return image
end
---@param previousScene Scene Has the current playdate.update function
---@param nextScene Scene Has the next playdate.update function
function transitionBetween(previousScene, nextScene)
playdate.wait(2) -- TODO: There's some sort of timing wack here.
playdate.update = update
previousSceneImage = gfx.image.new(C.Screen.W, C.Screen.H)
gfx.pushContext(previousSceneImage)
previousScene:update()
gfx.popContext()
nextSceneImage = gfx.image.new(C.Screen.W, C.Screen.H)
gfx.pushContext(nextSceneImage)
nextScene:update()
gfx.popContext()
previousSceneImage = getSceneRender(previousScene)
nextSceneImage = getSceneRender(nextScene)
previousSceneMask = gfx.image.new(C.Screen.W, C.Screen.H, gfx.kColorWhite)
previousSceneImage:setMaskImage(previousSceneMask)
Transitions.nextScene = nextScene
Transitions.previousScene = previousScene
-- Prevents bad transition calculations due to a long "delta"
playdate.resetElapsedTime()
end

View File

@ -1,3 +1,7 @@
local gfx <const> = playdate.graphics
local ButtonFont <const> = gfx.font.new("fonts/font-full-circle.pft")
--- Assumes that background image is of size
--- XXX
--- XOX
@ -21,6 +25,21 @@ function getDrawOffset(ballX, ballY)
return offsetX * 1.3, offsetY
end
local buttonBlinker = gfx.animation.blinker.new(750, 500, true)
buttonBlinker:start()
--- Requires calling `playdate.graphics.animation.blinker.updateAll()` during `update()` to blink correctly.
function drawButton(buttonLabel, x, y)
gfx.setColor(gfx.kColorWhite)
gfx.fillCircleAtPoint(x + 4, y + 7, 12)
gfx.setColor(gfx.kColorBlack)
if buttonBlinker.on then
gfx.setLineWidth(1)
gfx.drawCircleAtPoint(x + 4, y + 7, 10)
end
ButtonFont:drawText(buttonLabel, x, y)
end
---@class Blipper
---@field draw fun(self: self, disableBlipping: boolean, x: number, y: number)
blipper = {}
@ -28,7 +47,7 @@ blipper = {}
--- Build an object that simply "blips" between the given images at the given interval.
--- Expects `playdate.graphics.animation.blinker.updateAll()` to be called on every update.
function blipper.new(msInterval, spriteCollection)
local blinker = playdate.graphics.animation.blinker.new(msInterval, msInterval, true)
local blinker = gfx.animation.blinker.new(msInterval, msInterval, true)
blinker:start()
return {
blinker = blinker,

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -1,3 +1,7 @@
-- stylua: ignore start
import "control-screen.lua"
-- stylua: ignore end
---@class MainMenu
MainMenu = {
---@type { new: fun(settings: Settings): { update: fun(self) } }
@ -5,7 +9,7 @@ MainMenu = {
}
local gfx = playdate.graphics
local StartFont <const> = gfx.font.new("fonts/Roobert-11-Medium.pft")
local ScoreFont <const> = playdate.graphics.font.new("fonts/font-full-circle.pft")
local TinyFont <const> = gfx.font.new("fonts/Nano Sans.pft")
--- Take control of playdate.update
@ -14,11 +18,18 @@ local TinyFont <const> = gfx.font.new("fonts/Nano Sans.pft")
function MainMenu.start(next)
MenuMusic:play(0)
MainMenu.next = next
playdate.update = MainMenu.update
playdate.update = function()
MainMenu:update()
end
end
local inningCountSelection = 3
function MainMenu:showControls()
local next = ControlScreen.new(self)
transitionBetween(MainMenu, next)
end
local function startGame()
local next = MainMenu.next.new({
finalInning = inningCountSelection,
@ -54,7 +65,7 @@ local animatorY = gfx.animator.new(2000, 60, 200, pausingEaser(utils.easingHill)
animatorY.repeatCount = -1
animatorY.reverses = true
local crankStartPos = nil
local crankStartPos
---@generic T
---@param array T[]
@ -69,6 +80,16 @@ local currentLogo
--luacheck: ignore
function MainMenu:update()
if playdate.buttonJustPressed(playdate.kButtonA) then
startGame()
return
end
if playdate.buttonJustPressed(playdate.kButtonB) then
self:showControls()
return
end
playdate.timer.updateTimers()
crankStartPos = crankStartPos or playdate.getCrankPosition()
@ -84,13 +105,10 @@ function MainMenu:update()
currentLogo:drawScaled(20, C.Center.y + 40, 3)
end
if playdate.buttonJustPressed(playdate.kButtonA) then
startGame()
end
if playdate.buttonJustPressed(playdate.kButtonUp) then
if playdate.buttonJustPressed(playdate.kButtonUp) or playdate.buttonJustPressed(playdate.kButtonRight) then
inningCountSelection = math.min(99, inningCountSelection + 1)
end
if playdate.buttonJustPressed(playdate.kButtonDown) then
if playdate.buttonJustPressed(playdate.kButtonDown) or playdate.buttonJustPressed(playdate.kButtonLeft) then
inningCountSelection = math.max(1, inningCountSelection - 1)
end
@ -98,8 +116,14 @@ function MainMenu:update()
GameLogo:drawCentered(C.Center.x, logoCenter)
TinyFont:drawTextAligned("a game by Sage", C.Center.x, logoCenter + 35, kTextAlignment.center)
StartFont:drawTextAligned("Press A to start!", C.Center.x, 180, kTextAlignment.center)
gfx.drawTextAligned("with " .. inningCountSelection .. " innings", C.Center.x, 210, kTextAlignment.center)
local promptOffsetX = 120
ScoreFont:drawTextAligned(
"Press A to start with <" .. inningCountSelection .. "> innings",
C.Center.x - promptOffsetX,
180,
kTextAlignment.left
)
ScoreFont:drawTextAligned("Press B for controls", C.Center.x - promptOffsetX, 198, kTextAlignment.left)
local ball = {
x = animatorX:currentValue(),

View File

@ -17,6 +17,18 @@ local mockPlaydate = {
return utils.staticAnimator(0)
end,
},
animation = {
blinker = {
new = function()
return { start = function() end }
end,
},
},
font = {
new = function()
return {}
end,
},
},
}