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:
parent
d82ab06534
commit
aceefeb25c
|
@ -2,8 +2,7 @@
|
||||||
-- These warning-allieviators could also be injected directly into __types.lua
|
-- These warning-allieviators could also be injected directly into __types.lua
|
||||||
-- Base __types.lua can be found at https://github.com/balpha/playdate-types
|
-- Base __types.lua can be found at https://github.com/balpha/playdate-types
|
||||||
|
|
||||||
-- selene: allow(unused_variable)
|
---@type pd_playdate_lib
|
||||||
-- selene: allow(unscoped_variables)
|
|
||||||
playdate = playdate
|
playdate = playdate
|
||||||
|
|
||||||
-- selene: allow(unscoped_variables)
|
-- selene: allow(unscoped_variables)
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
-- GENERATED FILE - DO NOT EDIT
|
-- GENERATED FILE - DO NOT EDIT
|
||||||
-- Instead, edit the source file directly: assets.lua2p.
|
-- Instead, edit the source file directly: assets.lua2p.
|
||||||
|
|
||||||
|
-- luacheck: ignore
|
||||||
|
---@type pd_image
|
||||||
|
BallBackground = playdate.graphics.image.new("images/game/BallBackground.png")
|
||||||
|
|
||||||
-- luacheck: ignore
|
-- luacheck: ignore
|
||||||
---@type pd_image
|
---@type pd_image
|
||||||
BigBat = playdate.graphics.image.new("images/game/BigBat.png")
|
BigBat = playdate.graphics.image.new("images/game/BigBat.png")
|
||||||
|
|
|
@ -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
|
|
@ -33,7 +33,8 @@ local function update()
|
||||||
while seamAngle > math.rad(90) do
|
while seamAngle > math.rad(90) do
|
||||||
local deltaSeconds = playdate.getElapsedTime()
|
local deltaSeconds = playdate.getElapsedTime()
|
||||||
playdate.resetElapsedTime()
|
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))
|
local seamAngleDeg = math.floor(math.deg(seamAngle))
|
||||||
seamAngleDeg = seamAngleDeg - (seamAngleDeg % degStep)
|
seamAngleDeg = seamAngleDeg - (seamAngleDeg % degStep)
|
||||||
|
|
||||||
|
@ -68,25 +69,31 @@ function transitionTo(nextScene)
|
||||||
transitionBetween(previousScene, nextScene)
|
transitionBetween(previousScene, nextScene)
|
||||||
end
|
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 previousScene Scene Has the current playdate.update function
|
||||||
---@param nextScene Scene Has the next playdate.update function
|
---@param nextScene Scene Has the next playdate.update function
|
||||||
function transitionBetween(previousScene, nextScene)
|
function transitionBetween(previousScene, nextScene)
|
||||||
playdate.wait(2) -- TODO: There's some sort of timing wack here.
|
playdate.wait(2) -- TODO: There's some sort of timing wack here.
|
||||||
playdate.update = update
|
playdate.update = update
|
||||||
|
|
||||||
previousSceneImage = gfx.image.new(C.Screen.W, C.Screen.H)
|
previousSceneImage = getSceneRender(previousScene)
|
||||||
gfx.pushContext(previousSceneImage)
|
nextSceneImage = getSceneRender(nextScene)
|
||||||
previousScene:update()
|
|
||||||
gfx.popContext()
|
|
||||||
|
|
||||||
nextSceneImage = gfx.image.new(C.Screen.W, C.Screen.H)
|
|
||||||
gfx.pushContext(nextSceneImage)
|
|
||||||
nextScene:update()
|
|
||||||
gfx.popContext()
|
|
||||||
|
|
||||||
previousSceneMask = gfx.image.new(C.Screen.W, C.Screen.H, gfx.kColorWhite)
|
previousSceneMask = gfx.image.new(C.Screen.W, C.Screen.H, gfx.kColorWhite)
|
||||||
previousSceneImage:setMaskImage(previousSceneMask)
|
previousSceneImage:setMaskImage(previousSceneMask)
|
||||||
|
|
||||||
Transitions.nextScene = nextScene
|
Transitions.nextScene = nextScene
|
||||||
Transitions.previousScene = previousScene
|
Transitions.previousScene = previousScene
|
||||||
|
|
||||||
|
-- Prevents bad transition calculations due to a long "delta"
|
||||||
|
playdate.resetElapsedTime()
|
||||||
end
|
end
|
||||||
|
|
|
@ -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
|
--- Assumes that background image is of size
|
||||||
--- XXX
|
--- XXX
|
||||||
--- XOX
|
--- XOX
|
||||||
|
@ -21,6 +25,21 @@ function getDrawOffset(ballX, ballY)
|
||||||
return offsetX * 1.3, offsetY
|
return offsetX * 1.3, offsetY
|
||||||
end
|
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
|
---@class Blipper
|
||||||
---@field draw fun(self: self, disableBlipping: boolean, x: number, y: number)
|
---@field draw fun(self: self, disableBlipping: boolean, x: number, y: number)
|
||||||
blipper = {}
|
blipper = {}
|
||||||
|
@ -28,7 +47,7 @@ blipper = {}
|
||||||
--- Build an object that simply "blips" between the given images at the given interval.
|
--- 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.
|
--- Expects `playdate.graphics.animation.blinker.updateAll()` to be called on every update.
|
||||||
function blipper.new(msInterval, spriteCollection)
|
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()
|
blinker:start()
|
||||||
return {
|
return {
|
||||||
blinker = blinker,
|
blinker = blinker,
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 10 KiB |
|
@ -1,3 +1,7 @@
|
||||||
|
-- stylua: ignore start
|
||||||
|
import "control-screen.lua"
|
||||||
|
-- stylua: ignore end
|
||||||
|
|
||||||
---@class MainMenu
|
---@class MainMenu
|
||||||
MainMenu = {
|
MainMenu = {
|
||||||
---@type { new: fun(settings: Settings): { update: fun(self) } }
|
---@type { new: fun(settings: Settings): { update: fun(self) } }
|
||||||
|
@ -5,7 +9,7 @@ MainMenu = {
|
||||||
}
|
}
|
||||||
local gfx = playdate.graphics
|
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")
|
local TinyFont <const> = gfx.font.new("fonts/Nano Sans.pft")
|
||||||
|
|
||||||
--- Take control of playdate.update
|
--- Take control of playdate.update
|
||||||
|
@ -14,11 +18,18 @@ local TinyFont <const> = gfx.font.new("fonts/Nano Sans.pft")
|
||||||
function MainMenu.start(next)
|
function MainMenu.start(next)
|
||||||
MenuMusic:play(0)
|
MenuMusic:play(0)
|
||||||
MainMenu.next = next
|
MainMenu.next = next
|
||||||
playdate.update = MainMenu.update
|
playdate.update = function()
|
||||||
|
MainMenu:update()
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local inningCountSelection = 3
|
local inningCountSelection = 3
|
||||||
|
|
||||||
|
function MainMenu:showControls()
|
||||||
|
local next = ControlScreen.new(self)
|
||||||
|
transitionBetween(MainMenu, next)
|
||||||
|
end
|
||||||
|
|
||||||
local function startGame()
|
local function startGame()
|
||||||
local next = MainMenu.next.new({
|
local next = MainMenu.next.new({
|
||||||
finalInning = inningCountSelection,
|
finalInning = inningCountSelection,
|
||||||
|
@ -54,7 +65,7 @@ local animatorY = gfx.animator.new(2000, 60, 200, pausingEaser(utils.easingHill)
|
||||||
animatorY.repeatCount = -1
|
animatorY.repeatCount = -1
|
||||||
animatorY.reverses = true
|
animatorY.reverses = true
|
||||||
|
|
||||||
local crankStartPos = nil
|
local crankStartPos
|
||||||
|
|
||||||
---@generic T
|
---@generic T
|
||||||
---@param array T[]
|
---@param array T[]
|
||||||
|
@ -69,6 +80,16 @@ local currentLogo
|
||||||
|
|
||||||
--luacheck: ignore
|
--luacheck: ignore
|
||||||
function MainMenu:update()
|
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()
|
playdate.timer.updateTimers()
|
||||||
crankStartPos = crankStartPos or playdate.getCrankPosition()
|
crankStartPos = crankStartPos or playdate.getCrankPosition()
|
||||||
|
|
||||||
|
@ -84,13 +105,10 @@ function MainMenu:update()
|
||||||
currentLogo:drawScaled(20, C.Center.y + 40, 3)
|
currentLogo:drawScaled(20, C.Center.y + 40, 3)
|
||||||
end
|
end
|
||||||
|
|
||||||
if playdate.buttonJustPressed(playdate.kButtonA) then
|
if playdate.buttonJustPressed(playdate.kButtonUp) or playdate.buttonJustPressed(playdate.kButtonRight) then
|
||||||
startGame()
|
|
||||||
end
|
|
||||||
if playdate.buttonJustPressed(playdate.kButtonUp) then
|
|
||||||
inningCountSelection = math.min(99, inningCountSelection + 1)
|
inningCountSelection = math.min(99, inningCountSelection + 1)
|
||||||
end
|
end
|
||||||
if playdate.buttonJustPressed(playdate.kButtonDown) then
|
if playdate.buttonJustPressed(playdate.kButtonDown) or playdate.buttonJustPressed(playdate.kButtonLeft) then
|
||||||
inningCountSelection = math.max(1, inningCountSelection - 1)
|
inningCountSelection = math.max(1, inningCountSelection - 1)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -98,8 +116,14 @@ function MainMenu:update()
|
||||||
GameLogo:drawCentered(C.Center.x, logoCenter)
|
GameLogo:drawCentered(C.Center.x, logoCenter)
|
||||||
TinyFont:drawTextAligned("a game by Sage", C.Center.x, logoCenter + 35, kTextAlignment.center)
|
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)
|
local promptOffsetX = 120
|
||||||
gfx.drawTextAligned("with " .. inningCountSelection .. " innings", C.Center.x, 210, kTextAlignment.center)
|
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 = {
|
local ball = {
|
||||||
x = animatorX:currentValue(),
|
x = animatorX:currentValue(),
|
||||||
|
|
|
@ -17,6 +17,18 @@ local mockPlaydate = {
|
||||||
return utils.staticAnimator(0)
|
return utils.staticAnimator(0)
|
||||||
end,
|
end,
|
||||||
},
|
},
|
||||||
|
animation = {
|
||||||
|
blinker = {
|
||||||
|
new = function()
|
||||||
|
return { start = function() end }
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
font = {
|
||||||
|
new = function()
|
||||||
|
return {}
|
||||||
|
end,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue