diff --git a/__stub.ext.lua b/__stub.ext.lua index 1f41e90..4573b23 100644 --- a/__stub.ext.lua +++ b/__stub.ext.lua @@ -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) diff --git a/src/assets.lua b/src/assets.lua index 57d659f..f8f4e15 100644 --- a/src/assets.lua +++ b/src/assets.lua @@ -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") diff --git a/src/control-screen.lua b/src/control-screen.lua new file mode 100644 index 0000000..6aa0fb6 --- /dev/null +++ b/src/control-screen.lua @@ -0,0 +1,99 @@ +local gfx = playdate.graphics + +local HeaderFont = playdate.graphics.font.new("fonts/Roobert-11-Medium.pft") +local DetailFont = 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 = -(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 diff --git a/src/draw/transitions.lua b/src/draw/transitions.lua index a7ca79e..3153997 100644 --- a/src/draw/transitions.lua +++ b/src/draw/transitions.lua @@ -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 diff --git a/src/graphics.lua b/src/graphics.lua index d776cf5..7b2130e 100644 --- a/src/graphics.lua +++ b/src/graphics.lua @@ -1,3 +1,7 @@ +local gfx = playdate.graphics + +local ButtonFont = 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, diff --git a/src/images/game/BallBackground.png b/src/images/game/BallBackground.png new file mode 100644 index 0000000..15845b5 Binary files /dev/null and b/src/images/game/BallBackground.png differ diff --git a/src/main-menu.lua b/src/main-menu.lua index ef07832..67dce3c 100644 --- a/src/main-menu.lua +++ b/src/main-menu.lua @@ -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 = gfx.font.new("fonts/Roobert-11-Medium.pft") +local ScoreFont = playdate.graphics.font.new("fonts/font-full-circle.pft") local TinyFont = gfx.font.new("fonts/Nano Sans.pft") --- Take control of playdate.update @@ -14,11 +18,18 @@ local TinyFont = 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(), diff --git a/src/test/mocks.lua b/src/test/mocks.lua index 30b5ee1..1fca7ad 100644 --- a/src/test/mocks.lua +++ b/src/test/mocks.lua @@ -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, + }, }, }