noble engine migration

This commit is contained in:
2024-06-01 16:52:11 +03:00
parent ea0681b60d
commit 29a5ed2f62
140 changed files with 17016 additions and 1653 deletions

View File

@@ -0,0 +1,296 @@
--- Animation states using a spritesheet/imagetable. Ideal for use with `NobleSprite` objects. Suitable for other uses as well.
-- @module Noble.Animation
--
Noble.Animation = {}
--- Setup
-- @section setup
--- Create a new animation "state machine". This function is called automatically when creating a new `NobleSprite`.
-- @string __view This can be: the path to a spritesheet image file or an image table object (`Graphics.imagetable`). See Playdate SDK docs for imagetable file naming conventions.
-- @return `animation`, a new animation object.
-- @usage
-- local myHero = MyHero("path/to/spritesheet")
-- @usage
-- -- When extending NobleSprite (recommended), you don't call Noble.Animation.new(),
-- -- but you do feed its __view argument into MySprite.super.init()...
-- MyHero = {}
-- class("MyHero").extends(NobleSprite)
--
-- function MyHero:init()
-- MyHero.super.init(self, "assets/images/Hero")
-- -- ...
-- -- A new NobleSprite creates a Noble.Animation object named "self.animation"
-- self.animation:addState("idle", 1, 30)
-- self.animation:addState("jump", 31, 34, "float")
-- self.animation:addState("float", 35, 45)
-- self.animation:addState("turn", 46, 55, "idle")
-- self.animation:addState("walk", 56, 65)
-- -- ...
-- end
-- @usage
-- local myAnimation = Noble.Animation.new("path/to/spritesheet")
-- @usage
-- -- When extending playdate.graphics.Sprite, Noble.Animation.new() must be called manually...
-- MyHero = {}
-- class("MyHero").extends(Graphics.sprite)
--
-- function MyHero:init()
-- MyHero.super.init(self)
-- -- ...
-- self.animation = Noble.Animation.new("assets/images/Hero")
-- self.animation:addState("idle", 1, 30)
-- self.animation:addState("jump", 31, 34, "float")
-- self.animation:addState("float", 35, 45)
-- self.animation:addState("turn", 46, 55, "idle")
-- self.animation:addState("walk", 56, 65)
-- -- ...
-- end
-- @see NobleSprite:init
function Noble.Animation.new(__view)
local animation = {}
--- Properties
-- @section properties
--- The currently set animation state.
--
-- This is intended as `read-only`. You should not modify this property directly.
-- @see setState
animation.current = nil
--- The name of the current animation state. Calling this instead of `animation.current.name` is <em>just</em> a little faster.
--
-- This is intended as `read-only`. You should not modify this property directly.
animation.currentName = nil
--- The current frame of the animation. This is the index of the imagetable, not the frame of the current state.
--
-- Most of the time, you should not modify this directly, although you can if you're feeling saucy and are prepared for unpredictable results.
-- @see draw
animation.currentFrame = 1
--- This controls the flipping of the image when drawing. DIRECTION_RIGHT is unflipped, DIRECTION_LEFT is flipped on the X axis.
-- @usage
-- function MyHero:goLeft()
-- self.animation.direction = Noble.Animation.DIRECTION_LEFT
-- -- ...
-- end
animation.direction = Noble.Animation.DIRECTION_RIGHT
--- This animation's spritesheet. You can replace this with another `playdate.graphics.imagetable` object, but generally you would not want to.
-- @see new
if (type(__view) == "userdata") then
animation.imageTable = __view
else
animation.imageTable = Graphics.imagetable.new(__view)
end
-- The current count of frame durations. This is used to determine when to advance to the next frame.
animation.frameDurationCount = 1
-- The previous number of frame durations in the animation
animation.previousFrameDurationCount = 1
local empty = true
--- Setup
-- @section setup
--- Add an animation state. The first state added will be the default set for this animation.
--
-- <strong>NOTE:</strong> Added states are first-degree member objects of your Noble.Animation object, so do not use names of already existing methods/properties ("current", "draw", etc.).
-- @string __name The name of the animation, this is also used as the key for the animation.
-- @int __startFrame This is the first frame of this animation in the imagetable/spritesheet
-- @int __endFrame This is the final frame of this animation in the imagetable/spritesheet
-- @string[optional] __next By default, animation states will loop, but if you want to sequence an animation, enter the name of the next state here.
-- @bool[opt=true] __loop If you want a state to "freeze" on its final frame, instead of looping, enter `false` here.
-- @param[optional] __onComplete This function will run when this animation is complete. Be careful when using this on a looping animation!
-- @int[opt=1] __frameDuration This is the number of ticks between each frame in this animation. If not specified, it will be set to 1.
-- @usage
-- -- You can reference an animation's state's properties using bog-standard lua syntax:
--
-- animation.idle.startFrame -- 30
-- animation.walk.endFrame -- 65
-- animation.["walk"].endFrame -- 65
-- animation.jump.name -- "jump"
-- animation.["jump"].next -- "float"
-- animation.idle.next -- nil
function animation:addState(__name, __startFrame, __endFrame, __next, __loop, __onComplete, __frameDuration)
local loop = true
local frameDuration = 1
if (__loop ~= nil) then loop = __loop end
if(__frameDuration ~= nil) then frameDuration = __frameDuration end
self[__name] = {
name = __name,
startFrame = __startFrame,
endFrame = __endFrame,
next = __next,
loop = loop,
onComplete = __onComplete,
frameDuration = frameDuration,
}
-- Set this animation state as default if it is the first one added.
if (empty == true) then
empty = false
self.currentFrame = __startFrame
self.current = self[__name]
self.currentName = __name
self.frameDuration = frameDuration
end
end
--- Methods
-- @section methods
--- Sets the current animation state. This can be run in a object's `update` method because it only changes the animation state if the new state is different from the current one.
-- @tparam string|Noble.Animation __animationState The name of the animation to set. You can pass the name of the state, or the object itself.
-- @bool[opt=false] __continuous Set to true if your new state's frames line up with the previous one's, i.e.: two walk cycles but one is wearing a cute hat!
-- @tparam string|Noble.Animation __unlessThisState If this state is the current state, do not set the new one.
-- @usage animation:setState("walk")
-- @usage animation:setState(animation.walk)
-- @usage
-- animation:setState(animation.walkNoHat)
-- --
-- animation:setState(animation.walkYesHat, true)
-- @usage
-- function MyHero:update()
-- -- Input
-- -- ...
--
-- -- Physics/collisions
-- -- ...
--
-- -- Animation states
-- if (grounded) then
-- if (turning) then
-- self.animation:setState(self.animation.turn)
-- elseif (math.abs(self.velocity.x) > 15) then
-- self.animation:setState(self.animation.walk, false, self.animation.turn)
-- else
-- self.animation:setState(self.animation.idle, false, self.animation.turn)
-- end
-- else
-- self.animation:setState(self.animation.jump, false, self.animation.float)
-- end
--
-- groundedLastFrame = grounded
-- end
function animation:setState(__animationState, __continuous, __unlessThisState)
if (__unlessThisState ~= nil) then
if (type(__unlessThisState) == "string") then
if (self.currentName == __unlessThisState) then return end
elseif (type(__unlessThisState) == "table") then
if (self.current == __unlessThisState) then return end
end
end
local newState = nil
if (type(__animationState) == "string") then
if (self.currentName == __animationState) then return end
newState = self[__animationState]
self.currentName = __animationState
elseif (type(__animationState) == "table") then
if (self.current == __animationState) then return end
newState = __animationState
self.currentName = __animationState.name
end
local continuous = Utilities.handleOptionalBoolean(__continuous, false)
if (continuous) then
local localFrame = self.currentFrame - self.current.startFrame
self.currentFrame = newState.startFrame + localFrame
else
self.currentFrame = newState.startFrame
end
self.current = newState
end
--- Draw the current frame.
--
-- When attached to a NobleSprite, this is called by `NobleSprite:draw()` when added to a scene. For non-NobleSprite sprites, put this method inside your sprite's `draw()` method, or inside @{NobleScene:update|NobleScene:update}.
-- @number[opt=0] __x
-- @number[opt=0] __y
-- @bool[opt=true] __advance Advances to the next frame after drawing this one. Noble.Animation is frame-based, not "delta time"-based, so its speed is dependent on your game's framerate.
-- @usage
-- function MySprite:draw()
-- animation:draw()
-- end
-- @usage
-- function MyScene:update()
-- animation:draw(100,100)
-- end
function animation:draw(__x, __y, __advance)
--print(self.currentName .. " > " .. self.currentFrame .. " >> " .. tostring(self.current.loop))
if (__advance == nil) then __advance = true end
if(self.currentFrame < self.current.startFrame or self.currentFrame > self.current.endFrame + 1) then
self.currentFrame = self.current.startFrame -- Error correction.
elseif(self.currentFrame == self.current.endFrame + 1) then -- End frame behavior.
if (self.current.next ~= nil) then
self.currentFrame = self.current.next.startFrame -- Set to first frame of next animation.
self.frameDurationCount = 1 -- Reset ticks.
self.previousFrameDurationCount = self.frameDuration
self:setState(self.current.next) -- Set next animation state.
elseif (self.current.loop == true) then
self.currentFrame = self.current.startFrame -- Loop animation state. (TO-DO: account for continuous somehow?)
self.frameDurationCount = 1 -- Reset ticks.
self.previousFrameDurationCount = self.frameDuration
elseif(__advance) then
self.currentFrame = self.currentFrame - 1 -- Undo advance frame because we want to draw the same frame again.
end
if (self.current.onComplete ~= nil) then
self.current.onComplete()
end
end
local x = __x or 0
local y = __y or 0
self.imageTable:drawImage(self.currentFrame, x, y, self.direction)
if (__advance == true) then
self.frameDurationCount += 1
if((self.frameDurationCount - self.previousFrameDurationCount) >= self.current.frameDuration) then
self.currentFrame = self.currentFrame + 1
self.previousFrameDurationCount += self.current.frameDuration
end
end
--previousAnimationName = self.currentName
end
--- Sometimes, you just want to draw a specific frame.
-- Use this for objects or sprites that you want to control outside of update loops, such as score counters, flipbook-style objects that respond to player input, etc.
-- @int __frameNumber The frame to draw from the current state. This is not an imagetable index. Entering `1` will draw the selected state's `startFrame`.
-- @string[opt=self.currentName] __stateName The specific state to pull the __frameNumber from.
-- @number[opt=0] __x
-- @number[opt=0] __y
-- @param[opt=self.direction] __direction Override the current direction.
function animation:drawFrame(__frameNumber, __stateName, __x, __y, __direction)
local x = __x or 0
local y = __y or 0
local stateName = __stateName or self.currentName
local direction = __direction or self.direction
local frameNumber = self[stateName].startFrame - 1 + __frameNumber
self.imageTable:drawImage(frameNumber, x, y, direction)
end
return animation
end
--- Constants
-- @section constants
--- A re-contextualized instance of `playdate.graphics.kImageUnflipped`
Noble.Animation.DIRECTION_RIGHT = Graphics.kImageUnflipped
--- A re-contextualized instance of `playdate.graphics.kImageFlippedX`
Noble.Animation.DIRECTION_LEFT = Graphics.kImageFlippedX

View File

@@ -0,0 +1,108 @@
--- Engine-specific error handling.
-- Noble Engine overrides/supersedes some Playdate SDK behavior. A "bonk" is what happens when your game breaks the engine.
--
-- Most bonks will throw during normal operation, but others ("debug bonks") introduce some execution overhead so are not
-- checked by default.
--
-- @module Noble.Bonk
--
Noble.Bonk = {}
local bonksAreSetup = false
local checkingForBonks = false
local debugBonks = {}
-- You cannot run this directly. Run Noble.new() with __enableDebugBonkChecking as true to enable debug bonk checking.
function Noble.Bonk.enableDebugBonkChecking()
if (bonksAreSetup == false) then
if (Noble.engineInitialized() == false) then
debugBonks.update = playdate.update
debugBonks.pause = playdate.gameWillPause
debugBonks.resume = playdate.gameWillResume
debugBonks.crankDocked = playdate.crankDocked
debugBonks.crankUndocked = playdate.crankUndocked
bonksAreSetup = true
else
error("BONK-BONK: You cannot run this directly. Run Noble.new() with __enableDebugBonkChecking as true.")
end
else
print("BONK-BONK: You have already run Noble.new() with __enableDebugBonkChecking as true, you cannot run this directly.")
end
end
--- Begin checking for debug bonks, <strong>on every frame</strong>. This introduces needless overhead, so don't do it in a release build.
-- You can only run this if you ran previously `Noble.new()` with `__enableDebugBonkChecking` as true, which you should also not do in a release build.
-- @see Noble.new
function Noble.Bonk.startCheckingDebugBonks()
if (checkingForBonks == false) then
if (bonksAreSetup) then
checkingForBonks = true
else
error("BONK-BONK: You cannot run this unless debug bonk checking is enabled.")
end
end
end
--- Stop checking for debug bonks on every frame.
--
-- <strong>NOTE: You can only run this if debug bonk checking is enabled.</strong>
function Noble.Bonk.stopCheckingDebugBonks()
if (checkingForBonks) then
if (bonksAreSetup) then
checkingForBonks = false
else
error("BONK-BONK: You cannot run this unless debug bonk checking is enabled.")
end
end
end
--- Disable the ability to check for debug bonks. It frees up some memory. Once you disable debug bonk checking, you cannot re-enable it.
--
-- <strong>NOTE: You can only run this if debug bonk checking is enabled.</strong>
function Noble.Bonk.disableDebugBonkChecking()
if (bonksAreSetup) then
debugBonks = {}
bonksAreSetup = false
else
error("BONK-BONK: You cannot run this unless debug bonk checking is enabled.")
end
end
--- Are we debug bonk checking for debug bonks?
-- @treturn bool
function Noble.Bonk.checkingDebugBonks()
return checkingForBonks
end
--- Manually check for debug bonks.
-- This method runs every frame when `checkingDebugBonks` is true, but you may call it manually instead.
--
-- <strong>NOTE: You can only run this if debug bonk checking is enabled.</strong>
function Noble.Bonk.checkDebugBonks()
if (playdate.crankDocked ~= debugBonks.crankDocked) then
error("BONK: Don't manually define playdate.crankDocked(). Create a crankDocked() inside of an inputHandler instead.")
end
if (playdate.crankUndocked ~= debugBonks.crankUndocked) then
error("BONK: Don't manually define playdate.crankUndocked(). Create a crankUndocked() inside of an inputHandler instead.")
end
if (playdate.update ~= debugBonks.update) then
error("BONK: Don't manually define playdate.update(). Put update code in your scenes' update() methods instead.")
end
if (playdate.gameWillPause ~= debugBonks.pause) then
error("BONK: Don't manually define playdate.gameWillPause(). Put pause code in your scenes' pause() methods instead.")
end
if (playdate.gameWillResume ~= debugBonks.resume) then
error("BONK: Don't manually define playdate.gameWillResume(). Put resume code in your scenes' resume() methods instead.")
end
if (Graphics.sprite.getAlwaysRedraw() == false) then
error("BONK: Don't use Graphics.sprite.setAlwaysRedraw(false) unless you know what you're doing...")
end
if (Noble.currentScene.backgroundColor == Graphics.kColorClear) then
error("BONK: Don't set a scene's backgroundColor to Graphics.kColorClear, silly.")
end
end

View File

@@ -0,0 +1,327 @@
--- Operations for game data / save slots.
-- @module Noble.GameData
--
Noble.GameData = {}
local gameDatas = {} -- This is the actual "GameDatas" object, which holds multiple GameData slots. We keep it local to avoid direct tampering.
local gameDataDefault = nil
local numberOfGameDataSlotsAtSetup = 1
local numberOfSlots = 1
local currentSlot = 1 -- This is a helper value, so you don't have to specify a save slot with every GameData operation.
local function keyChange(__dataDefault, __data)
local defaultKeys = {}
local keys = {}
for key, value in pairs(__dataDefault.data) do
table.insert(defaultKeys, key)
end
for key, value in pairs(__data.data) do
table.insert(keys, key)
end
for i = 1, #keys, 1 do
if (defaultKeys[i] ~= keys[i]) then return true end
end
return false
end
local function exists(__gameDataSlot, __key)
-- Check for valid gameSlot.
if (__gameDataSlot > #gameDatas or __gameDataSlot <= 0 ) then
error("BONK: Game Slot number " .. __gameDataSlot .. " does not exist. Use Noble.GameData.addSlot().", 3)
return false
end
if (__key ~= nil) then
-- Check for valid data item.
for key, value in pairs(gameDatas[__gameDataSlot].data) do
if __key == key then
return true
end
end
else
return true
end
error("BONK: Game Datum \"" .. __key .. "\" does not exist. Maybe you spellet it wronlgly.", 3)
return false
end
local function updateTimestamp(__gameData)
__gameData.timestamp = playdate.getGMTTime()
end
local gameDataHasBeenSetup = false
--- Sets up the GameDatas (save slots) for your game, and/or loads any existing GameDatas from disk.
-- You can only run this once, ideally in your main.lua before you load your first scene.
-- @tparam table __keyValuePairs All the data items for a saved game, and their default values, as key/value pairs. <strong>NOTE:</strong> Do not use "nil" as a value.
-- @int[opt=1] __numberOfSlots If you want multiple save slots, enter an integer here. You can add additional slots later.
-- @bool[opt=true] __saveToDisk Saves your default values immediately to disk.
-- @bool[opt=true] __modifyExistingOnKeyChange Updates the existing gameData objects on disk if you make changes to your keys (not values) during development or when updating your game.
-- @usage
-- Noble.GameData.setup(
-- {
-- name = "",
-- checkpointReached = 0,
-- score = 0
-- },
-- 3,
-- true,
-- true
-- )
-- Noble.GameData.set("name", "Game A", 1)
-- Noble.GameData.set("name", "Game B", 2)
-- Noble.GameData.set("name", "Game C", 3)
-- @see addSlot
-- @see deleteSlot
function Noble.GameData.setup(__keyValuePairs, __numberOfSlots, __saveToDisk, __modifyExistingOnKeyChange)
if (gameDataHasBeenSetup) then
error("BONK: You can only run Noble.GameData.setup() once.")
return
else
gameDataHasBeenSetup = true
end
numberOfSlots = __numberOfSlots or numberOfSlots
numberOfGameDataSlotsAtSetup = numberOfSlots
local saveToDisk = Utilities.handleOptionalBoolean(__saveToDisk, true)
local modifyExistingOnKeyChange = Utilities.handleOptionalBoolean(__modifyExistingOnKeyChange, true)
gameDataDefault = {
data = __keyValuePairs,
timestamp = playdate.getGMTTime()
}
local createdNewData = false
-- Noble Engine checks on disk for GameDatas, including ones that were
-- added with addSlot, but it assumes your game will have no greater than 1000 of them.
for i = 1, 1000, 1 do
-- We use a local here to avoid adding a nil item to the gameDatas table.
local gameData = Datastore.read("Game" .. i)
if (gameData == nil) then
if (i <= numberOfSlots) then
-- No gameData on disk, so we create a new ones using default values
-- up to the numberOfGameDataSlots.
gameDatas[i] = table.deepcopy(gameDataDefault)
createdNewData = true
else
-- We can't find any more GameDatas on disk, so we update the
-- value of numberOfGameDataSlots if necessary and get outta town!
numberOfSlots = i - 1
print("Total number of game slots: " .. numberOfSlots)
if (saveToDisk and createdNewData) then
Noble.GameData.saveAll()
end
return -- This is our only way out!
end
else
-- We found a gameData on disk, so we use it (either as-is or modified by a key change).
if (modifyExistingOnKeyChange and keyChange(gameDataDefault, gameData)) then
-- Found gameData on disk, but key changes have been made...
-- ...so we start with a new one, with default values...
local existingGameData = table.deepcopy(gameData)
gameData = table.deepcopy(gameDataDefault)
for key, _ in pairs(gameData.data) do
-- ...then copy data with unchanged keys to the new object,
-- naturally discarding keys that don't exist anymore.
if (existingGameData.data[key] ~= nil) then gameData.data[key] = existingGameData.data[key] end
end
gameDatas.timestamp = existingGameData.timestamp
createdNewData = true
end
gameDatas[i] = gameData
end
end
end
--- Returns the value of the requested data item.
-- @string __dataItemName The name of the data item.
-- @int[opt] __gameDataSlot If set, uses a specific GameData slot. If not, uses the most recently touched GameData slot.
-- @treturn any
-- @usage Noble.GameData.get("equippedItem")
-- @usage Noble.GameData.get("equippedItem", 2)
function Noble.GameData.get(__dataItemName, __gameDataSlot)
currentSlot = __gameDataSlot or currentSlot
if (exists(currentSlot, __dataItemName)) then
return gameDatas[currentSlot].data[__dataItemName]
end
end
--- Set the value of a GameData item.
-- @string __dataItemName The name of the data item.
-- @tparam any __value The data item's new value
-- @int[opt] __gameDataSlot If set, uses a specific GameData slot. If not, uses the most recently touched GameData slot.
-- @bool[opt=true] __saveToDisk Saves to disk immediately. Set to false if you prefer to manually save (via a checkpoint or menu).
-- @bool[opt=true] __updateTimestamp Sets the timestamp of this GameData to the current time. Leave false to retain existing timestamp.
-- @usage Noble.GameData.set("score", 74205)
-- @usage Noble.GameData.set("score", Noble.GameData.get("score") + 100)
-- @see save
function Noble.GameData.set(__dataItemName, __value, __gameDataSlot, __saveToDisk, __updateTimestamp)
currentSlot = __gameDataSlot or currentSlot
if (exists(currentSlot, __dataItemName)) then
gameDatas[currentSlot].data[__dataItemName] = __value
local setTimestamp = Utilities.handleOptionalBoolean(__updateTimestamp, true)
if (setTimestamp) then updateTimestamp(gameDatas[currentSlot]) end
local saveToDisk = Utilities.handleOptionalBoolean(__saveToDisk, true)
if (saveToDisk) then Noble.GameData.save() end
end
end
--- Reset a GameData item to its default value, defined in @{setup|setup}.
-- @string __dataItemName The name of the data item.
-- @int[opt] __gameDataSlot If set, uses a specific GameData slot. If not, uses the most recently touched GameData slot.
-- @bool[opt=true] __saveToDisk Saves to disk immediately. Set to false if you prefer to manually save (via a checkpoint or menu).
-- @bool[opt=true] __updateTimestamp Resets the timestamp of this GameData to the current time. Leave false to retain existing timestamp.
-- @see save
function Noble.GameData.reset(__dataItemName, __gameDataSlot, __saveToDisk, __updateTimestamp)
currentSlot = __gameDataSlot or currentSlot
if (exists(currentSlot, __dataItemName)) then
gameDatas[currentSlot].data[__dataItemName] = gameDataDefault.data[__dataItemName]
local setTimestamp = Utilities.handleOptionalBoolean(__updateTimestamp, true)
if (setTimestamp) then updateTimestamp(gameDatas[currentSlot]) end
local saveToDisk = Utilities.handleOptionalBoolean(__saveToDisk, true)
if (saveToDisk) then Noble.GameData.save(currentSlot) end
end
end
--- Reset all values in a GameData slot to the default values, defined in @{setup|setup}.
-- @int[opt] __gameDataSlot If set, uses a specific GameData slot. If not, uses the most recently touched GameData slot.
-- @bool[opt=true] __saveToDisk Saves to disk immediately. Set to false if you prefer to manually save (via a checkpoint or menu).
-- @bool[opt=true] __updateTimestamp Resets the timestamp of this GameData to the current time. Leave false to retain existing timestamp.
-- @see save
function Noble.GameData.resetAll(__gameDataSlot, __saveToDisk, __updateTimestamp)
currentSlot = __gameDataSlot or currentSlot
for key, _ in pairs(gameDatas[currentSlot].data) do
gameDatas[currentSlot].data[key] = gameDataDefault.data[key]
end
local setTimestamp = Utilities.handleOptionalBoolean(__updateTimestamp, true)
if (setTimestamp) then updateTimestamp(gameDatas[currentSlot]) end
local saveToDisk = Utilities.handleOptionalBoolean(__saveToDisk, true)
if (saveToDisk) then Noble.GameData.save(currentSlot) end
end
--- Add a save slot to your game. This is useful for games which have arbitrary save slots, or encourage save scumming.
-- @int[opt=1] __numberToAdd What it says on the tin.
-- @bool[opt=true] __saveToDisk Saves to disk immediately. Set to false if you prefer to manually save (via a checkpoint or menu).
-- @usage Noble.GameData.addSlot()
-- @usage Noble.GameData.addSlot(10)
function Noble.GameData.addSlot(__numberToAdd, __saveToDisk)
local numberToAdd = __numberToAdd or 1
local saveToDisk = Utilities.handleOptionalBoolean(__saveToDisk, true)
if (__numberToAdd < 1) then error ("BONK: Don't use a number smaller than 1, silly.", 2) return end
for i = 1, numberToAdd, 1 do
local newGameData = table.deepcopy(gameDataDefault)
updateTimestamp(newGameData)
table.insert(gameDatas, newGameData)
if (saveToDisk) then Noble.GameData.save() end
end
numberOfSlots = numberOfSlots + numberToAdd
print("Added " .. numberToAdd .. " GameData slots. Total GameData slots: " .. numberOfSlots)
end
--- Deletes a GameData from disk if its save slot is greater than the default number established in `setup`.
-- Otherwise, resets all data items to default values using @{resetAll|resetAll}.
--
-- Generally, you won't need this unless you've added save slots using @{addSlot|addSlot}. In other cases, use @{resetAll|resetAll}.
-- @int __gameDataSlot The slot holding the GameData to delete. Unlike other methods that take this argument, this is not optional.
-- @bool[opt=true] __collapseGameDatas Re-sorts the gameDatas table (and renames existing JSON files on disk) to fill the gap left by the deleted GameData.
-- @see deleteAllSlots
-- @see addSlot
-- @see resetAll
-- @usage Noble.GameData.deleteSlot(6)
-- @usage Noble.GameData.deleteSlot(15, false)
function Noble.GameData.deleteSlot(__gameDataSlot, __collapseGameDatas)
if (__gameDataSlot == nil) then
error("BONK: You must specify a GameData slot to delete.")
end
local collapseGameDatas = Utilities.handleOptionalBoolean(__collapseGameDatas, true)
if (exists(__gameDataSlot)) then
if (__gameDataSlot > numberOfGameDataSlotsAtSetup) then
-- If this GameData is not one of the default ones from setup(), it is removed from disk.
if (playdate.file.exists("Game" .. __gameDataSlot)) then
gameDatas[__gameDataSlot] = nil -- Clear from memory.
Datastore.delete("Game" .. __gameDataSlot) -- Clear from disk.
if (currentSlot == __gameDataSlot) then currentSlot = 1 end -- Safety!
if (collapseGameDatas) then
-- Collapse GameDatas
local newGameDatas = {}
for i = 1, numberOfSlots do
if(gameDatas[i] ~= nil) then
table.insert(newGameDatas, gameDatas[i])
if (i >= __gameDataSlot) then
playdate.file.rename("Game" .. __gameDataSlot, "Game" .. __gameDataSlot-1)
end
end
end
gameDatas = newGameDatas
numberOfSlots = numberOfSlots - 1
end
else
error("BONK: This GameData is in memory, but its file doesn't exist on disk, so it can't be deleted.", 2)
end
else
-- If this GameData is one of the default ones from setup(), it is reset to default values.
Noble.GameData.resetAll(__gameDataSlot)
end
end
end
--- Deletes all GameDatas from disk, except for the number specified in setup, which are reset to default values.
-- Use this to clear all data as if you were running setup again.
-- Generally, you don't need this unless you've added save slots using @{addSlot|addSlot}. In other cases, use @{resetAll|resetAll} on each slot.
-- @see deleteSlot
-- @see addSlot
function Noble.GameData.deleteAllSlots()
local numberToDelete = numberOfSlots
for i = numberToDelete, 1, -1 do
Noble.GameData.deleteSlot(i, false) -- Don't need to collapse the table because we're deleting them in reverse order.
end
end
--- Returns the timestamp of the requested GameData, as a tuple (local time, GMT time). The timestamp is updated
-- See Playdate SDK for details on how a time object is formatted. <strong>NOTE: Timestamps are stored internally in GMT.</strong>
-- @int __gameDataSlot The GameData slot to get the timestamp of. Unlike other methods that take this argument, this is not optional.
-- @treturn table Local time
-- @treturn table GMT time
-- @usage Noble.GameData.getTimestamp(1)
function Noble.GameData.getTimestamp(__gameDataSlot)
if (exists(__gameDataSlot)) then
return playdate.timeFromEpoch(playdate.epochFromGMTTime(gameDatas[__gameDataSlot].timestamp)), gameDatas[__gameDataSlot].timestamp
end
end
--- Returns the current number of GameData slots.
-- @treturn int
function Noble.GameData.getNumberOfSlots() return numberOfSlots end
--- Returns the number of the current GameData slot.
-- @treturn int
function Noble.GameData.getCurrentSlot() return currentSlot end
--- Saves a single GameData to disk. If you want to save all GameDatas, use @{saveAll|saveAll} instead.
-- @int[opt] __gameDataSlot If set, uses a specific GameData slot. If not, uses the most recently touched GameData slot.
-- @see saveAll
-- @usage Noble.GameData.save()
-- @usage Noble.GameData.save(3)
function Noble.GameData.save(__gameDataSlot)
local gameDataSlot = __gameDataSlot or currentSlot
if (exists(gameDataSlot)) then
currentSlot = gameDataSlot
Datastore.write(gameDatas[currentSlot], "Game" .. currentSlot)
end
end
--- Save all GameDatas to disk. If you only have one, or want to save a specific one, use @{save|save} instead.
-- @see save
function Noble.GameData.saveAll()
for i = 1, numberOfSlots, 1 do
Datastore.write(gameDatas[i], "Game" .. currentSlot)
end
end

View File

@@ -0,0 +1,333 @@
--- A complete encapsulation of the Playdate's input system. The Playdate SDK gives developers multiple ways to manage input. Noble Engine's approach revolves around the SDK's "inputHandlers," extending them to include additional input methods, and pull in other hardware functions that the SDK puts elsewhere. See usage below for the full list of supported methods.
-- <br><br>By default, Noble Engine assumes each scene will have an inputManager assigned to it. So, for example, you can define one inputManager for menu screens and another for gameplay scenes in your `main.lua`, and then in each scene, set which one that scene uses. You can instead define a unique inputHandler in each scene.
-- <br><br>You may also create and manage inputManagers within and outside of scenes. When a NobleScene is loaded, its inputHandler will become active, thus, inputHandlers do not carry across scenes, and all input is suspended during scene transitions. An advanced use-case is to leave a scene's inputHandler as nil, and manage it separately.
-- <br><br><strong>NOTE:</strong> While the Playdate SDK allows you to stack as many inputHandlers as you want, Noble Engine assumes only one <em>active</em> inputHandler at a time. You may still manually call `playdate.inputHandlers.push()` and `playdate.inputHandlers.pop()` yourself, but Noble Engine will not know about it and it may cause unexpected behavior.
-- <br><br>In addition, you may directly query button status using the SDK's methods for that, but it is not advised to use that as the primary way to manage input for Noble Engine projects, because much of Noble.Input's functionality will not apply.
-- @module Noble.Input
-- @usage
-- local myInputHandler = {
-- AButtonDown = function() end, -- Fires once when button is pressed down.
-- AButtonHold = function() end, -- Fires each frame while a button is held (Noble Engine implementation).
-- AButtonHeld = function() end, -- Fires once after button is held for 1 second (available for A and B).
-- AButtonUp = function() end, -- Fires once when button is released.
-- BButtonDown = function() end,
-- BButtonHold = function() end,
-- BButtonHeld = function() end,
-- BButtonUp = function() end,
-- downButtonDown = function() end,
-- downButtonHold = function() end,
-- downButtonUp = function() end,
-- leftButtonDown = function() end,
-- leftButtonHold = function() end,
-- leftButtonUp = function() end,
-- rightButtonDown = function() end,
-- rightButtonHold = function() end,
-- rightButtonUp = function() end,
-- upButtonDown = function() end,
-- upButtonHold = function() end
-- upButtonUp = function() end,
--
-- cranked = function(change, acceleratedChange) end, -- See Playdate SDK.
-- crankDocked = function() end, -- Noble Engine implementation.
-- crankUndocked = function() end, -- Noble Engine implementation.
--
-- orientationChanged = function() end -- Noble Engine implementation.
-- }
-- @see NobleScene.inputHandler
--
Noble.Input = {}
local currentHandler = {}
--- Get the currently active input handler. Returns nil if none are active.
-- @treturn table A table of callbacks which handle input events.
-- @see NobleScene.inputHandler
function Noble.Input.getHandler()
return currentHandler
end
--- Use this to change the active inputHandler.
-- <br><br>Enter `nil` to disable input. Use @{setEnabled} to disable/enable input without losing track of the current inputHandler.
-- @tparam[opt=nil] table __inputHandler A table of callbacks which handle input events.
-- @see NobleScene.inputHandler
-- @see clearHandler
-- @see setEnabled
function Noble.Input.setHandler(__inputHandler)
if (currentHandler ~= nil) then
playdate.inputHandlers.pop()
end
if (__inputHandler == nil) then
currentHandler = nil
else
currentHandler = __inputHandler
playdate.inputHandlers.push(__inputHandler, true) -- The Playdate SDK allows for multiple inputHandlers to mix and match methods. Noble Engine removes this functionality.
end
end
--- A helper function that calls Noble.Input.setHandler() with no argument.
-- @see setHandler
function Noble.Input.clearHandler()
Noble.Input.setHandler()
end
local cachedInputHandler = nil
--- Enable and disable user input without dealing with inputHanders.
-- The Playdate SDK requires removing all inputHanders to halt user input, so while the currentHandler is cleared when `false` is passed to this method,
-- it is cached so it can be later re-enabled by passing `true` it.
-- @bool __value Set to false to halt input. Set to true to resume accepting input.
-- @see getHandler
-- @see clearHandler
function Noble.Input.setEnabled(__value)
local value = Utilities.handleOptionalBoolean(__value, true)
if (value == true) then
Noble.Input.setHandler(cachedInputHandler or currentHandler)
cachedInputHandler = nil
else
cachedInputHandler = currentHandler
Noble.Input.clearHandler()
end
end
--- Checks to see that there is an active inputHandler
-- @treturn bool Returns true if the input system is enabled. Returns false if `setEnabled(false)` was used, or if currentHandler is `nil`.
function Noble.Input.getEnabled()
return cachedInputHandler == nil
end
local crankIndicatorActive = false
local crankIndicatorForced = false
--- Enable/disable on-screen system crank indicator.
--
-- <strong>NOTE: The indicator will only ever show if the crank is docked, unless `__evenWhenUndocked` is true.</strong>
-- @bool __active Set true to start showing the on-screen crank indicator. Set false to stop showing it.
-- @bool[opt=false] __evenWhenUndocked Set true to show the crank indicator even if the crank is already undocked (`__active` must also be true).
function Noble.Input.setCrankIndicatorStatus(__active, __evenWhenUndocked)
if (__active) then
UI.crankIndicator:start()
end
crankIndicatorActive = __active
crankIndicatorForced = Utilities.handleOptionalBoolean(__evenWhenUndocked, false)
end
--- Checks whether the system crank indicator status. Returns a tuple.
--
-- @treturn bool Is the crank indicator active?
-- @treturn bool Is the crank indicator being forced when active, even when the crank is undocked?
-- @see setCrankIndicatorStatus
function Noble.Input.getCrankIndicatorStatus()
return crankIndicatorActive, crankIndicatorForced
end
-- Noble Engine defines extra "buttonHold" methods that run every frame that a button is held down, but to implement them, we need to do some magic.
local buttonHoldBufferAmount = 3 -- This is how many frames to wait before the engine determines that a button is being held down. Using !buttonJustPressed() provides only 1 frame, which isn't enough.
local AButtonHoldBufferCount = 0
local BButtonHoldBufferCount = 0
local upButtonHoldBufferCount = 0
local downButtonHoldBufferCount = 0
local leftButtonHoldBufferCount = 0
local rightButtonHoldBufferCount = 0
-- Store the latest orientation in order to know when to run the orientationChanged callback
local orientation = nil
local accelerometerValues = nil
--- Checks the current display orientation of the device. Returns a tuple.
-- If the accelerometer is not currently enabled, this method will turn it on, return current values, and then turn it off.
-- If you are trying to get raw accelerometer values rather than the display orientation, you may want to use `playdate.readAccelerometer()` instead.
-- @bool[opt=false] __getStoredValues If true, this method will simply return the most recently stored values, rather than use the accelerometer to check for new ones.
-- @treturn str The named orientation of the device (a pseudo enum Noble.Input.ORIENTATION_XX)
-- @treturn list Accelerometer values, where list[1] is x, list[2] is y and list[3] is z
-- @see Noble.Input.ORIENTATION_UP
-- @see Noble.Input.ORIENTATION_DOWN
-- @see Noble.Input.ORIENTATION_LEFT
-- @see Noble.Input.ORIENTATION_RIGHT
function Noble.Input.getOrientation(__getStoredValues)
local getStoredValues = Utilities.handleOptionalBoolean(__getStoredValues, false)
if (not getStoredValues) then
local turnOffAfterUse = false
if (not playdate.accelerometerIsRunning()) then
playdate.startAccelerometer()
turnOffAfterUse = true
end
local x, y, z = playdate.readAccelerometer()
if (turnOffAfterUse) then
playdate.stopAccelerometer()
end
local newOrientation = nil
if (x <= -0.7) then
newOrientation = Noble.Input.ORIENTATION_LEFT
elseif (x >= 0.7) then
newOrientation = Noble.Input.ORIENTATION_RIGHT
elseif (y <= -0.3) then
newOrientation = Noble.Input.ORIENTATION_DOWN
else
newOrientation = Noble.Input.ORIENTATION_UP
end
accelerometerValues = {x, y, z}
if (newOrientation ~= orientation) then
if (currentHandler.orientationChanged ~= nil) then
currentHandler.orientationChanged(orientation, accelerometerValues)
end
orientation = newOrientation
end
end
return orientation, accelerometerValues
end
-- Do not call this method directly, or modify it, thanks. :-)
function Noble.Input.update()
if (currentHandler == nil) then return end
if (currentHandler.AButtonHold ~= nil) then
if (playdate.buttonIsPressed(playdate.kButtonA)) then
if (AButtonHoldBufferCount >= buttonHoldBufferAmount) then currentHandler.AButtonHold(AButtonHoldBufferCount) end -- Execute!
AButtonHoldBufferCount = AButtonHoldBufferCount + 1 -- Wait another frame!
end
if (playdate.buttonJustReleased(playdate.kButtonA)) then AButtonHoldBufferCount = 0 end -- Reset!
end
if (currentHandler.BButtonHold ~= nil) then
if (playdate.buttonIsPressed(playdate.kButtonB)) then
if (BButtonHoldBufferCount >= buttonHoldBufferAmount) then currentHandler.BButtonHold(BButtonHoldBufferCount) end
BButtonHoldBufferCount = BButtonHoldBufferCount + 1
end
if (playdate.buttonJustReleased(playdate.kButtonB)) then BButtonHoldBufferCount = 0 end
end
if (currentHandler.upButtonHold ~= nil) then
if (playdate.buttonIsPressed(playdate.kButtonUp)) then
if (upButtonHoldBufferCount >= buttonHoldBufferAmount) then currentHandler.upButtonHold(upButtonHoldBufferCount) end
upButtonHoldBufferCount = upButtonHoldBufferCount + 1
end
if (playdate.buttonJustReleased(playdate.kButtonUp)) then upButtonHoldBufferCount = 0 end
end
if (currentHandler.downButtonHold ~= nil) then
if (playdate.buttonIsPressed(playdate.kButtonDown)) then
if (downButtonHoldBufferCount >= buttonHoldBufferAmount) then currentHandler.downButtonHold(downButtonHoldBufferCount) end
downButtonHoldBufferCount = downButtonHoldBufferCount + 1
end
if (playdate.buttonJustReleased(playdate.kButtonDown)) then downButtonHoldBufferCount = 0 end
end
if (currentHandler.leftButtonHold ~= nil) then
if (playdate.buttonIsPressed(playdate.kButtonLeft)) then
if (leftButtonHoldBufferCount >= buttonHoldBufferAmount) then currentHandler.leftButtonHold(leftButtonHoldBufferCount) end
leftButtonHoldBufferCount = leftButtonHoldBufferCount + 1
end
if (playdate.buttonJustReleased(playdate.kButtonLeft)) then leftButtonHoldBufferCount = 0 end
end
if (currentHandler.rightButtonHold ~= nil) then
if (playdate.buttonIsPressed(playdate.kButtonRight)) then
if (rightButtonHoldBufferCount >= buttonHoldBufferAmount) then currentHandler.rightButtonHold(rightButtonHoldBufferCount) end
rightButtonHoldBufferCount = rightButtonHoldBufferCount + 1
end
if (playdate.buttonJustReleased(playdate.kButtonRight)) then rightButtonHoldBufferCount = 0 end
end
if (playdate.accelerometerIsRunning()) then
Noble.Input.getOrientation()
end
end
-- Do not call this method directly, or modify it, thanks. :-)
function playdate.crankDocked()
if (currentHandler ~= nil and currentHandler.crankDocked ~= nil and Noble.Input.getEnabled() == true) then
currentHandler.crankDocked()
end
end
-- Do not call this method directly, or modify it, thanks. :-)
function playdate.crankUndocked()
if (currentHandler ~= nil and currentHandler.crankUndocked ~= nil and Noble.Input.getEnabled() == true) then
currentHandler.crankUndocked()
end
end
--- Constants
-- . A set of constants referencing device inputs, stored as strings. Can be used for querying button input,
-- but are mainly for on-screen prompts or other elements where a string literal is useful, such as a filename, GameData value, or localization key.
-- For faster performance, use the ones that exist in the Playdate SDK (i.e.: `playdate.kButtonA`), which are stored as binary numbers.
-- @usage
-- function newPrompt(__input, __promptString)
-- -- ...
-- local icon = Graphics.image.new("assets/images/UI/Icon_" .. __input)
-- -- ...
-- end
--
-- promptMove = newPrompt(Noble.Input.DPAD_HORIZONTAL, "Move!") -- assets/images/UI/Icon_dPadHorizontal.png"
-- promptJump = newPrompt(Noble.Input.BUTTON_A, "Jump!") -- assets/images/UI/Icon_buttonA.png"
-- promptCharge = newPrompt(Noble.Input.CRANK_FORWARD, "Charge the battery!") -- assets/images/UI/Icon_crankForward.png"
-- @section constants
--- `"buttonA"`
Noble.Input.BUTTON_A = "buttonA"
--- `"buttonB"`
Noble.Input.BUTTON_B = "buttonB"
--- The system menu button.
--
-- `"buttonMenu"`
Noble.Input.BUTTON_MENU = "buttonMenu"
--- Referencing the D-pad component itself, rather than an input.
--
-- `"dPad"`
Noble.Input.DPAD = "dPad"
--- Referencing the left and right input D-pad inputs.
--
-- `"dPadHorizontal"`
Noble.Input.DPAD_HORIZONTAL = "dPadHorizontal"
--- Referencing the up and down input D-pad inputs.
--
-- `"dPadVertical"`
Noble.Input.DPAD_VERTICAL = "dPadVertical"
--- `"dPadUp"`
Noble.Input.DPAD_UP = "dPadUp"
--- `"dPadDown"`
Noble.Input.DPAD_DOWN = "dPadDown"
--- `"dPadLeft"`
Noble.Input.DPAD_LEFT = "dPadLeft"
--- `"dPadRight"`
Noble.Input.DPAD_RIGHT = "dPadRight"
--- Referencing the crank component itself, rather than an input.
--
-- `"crank"`
Noble.Input.CRANK = "crank"
--- AKA: Clockwise. See Playdate SDK.
--
-- `"crankForward"`
Noble.Input.CRANK_FORWARD = "crankForward"
--- AKA: Anticlockwise. See Playdate SDK.
--
-- `"crankReverse"`
Noble.Input.CRANK_REVERSE = "crankReverse"
--- Referencing the action of docking the crank.
--
-- `"crankDock"`
Noble.Input.CRANK_DOCK = "crankDock"
--- Referencing the action of undocking the crank.
--
-- `"crankUndock"`
Noble.Input.CRANK_UNDOCK = "crankUndock"
--- Referencing the display orientations.
--
-- `"orientationUp"`
Noble.Input.ORIENTATION_UP = "orientationUp"
-- `"orientationDown"`
Noble.Input.ORIENTATION_DOWN = "orientationDown"
-- `"orientationLeft"`
Noble.Input.ORIENTATION_LEFT = "orientationLeft"
-- `"orientationRight"`
Noble.Input.ORIENTATION_RIGHT = "orientationRight"

View File

@@ -0,0 +1,570 @@
--- An extended implementation of `playdate.ui.gridview`, meant for 1-dimensional, single-screen text menus.
-- @module Noble.Menu
--
Noble.Menu = {}
--- Setup
-- @section setup
--- Create a new menu object.
-- @bool[opt=true] __activate @{activate|Activate} this menu upon creation.
-- @param[opt=Noble.Text.ALIGN_LEFT] __alignment The text alignment of menu items.
-- @bool[opt=false] __localized If true, menu item names are localization keys rather than display names.
-- @param[opt=Graphics.kColorBlack] __color The color of menu item text. The selected highlight will be the inverse color.
-- @int[opt=2] __padding Cell padding for menu items.
-- @int[opt] __horizontalPadding Use this to override horizontal padding, useful for certain fonts. If nil, uses __padding.
-- @int[opt=2] __margin Spacing between menu items.
-- @param[opt=Noble.Text.getCurrentFont()] __font If nil, uses current set font.
-- @int[opt=__font:getHeight()/4] __selectedCornerRadius Sets rounded corners for a selected menu item.
-- @int[opt=1] __selectedOutlineThickness Sets the outline thickness for selected items.
-- @return `menu`, a new menu item.
-- @usage
-- local menu = Noble.Menu.new(
-- true,
-- Noble.Text.ALIGN_CENTER,
-- false,
-- Graphics.kColorWhite,
-- 4, 6,
-- Noble.Text.large,
-- nil, 3
-- )
-- menu:addItem("Play Game", function() TitleScreen:playGame() end)
-- menu:addItem("Options", function() Noble.transition(OptionsScreen) end)
-- menu:addItem("Credits", function() Noble.transition(CreditsScreen) end)
-- @see addItem
function Noble.Menu.new(__activate, __alignment, __localized, __color, __padding, __horizontalPadding, __margin, __font, __selectedCornerRadius, __selectedOutlineThickness)
-- Prep for creating the gridview object
local paddingLocal = __padding or 2
local fontLocal = __font or Noble.Text.getCurrentFont()
local textHeightLocal = fontLocal:getHeight()
-- Create gridview object
local menu = UI.gridview.new(0, textHeightLocal + paddingLocal)
--- Properties
--@section properties
menu.alignment = __alignment or Noble.Text.ALIGN_LEFT
--- @bool[opt=false] _ Indicates whether this menu's item names are treated as localization keys.
menu.localized = Utilities.handleOptionalBoolean(__localized, false)
menu.textHeight = textHeightLocal
menu.padding = paddingLocal
menu.horizontalPadding = __horizontalPadding or menu.padding
menu.margin = __margin or 2
menu.font = fontLocal
menu.selectedCornerRadius = __selectedCornerRadius or textHeightLocal/4
menu.selectedOutlineThickness = __selectedOutlineThickness or 1
-- Local cleanup. We don't need these anymore.
paddingLocal = nil
fontLocal = nil
textHeightLocal = nil
-- Colors
menu.color = __color or Graphics.kColorBlack -- TO-DO allow for copy fill mode instead of color.
menu.fillMode = Graphics.kDrawModeFillBlack
menu.otherColor = Graphics.kColorWhite
menu.otherFillMode = Graphics.kDrawModeFillWhite
if (menu.color == Graphics.kColorWhite) then
menu.fillMode = Graphics.kDrawModeFillWhite
menu.otherColor = Graphics.kColorBlack
menu.otherFillMode = Graphics.kDrawModeFillBlack
end
-- Gridview properties
menu:setNumberOfColumns(1)
menu:setCellPadding(0, 0, 0, 0)
menu.changeRowOnColumnWrap = false
--- Tables
--@section tables
--- A string "array" of menu item strings/keys.
-- <strong>You cannot add or remove menu items by modifying this table</strong>.
-- It is meant as a <strong>read-only</strong> table, provided for convenience when iterating, etc. Modifying its values may break other methods.
-- @usage
-- for i = 1, #menu.itemNames, 1 do
-- menu.clickHandlers[menu.itemNames[i]] = nil -- Clears all click handlers, for some reason.
-- end
-- @see addItem
menu.itemNames = {}
-- This is an internal table. Modifying its may break other methods.
menu.displayNames = {}
-- This is an internal table. Modifying its may break other methods.
menu.displayNamesAreLocalized = {}
--- A table of functions associated with menu items. Items are a defined when calling @{addItem|addItem}, but their associated functions may be modified afterward.
--
-- <strong>You cannot add or remove menu items by modifying this table</strong>.
-- @usage
-- local menu = Noble.Menu.new(true)
-- menu.addItem("Play Game")
-- menu.addItem("Options")
--
-- menu.clickHandlers["Play Game"] = function() TitleScreen:playGame() end
-- menu.clickHandlers["Options"] = function() Noble.transition(OptionsScreen) end
-- @usage
-- local menu = Noble.Menu.new(true)
-- menu.addItem("Play Game")
-- menu.addItem("Options")
--
-- menu.clickHandlers = {
-- ["Play Game"] = function TitleScreen:playGame() end,
-- ["Options"] = function() Noble.transition(OptionsScreen) end
-- }
-- @see addItem
-- @see removeItem
menu.clickHandlers = {}
--- A key/value table of menu item indices.
--
-- This is meant as a <strong>read-only</strong> table, provided for convenience. Modifying its values will break other methods.
-- @usage
-- menu.itemPositions["Play Game"] -- 1
-- menu.itemPositions["Options"] -- 2
menu.itemPositions = {}
--- A key/value table of pixel widths for each menu item, based on its text. Useful for animation, layout, etc.
--
-- This is meant as a <strong>read-only</strong> table, provided for convenience. Modifying its values will break other methods.
-- @usage local playGameMenuItemWidth = menu.itemWidths["Play Game"]
menu.itemWidths = {}
--- Properties
-- @section properties
--- @int _
-- The current menu item's index.
--
-- This is meant as a <strong>read-only</strong> value. Do not modify it directly.
-- @see select
menu.currentItemNumber = 1
--- @string _
-- The current menu item's name.
--
-- This is meant as a <strong>read-only</strong> value. Do not modify it directly.
-- @see select
menu.currentItemName = menu.itemNames[1]
--- @int _
-- The width of the widest menu item plus the menu's horizontal padding.
--
-- This is meant as a <strong>read-only</strong> value. Do not modify it directly.
menu.width = 0
--- Setup
-- @section setup
--- Adds a item to this menu.
-- @string __nameOrKey The name of this menu item. It can be a display name or a localization key. <strong>Must be unique.</strong>
-- @tparam[opt] function __clickHandler The function that runs when this menu item is "clicked."
-- @int[opt] __position Insert the item at a specific position. If not set, adds to the end of the list.
-- @string[opt] __displayName You can create an optional, separate display name for this item. You can add or change this at runtime via @{setItemDisplayName|setItemDisplayName}.
-- @bool[opt=false] __displayNameIsALocalizationKey If true, will treat the `__displayName` as a localization key. This is separate from this menu's @{localized|localized} value.
-- @see new
-- @see removeItem
-- @see setItemDisplayName
function menu:addItem(__nameOrKey, __clickHandler, __position, __displayName, __displayNameIsALocalizationKey)
local clickHandler = __clickHandler or function () print("Menu item \"" .. __nameOrKey .. "\" clicked!") end
if (__position ~= nil) then
if (__position <= 0 or __position > #self.itemNames) then error("BONK: Menu item out of range.", 3) return end
table.insert(self.itemNames, __position, __nameOrKey)
for key, value in pairs(self.itemPositions) do
if (value >= __position) then
self.itemPositions[key] = self.itemPositions[key] + 1
end
end
self.itemPositions[__nameOrKey] = __position
else
table.insert(self.itemNames, __nameOrKey)
self.itemPositions[__nameOrKey] = #self.itemNames
end
self.clickHandlers[__nameOrKey] = clickHandler
-- Item name
local nameOrKey
if (self.localized) then
nameOrKey = Graphics.getLocalizedText(__nameOrKey)
else
nameOrKey = __nameOrKey
end
-- Display name
local displayName = nil
if (__displayName ~= nil) then
if (__displayNameIsALocalizationKey == true) then
displayName = Graphics.getLocalizedText(__displayName)
else
displayName = __displayName
end
self.displayNames[__nameOrKey] = displayName
end
if (displayName == nil) then
self:updateWidths(__nameOrKey, nameOrKey)
else
self:updateWidths(__nameOrKey, displayName)
end
self:setNumberOfRows(#self.itemNames)
end
-- Internal method.
function menu:updateWidths(__nameOrKey, __string)
if (__string == nil) then
__string = __nameOrKey
end
-- Item width
self.itemWidths[__nameOrKey] = self.font:getTextWidth(__string)
-- Menu width
local width = 0
for _, value in pairs(self.itemWidths) do
if value > width then width = value end
end
self.width = width + (self.horizontalPadding * 2) + (self.selectedOutlineThickness * 2)
end
--- Removes a item from this menu.
-- @tparam[opt=#menu.itemNames] int|string __menuItem The menu item to remove. You can enter either the item's name/key or it's position. If left blank, removes the last item.
-- @see addItem
function menu:removeItem(__menuItem)
local itemString = nil
local itemPosition = nil
if (__menuItem == nil) then
__menuItem = #self.itemNames
end
if (type(__menuItem) == "number") then
if (__menuItem <= 0 or __menuItem > #self.itemNames) then error("BONK: Menu item out of range.", 3) return end
itemString = self.itemNames[__menuItem]
itemPosition = __menuItem
elseif (type(__menuItem) == "string") then
itemString = __menuItem
itemPosition = self.itemPositions[__menuItem]
if (itemPosition == nil) then error("BONK: Menu item not found.", 3) return end
end
for key, value in pairs(self.itemPositions) do
if (value > itemPosition) then
self.itemPositions[key] = self.itemPositions[key] - 1
end
end
table.remove(self.itemNames, itemPosition)
self.itemPositions[itemString] = nil
self.clickHandlers[itemString] = nil
self.itemWidths[itemString] = nil
self.displayNames[itemString] = nil
self.displayNamesAreLocalized[itemString] = nil
-- In case the current item is selected.
if (self.currentItemNumber == itemPosition and self.currentItemNumber ~= 1) then
self:select(self.currentItemNumber - 1, true)
if (self.isActive() == false) then
self:setSelectedRow(0)
end
end
self:setNumberOfRows(#self.itemNames)
-- Update width
local width = 0
for _, value in pairs(self.itemWidths) do
if value > width then width = value end
end
self.width = width + (self.horizontalPadding * 2) + (self.selectedOutlineThickness * 2)
end
--
local active = Utilities.handleOptionalBoolean(__activate, true)
if (active) then
menu.currentItemNumber = 1
menu.currentItemName = menu.itemNames[1]
menu:setSelectedRow(1)
else
menu:setSelectedRow(0)
end
--- Methods
--@section methods
--- Activate this menu.
-- This selects the most recently selected menu item (or the first item if none have been previously selected), and enables this menu's @{selectPrevious|selectPrevious}, @{selectNext|selectNext}, and @{click|click} methods.
-- @usage
-- local menu = Noble.Menu.new(false)
-- menu:activate()
function menu:activate()
active = true
self:select(self.currentItemNumber)
end
--- Deactivate this menu.
-- This deselects all menu items, and disables this menu's @{selectPrevious|selectPrevious}, @{selectNext|selectNext}, and @{click|click} methods.
-- @usage
-- local menu = Noble.Menu.new(true)
-- menu:deactivate()
function menu:deactivate()
self:setSelectedRow(0)
active = false
end
--- Check to see if this menu is currently active.
-- @treturn bool
function menu:isActive()
return active
end
--- Selects the previous item in this menu. <strong>This menu must be active.</strong>
-- @bool[opt=false] __force Force this method to run, even if this menu is not active.
-- @bool[opt=true] __wrapSelection Selects the final menu item if the first menu item is currently selected.
-- @see activate
-- @usage
-- TitleScreen.inputHandler.upButtonDown = function()
-- menu:selectPrevious()
-- end
function menu:selectPrevious(__force, __wrapSelection)
if (self:isActive() or __force) then
local wrapSelection = Utilities.handleOptionalBoolean(__wrapSelection, true)
self:selectPreviousRow(wrapSelection, false, false)
local _, row, _ = self:getSelection()
self.currentItemNumber = row
self.currentItemName = self.itemNames[row]
end
end
--- Selects the next previous item in this menu. <strong>This menu must be active.</strong>
-- @bool[opt=false] __force Force this method to run, even if this menu is not active.
-- @bool[opt=true] __wrapSelection Selects the first menu item if the final menu item is currently selected.
-- @see activate
-- @usage
-- TitleScreen.inputHandler.downButtonDown = function()
-- menu:selectNext()
-- end
function menu:selectNext(__force, __wrapSelection)
if (self:isActive() or __force) then
local wrapSelection = Utilities.handleOptionalBoolean(__wrapSelection, true)
self:selectNextRow(wrapSelection, false, false)
local _, row, _ = self:getSelection()
self.currentItemNumber = row
self.currentItemName = self.itemNames[row]
end
end
--- Selects a specific item in this menu, either by it's index, or it's name. <strong>This menu must be active.</strong>
-- @tparam int|string __menuItem The menu item to select. You can enter the item's number or it's name/key.
-- @bool[opt=false] __force Force this method to run, even if this menu is not active.
-- @see activate
-- @usage
-- function resetMenu()
-- menu:select(1, true)
-- menu:deactivate()
-- end
-- @usage
-- function resetMenu()
-- menu:select("Play Game", true)
-- menu:deactivate()
-- end
function menu:select(__menuItem, __force)
if (self:isActive() or __force) then
if (type(__menuItem) == 'number') then
if (__menuItem < 1) then
error("BONK: _menuItem must be a number greater than 0 (or a string).")
end
self:setSelectedRow(__menuItem)
elseif (type(__menuItem) == 'string') then
self:setSelectedRow(self.itemPositions[__menuItem])
else
error("BONK: _menuItem must be a number or string, silly.")
end
local _, row, _ = self:getSelection()
self.currentItemNumber = row
self.currentItemName = self.itemNames[row]
end
end
--- Runs the function associated with the currently selected menu item. <strong>This menu must be active.</strong>
-- @bool[opt=false] __force Force this method to run, even if this menu is not active.
-- @see activate
-- @usage
-- TitleScreen.inputHandler.AButtonDown = function()
-- menu:click()
-- end
function menu:click(__force)
if ((self:isActive() or __force) and self.clickHandlers[self.currentItemName] ~= nil) then
self.clickHandlers[self.currentItemName]()
end
end
--- Gets the display name of a menu item.
--
-- If a menu item does not have a display name, then the `__nameOrKey` (or its localized string) will be returned instead. This method is used internally when @{draw|draw} is called.
--
-- If this menu's `localized` value is true, a returned `__nameOrKey` will always be localized, but a returned display name is only localized if the `__displayNameIsALocalizationKey` argument was set to `true` when the display name was added.
-- @string __itemName The menu item you want the display name of.
-- @treturn string
-- @see addItem
-- @see setItemDisplayName
function menu:getItemDisplayName(__itemName)
if (self.displayNames[__itemName] == nil) then
-- No display name.
if (self.localized) then
return Graphics.getLocalizedText(__itemName)
else
return __itemName
end
else
-- Has display name.
if (self.displayNamesAreLocalized[__itemName] == true) then
return Graphics.getLocalizedText(self.displayNames[__itemName])
else
return self.displayNames[__itemName]
end
end
end
--- When you add a menu item, you can give it a display name that's different from it's actual name. This method adds or changes the display name of a menu item.
-- @string __itemName The menu item name (or key if this menu uses localization keys).
-- @string __displayName The display name.
-- @bool[opt=false] __displayNameIsALocalizationKey Set to use to indicate that this display name is a localization key. This setting is separate from @{localized|localized}
-- @usage
-- function changeDifficultyLevel(__level)
-- menu:setItemDisplayName("Difficulty", "Difficulty: " .. __level)
-- end
function menu:setItemDisplayName(__itemName, __displayName, __displayNameIsALocalizationKey)
self.displayNames[__itemName] = __displayName
self.displayNamesAreLocalized[__itemName] = Utilities.handleOptionalBoolean(__displayNameIsALocalizationKey, false)
local displayName
if (__displayNameIsALocalizationKey == true) then
displayName = Graphics.getLocalizedText(__displayName)
else
displayName = __displayName
end
-- If we're "resetting" the display name by setting it to nil, then use __nameOrKey instead (checking for localization before calling updateWidths)
if (displayName == nil) then
if (self.localized) then
displayName = Graphics.getLocalizedText(__itemName)
else
displayName = __itemName
end
end
self:updateWidths(__itemName, displayName)
end
--- Drawing
--@section drawing
--- Draw's this menu to the screen. You may call this manually, but ideally, you will put it in in your scene's @{NobleScene:update|update} or @{NobleScene:drawBackground|drawBackground} method.
-- @usage
-- function YourScene:update()
-- YourScene.super.update(self)
-- menu:draw(50, 100)
-- end
function menu:draw(__x, __y)
local xAdjustment = 0
if (self.alignment == Noble.Text.ALIGN_CENTER) then
xAdjustment = self.width/2
elseif (self.alignment == Noble.Text.ALIGN_RIGHT) then
xAdjustment = self.width
end
self:drawInRect(__x - xAdjustment, __y, self.width, ((self.textHeight + self.padding + self.margin) * #self.itemNames) + (self.selectedOutlineThickness * 2) - self.margin)
end
--- This method is called for every <strong>non-selected</strong> item when @{draw|draw} is called. You shouldn't call this directly, but you may re-implement it if you wish.
-- @usage
-- -- This is the default implementation for this method.
-- function menu:drawItem(__x, __y, __itemIndex)
-- Graphics.setImageDrawMode(self.fillMode)
-- local xAdjustment = 0
-- if (self.alignment == Noble.Text.ALIGN_CENTER) then
-- xAdjustment = self.width/2 - self.horizontalPadding/2
-- elseif (self.alignment == Noble.Text.ALIGN_RIGHT) then
-- xAdjustment = self.width - self.horizontalPadding
-- end
-- Noble.Text.draw(self.itemNames[__itemIndex], __x + self.horizontalPadding/2 + xAdjustment, __y + self.padding/2, self.alignment, self.localized, self.font)
-- end
-- @see Noble.Text.draw
function menu:drawItem(__x, __y, __itemIndex)
Graphics.setImageDrawMode(self.fillMode)
local xAdjustment = self.selectedOutlineThickness
if (self.alignment == Noble.Text.ALIGN_CENTER) then
xAdjustment = self.width/2 - self.horizontalPadding/2
elseif (self.alignment == Noble.Text.ALIGN_RIGHT) then
xAdjustment = self.width - self.horizontalPadding - self.selectedOutlineThickness
end
Noble.Text.draw(
self:getItemDisplayName(self.itemNames[__itemIndex]),
__x + self.horizontalPadding/2 + xAdjustment, __y + self.padding/2 + self.selectedOutlineThickness + (self.margin * (__itemIndex -1)),
self.alignment, false, self.font
)
end
--- This method is called for every <strong>selected</strong> item when @{draw|draw} is called. You shouldn't call this directly, but you may re-implement it if you wish.
-- @usage
-- -- This is the default implementation for this method.
-- function menu:drawSelectedItem(__x, __y, __itemIndex)
-- local xAdjustmentText = 0
-- local xAdjustmentRect = 0
-- if (self.alignment == Noble.Text.ALIGN_CENTER) then
-- xAdjustmentText = self.width/2 - self.horizontalPadding/2
-- xAdjustmentRect = self.width/2 - self.itemWidths[self.itemNames[__itemIndex]]/2 - self.horizontalPadding/2
-- elseif (self.alignment == Noble.Text.ALIGN_RIGHT) then
-- xAdjustmentText = self.width - self.horizontalPadding
-- xAdjustmentRect = self.width - self.itemWidths[self.itemNames[__itemIndex]] - self.horizontalPadding
-- end
-- Graphics.setColor(self.color)
-- Graphics.fillRoundRect(__x + xAdjustmentRect, __y, self.itemWidths[self.itemNames[__itemIndex]]+self.horizontalPadding, self.textHeight+self.padding, self.selectedCornerRadius)
-- Graphics.setColor(self.otherColor)
-- Graphics.setLineWidth(self.selectedOutlineThickness)
-- Graphics.drawRoundRect(__x + xAdjustmentRect, __y, self.itemWidths[self.itemNames[__itemIndex]]+self.horizontalPadding, self.textHeight+self.padding, self.selectedCornerRadius)
-- Graphics.setImageDrawMode(self.otherFillMode)
-- Noble.Text.draw(self.itemNames[__itemIndex], __x + self.horizontalPadding/2 + xAdjustmentText, __y+self.padding/2, self.alignment, self.localized, self.font)
-- end
-- @see Noble.Text.draw
function menu:drawSelectedItem(__x, __y, __itemIndex)
local xAdjustmentText = self.selectedOutlineThickness
local xAdjustmentRect = self.selectedOutlineThickness
if (self.alignment == Noble.Text.ALIGN_CENTER) then
xAdjustmentText = self.width/2 - self.horizontalPadding/2
xAdjustmentRect = self.width/2 - self.itemWidths[self.itemNames[__itemIndex]]/2 - self.horizontalPadding/2
elseif (self.alignment == Noble.Text.ALIGN_RIGHT) then
xAdjustmentText = self.width - self.horizontalPadding - self.selectedOutlineThickness
xAdjustmentRect = self.width - self.itemWidths[self.itemNames[__itemIndex]] - self.horizontalPadding - self.selectedOutlineThickness
end
Graphics.setColor(self.color)
Graphics.fillRoundRect(__x + xAdjustmentRect, __y + self.selectedOutlineThickness + (self.margin * (__itemIndex -1)), self.itemWidths[self.itemNames[__itemIndex]]+self.horizontalPadding, self.textHeight+self.padding, self.selectedCornerRadius)
Graphics.setColor(self.otherColor)
Graphics.setLineWidth(self.selectedOutlineThickness)
Graphics.drawRoundRect(__x + xAdjustmentRect, __y + self.selectedOutlineThickness + (self.margin * (__itemIndex -1)), self.itemWidths[self.itemNames[__itemIndex]]+self.horizontalPadding, self.textHeight+self.padding, self.selectedCornerRadius)
Graphics.setImageDrawMode(self.otherFillMode)
Noble.Text.draw(
self:getItemDisplayName(self.itemNames[__itemIndex]),
__x + self.horizontalPadding/2 + xAdjustmentText, __y + self.padding/2 + self.selectedOutlineThickness + (self.margin * (__itemIndex -1)),
self.alignment, false, self.font
)
end
-- Don't call or modify this function.
function menu:drawCell(_, row, _, selected, x, y, width, height)
if selected then
self:drawSelectedItem(x, y, row)
else
self:drawItem(x, y, row)
end
end
return menu
end

View File

@@ -0,0 +1,158 @@
--- Operations for game settings / stats.
-- @module Noble.Settings
--
Noble.Settings = {} -- This is the "class" that holds methods.
local settings = nil -- This is the actual settings object. We keep it local to avoid direct tampering.
local settingsDefault = nil -- We keep track of default values so they can be reset.
local function keyChange(__dataDefault, __data)
local defaultKeys = {}
local keys = {}
for key, value in pairs(__dataDefault) do table.insert(defaultKeys, key) end
for key, value in pairs(__data) do table.insert(keys, key) end
for i = 1, #keys, 1 do
if (defaultKeys[i] ~= keys[i]) then return true end
end
return false
end
local function settingExists(__key)
-- Check for valid data item.
for key, value in pairs(settings) do
if __key == key then
return true
end
end
error("BONK: Setting \'" .. __key .. "\' does not exist. Maybe you spellet ti wronlgly.", 3)
return false
end
local settingsHaveBeenSetup = false
--- Sets up the settings for your game. You can only run this once, and you must run it before using other `Noble.Settings` functions. It is recommended to place it in your main.lua, before `Noble.new()`.
--
-- <strong>NOTE:</strong> You will *not* be able to add new keys via the `Noble.Settings.set` method. This means you need to specify the keys and default values of all of the settings in your game at setup.
-- If you need to add keys that are not known during setup, it is probably not a setting and you should consider using `Noble.GameData` instead.
-- @tparam table __keyValuePairs table. Your game's settings, and thier default values, as key/value pairs. <strong>NOTE:</strong> Do not use "nil" as a value.
-- @bool[opt=true] __saveToDisk Saves your default values immediatly to disk.
-- @bool[opt=true] __modifyExistingOnKeyChange Updates the existing settings object on disk if you make changes to your settings keys (not values) during development or when updating your game.
-- @usage
-- Noble.Settings.setup({
-- difficulty = "normal",
-- music = true,
-- sfx = true,
-- players = 2,
-- highScore = 0 -- You can store persistant stats here, too!
-- })
function Noble.Settings.setup(__keyValuePairs, __saveToDisk, __modifyExistingOnKeyChange)
if (settingsHaveBeenSetup) then
error("BONK: You can only run Noble.Settings.setup() once.")
return
else
settingsHaveBeenSetup = true
end
-- Prevent using the setup() method if there are no settings to register
if (__keyValuePairs == nil or table.getSize(__keyValuePairs) == 0) then
error("BONK: Do not use Noble.Settings.setup if you do not have any settings to register. New settings cannot be added via Noble.Settings.set and must be all declared upfront in the Noble.Settings.setup method.")
return
end
local saveToDisk = Utilities.handleOptionalBoolean(__saveToDisk, true)
local modifyExistingOnKeyChange = Utilities.handleOptionalBoolean(__modifyExistingOnKeyChange, true)
settingsDefault = __keyValuePairs
-- Get existing settings from disk, if any.
settings = Datastore.read("Settings")
if (settings == nil) then
-- No settings on disk, so we create a new settings object using default values.
settings = table.deepcopy(settingsDefault)
elseif (modifyExistingOnKeyChange and keyChange(settingsDefault, settings)) then
-- Found settings on disk, but key changes have been made...
-- ...so we start with a new default settings object...
local existingSettings = table.deepcopy(settings)
settings = table.deepcopy(settingsDefault)
for key, value in pairs(settings) do
-- ...then copy settings with unchanged keys to the new settings object,
-- naturally discarding keys that don't exist anymore.
if (existingSettings[key] ~= nil) then settings[key] = existingSettings[key] end
end
end
if (saveToDisk) then
Noble.Settings.save()
end
end
--- Get the value of a setting.
-- @string __settingName The name of the setting.
-- @treturn any The value of the requested setting.
-- @see set
function Noble.Settings.get(__settingName)
if (settingExists(__settingName)) then
return settings[__settingName]
end
end
--- Set the value of a setting.
--
-- <strong>NOTE:</strong> If __settingName is not a key in the __keyValuePairs dictionary given to the `setup` method it will not be added to the Settings.
-- @string __settingName The name of the setting.
-- @tparam any __value The setting's new value
-- @bool[opt=true] __saveToDisk Saves to disk immediately. Set to false if you prefer to manually save (via a confirm button, etc).
-- @see setup
-- @see get
-- @see save
function Noble.Settings.set(__settingName, __value, __saveToDisk)
if (settingExists(__settingName)) then
settings[__settingName] = __value
local saveToDisk = Utilities.handleOptionalBoolean(__saveToDisk, true)
if (saveToDisk) then Noble.Settings.save() end
end
end
--- Resets the value of a setting to its default value defined in `setup()`.
-- @string __settingName The name of the setting.
-- @bool[opt=true] __saveToDisk Saves to disk immediately. Set to false if you prefer to manually save (via a confirm button, etc).
-- @see resetSome
-- @see resetAll
-- @see save
function Noble.Settings.reset(__settingName, __saveToDisk)
if (settingExists(__settingName)) then
settings[__settingName] = settingsDefault[__settingName]
local saveToDisk = Utilities.handleOptionalBoolean(__saveToDisk, true)
if (saveToDisk) then Noble.Settings.save() end
end
end
--- Resets the value of multiple settings to thier default value defined in `setup()`. This is useful if you are storing persistant stats like high scores in `Settings` and want the player to be able to reset them seperately.
-- @tparam table __settingNames The names of the settings, in an array-style table.
-- @bool[opt=true] __saveToDisk Saves to disk immediately. Set to false if you prefer to manually save (via a confirm button, etc).
-- @see resetAll
-- @see save
function Noble.Settings.resetSome(__settingNames, __saveToDisk)
for i = 1, #__settingNames, 1 do
Noble.Settings.reset(__settingNames[i], __saveToDisk)
end
end
--- Resets all settings to thier default values defined in `setup()`.
-- @bool[opt=true] __saveToDisk Saves to disk immediately. Set to false if you prefer to manually save (via a confirm button, etc).
-- @see resetSome
-- @see save
function Noble.Settings.resetAll(__saveToDisk)
settings = table.deepcopy(settingsDefault)
local saveToDisk = Utilities.handleOptionalBoolean(__saveToDisk, true)
if (saveToDisk) then Noble.Settings.save() end
end
--- Saves settings to disk.
-- You don't need to call this unless you set `__saveToDisk` as false when setting or resetting a setting (say that five times fast!).
-- @see set
-- @see reset
-- @see resetAll
function Noble.Settings.save()
Datastore.write(settings, "Settings")
end

View File

@@ -0,0 +1,74 @@
--- Text and font handling.
-- @module Noble.Text
Noble.Text = {}
--- Fonts
--
-- You can use these fonts in your project, or override them with your own if you wish.
-- @section fonts
-- @usage
-- Noble.Text.FONT_SMALL = Graphics.font.new("assets/fonts/MySmallFont")
-- Noble.Text.setFont(Noble.Text.FONT_SMALL)
--- The Playdate system font.
Noble.Text.FONT_SYSTEM = Graphics.getSystemFont()
--- <strong>Noble Sans</strong>: A sans-serif 8×9 font, with English and Japanese-Kana character sets.
Noble.Text.FONT_SMALL = Graphics.font.new("libraries/noble/assets/fonts/NobleSans")
--- <strong>Noble Slab</strong>: (This font is not ready yet!)
Noble.Text.FONT_MEDIUM = Graphics.font.new("libraries/noble/assets/fonts/NobleSlab")
--- <strong>Satchel Roughed</strong>: A sans-serif 17×14 rounded font, with an English character set.
Noble.Text.FONT_LARGE = Graphics.font.new("libraries/noble/assets/fonts/SatchelRoughed")
--- Constants
--@section constants
--- An alternate way to call Playdate SDK's `kTextAlignment.left`
Noble.Text.ALIGN_LEFT = kTextAlignment.left
--- An alternate way to call Playdate SDK's `kTextAlignment.right`
Noble.Text.ALIGN_RIGHT = kTextAlignment.right
--- An alternate way to call Playdate SDK's `kTextAlignment.center`
Noble.Text.ALIGN_CENTER = kTextAlignment.center
local currentFont = Noble.Text.FONT_SYSTEM
--- Functions
--@section functions
---
-- @return The currently set font.
function Noble.Text.getCurrentFont()
return currentFont
end
--- Set the current font
-- @param __font any
-- @param __variant any
function Noble.Text.setFont(__font, __variant)
currentFont = __font
local variant = __variant or Graphics.font.kVariantNormal
Graphics.setFont(__font, variant)
end
--- Abstracts multiple `playdate.text` drawing functions into one.
-- @string __string Display text or localization key.
-- @number __x
-- @number __y
-- @param[opt=Noble.Text.ALIGN_LEFT] __alignment Left, right, or center!
-- @bool[opt=false] __localized If true, `__string` is a localization key rather than display text.
-- @param[opt=Noble.Text.getCurrentFont()] __font A font to use. If not set, the `currentFont` is used. If set, the `currentFont` is not updated.
function Noble.Text.draw(__string, __x, __y, __alignment, __localized, __font)
local alignment = __alignment or Noble.Text.ALIGN_LEFT
local localized = Utilities.handleOptionalBoolean(__localized, false)
local string = __string or ""
if (__font ~= nil) then Graphics.setFont(__font) end -- Temporary font
if (localized) then
Graphics.drawLocalizedTextAligned(string, __x, __y, alignment)
else
Graphics.drawTextAligned(string, __x, __y, alignment)
end
if (__font ~= nil) then Graphics.setFont(currentFont) end -- Reset
end

View File

@@ -0,0 +1,208 @@
--- An abstract class from which transition types are extended.
-- @module Noble.Transition
Noble.Transition = {}
class("Transition", nil, Noble).extends()
Noble.Transition.Type = {}
--- A transition type where no time at all passes between scenes.
-- @see Noble.Transition.Cut
Noble.Transition.Type.CUT = "Cut"
--- A transition type that has an "Enter" phase and an "Exit" phase. The new scene does not become active until the Enter phase is complete. A "holdTime" value determines how long to wait after the Enter phase completes before starting the Exit phase.
-- @see Noble.Transition.Dip
-- @see Noble.Transition.Imagetable
-- @see Noble.Transition.Spotlight
Noble.Transition.Type.COVER = "Cover"
--- A transition type that takes a screenshot of the exiting scene and activates the new scene before beginning the transition, allowing for both scenes to appear to be visible during the transition.
-- @see Noble.Transition.CrossDissolve
-- @see Noble.Transition.SlideOff
-- @see Noble.Transition.ImagetableMask
Noble.Transition.Type.MIX = "Mix"
--- A transition may have unique properties that can be set by the user when invoked. This table holds the default values for those properties.
-- @see setDefaultProperties
Noble.Transition.defaultProperties = {}
function Noble.Transition:init(__duration, __arguments)
self.duration = __duration or Noble.getConfig().defaultTransitionDuration
self.durationEnter = __arguments.durationEnter or self.duration/2
self.durationExit = __arguments.durationExit or self.duration/2
if (__arguments.durationEnter and not __arguments.durationExit) then
warn("Soft-BONK: You've specified 'durationEnter' but not 'durationExit' for this transition. Thus, 'durationExit' will be half the value of 'duration'. Did you intend to do that?")
elseif (__arguments.durationExit and not __arguments.durationEnter) then
warn("Soft-BONK: You've specified 'durationExit' but not 'durationEnter' for this transition. Thus, 'durationEnter' will be half the value of 'duration'. Did you intend to do that?")
end
self.sequence = nil
self._captureScreenshotsDuringTransition = self._captureScreenshotsDuringTransition or false
self.midpointReached = false
self.holdTimeElapsed = false
self.drawMode = self.drawMode or __arguments.drawMode or Graphics.kDrawModeCopy
self.holdTime = self.holdTime or __arguments.holdTime or self.defaultProperties.holdTime or 0
if (self._type == Noble.Transition.Type.MIX) then
self._sequenceStartValue = self._sequenceStartValue or 0
self._sequenceCompleteValue = self._sequenceCompleteValue or 1
self.ease = self.ease or __arguments.ease or self.defaultProperties.ease or Ease.linear
if ((__arguments.easeEnter or __arguments.easeExit) ~= nil) then
warn("BONK: You've specified an 'easeEnter' and/or 'easeExit' argument for a transition of type 'Noble.Transition.Type.MIX'. This will have no effect. Use 'ease' instead, or specify a transition of type 'Noble.Transition.Type.COVER'.")
end
self.oldSceneScreenshot = Utilities.screenshot()
elseif (self._type == Noble.Transition.Type.COVER) then
self._sequenceStartValue = self._sequenceStartValue or 0
self._sequenceMidpointValue = self._sequenceMidpointValue or 1
self._sequenceResumeValue = self._sequenceResumeValue or 1
self._sequenceCompleteValue = self._sequenceCompleteValue or 0
local ease = self.ease or __arguments.ease or self.defaultProperties.ease or Ease.linear
if (ease) then
self.easeEnter = self.easeEnter or self.defaultProperties.easeEnter or Ease.enter(ease) or ease
self.easeExit = self.easeExit or self.defaultProperties.easeExit or Ease.exit(ease) or ease
if (Ease.enter(ease) == nil or Ease.exit(ease) == nil) then
warn("Soft-BONK: You've specified an 'ease' value for a transition of type 'Noble.Transition.Type.COVER' that isn't in the form of 'Ease.inOutXxxx' or an 'Ease.outInXxxx'. As a result, this value will be used for both 'easeEnter' and 'easeExit'. Did you mean to do that?")
end
else
self.easeEnter = self.easeEnter or __arguments.easeEnter or self.defaultProperties.easeEnter or self.easeEnter or Ease.linear
self.easeExit = self.easeExit or __arguments.easeExit or self.defaultProperties.easeExit or self.easeExit or Ease.linear
end
end
self:setProperties(__arguments)
end
--- Use this to modify multiple default properties of a transition. Having default properties avoids having to set them every time a transition is called.
-- Properties added here are merged with the existing default properties table. Overwrites only happen when a new value is set.
-- @usage
-- Noble.Transition.setDefaultProperties(Noble.Transition.CrossDissolve, {
-- dither = Graphics.image.kDitherTypeDiagonalLine
-- ease = Ease.outQuint
-- })
-- Noble.Transition.setDefaultProperties(Noble.Transition.SpotlightMask, {
-- x = 325,
-- y = 95,
-- invert = true
-- })
-- @see defaultProperties
function Noble.Transition.setDefaultProperties(__transition, __properties)
table.merge(__transition.defaultProperties, __properties)
end
function Noble.Transition:execute()
local onStart = function()
Noble.transitionStartHandler()
self:onStart() -- If this transition has any custom code to run here, run it.
end
local onMidpoint = function()
Noble.transitionMidpointHandler()
self.midpointReached = true
self:onMidpoint() -- If this transition has any custom code to run here, run it.
end
local onHoldTimeElapsed = function()
self.holdTimeElapsed = true
self:onHoldTimeElapsed()
end
local onComplete = function()
self:onComplete() -- If this transition has any custom code to run here, run it.
Noble.transitionCompleteHandler()
end
local type = self._type
local holdTime = self.holdTime
if (type == Noble.Transition.Type.CUT) then
onStart()
onMidpoint()
onHoldTimeElapsed()
onComplete()
elseif (type == Noble.Transition.Type.COVER) then
onStart()
self.sequence = Sequence.new()
:from(self._sequenceStartValue)
:to(self._sequenceMidpointValue, self.durationEnter-(holdTime/2), self.easeEnter)
:callback(onMidpoint)
:sleep(holdTime)
:callback(onHoldTimeElapsed)
:to(self._sequenceResumeValue, 0)
:to(self._sequenceCompleteValue, self.durationExit-(holdTime/2), self.easeExit)
:callback(onComplete)
:start()
elseif (type == Noble.Transition.Type.MIX) then
onStart()
onMidpoint()
onHoldTimeElapsed()
self.sequence = Sequence.new()
:from(self._sequenceStartValue)
:to(self._sequenceCompleteValue, self.duration, self.ease)
:callback(onComplete)
:start()
end
end
--- *Do not call this directly.* Implement this in a custom transition in order to set properties from user arguments given in `Noble.transition()`. See existing transitions for implementation examples.
-- @see Noble.transition
function Noble.Transition:setProperties(__arguments) end
--- *Do not call this directly.* Implement this in a custom transition in order to run custom code when the transition starts. Default transitions in Noble Engine do not use this.
function Noble.Transition:onStart() end
--- *Do not call this directly.* Implement this in a custom transition in order to run custom code when the transition reaches its midpoint. Default transitions in Noble Engine do not use this.
function Noble.Transition:onMidpoint() end
--- *Do not call this directly.* Implement this in a custom transition in order to run custom code when the transition's hold time has elapsed. Default transitions in Noble Engine do not use this.
function Noble.Transition:onHoldTimeElapsed() end
--- *Do not call this directly.* Implement this in a custom transition in order to run custom code when the transition completes. Default transitions in Noble Engine do not use this.
function Noble.Transition:onComplete() end
--- *Do not call this directly.* Implement this in a custom transition to draw the transition. This runs once per frame while the transition is running. See existing transitions for implementation examples.
function Noble.Transition:draw() end
-- Noble Engine built-in transitions.
import 'libraries/noble/modules/Noble.Transition/Cut.lua'
--
import 'libraries/noble/modules/Noble.Transition/CrossDissolve.lua'
import 'libraries/noble/modules/Noble.Transition/Dip.lua'
import 'libraries/noble/modules/Noble.Transition/DipToBlack.lua'
import 'libraries/noble/modules/Noble.Transition/DipToWhite.lua'
--
import 'libraries/noble/modules/Noble.Transition/Imagetable.lua'
import 'libraries/noble/modules/Noble.Transition/ImagetableMask.lua'
import 'libraries/noble/modules/Noble.Transition/Spotlight.lua'
import 'libraries/noble/modules/Noble.Transition/SpotlightMask.lua'
--
import 'libraries/noble/modules/Noble.Transition/SlideOff.lua'
import 'libraries/noble/modules/Noble.Transition/SlideOffLeft.lua'
import 'libraries/noble/modules/Noble.Transition/SlideOffRight.lua'
import 'libraries/noble/modules/Noble.Transition/SlideOffUp.lua'
import 'libraries/noble/modules/Noble.Transition/SlideOffDown.lua'
import 'libraries/noble/modules/Noble.Transition/SlideOn.lua'
import 'libraries/noble/modules/Noble.Transition/SlideOnLeft.lua'
import 'libraries/noble/modules/Noble.Transition/SlideOnRight.lua'
import 'libraries/noble/modules/Noble.Transition/SlideOnUp.lua'
import 'libraries/noble/modules/Noble.Transition/SlideOnDown.lua'
--
import 'libraries/noble/modules/Noble.Transition/MetroNexus.lua'
import 'libraries/noble/modules/Noble.Transition/WidgetSatchel.lua'

View File

@@ -0,0 +1,26 @@
---
-- @submodule Noble.Transition
class("CrossDissolve", nil, Noble.Transition).extends(Noble.Transition)
local transition = Noble.Transition.CrossDissolve
transition.name = "Cross Dissolve"
-- Type
transition._type = Noble.Transition.Type.MIX
--- A simple cross-fade.
-- @table Noble.Transition.CrossDissolve.defaultProperties
-- @tparam[opt=Ease.outCubic] Ease ease
-- @tparam[opt=Graphics.image.kDitherTypeBayer4x4] Graphics.image.kDither dither
transition.defaultProperties = {
ease = Ease.outCubic,
dither = Graphics.image.kDitherTypeBayer4x4
}
function transition:setProperties(__properties)
self.dither = __properties.dither or self.defaultProperties.dither
end
function transition:draw()
self.oldSceneScreenshot:drawFaded(0, 0, 1 - self.sequence:get(), self.dither)
end

View File

@@ -0,0 +1,13 @@
---
-- @submodule Noble.Transition
class("Cut", nil, Noble.Transition).extends(Noble.Transition)
local transition = Noble.Transition.Cut
transition.name = "Cut"
-- Properties
transition._type = Noble.Transition.Type.CUT
--- An all-time classic.
-- This transition has no properties.
-- @table Noble.Transition.Cut.defaultProperties

View File

@@ -0,0 +1,37 @@
---
-- @submodule Noble.Transition
class("Dip", nil, Noble.Transition).extends(Noble.Transition)
local transition = Noble.Transition.Dip
transition.name = "Dip"
-- Type
transition._type = Noble.Transition.Type.COVER
--- Fade to an image, then to the next scene.
-- @table Noble.Transition.Dip.defaultProperties
-- @number[opt=0.25] holdTime
-- @tparam Graphics.image panelImage
-- @tparam[opt=Graphics.image.kDitherTypeBayer4x4] Graphics.image.kDither dither
-- @tparam[opt=Ease.outInQuad] Ease ease
-- @number[opt=0] x
-- @number[opt=0] y
transition.defaultProperties = {
holdTime = 0.25,
ease = Ease.outInQuad,
dither = Graphics.image.kDitherTypeBayer4x4,
panelImage = nil,
x = 0,
y = 0
}
function transition:setProperties(__arguments)
self.dither = __arguments.dither or self.defaultProperties.dither
self.panelImage = __arguments.panelImage or self.defaultProperties.panelImage
self.x = __arguments.x or self.defaultProperties.x
self.y = __arguments.y or self.defaultProperties.y
end
function transition:draw()
self.panelImage:drawFaded(self.x, self.y, self.sequence:get(), self.dither)
end

View File

@@ -0,0 +1,19 @@
---
-- @submodule Noble.Transition
class("DipToBlack", nil, Noble.Transition).extends(Noble.Transition.Dip)
local transition = Noble.Transition.DipToBlack
transition.name = "Dip to Black"
--- Fade to black, then to the next scene.
-- NOTE: The `panelImage` property is locked.
-- @see Noble.Transition.Dip.defaultProperties
-- @table Noble.Transition.DipToBlack.defaultProperties
transition.panelImage = Graphics.image.new(400, 240, Graphics.kColorBlack)
function transition:setCustomArguments(__arguments)
transition.super.setCustomArguments(self, __arguments)
self.x = 0
self.y = 0
end

View File

@@ -0,0 +1,19 @@
---
-- @submodule Noble.Transition
class("DipToWhite", nil, Noble.Transition).extends(Noble.Transition.Dip)
local transition = Noble.Transition.DipToWhite
transition.name = "Dip to White"
--- Fade to white, then to the next scene.
-- NOTE: The `panelImage` property is locked.
-- @see Noble.Transition.Dip.defaultProperties
-- @table Noble.Transition.DipToWhite.defaultProperties
transition.panelImage = Graphics.image.new(400, 240, Graphics.kColorWhite)
function transition:setCustomArguments(__arguments)
transition.super.setCustomArguments(self, __arguments)
self.x = 0
self.y = 0
end

View File

@@ -0,0 +1,131 @@
---
-- @submodule Noble.Transition
class("Imagetable", nil, Noble.Transition).extends(Noble.Transition)
local transition = Noble.Transition.Imagetable
transition.name = "Imagetable"
-- Type
transition._type = Noble.Transition.Type.COVER
-- Overrides
transition.easeEnter = Ease.linear
transition.easeExit = Ease.linear
--- A dip-style transition using one or two imagetables.
-- @see Noble.Transition.ImagetableMask.defaultProperties
-- @table Noble.Transition.Imagetable.defaultProperties
-- @number[opt=0] holdTime
-- @tparam Graphics.imagetable imagetable
-- @bool[opt=false] reverse
-- @bool[opt=false] flipX
-- @bool[opt=false] flipY
-- @bool[opt=false] rotate
-- @tparam Graphics.imagetable imagetableEnter
-- @bool[opt=nil] reverseEnter
-- @bool[opt=nil] flipXEnter
-- @bool[opt=nil] flipYEnter
-- @bool[opt=nil] rotateEnter
-- @tparam Graphics.imagetable imagetableExit
-- @bool[opt=nil] reverseExit
-- @bool[opt=nil] flipXExit
-- @bool[opt=nil] flipYExit
-- @bool[opt=nil] rotateExit
transition.defaultProperties = {
holdTime = 0,
imagetable = nil,
imagetableEnter = Graphics.imagetable.new("libraries/noble/assets/images/BoltTransitionEnter"),
imagetableExit = Graphics.imagetable.new("libraries/noble/assets/images/BoltTransitionExit"),
reverse = false,
reverseEnter = nil,
reverseExit = nil,
flipX = false,
flipY = false,
flipXEnter = nil,
flipYEnter = nil,
flipXExit = nil,
flipYExit = nil,
rotate = false,
rotateEnter = nil,
rotateExit = nil,
}
function transition:setProperties(__properties)
self.imagetable = __properties.imagetable or self.defaultProperties.imagetable
self.imagetableEnter = __properties.imagetableEnter or self.defaultProperties.imagetableEnter or self.imagetable
self.imagetableExit = __properties.imagetableExit or self.defaultProperties.imagetableExit or self.imagetable
self.reverse = __properties.reverse or self.defaultProperties.reverse
self.reverseEnter = __properties.reverseEnter or self.defaultProperties.reverseEnter or self.reverse
self.reverseExit = __properties.reverseExit or self.defaultProperties.reverseExit or self.reverse
self.flipX = __properties.flipX or self.defaultProperties.flipX
self.flipY = __properties.flipY or self.defaultProperties.flipY
self.flipXEnter = __properties.flipXEnter or self.defaultProperties.flipXEnter or self.flipX
self.flipYEnter = __properties.flipYEnter or self.defaultProperties.flipYEnter or self.flipY
self.flipXExit = __properties.flipXExit or self.defaultProperties.flipXExit or self.flipX
self.flipYExit = __properties.flipYExit or self.defaultProperties.flipYExit or self.flipY
self.rotate = __properties.rotate or self.defaultProperties.rotate
self.rotateEnter = __properties.rotateEnter or self.defaultProperties.rotateEnter or self.rotate
self.rotateExit = __properties.rotateExit or self.defaultProperties.rotateExit or self.rotate
-- "Private" variables
self._frameCountEnter = self.imagetableEnter and #self.imagetableEnter or 0
self._frameCountExit = self.imagetableExit and #self.imagetableExit or 0
self._flipValueEnter = Noble.Transition.Imagetable.getFlipValue(self.rotateEnter, self.flipXEnter, self.flipYEnter)
self._flipValueExit = Noble.Transition.Imagetable.getFlipValue(self.rotateExit, self.flipXExit, self.flipYExit)
local sequence0 = (not self.reverseEnter) and 0 or 1
local sequence1 = (not self.reverseEnter) and 1 or 0
local sequenceExit0 = (not self.reverseExit) and 0 or 1
local sequenceExit1 = (not self.reverseExit) and 1 or 0
if (self.imagetableEnter == self.imagetableExit) then
self._sequenceStartValue = sequence0
self._sequenceMidpointValue = sequence1
self._sequenceResumeValue = sequence1
self._sequenceCompleteValue = sequence0
else
self._sequenceStartValue = sequence0
self._sequenceMidpointValue = sequence1
self._sequenceResumeValue = sequenceExit0
self._sequenceCompleteValue = sequenceExit1
end
-- Warnings
if ((__properties.ease or __properties.easeEnter or __properties.easeExit) ~= nil) then
warn("BONK: You've specified an ease value for an Noble.Transition.Imagetable transition. This will have no effect.")
end
end
function transition:draw()
local progress = self.sequence:get()
local imagetable
local frameCount
local flipValue
if not self.holdTimeElapsed then
imagetable = self.imagetableEnter
frameCount = self._frameCountEnter
flipValue = self._flipValueEnter
else
imagetable = self.imagetableExit
frameCount = self._frameCountExit
flipValue = self._flipValueExit
end
local index = math.clamp((progress * frameCount) // 1, 1, frameCount)
imagetable[index]:draw(0, 0, flipValue)
end
function Noble.Transition.Imagetable.getFlipValue(__rotate, __flipX, __flipY)
if(__rotate or (__flipX and __flipY)) then
return Graphics.kImageFlippedXY
else
if(__flipX) then return Graphics.kImageFlippedX
elseif(__flipY) then return Graphics.kImageFlippedY end
end
return Graphics.kImageUnflipped
end

View File

@@ -0,0 +1,83 @@
---
-- @submodule Noble.Transition
class("ImagetableMask", nil, Noble.Transition).extends(Noble.Transition)
local transition = Noble.Transition.ImagetableMask
transition.name = "Imagetable Mask"
-- Type
transition._type = Noble.Transition.Type.MIX
-- Overrides
transition.ease = Ease.linear
--- A wipe transition using an animated mask in the form of an imagetable.
-- @see Noble.Transition.Imagetable.defaultProperties
-- @table Noble.Transition.ImagetableMask.defaultProperties
-- @tparam Graphics.imagetable imagetable
-- @bool[opt=false] reverse Set `true` to play the imagetable's frames in reverse order.
-- @bool[opt=false] flipX
-- @bool[opt=false] flipY
-- @bool[opt=false] rotate Set as `true` to rotate the image 180-degrees
-- @bool[opt=true] hasTransparency Set as `true` if the imagetable asset has transparent pixels. Set as `false` if the image uses white pixels for transparency.
-- @bool[opt=false] invert Set as `true` to invert the image mask.
transition.defaultProperties = {
imagetable = Graphics.imagetable.new("libraries/noble/assets/images/BoltTransitionEnter"),
reverse = false,
flipX = false,
flipY = false,
rotate = false,
hasTransparency = true,
invert = false
}
function transition:setProperties(__properties)
self.imagetable = __properties.imagetable or self.defaultProperties.imagetable
self.reverse = __properties.reverse or self.defaultProperties.reverse
self.flipX = __properties.flipX or self.defaultProperties.flipX
self.flipY = __properties.flipY or self.defaultProperties.flipY
self.rotate = __properties.rotate or self.defaultProperties.rotate
self.hasTransparency = __properties.hasTransparency or self.defaultProperties.hasTransparency
self.invert = __properties.invert or self.defaultProperties.invert
-- "Private" variables
self._flipValue = Noble.Transition.Imagetable.getFlipValue(self.rotate, self.flipX, self.flipY)
self._imagetableLength = self.imagetable and #self.imagetable or 0
self._maskBackground = nil
self._maskForegroundDrawMode = nil
if (self.invert ~= true) then
self._maskBackground = Graphics.image.new(400, 240, Graphics.kColorWhite)
self._maskForegroundDrawMode = Graphics.kDrawModeFillBlack
else
self._maskBackground = Graphics.image.new(400, 240, Graphics.kColorBlack)
self._maskForegroundDrawMode = Graphics.kDrawModeFillWhite
end
-- Warnings
if (__properties.imagetableExit ~= nil) then
warn("BONK: You've specified an 'imagetableExit' for an Noble.Transition.ImagetableMask transition. This will have no effect. ")
end
if ((__properties.ease or __properties.easeEnter or __properties.easeExit) ~= nil) then
warn("BONK: You've specified an ease value for an Noble.Transition.ImagetableMask transition. This will have no effect.")
end
end
function transition:draw()
local progress = self.sequence:get()
local length = self._imagetableLength
local index = math.clamp((progress * length) // 1, 1, length)
local mask = Graphics.image.new(400, 240)
Graphics.pushContext(mask)
Graphics.setImageDrawMode(Graphics.kDrawModeCopy)
self._maskBackground:draw(0,0)
if (self.hasTransparency) then Graphics.setImageDrawMode(self._maskForegroundDrawMode) end
self.imagetable[index]:draw(0,0, self._flipValue)
Graphics.popContext()
self.oldSceneScreenshot:setMaskImage(mask)
self.oldSceneScreenshot:draw(0,0)
end

View File

@@ -0,0 +1,58 @@
---
-- @submodule Noble.Transition
class("MetroNexus", nil, Noble.Transition).extends(Noble.Transition)
local transition = Noble.Transition.MetroNexus
transition.name = "Metro Nexus"
-- Type
transition._type = Noble.Transition.Type.COVER
-- Overrides
transition._sequenceResumeValue = 0
transition._sequenceCompleteValue = 1
transition.easeEnter = Ease.linear
transition.easeExit = Ease.linear
--- A "cascade" wipe transition, taken from "Metro Nexus" by Noble Robot.
-- This transition has no properties.
-- @table Noble.Transition.MetroNexus.defaultProperties
-- "Static" variables
local panels
function transition:setProperties(__arguments)
if (panels == nil) then
panels = {
Graphics.image.new(80,240, Graphics.kColorWhite),
Graphics.image.new(80,240, Graphics.kColorWhite),
Graphics.image.new(80,240, Graphics.kColorWhite),
Graphics.image.new(80,240, Graphics.kColorWhite),
Graphics.image.new(80,240, Graphics.kColorWhite)
}
end
-- Warnings
if (__arguments.easeEnter or __arguments.easeEnter or __arguments.ease) then
warn("BONK: 'Noble.Transition.MetroNexus' does not support custom ease values.")
end
end
function transition:draw()
local progress = self.sequence:get()
if (not self.holdTimeElapsed) then
panels[1]:draw(000, (-1 + Ease.outQuint(progress, 0, 1, 1)) * 240)
panels[2]:draw(080, (-1 + Ease.outQuart(progress, 0, 1, 1)) * 240)
panels[3]:draw(160, (-1 + Ease.outQuart(progress, 0, 1, 1)) * 240)
panels[4]:draw(240, (-1 + Ease.outCubic(progress, 0, 1, 1)) * 240)
panels[5]:draw(320, (-1 + Ease.outSine (progress, 0, 1, 1)) * 240)
else
panels[1]:draw(000, (1 - Ease.inQuint(progress, 0, 1, 1)) * -240 + 240)
panels[2]:draw(080, (1 - Ease.inQuart(progress, 0, 1, 1)) * -240 + 240)
panels[3]:draw(160, (1 - Ease.inQuart(progress, 0, 1, 1)) * -240 + 240)
panels[4]:draw(240, (1 - Ease.inCubic(progress, 0, 1, 1)) * -240 + 240)
panels[5]:draw(320, (1 - Ease.inSine (progress, 0, 1, 1)) * -240 + 240)
end
end

View File

@@ -0,0 +1,38 @@
---
-- @submodule Noble.Transition
class("SlideOff", nil, Noble.Transition).extends(Noble.Transition)
local transition = Noble.Transition.SlideOff
transition.name = "Slide Off"
-- Type
transition._type = Noble.Transition.Type.MIX
--- The previous scene slides off the screen, revealing the next scene.
-- @table Noble.Transition.SlideOff.defaultProperties
-- @tparam[opt=Ease.outInQuad] Ease ease
-- @number[opt=0] x
-- @number[opt=0] y
-- @number[opt=0] rotation
transition.defaultProperties = {
ease = Ease.inQuart,
x = 0,
y = 0,
rotation = 0
}
function transition:setProperties(__arguments)
self.x = __arguments.x or self.defaultProperties.x
self.y = __arguments.y or self.defaultProperties.y
self.rotation = __arguments.rotation or self.defaultProperties.rotation
end
function transition:draw()
transition.super.draw(self)
local progress = self.sequence:get()
self.oldSceneScreenshot:drawRotated(
self.x * progress + 200,
self.y * progress + 120,
self.rotation * progress
)
end

View File

@@ -0,0 +1,18 @@
---
-- @submodule Noble.Transition
class("SlideOffDown", nil, Noble.Transition).extends(Noble.Transition.SlideOff)
local transition = Noble.Transition.SlideOffDown
transition.name = "Slide Off (Down)"
--- The previous scene slides off the bottom of the screen, revealing the next scene.
-- NOTE: The `x`, `y`, and `rotation` properties are locked.
-- @see Noble.Transition.SlideOff.defaultProperties
-- @table Noble.Transition.SlideOffDown.defaultProperties
function transition:setProperties(__arguments)
transition.super.setProperties(self, __arguments)
self.x = 0
self.y = 240
self.rotation = 0
end

View File

@@ -0,0 +1,18 @@
---
-- @submodule Noble.Transition
class("SlideOffLeft", nil, Noble.Transition).extends(Noble.Transition.SlideOff)
local transition = Noble.Transition.SlideOffLeft
transition.name = "Slide Off (Left)"
--- The previous scene slides off the left side of the screen, revealing the next scene.
-- NOTE: The `x`, `y`, and `rotation` properties are locked.
-- @see Noble.Transition.SlideOff.defaultProperties
-- @table Noble.Transition.SlideOffLeft.defaultProperties
function transition:setProperties(__arguments)
transition.super.setProperties(self, __arguments)
self.x = -400
self.y = 0
self.rotation = 0
end

View File

@@ -0,0 +1,18 @@
---
-- @submodule Noble.Transition
class("SlideOffRight", nil, Noble.Transition).extends(Noble.Transition.SlideOff)
local transition = Noble.Transition.SlideOffRight
transition.name = "Slide Off (Right)"
--- The previous scene slides off the right side of the screen, revealing the next scene.
-- NOTE: The `x`, `y`, and `rotation` properties are locked.
-- @see Noble.Transition.SlideOff.defaultProperties
-- @table Noble.Transition.SlideOffRight.defaultProperties
function transition:setProperties(__arguments)
transition.super.setProperties(self, __arguments)
self.x = 400
self.y = 0
self.rotation = 0
end

View File

@@ -0,0 +1,18 @@
---
-- @submodule Noble.Transition
class("SlideOffUp", nil, Noble.Transition).extends(Noble.Transition.SlideOff)
local transition = Noble.Transition.SlideOffUp
transition.name = "Slide Off (Up)"
--- The previous scene slides off the top of the screen, revealing the next scene.
-- NOTE: The `x`, `y`, and `rotation` properties are locked.
-- @see Noble.Transition.SlideOff.defaultProperties
-- @table Noble.Transition.SlideOffUp.defaultProperties
function transition:setProperties(__arguments)
transition.super.setProperties(self, __arguments)
self.x = 0
self.y = -240
self.rotation = 0
end

View File

@@ -0,0 +1,44 @@
---
-- @submodule Noble.Transition
class("SlideOn", nil, Noble.Transition).extends(Noble.Transition)
local transition = Noble.Transition.SlideOn
transition.name = "Slide On"
-- Type
transition._type = Noble.Transition.Type.MIX
-- Overrides
transition._sequenceStartValue = 1
transition._sequenceCompleteValue = 0
transition._captureScreenshotsDuringTransition = true
--- The next scene slides on the screen, covering up the previous scene.
-- @table Noble.Transition.SlideOn.defaultProperties
-- @tparam[opt=Ease.outInQuad] Ease ease
-- @number[opt=0] x
-- @number[opt=0] y
-- @number[opt=0] rotation
transition.defaultProperties = {
ease = Ease.outQuart,
x = 0,
y = 0,
rotation = 0
}
function transition:setProperties(__arguments)
self.x = __arguments.x or self.defaultProperties.x
self.y = __arguments.y or self.defaultProperties.y
self.rotation = __arguments.rotation or self.defaultProperties.rotation
end
function transition:draw()
transition.super.draw(self)
local progress = self.sequence:get()
self.oldSceneScreenshot:draw(0,0)
self.newSceneScreenshot:drawRotated(
self.x * progress + 200,
self.y * progress + 120,
self.rotation * progress
)
end

View File

@@ -0,0 +1,18 @@
---
-- @submodule Noble.Transition
class("SlideOnDown", nil, Noble.Transition).extends(Noble.Transition.SlideOn)
local transition = Noble.Transition.SlideOnDown
transition.name = "Slide On (Down)"
--- The next scene slides onto the screen from the top, covering up the previous scene.
-- NOTE: The `x`, `y`, and `rotation` properties are locked.
-- @see Noble.Transition.SlideOn.defaultProperties
-- @table Noble.Transition.SlideOnDown.defaultProperties
function transition:setProperties(__arguments)
transition.super.setProperties(self, __arguments)
self.x = 0
self.y = -240
self.rotation = 0
end

View File

@@ -0,0 +1,18 @@
---
-- @submodule Noble.Transition
class("SlideOnLeft", nil, Noble.Transition).extends(Noble.Transition.SlideOn)
local transition = Noble.Transition.SlideOnLeft
transition.name = "Slide On (Left)"
--- The next scene slides onto the screen right-to-left, covering up the previous scene.
-- NOTE: The `x`, `y`, and `rotation` properties are locked.
-- @see Noble.Transition.SlideOn.defaultProperties
-- @table Noble.Transition.SlideOnLeft.defaultProperties
function transition:setProperties(__arguments)
transition.super.setProperties(self, __arguments)
self.x = 400
self.y = 0
self.rotation = 0
end

View File

@@ -0,0 +1,18 @@
---
-- @submodule Noble.Transition
class("SlideOnRight", nil, Noble.Transition).extends(Noble.Transition.SlideOn)
local transition = Noble.Transition.SlideOnRight
transition.name = "Slide On (Right)"
--- The next scene slides onto the screen left-to-right, covering up the previous scene.
-- NOTE: The `x`, `y`, and `rotation` properties are locked.
-- @see Noble.Transition.SlideOn.defaultProperties
-- @table Noble.Transition.SlideOnRight.defaultProperties
function transition:setProperties(__arguments)
transition.super.setProperties(self, __arguments)
self.x = -400
self.y = 0
self.rotation = 0
end

View File

@@ -0,0 +1,18 @@
---
-- @submodule Noble.Transition
class("SlideOnUp", nil, Noble.Transition).extends(Noble.Transition.SlideOn)
local transition = Noble.Transition.SlideOnUp
transition.name = "Slide On (Up)"
--- The next scene slides onto the screen from the bottom, covering up the previous scene.
-- NOTE: The `x`, `y`, and `rotation` properties are locked.
-- @see Noble.Transition.SlideOn.defaultProperties
-- @table Noble.Transition.SlideOnUp.defaultProperties
function transition:setProperties(__arguments)
transition.super.setProperties(self, __arguments)
self.x = 0
self.y = 240
self.rotation = 0
end

View File

@@ -0,0 +1,100 @@
---
-- @submodule Noble.Transition
class("Spotlight", nil, Noble.Transition).extends(Noble.Transition)
local transition = Noble.Transition.Spotlight
transition.name = "Spotlight"
-- Type
transition._type = Noble.Transition.Type.COVER
--- A spotlight in-out transition.
-- @see Noble.Transition.SpotlightMask.defaultProperties
-- @table Noble.Transition.Spotlight.defaultProperties
-- @number[opt=0.25] holdTime
-- @tparam Graphics.image panelImage
-- @tparam[opt=Graphics.image.kDitherTypeBayer4x4] Graphics.image.kDither dither
-- @tparam[opt=Ease.outInQuad] Ease ease
-- @number[opt=200] x
-- @number[opt=120] y
-- @tparam[opt=nil] Ease easeEnter
-- @number[opt=nil] xEnter
-- @number[opt=nil] yEnter
-- @number[opt=nil] xEnterStart
-- @number[opt=nil] yEnterStart
-- @number[opt=nil] xEnterEnd
-- @number[opt=nil] yEnterEnd
-- @tparam[opt=nil] Ease easeEnter
-- @number[opt=nil] xExit
-- @number[opt=nil] yExit
-- @number[opt=nil] xExitStart
-- @number[opt=nil] yExitStart
-- @number[opt=nil] xExitEnd
-- @number[opt=nil] yExitEnd
transition.defaultProperties = {
holdTime = 0.25,
panelImage = nil,
dither = Graphics.image.kDitherTypeBayer4x4,
ease = Ease.outInQuad,
x = 200,
y = 120,
easeEnter = nil,
xEnter = nil,
yEnter = nil,
xEnterStart = nil,
yEnterStart = nil,
xEnterEnd = nil,
yEnterEnd = nil,
easeExit = nil,
xExit = nil,
yExit = nil,
xExitStart = nil,
yExitStart = nil,
xExitEnd = nil,
yExitEnd = nil
}
-- "Static" variables
local defaultPanelImage
function transition:setProperties(__arguments)
if (defaultPanelImage == nil) then defaultPanelImage = Graphics.image.new(400,240, Graphics.kColorBlack) end
self.panelImage = __arguments.panelImage or self.defaultProperties.panelImage or defaultPanelImage
self.dither = __arguments.dither or self.defaultProperties.dither
self.x = __arguments.x or self.defaultProperties.x
self.y = __arguments.y or self.defaultProperties.y
self.xEnter = __arguments.xEnter or self.defaultProperties.xEnter or self.x
self.yEnter = __arguments.yEnter or self.defaultProperties.yEnter or self.y
self.xEnterStart = __arguments.xEnterStart or self.defaultProperties.xEnterStart or self.xEnter
self.yEnterStart = __arguments.yEnterStart or self.defaultProperties.yEnterStart or self.yEnter
self.xEnterEnd = __arguments.xEnterEnd or self.defaultProperties.xEnterEnd or self.xEnter
self.yEnterEnd = __arguments.yEnterEnd or self.defaultProperties.yEnterEnd or self.yEnter
self.xExit = __arguments.xExit or self.defaultProperties.xExit or self.x
self.yExit = __arguments.yExit or self.defaultProperties.yExit or self.y
self.xExitStart = __arguments.xExitStart or self.defaultProperties.xExitStart or self.xExit
self.yExitStart = __arguments.yExitStart or self.defaultProperties.yExitStart or self.yExit
self.xExitEnd = __arguments.xExitEnd or self.defaultProperties.xExitEnd or self.xExit
self.yExitEnd = __arguments.yExitEnd or self.defaultProperties.yExitEnd or self.yExit
end
function transition:draw()
local progress = self.sequence:get()
self.panelImage:drawFaded(0, 0, progress, self.dither)
Graphics.setColor(Graphics.kColorClear)
if (not self.midpointReached) then
Graphics.fillCircleAtPoint(
math.lerp(self.xEnterStart, self.xEnterEnd, progress),
math.lerp(self.yEnterStart, self.yEnterEnd, progress),
(1 - progress) * 233
)
else
Graphics.fillCircleAtPoint(
math.lerp(self.xExitStart, self.xExitEnd, progress),
math.lerp(self.yExitStart, self.yExitEnd, progress),
(1 - progress) * 233
)
end
Graphics.setColor(Graphics.kColorBlack)
end

View File

@@ -0,0 +1,99 @@
---
-- @submodule Noble.Transition
class("SpotlightMask", nil, Noble.Transition).extends(Noble.Transition)
local transition = Noble.Transition.SpotlightMask
transition.name = "Spotlight Mask"
-- Type
transition._type = Noble.Transition.Type.MIX
--- A circle wipe transition.
-- @see Noble.Transition.Spotlight.defaultProperties
-- @table Noble.Transition.SpotlightMask.defaultProperties
-- @tparam[opt=Ease.outQuad] Ease ease
-- @number[opt=200] x
-- @number[opt=120] y
-- @number[opt=nil] xStart
-- @number[opt=nil] yStart
-- @number[opt=nil] xEnd
-- @number[opt=nil] yEnd
-- @bool[opt=false] invert
transition.defaultProperties = {
ease = Ease.outQuad,
x = 200,
y = 120,
xStart = nil,
yStart = nil,
xEnd = nil,
yEnd = nil,
invert = false
}
function transition:setProperties(__arguments)
self.x = __arguments.x or self.defaultProperties.x
self.y = __arguments.y or self.defaultProperties.y
self.xStart = __arguments.xStart or self.defaultProperties.xStart or self.x
self.yStart = __arguments.yStart or self.defaultProperties.yStart or self.y
self.xEnd = __arguments.xEnd or self.defaultProperties.xEnd or self.x
self.yEnd = __arguments.yEnd or self.defaultProperties.yEnd or self.y
self.invert = __arguments.invert or self.defaultProperties.invert
if (self.invert) then
self.ease = Ease.reverse(self.ease)
end
-- "Private" variables
self._maskBackground = nil
self._maskForegroundDrawMode = nil
if (self.invert ~= true) then
self._maskBackground = Graphics.image.new(400, 240, Graphics.kColorWhite)
self._maskForegroundDrawMode = Graphics.kDrawModeFillBlack
else
self._maskBackground = Graphics.image.new(400, 240, Graphics.kColorBlack)
self._maskForegroundDrawMode = Graphics.kDrawModeFillWhite
end
self._startRadius = math.max(
Geometry.distanceToPoint(self.xStart, self.yStart, 0, 0),
Geometry.distanceToPoint(self.xStart, self.yStart, 400, 0),
Geometry.distanceToPoint(self.xStart, self.yStart, 400, 240),
Geometry.distanceToPoint(self.xStart, self.yStart, 0, 240)
)
self._endRadius = math.max(
Geometry.distanceToPoint(self.xEnd, self.yEnd, 0, 0),
Geometry.distanceToPoint(self.xEnd, self.yEnd, 400, 0),
Geometry.distanceToPoint(self.xEnd, self.yEnd, 400, 240),
Geometry.distanceToPoint(self.xEnd, self.yEnd, 0, 240)
)
end
function transition:draw()
local progress = self.sequence:get()
if (not self.invert) then
self.oldSceneScreenshot:draw(0, 0)
Graphics.setColor(Graphics.kColorClear)
Graphics.fillCircleAtPoint(
math.lerp(self.xStart, self.xEnd, progress),
math.lerp(self.yStart, self.yEnd, progress),
progress * self._endRadius
)
Graphics.setColor(Graphics.kColorBlack)
else
local mask = Graphics.image.new(400, 240, Graphics.kColorBlack)
Graphics.pushContext(mask)
Graphics.setColor(Graphics.kColorWhite)
Graphics.fillCircleAtPoint(
math.lerp(self.xStart, self.xEnd, progress),
math.lerp(self.yStart, self.yEnd, progress),
(1 - progress) * self._startRadius
)
Graphics.popContext()
self.oldSceneScreenshot:setMaskImage(mask)
self.oldSceneScreenshot:draw(0, 0)
end
end

View File

@@ -0,0 +1,73 @@
---
-- @submodule Noble.Transition
class("WidgetSatchel", nil, Noble.Transition).extends(Noble.Transition)
local transition = Noble.Transition.WidgetSatchel
transition.name = "Widget Satchel"
-- Type
transition._type = Noble.Transition.Type.COVER
-- Overrides
transition._sequenceCompleteValue = 2
transition.easeEnter = Ease.outCubic
transition.easeExit = Ease.inCubic
--- An "accordion" transition, taken from "Widget Satchel" by Noble Robot.
-- This transition has no properties.
-- @table Noble.Transition.MetroNexus.defaultProperties
-- "Static" variables
local panels
function transition:setProperties(__arguments)
if (panels == nil) then
panels = {
Graphics.image.new(400,48, Graphics.kColorWhite),
Graphics.image.new(400,48, Graphics.kColorWhite),
Graphics.image.new(400,48, Graphics.kColorWhite),
Graphics.image.new(400,48, Graphics.kColorWhite),
Graphics.image.new(400,48, Graphics.kColorWhite)
}
Graphics.lockFocus(panels[1])
Graphics.setDitherPattern(0.4, Graphics.image.kDitherTypeScreen)
Graphics.fillRect(0,0,400,48)
Graphics.lockFocus(panels[2])
Graphics.setDitherPattern(0.7, Graphics.image.kDitherTypeScreen)
Graphics.fillRect(0,0,400,48)
Graphics.lockFocus(panels[3])
Graphics.setDitherPattern(0.25, Graphics.image.kDitherTypeBayer8x8)
Graphics.fillRect(0,0,400,48)
Graphics.lockFocus(panels[4])
Graphics.setDitherPattern(0.5, Graphics.image.kDitherTypeDiagonalLine)
Graphics.fillRect(0,0,400,48)
Graphics.lockFocus(panels[5])
Graphics.setDitherPattern(0.8, Graphics.image.kDitherTypeHorizontalLine)
Graphics.fillRect(0,0,400,48)
Graphics.unlockFocus()
end
-- Warnings
if (__arguments.easeEnter or __arguments.easeEnter or __arguments.ease) then
warn("BONK: 'Noble.Transition.WidgetSatchel' does not support custom ease values.")
end
end
function transition:draw()
local progress = self.sequence:get()
if (not self.midpointReached ) then
panels[1]:draw(0, -48 + (progress * (48*1)) )
panels[2]:draw(0, -48 + (progress * (48*2)) )
panels[3]:draw(0, -48 + (progress * (48*3)) )
panels[4]:draw(0, -48 + (progress * (48*4)) )
panels[5]:draw(0, -48 + (progress * (48*5)) )
else
panels[1]:draw(0, 48*0 + (progress - 1) * 48*5)
panels[2]:draw(0, 48*1 + (progress - 1) * 48*4)
panels[3]:draw(0, 48*2 + (progress - 1) * 48*3)
panels[4]:draw(0, 48*3 + (progress - 1) * 48*2)
panels[5]:draw(0, 48*4 + (progress - 1) * 48*1)
end
end

View File

@@ -0,0 +1,242 @@
---
-- An abstract scene class.
-- Do not copy this file as a template for your scenes. Instead, your scenes will extend this class.
-- See <a href="../examples/SceneTemplate.lua.html">templates/SceneTemplate.lua</a> for a blank scene that you can copy and modify for your own scenes.
-- If you are using <a href="http://github.com/NobleRobot/NobleEngine-ProjectTemplate">NobleEngine-ProjectTemplate</a>,
-- see `scenes/ExampleScene.lua` for an implementation example.
-- @usage
-- YourSceneName = {}
-- class("YourSceneName").extends(NobleScene)
-- local scene = YourSceneName
--
-- @classmod NobleScene
--
NobleScene = {}
class("NobleScene").extends(Object)
--- Properties
-- @section properties
--- The name of this scene. Optional.
-- If you do not set this value, it will take on the scene's `className`.
NobleScene.name = ""
--- This is the background color of this scene.
--
NobleScene.backgroundColor = Graphics.kColorWhite
--- Tables
-- @section tables
--- All scenes have a default inputHandler which is made active when the scene starts.
-- If you do not define your scene's `inputHandler`, it is `nil` and input is disabled when this scene
-- starts.
-- @see Noble.Input.setHandler
--
-- @usage
-- YourSceneName.inputHandler = {
-- AButtonDown = function()
-- // Your code here
-- end,
-- AButtonHold = function()
-- // Your code here
-- end,
-- -- ...
-- -- ...
-- }
-- -- OR...
-- -- Use a non-scene-specific inputHandler, defined elsewhere.
-- YourSceneName.inputHandler = somePreviouslyDefinedInputHandler
-- -- OR...
-- -- Reuse another scene's inputHandler.
-- YourSceneName.inputHandler = SomeOtherSceneName.inputHandler
NobleScene.inputHandler = {}
--- When you add a sprite to your scene, it is put in this table so the scene can keep track of it.
--
-- This is intended as `read-only`. You should not modify this table directly.
-- @see addSprite
NobleScene.sprites = {}
--- Methods
-- @section Methods
--- Use this to add sprites to your scene instead of `playdate.graphics.sprite:add()`.
--
-- If your sprite is a `NobleSprite`, using `NobleSprite:add()` will also call this method.
--
-- Sprites added with this method that are tracked by the scene. Any not manually removed before transitioning to another scene are automatically removed in @{finish|finish}.
-- @tparam playdate.graphics.sprite __sprite The sprite to add to the scene.
-- @see NobleSprite:add
-- @see removeSprite
function NobleScene:addSprite(__sprite)
if (__sprite.isNobleSprite == true) then
__sprite:superAdd()
else
__sprite:add()
end
if (table.indexOfElement(self.sprites, __sprite) == nil) then
table.insert(self.sprites, __sprite)
end
end
--- Use this to remove sprites from your scene instead of `playdate.graphics.sprite:remove()`.
--
-- If your sprite is a `NobleSprite`, using `NobleSprite:remove()` will also call this method.
--
-- Sprites not manually removed before transitioning to another scene are automatically removed in @{finish|finish}.
-- @tparam playdate.graphics.sprite __sprite The sprite to add to the scene.
-- @see NobleSprite:remove
-- @see addSprite
function NobleScene:removeSprite(__sprite)
if (__sprite.isNobleSprite == true) then
__sprite:superRemove()
else
__sprite:remove()
end
if (table.indexOfElement(self.sprites, __sprite) ~= nil) then
table.remove(self.sprites, table.indexOfElement(self.sprites, __sprite))
end
end
--- Callbacks
-- @section callbacks
--- Implement this in your scene if you have code to run when your scene's object is created.
--
-- @usage
-- function YourSceneName:init()
-- YourSceneName.super.init(self)
-- --[Your code here]--
-- end
--
function NobleScene:init()
self.name = self.className
self.sprites = {}
end
--- Implement if you want to run code as the transition to this scene begins, such as UI animation, triggers, etc.
--
-- @usage
-- function YourSceneName:enter()
-- YourSceneName.super.enter(self)
-- --[Your code here]--
-- end
--
function NobleScene:enter() end
--- Implement if you have code to run once the transition to this scene is complete. This method signifies the full activation of a scene. If this scene's `inputHandler` is defined, it is enabled now.
-- @see inputHandler
-- @usage
-- function YourSceneName:start()
-- YourSceneName.super.start(self)
-- --[Your code here]--
-- end
--
function NobleScene:start()
Noble.Input.setHandler(self.inputHandler)
end
--- Implement to run scene-specific code on every frame while this scene is active.
-- <strong>NOTE:</strong> you may use coroutine.yield() here, because it only runs inside of playdate.update(), which is a coroutine.
--
-- @usage
-- function YourSceneName:update()
-- YourSceneName.super.update(self)
-- --[Your code here]--
-- end
--
function NobleScene:update() end
--- Implement this function to draw background visual elements in your scene.
--- This runs when the engine need to redraw a background area.
--- By default it runs every frame and fills the background with self.backgroundColor. All arguments are optional.
--- Use `Graphics.sprite.setAlwaysRedraw(false)` after `Noble.new()` to optimize partial redraw.
--
-- @usage
-- function YourSceneName:drawBackground(__x, __y, __width, __height)
-- YourSceneName.super.drawBackground(self) -- optional, invokes default behavior.
-- --[Your code here]--
-- end
--
function NobleScene:drawBackground(__x, __y, __width, __height)
__x = __x or 0
__y = __y or 0
__width = __width or Display.getWidth()
__height = __height or Display.getHeight()
-- Cache the currently set color/pattern.
local color <const> = Graphics.getColor()
local color_type <const> = type(color)
-- Draw background.
Graphics.setColor(self.backgroundColor)
Graphics.fillRect(__x, __y, __width, __height)
-- Reset color/pattern from cache.
if color_type == 'number' then
Graphics.setColor(color)
elseif color_type == 'table' then
Graphics.setPattern(color)
end
end
--- Implement this in your scene if you have "goodbye" code to run when a transition to another scene
-- begins, such as UI animation, saving to disk, etc.
--
-- @usage
-- function YourSceneName:exit()
-- YourSceneName.super.exit(self)
-- --[Your code here]--
-- end
--
function NobleScene:exit()
for _, sprite in ipairs(self.sprites) do
sprite:setUpdatesEnabled(false)
sprite:setCollisionsEnabled(false)
end
end
--- Implement this in your scene if you have code to run when a transition to another scene
-- is complete, such as resetting variables.
--
-- @usage
-- function YourSceneName:finish()
-- YourSceneName.super.finish(self)
-- --[Your code here]--
-- end
--
function NobleScene:finish()
for _, sprite in ipairs(self.sprites) do
if (sprite.isNobleSprite) then
sprite:superRemove()
else
sprite:remove()
end
end
end
--- `pause()` / `resume()`
--
-- Implement one or both of these in your scene if you want something to happen when the game is paused/unpaused
-- by the system. The Playdate SDK does not require you to write pause logic, but these are useful if you want a
-- custom menu image (see Playdate SDK for more details), want to obscure game elements to prevent players from
-- cheating in a time-sensitive game, want to count the number of times the player pauses the game, etc.
--
-- @usage
-- function YourSceneName:pause()
-- YourSceneName.super.pause(self)
-- --[Your code here]--
-- end
function NobleScene:pause() end
--- <span></span>
-- @usage
-- function YourSceneName:resume()
-- YourSceneName.super.resume(self)
-- --[Your code here]--
-- end
function NobleScene:resume() end

View File

@@ -0,0 +1,154 @@
---
-- An extension of Playdate's sprite object, incorporating `Noble.Animation` and other Noble Engine features.
-- Use this in place of `playdate.graphics.sprite` in most cases.
--
-- `NobleSprite` is a child class of `playdate.graphics.sprite`, so see the Playdate SDK documentation for additional methods and properties.
--
-- @classmod NobleSprite
--
NobleSprite = {}
class("NobleSprite").extends(Graphics.sprite)
--- Do not call an "init" method directly. Use `NobleSprite()` (see usage examples).
-- @string[opt] __view This can be: the path to an image or spritesheet image file, an image object (`Graphics.image`) or an animation object (`Noble.Animation`)
-- @bool[opt=false] __viewIsSpritesheet Set this to `true` to indicate that `__view` is a spritesheet. Will only be considered if `__view` is a string path to an image.
-- @bool[opt=false] __singleState If this sprite has just one animation, set this to true. It saves you from having to use Noble.Animation.addState()
-- @bool[opt=true] __singleStateLoop If using a single state animation, should it loop?
--
-- @usage
-- -- Provide a spritesheet image file to create a new `Noble.Animation` for a NobleSprite's view.
-- myNobleSprite = NobleSprite("path/to/spritesheet", true)
--
-- @usage
-- -- Provide an image file to create a new `Graphics.image` for a NobleSprite's view.
-- myNobleSprite = NobleSprite("path/to/image")
--
-- @usage
-- -- Use an existing `Noble.Animation` for a NobleSprite's view.
-- local myAnimation = Noble.Animation.new("path/to/spritesheet")
-- myAnimation:addState("default", 1, animation.imageTable:getLength(), nil, true)
-- myNobleSprite = NobleSprite(myAnimation)
--
-- @usage
-- -- Use an existing `Graphics.image` object for a NobleSprite's view.
-- local myImage = Graphics.image.new("path/to/image")
-- myNobleSprite = NobleSprite(myImage)
--
-- @usage
-- -- Extending NobleSprite.
--
-- -- MyCustomSprite.lua
-- MyCustomSprite = {}
-- class("MyCustomSprite").extends(NobleSprite)
--
-- function MyCustomSprite:init(__x, __y, __anotherFunArgument)
-- MyCustomSprite.super.init(self, "path/to/spritesheet", true)
-- -- Etc. etc.
-- end
--
-- -- MyNobleScene.lua
-- myNobleSprite = MyCustomSprite(100, 100, "Fun!")
--
-- @see Noble.Animation:addState
-- @see Noble.Animation.new
--
function NobleSprite:init(__view, __viewIsSpritesheet, __singleState, __singleStateLoop)
NobleSprite.super.init(self)
self.isNobleSprite = true -- This is important so other methods don't confuse this for a playdate.graphics.sprite. DO NOT modify this value at runtime.
if (__view ~= nil) then
-- __view is the path to an image or spritesheet file.
if (type(__view) == "string") then
self.animated = __viewIsSpritesheet -- NO NOT modify self.animated at runtime.
if (__viewIsSpritesheet == true) then
-- Create a new Noble.Animation object.
--- The animation for this NobleSprite.
-- @see Noble.Animation.new
self.animation = Noble.Animation.new(__view)
local singleStateLoop = true
if (__singleStateLoop ~= nil) then singleStateLoop = __singleStateLoop end
if (__singleState == true) then
self.animation:addState("default", 1, self.animation.imageTable:getLength(), nil, singleStateLoop)
end
else
-- Create a new Graphics.image object.
self:setImage(Graphics.image.new(__view))
end
-- __view is an existing Graphics.image object.
elseif (type(__view) == "userdata") then
self.animated = false
self:setImage(__view)
-- __view is an existing Noble.Animation object.
elseif (type(__view) == "table") then
self.animated = true
self.animation = __view
end
end
end
function NobleSprite:draw()
if (self.animation ~= nil) then
self.animation:draw()
self:markDirty()
end
end
--- This will enable the update loop for this NobleSprite, which also causes its Noble.Animation to play.
function NobleSprite:play()
self:setUpdatesEnabled(true)
end
--- This will disable the update loop for this NobleSprite, which also causes its Noble.Animation to pause.
function NobleSprite:pause()
self:setUpdatesEnabled(false)
end
--- This will disable the update loop for this NobleSprite, and also reset its Noble.Animation (if it exists) to the first frame of its current state.
function NobleSprite:stop()
self:setUpdatesEnabled(false)
if (self.animation ~= nil) then
self.animation.currentFrame = self.animation.current.startFrame
end
end
--- Use this to add this NobleSprite to your scene. This replaces `playdate.graphics.sprite:add()` to allow NobleSprites to be tracked by the current NobleScene.
--
-- To add a `playdate.graphics.sprite` to a scene, use `NobleScene:addSprite(__sprite)`.
-- @see NobleScene:addSprite
function NobleSprite:add(__x, __y)
local x = __x or 0
local y = __y or 0
self:moveTo(x, y)
Noble.currentScene():addSprite(self)
end
function NobleSprite:superAdd()
NobleSprite.super.add(self)
end
--- Use this to remove this NobleSprite from your scene. This replaces `playdate.graphics.sprite:remove()` to allow NobleSprites to be tracked by the current NobleScene.
--
-- To remove a `playdate.graphics.sprite` from a scene, use `NobleScene:removeSprite(__sprite)`.
-- @see NobleScene:removeSprite
function NobleSprite:remove()
if (self.animation ~= nil) then
self:stop()
self:setUpdatesEnabled(true) -- reset!
end
Noble.currentScene():removeSprite(self)
end
function NobleSprite:superRemove()
NobleSprite.super.remove(self)
end