diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..38536aa --- /dev/null +++ b/.gitignore @@ -0,0 +1,44 @@ +# Compiled Lua sources +luac.out + +# luarocks build files +*.src.rock +*.zip +*.tar.gz + +# Object files +*.o +*.os +*.ko +*.obj +*.elf + +# Precompiled Headers +*.gch +*.pch + +# Libraries +*.lib +*.a +*.la +*.lo +*.def +*.exp + +# Shared objects (inc. Windows DLLs) +*.dll +*.so +*.so.* +*.dylib + +# Executables +*.exe +*.out +*.app +*.i*86 +*.x86_64 +*.hex + +# Builds and Code Editor +builds/ +.sentry-native/ diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..c0c291d --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,6 @@ +{ + "recommendations": [ + "sumneko.lua", + "jep-a.lua-plus" + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..cc03634 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,10 @@ +{ + "Lua.runtime.version": "Lua 5.4", + "Lua.diagnostics.disable": ["undefined-global", "lowercase-global"], + "Lua.diagnostics.globals": ["playdate", "import"], + "Lua.runtime.nonstandardSymbol": ["+=", "-=", "*=", "/="], + "Lua.workspace.library": ["$PLAYDATE_SDK_PATH/CoreLibs"], + "Lua.workspace.preloadFileSize": 1000, + "terminal.integrated.defaultProfile.windows": "PowerShell", + "Lua.runtime.unicodeName": true +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..406c623 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,77 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + "label": "Invoke Build and Run script", + "type": "shell", + "command": "&", + "args": [ + "${workspaceFolder}\\Build and Run (Simulator).ps1", + "-build", + "'${workspaceFolder}\\builds'", + "-source", + "'${workspaceFolder}\\source'", + "-name", + "'${workspaceFolderBasename}'" + ], + "presentation": { + "showReuseMessage": false, + "reveal": "always", + "panel": "shared" + } + }, + { + "label": "Invoke Run script", + "type": "shell", + "command": "&", + "args": [ + "${workspaceFolder}\\Build and Run (Simulator).ps1", + "-build", + "'${workspaceFolder}\\builds'", + "-source", + "'${workspaceFolder}\\source'", + "-name", + "'${workspaceFolderBasename}'", + "-dontbuild" + ], + "presentation": { + "showReuseMessage": false, + "reveal": "always", + "panel": "shared" + } + }, + { + "label": "Build and Run (Simulator)", + "dependsOn": ["Invoke Build and Run script"], + "dependsOrder": "sequence", + "presentation": { + "showReuseMessage": false, + "reveal": "always", + "panel": "shared" + }, + "problemMatcher": [], + "group": { + "kind": "build", + "isDefault": true + } + }, + { + "label": "Run (Simulator)", + "dependsOn": ["Invoke Run script"], + "dependsOrder": "sequence", + "presentation": { + "showReuseMessage": false, + "reveal": "always", + "panel": "shared" + }, + "problemMatcher": [], + "group": { + "kind": "test", + "isDefault": true + } + } + ] + } + \ No newline at end of file diff --git a/ADD_ENV_VARIABLE.cmd b/ADD_ENV_VARIABLE.cmd new file mode 100644 index 0000000..65e5c49 --- /dev/null +++ b/ADD_ENV_VARIABLE.cmd @@ -0,0 +1,53 @@ +@echo off +:: CHANGE PATH HERE +:: Replace YOUR CUSTOM SDK PATH HERE with your custom path. +:: Use \ as separator +:: Must not end with \ +set SDKPATH="YOUR CUSTOM SDK PATH HERE" + + +if %SDKPATH% == "YOUR CUSTOM SDK PATH HERE" ( + FOR /F "tokens=2* skip=2" %%a in ('reg query "HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders" /v "Personal"') do set SDKPATH=%%b\PlaydateSDK +) +:: BatchGotAdmin +:------------------------------------- +REM --> Check for permissions + IF "%PROCESSOR_ARCHITECTURE%" EQU "amd64" ( +>nul 2>&1 "%SYSTEMROOT%\SysWOW64\cacls.exe" "%SYSTEMROOT%\SysWOW64\config\system" +) ELSE ( +>nul 2>&1 "%SYSTEMROOT%\system32\cacls.exe" "%SYSTEMROOT%\system32\config\system" +) + +REM --> If error flag set, we do not have admin. +if '%errorlevel%' NEQ '0' ( + echo Requesting administrative privileges... + goto UACPrompt +) else ( goto gotAdmin ) + +:UACPrompt + echo Set UAC = CreateObject^("Shell.Application"^) > "%temp%\getadmin.vbs" + set params= %* + echo UAC.ShellExecute "cmd.exe", "/c ""%~s0"" %params:"=""%", "", "runas", 1 >> "%temp%\getadmin.vbs" + + "%temp%\getadmin.vbs" + del "%temp%\getadmin.vbs" + exit /B + +:gotAdmin + pushd "%CD%" + CD /D "%~dp0" +:-------------------------------------- + +:: Add PLAYDATE_SDK_PATH env variable +setx /M PLAYDATE_SDK_PATH "%SDKPATH%" 2> nul +:: Add pcd.exe to the PATH env variable +set pathkey="HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager\Environment" +for %%p in (pdc.exe) do (set pdcCheck=%%~$PATH:p) +if not defined pdcCheck ( + for /F "usebackq skip=2 tokens=2*" %%A IN (`reg query %pathkey% /v Path`) do (reg add %pathkey% /f /v Path /t REG_SZ /d "%%B;%SDKPATH%\bin") +) +:: Update register +powershell -command "& {$md=\"[DllImport(`\"user32.dll\"\",SetLastError=true,CharSet=CharSet.Auto)]public static extern IntPtr SendMessageTimeout(IntPtr hWnd,uint Msg,UIntPtr wParam,string lParam,uint fuFlags,uint uTimeout,out UIntPtr lpdwResult);\"; $sm=Add-Type -MemberDefinition $md -Name NativeMethods -Namespace Win32 -PassThru;$result=[uintptr]::zero;$sm::SendMessageTimeout(0xffff,0x001A,[uintptr]::Zero,\"Environment\",2,5000,[ref]$result)}" +echo SUCCESS! +pause +exit \ No newline at end of file diff --git a/Build and Run (Simulator).ps1 b/Build and Run (Simulator).ps1 new file mode 100644 index 0000000..8128826 --- /dev/null +++ b/Build and Run (Simulator).ps1 @@ -0,0 +1,47 @@ + param ( + [string]$build = ".\builds", + [string]$source = ".\source", + [string]$name = (Get-Item -Path .).BaseName, + [switch]$dontbuild = $false + ) +$pdx = Join-Path -Path "$build" -ChildPath "$name.pdx" + +# Create build folder if not present +if (!$dontbuild) +{ + New-Item -ItemType Directory -Force -Path "$build" +} + +# Clean build folder +if (!$dontbuild) +{ + Remove-Item "$build\*" -Recurse -Force +} + +# Build +if (!$dontbuild) +{ + pdc -sdkpath "$Env:PLAYDATE_SDK_PATH" "$source" "$pdx" +} + +# Close Simulator +$sim = Get-Process "PlaydateSimulator" -ErrorAction SilentlyContinue + +if ($sim) +{ + $sim.CloseMainWindow() + $count = 0 + while (!$sim.HasExited) + { + Start-Sleep -Milliseconds 10 + $count += 1 + + if ($count -ge 5) + { + $sim | Stop-Process -Force + } + } +} + +# Run (Simulator) +& "$Env:PLAYDATE_SDK_PATH\bin\PlaydateSimulator.exe" "$pdx" \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..1e34030 --- /dev/null +++ b/README.md @@ -0,0 +1,44 @@ +# Link's Awakening Overworld (Demo) for Playdate +![Link in fron of Egg](demoimages/egg.png) + +## First things first +This code was provided unter the [MIT License](LICENSE). If you implement this code into one of your projects, then please credit me in some way. + +This is a Fan Project and not related to Nintendo in any way. Please support the [official release](https://www.nintendo.com/store/products/the-legend-of-zelda-links-awakening-switch/)! + + +## Download / Install +You can download the pdx of this project to sideload [here](Links-Awakening-Overworld-Download.zip). + +A guide on how to sideload this game can be found [here](https://help.play.date/games/sideloading/). + +![Link on the starting beach](demoimages/sea.png) + +## The project +This is a demo of a static verison of Link's Awakenings overworld running on the playdate. + +Walk with the D-Pad and sprint using the B-Button. + +There are currently no collisions, so you can just walk around the world and enjoy it without boundaries. + +![Link walking to the egg](demoimages/walking.gif) + +This demo works with a variable framerate and solid 49 fps on device. By replacing the images under source/images/assets you could also get Pokémon Red/Blue/Yellows map or any other games map running. + + +## Assets and tools used: + +- [Song used made by The Accordion Guy](https://www.youtube.com/watch?v=Xp6UEZFfPbs) +- [Source of the map provided by Donut Jacky on Pintrest](https://www.pinterest.ch/pin/798896421383750450/) +- [Link Sprites provided by +ProfessorCreepyPasta on deviantart](https://www.deviantart.com/professorcreepypasta/art/Link-s-Awakening-Link-Sprites-for-RPG-Maker-MV-826890016) +- [AnimatedSprite by Whitebrim +](https://github.com/Whitebrim/AnimatedSprite/wiki) +- [VSCode-PlaydateTemplate by Whitebrim](https://github.com/Whitebrim/VSCode-PlaydateTemplate) +- [Dithering of the map using Ditherlicious](https://29a.ch/ditherlicious/) + + +## Collaborations +If you wanna continue developing this, then hit me up on [Twitter @PizzaFuelDev](https://twitter.com/PizzaFuelDev) or on the Playdate Squad Discord. + +![Game running on Playdate](demoimages/playdate.png) \ No newline at end of file diff --git a/demoimages/egg.png b/demoimages/egg.png new file mode 100644 index 0000000..0df4ef4 Binary files /dev/null and b/demoimages/egg.png differ diff --git a/demoimages/playdate.png b/demoimages/playdate.png new file mode 100644 index 0000000..6cc2e16 Binary files /dev/null and b/demoimages/playdate.png differ diff --git a/demoimages/sea.png b/demoimages/sea.png new file mode 100644 index 0000000..05cb00b Binary files /dev/null and b/demoimages/sea.png differ diff --git a/demoimages/walking.gif b/demoimages/walking.gif new file mode 100644 index 0000000..9454d10 Binary files /dev/null and b/demoimages/walking.gif differ diff --git a/source/images/assets/map.png b/source/images/assets/map.png new file mode 100644 index 0000000..14e4f71 Binary files /dev/null and b/source/images/assets/map.png differ diff --git a/source/images/assets/player/walk/down.gif b/source/images/assets/player/walk/down.gif new file mode 100644 index 0000000..32b8ad5 Binary files /dev/null and b/source/images/assets/player/walk/down.gif differ diff --git a/source/images/assets/player/walk/left.gif b/source/images/assets/player/walk/left.gif new file mode 100644 index 0000000..ee391f3 Binary files /dev/null and b/source/images/assets/player/walk/left.gif differ diff --git a/source/images/assets/player/walk/right.gif b/source/images/assets/player/walk/right.gif new file mode 100644 index 0000000..8c4431d Binary files /dev/null and b/source/images/assets/player/walk/right.gif differ diff --git a/source/images/assets/player/walk/up.gif b/source/images/assets/player/walk/up.gif new file mode 100644 index 0000000..2872ab0 Binary files /dev/null and b/source/images/assets/player/walk/up.gif differ diff --git a/source/images/cards/card.png b/source/images/cards/card.png new file mode 100644 index 0000000..751e452 Binary files /dev/null and b/source/images/cards/card.png differ diff --git a/source/import/animiatedsprite.lua b/source/import/animiatedsprite.lua new file mode 100644 index 0000000..f5bb77b --- /dev/null +++ b/source/import/animiatedsprite.lua @@ -0,0 +1,467 @@ +----------------------------------------------- +--- Sprite class extension with support of --- +--- imagetables and finite state machine, --- +--- with json configuration and autoplay. --- +--- By @Whitebrim git.brim.ml --- +----------------------------------------------- + +-- You can find examples and docs at https://github.com/Whitebrim/AnimatedSprite/wiki +-- Comments use EmmyLua style + +local gfx = playdate.graphics +local function emptyFunc()end + +class("AnimatedSprite").extends(gfx.sprite) + +---@param imagetable table +---@param states? table If provided, calls `setStates(states)` after initialisation +---@param animate? boolean If `True`, then the animation of default state will start after initialisation. Default: `False` +function AnimatedSprite.new(imagetable, states, animate) + return AnimatedSprite(imagetable, states, animate) +end + +function AnimatedSprite:init(imagetable, states, animate) + AnimatedSprite.super.init(self) + + ---@type table + self.imagetable = imagetable + assert(self.imagetable, "Imagetable is nil. Check if it was loaded correctly.") + + self:add() + + self.globalFlip = gfx.kImageUnflipped + self.defaultState = "default" + self.states = { + default = { + name = "default", + ---@type integer|string + firstFrameIndex = 1, + framesCount = #self.imagetable, + animationStartingFrame = 1, + tickStep = 1, + frameStep = 1, + reverse = false, + ---@type boolean|integer + loop = true, + yoyo = false, + flip = gfx.kImageUnflipped, + xScale = 1, + yScale = 1, + nextAnimation = nil, + + onFrameChangedEvent = emptyFunc, + onStateChangedEvent = emptyFunc, + onLoopFinishedEvent = emptyFunc, + onAnimationEndEvent = emptyFunc + } + } + + self._enabled = false + self._currentFrame = 0 -- purposely + self._ticks = 1 + self._previousTicks = 1 + self._loopsFinished = 0 + self._currentYoyoDirection = true + + if (states) then + self:setStates(states) + end + + if (animate) then + self:playAnimation() + end +end + +local function drawFrame(self) + local state = self.states[self.currentState] + self:setImage(self._image, state.flip ~ self.globalFlip, state.xScale, state.yScale) +end + +local function setImage(self) + local frames = self.states[self.currentState].frames + if (frames) then + self._image = self.imagetable[frames[self._currentFrame]] + else + self._image = self.imagetable[self._currentFrame] + end +end + +---Start/resume the animation +---If `currentState` is nil then `defaultState` will be choosen as current +function AnimatedSprite:playAnimation() + + local state = self.states[self.currentState] + + if (type(self.currentState) == 'nil') then + self.currentState = self.defaultState + state = self.states[self.currentState] + self._currentFrame = state.animationStartingFrame + state.firstFrameIndex - 1 + end + + if (self._currentFrame == 0) then + self._currentFrame = state.animationStartingFrame + state.firstFrameIndex - 1 + end + + self._enabled = true + self._previousTicks = self._ticks + setImage(self) + drawFrame(self) + if (state.framesCount == 1) then + self._loopsFinished += 1 + state.onFrameChangedEvent(self) + state.onLoopFinishedEvent(self) + else + state.onFrameChangedEvent(self) + end +end + +---Stop the animation without resetting +function AnimatedSprite:pauseAnimation() + self._enabled = false +end + +---Play/Pause animation based on current state +function AnimatedSprite:toggleAnimation() + if (self._enabled) then + self:pauseAnimation() + else + self:playAnimation() + end +end + +---Stop and reset the animation +---After calling `playAnimation` `defaulState` will be played +function AnimatedSprite:stopAnimation() + self:pauseAnimation() + self.currentState = nil + self._currentFrame = 0 -- purposely + self._ticks = 1 + self._previousTicks = self._ticks + self._loopsFinished = 0 + self._currentYoyoDirection = true +end + +local function addState(self, params) + assert(params.name, "The animation state is unnamed!") + if (self.defaultState == "default") then + self.defaultState = params.name -- Init first added state as default + end + + self.states[params.name] = {} + local state = self.states[params.name] + setmetatable(state, {__index = self.states.default}) + + params = params or {} + + state.name = params.name + if (params.frames ~= nil) then + state["frames"] = params.frames -- Custom animation for non-sequential frames from the imagetable + params.framesCount = params.framesCount or #params.frames + if (type(params.firstFrameIndex) ~= "string") then + params.firstFrameIndex = params.firstFrameIndex or 1 + end + end + if (type(params.firstFrameIndex) == "string") then + local thatState = self.states[params.firstFrameIndex] + state["firstFrameIndex"] = thatState.firstFrameIndex + thatState.framesCount + else + state["firstFrameIndex"] = params.firstFrameIndex -- index in the imagetable for the firstFrame + end + state["framesCount"] = params.framesCount and params.framesCount or (self.states.default.framesCount - state.firstFrameIndex + 1) -- This state frames count + state["nextAnimation"] = params.nextAnimation -- Animation to switch to after this finishes + if (params.nextAnimation == nil) then + state["loop"] = params.loop -- You can put in number of loops or true for endless loop + else + state["loop"] = params.loop or false + end + state["reverse"] = params.reverse -- You can reverse animation sequence + state["animationStartingFrame"] = params.animationStartingFrame or (state.reverse and state.framesCount or 1) -- Frame to start the animation from + state["tickStep"] = params.tickStep -- Speed of animation (2 = every second frame) + state["frameStep"] = params.frameStep -- Number of images to skip on next frame + state["yoyo"] = params.yoyo -- Ping-pong animation (from 1 to n to 1 to n) + state["flip"] = params.flip -- You can set up flip mode, read Playdate SDK Docs for more info + state["xScale"] = params.xScale -- Optional scale for horizontal axis + state["yScale"] = params.yScale -- Optional scale for vertical axis + + state["onFrameChangedEvent"] = params.onFrameChangedEvent -- Event that will be raised when animation moves to the next frame + state["onStateChangedEvent"] = params.onStateChangedEvent -- Event that will be raised when animation state changes + state["onLoopFinishedEvent"] = params.onLoopFinishedEvent -- Event that will be raised when animation changes to the final frame + state["onAnimationEndEvent"] = params.onAnimationEndEvent -- Event that will be raised after animation in this state ends + + return state +end + +---Parse `json` file with animation configuration +---@param path string Path to the file +---@return table config You can use it in `setStates(states)` +function AnimatedSprite.loadStates(path) + return assert(json.decodeFile(path), "Requested JSON parse failed. Path: " .. path) +end + +---Returns imagetable frame index that is currently displayed +---@return integer Current frame index +function AnimatedSprite:getCurrentFrameIndex() + if (self.currentState and self.states[self.currentState].frames) then + return self.states[self.currentState].frames[self._currentFrame] + else + return self._currentFrame + end +end + +---Returns reference to the current states +---@return table states Reference to the current states +function AnimatedSprite:getLocalStates() + return self.states +end + +---Copies states +---@return table states Deepcopy of the current states +function AnimatedSprite:copyLocalStates() + return table.deepcopy(self.states) +end + +---All states from the `states` will be added to the current state machine (overwrites values in case of conflict) +---@param states table State machine state list, you can get one by calling `loadStates` +---@param animate? boolean If `True`, then the animation of default/current state will start immediately after. Default: `False` +---@param defaultState? string If provided, changes default state +function AnimatedSprite:setStates(states, animate, defaultState) + local statesCount = #states + + local function proceedState(state) + if (state.name ~= "default") then + addState(self, state) + else + local default = self.states.default + for key, value in pairs(state) do + default[key] = value + end + end + end + + if (statesCount == 0) then + proceedState(states) + if (defaultState) then + self.defaultState = defaultState + end + if (animate) then + self:playAnimation() + end + return + end + + for i = 1, statesCount do + proceedState(states[i]) + end + if (defaultState) then + self.defaultState = defaultState + end + if (animate) then + self:playAnimation() + end +end + +---You can add new states to the state machine using this function +---@param name string Name of the state, should be unique, used as id +---@param startFrame? integer Index of the first frame in the imagetable (starts from 1). Default: `1` (from states.default) +---@param endFrame? integer Index of the last frame in the imagetable. Default: last frame (from states.default) +---@param params? table See examples +---@param animate? boolean If `True`, then the animation of this state will start immediately after. Default: `False` +function AnimatedSprite:addState(name, startFrame, endFrame, params, animate) + params = params or {} + params.firstFrameIndex = startFrame or 1 + params.framesCount = endFrame and (endFrame - params.firstFrameIndex + 1) or nil + params.name = name + + addState(self, params) + + if (animate) then + self.currentState = name + self:playAnimation() + end + + return { + asDefault = function () + self.defaultState = name + end + } +end + +---Changes current state to an existing state +---@param name string New state name +---@param play? boolean If new animation should be played right away. Default: `True` +function AnimatedSprite:changeState(name, play) + if (name == self.currentState) then + return + end + play = type(play) == "nil" and true or play + local state = self.states[name] + assert (state, "There's no state named \""..name.."\".") + self.currentState = name + self._currentFrame = 0 -- purposely + self._loopsFinished = 0 + self._currentYoyoDirection = true + state.onStateChangedEvent(self) + if (play) then + self:playAnimation() + end +end + +---Force to move animation state machine to the next state +---@param instant? boolean If `False` change will be performed after the final frame of this loop iteration. Default: `True` +---@param state? string Name of the state to change to. If not provided, animator will try to change to the next animation, else stop the animation. +function AnimatedSprite:forceNextAnimation(instant, state) + instant = type(instant) == "nil" and true or instant + local currentState = self.states[self.currentState] + self.forcedState = state + + if (instant) then + self.forcedSwitchOnLoop = nil + currentState.onAnimationEndEvent(self) + if (currentState.name == self.currentState) then -- If state was not changed during the event then proceed + if (type(self.forcedState) == "string") then + self:changeState(self.forcedState) + self.forcedState = nil + elseif (currentState.nextAnimation) then + self:changeState(currentState.nextAnimation) + else + self:stopAnimation() + end + end + else + self.forcedSwitchOnLoop = self._loopsFinished + 1 + end +end + +---Sets default state. +---@param name string Name of an existing state +function AnimatedSprite:setDefaultState(name) + assert (self.states[name], "State name is nil.") + self.defaultState = name +end + +---Print all states from this state machine table to the console +function AnimatedSprite:printAllStates() + printTable(self.states) +end + +---Function that will procees the animation to the next step without redrawing sprite +local function processAnimation(self) + local state = self.states[self.currentState] + + local function changeFrame(value) + value += state.firstFrameIndex + self._currentFrame = value + state.onFrameChangedEvent(self) + end + + local reverse = state.reverse + local frame = self._currentFrame - state.firstFrameIndex + local framesCount = state.framesCount + local frameStep = state.frameStep + + if (self._currentFrame == 0) then -- true only after changing state + self._currentFrame = state.animationStartingFrame + state.firstFrameIndex - 1 + if (framesCount == 1) then + self._loopsFinished += 1 + state.onFrameChangedEvent(self) + state.onLoopFinishedEvent(self) + return + else + state.onFrameChangedEvent(self) + end + setImage(self) + return + end + + if (framesCount == 1) then -- if this state is only 1 frame long + self._loopsFinished += 1 + state.onFrameChangedEvent(self) + state.onLoopFinishedEvent(self) + return + end + + if (state.yoyo) then + if (reverse ~= self._currentYoyoDirection) then + if (frame + frameStep + 1 < framesCount) then + changeFrame(frame + frameStep) + else + if (frame ~= framesCount - 1) then + self._loopsFinished += 1 + changeFrame(2 * framesCount - frame - frameStep - 2) + state.onLoopFinishedEvent(self) + else + changeFrame(2 * framesCount - frame - frameStep - 2) + end + self._currentYoyoDirection = not self._currentYoyoDirection + end + else + if (frame - frameStep > 0) then + changeFrame(frame - frameStep) + else + if (frame ~= 0) then + self._loopsFinished += 1 + changeFrame(frameStep - frame) + state.onLoopFinishedEvent(self) + else + changeFrame(frameStep - frame) + end + self._currentYoyoDirection = not self._currentYoyoDirection + end + end + else + if (reverse) then + if (frame - frameStep > 0) then + changeFrame(frame - frameStep) + else + if (frame ~= 0) then + self._loopsFinished += 1 + changeFrame((frame - frameStep) % framesCount) + state.onLoopFinishedEvent(self) + else + changeFrame((frame - frameStep) % framesCount) + end + end + else + if (frame + frameStep + 1 < framesCount) then + changeFrame(frame + frameStep) + else + if (frame ~= framesCount - 1) then + self._loopsFinished += 1 + changeFrame((frame + frameStep) % framesCount) + state.onLoopFinishedEvent(self) + else + changeFrame((frame + frameStep) % framesCount) + end + end + end + end + + setImage(self) +end + +---Called by default in the `:update()` function. +---Must be called once per frame if you overwrite `:update()`. +---Invoke manually to move the animation to the next frame. +function AnimatedSprite:updateAnimation() + if (self._enabled) then + self._ticks += 1 + if ((self._ticks - self._previousTicks) >= self.states[self.currentState].tickStep) then + local state = self.states[self.currentState] + local loop = state.loop + local loopsFinished = self._loopsFinished + if (type(loop) == "number" and loop <= loopsFinished or + type(loop) == "boolean" and not loop and loopsFinished >= 1 or + self.forcedSwitchOnLoop == loopsFinished) then + self:forceNextAnimation(true) + return + end + processAnimation(self) + drawFrame(self) + self._previousTicks += state.tickStep + end + end +end + +function AnimatedSprite:update() + self:updateAnimation() +end \ No newline at end of file diff --git a/source/main.lua b/source/main.lua new file mode 100644 index 0000000..f066bb4 --- /dev/null +++ b/source/main.lua @@ -0,0 +1,46 @@ +-- Source: https://github.com/PizzaFuel/Links-Awakening-Overworld-for-Playdate + +--[[ + Main: + - Handles Settings + - Initializes OverworldScene + - Displays FPS if needed + - Handles main imports +--]] + +import "CoreLibs/object" +import "CoreLibs/graphics" +import "CoreLibs/sprites" +import "import/animiatedsprite" +import "overworld/overworldscene" +import "splashscreenscene" + +local currentScene = SplashScreenScene() +local showFPS = false + + +local function initialize() + playdate.display.setRefreshRate(50) +end + +-- Executed on every frame +function playdate.update() + currentScene:update() + if showFPS then + playdate.drawFPS(0,0) end +end + +-- Very simple scene handler: +function changeScene(scenename) + if scenename == "overworld" then + currentScene = OverworldScene() + end +end + +initialize() + +-- Toggle FPS shown on or off +local menu = playdate.getSystemMenu() +local checkmarkMenuItem, error = menu:addCheckmarkMenuItem("Show FPS", false, function(value) + showFPS = value +end) diff --git a/source/overworld/overworldimages.lua b/source/overworld/overworldimages.lua new file mode 100644 index 0000000..bc1f3b2 --- /dev/null +++ b/source/overworld/overworldimages.lua @@ -0,0 +1,21 @@ +--[[ + Overworld Images Handler: + - Loads all used images and imagetables on startup for performance optimization +--]] + +class("OverworldImages", +{ + Player_Walk_Up = playdate.graphics.imagetable.new("images/assets/player/walk/up"), + Player_Walk_Left = playdate.graphics.imagetable.new("images/assets/player/walk/left"), + Player_Walk_Right = playdate.graphics.imagetable.new("images/assets/player/walk/right"), + Player_Walk_Down = playdate.graphics.imagetable.new("images/assets/player/walk/down"), + Background = playdate.graphics.image.new("images/assets/map"):scaledImage(3) +}).extends() + +function OverworldImages:init() + +end + +function OverworldImages:update() + +end \ No newline at end of file diff --git a/source/overworld/overworldplayer.lua b/source/overworld/overworldplayer.lua new file mode 100644 index 0000000..b099b13 --- /dev/null +++ b/source/overworld/overworldplayer.lua @@ -0,0 +1,79 @@ +--[[ + Player Class: + - Moves Player + - Handles Player Sprite + - Handles collisions +--]] + +class("OverworldPlayer", +{ + CurrentSprite = nil, + Scene = nil +}).extends() + +function OverworldPlayer:init(scene) + self.Scene = scene + self.CurrentSprite = AnimatedSprite.new(self.Scene.Images.Player_Walk_Down) + self.CurrentSprite:moveTo(self.Scene.DefaultPlayerX, self.Scene.DefaultPlayerY) + self.CurrentSprite:addState('idle',1,3,{ tickStep = 15 }) + self.CurrentSprite:setCollideRect(5,20,22,12) + self.CurrentSprite:playAnimation() +end + +-- Executed on every frame +function OverworldPlayer:update() + x, y = self.CurrentSprite:getPosition() + + if not(self.CurrentSprite._enabled) then + self.CurrentSprite:playAnimation() + end + + -- Calculates how far the player will move + moveDistance = self.Scene.ElapsedTime * 90 + if playdate.buttonIsPressed(playdate.kButtonB) then + moveDistance *= 2 end + + -- Handle player movement + if playdate.buttonIsPressed(playdate.kButtonLeft) then + self:changeDirection("walk_left") + self.CurrentSprite:moveWithCollisions(x - moveDistance, y) + elseif playdate.buttonIsPressed(playdate.kButtonRight) then + self:changeDirection("walk_right") + self.CurrentSprite:moveWithCollisions(x + moveDistance, y) + elseif playdate.buttonIsPressed(playdate.kButtonUp) then + self:changeDirection("walk_up") + self.CurrentSprite:moveWithCollisions(x, y - moveDistance) + elseif playdate.buttonIsPressed(playdate.kButtonDown) then + self:changeDirection("walk_down") + self.CurrentSprite:moveWithCollisions(x, y + moveDistance) + else + -- Pause animation if no direction is pressed + self.CurrentSprite:pauseAnimation() + end +end + +-- Updates Player Sprite if needed +function OverworldPlayer:changeDirection(direction) + newSprite = nil + + if direction == "walk_up" then + newSprite = self.Scene.Images.Player_Walk_Up + elseif direction == "walk_left" then + newSprite = self.Scene.Images.Player_Walk_Left + elseif direction == "walk_right" then + newSprite = self.Scene.Images.Player_Walk_Right + elseif direction == "walk_down" then + newSprite = self.Scene.Images.Player_Walk_Down + end + + -- Replace Player Sprite if new direction was chosen + if newSprite ~= nil and self.CurrentSprite.imagetable ~= newSprite then + x, y = self.CurrentSprite:getPosition() + self.CurrentSprite:remove() + self.CurrentSprite = AnimatedSprite.new(newSprite) + self.CurrentSprite:moveTo(x, y) + self.CurrentSprite:setCollideRect(5,20,22,12) + self.CurrentSprite:addState('idle',1,3,{ tickStep = 15 }) + self.CurrentSprite:playAnimation() + end +end \ No newline at end of file diff --git a/source/overworld/overworldscene.lua b/source/overworld/overworldscene.lua new file mode 100644 index 0000000..4de1648 --- /dev/null +++ b/source/overworld/overworldscene.lua @@ -0,0 +1,51 @@ +--[[ + Overworld Scene: + - Imports, intializes and updates all overworld clases + - Handles camera movement +--]] + +import "overworld/overworldplayer" +import "overworld/overworldimages" + +class("OverworldScene", +{ + Player = nil, + Images = OverworldImages(), + Background = nil, + DefaultPlayerX = 200, + DefaultPlayerY = 120, + Song = nil, + ElapsedTime = 0, +}).extends() + +function OverworldScene:init() + self.Player = OverworldPlayer(self) + self.Background = playdate.graphics.sprite.new(self.Images.Background) + self.Background:moveTo(2020, -511) + self.Background:setZIndex(-1) + self.Background:add() + self.Song = playdate.sound.sampleplayer.new("sounds/theme") + self.Song:play(100) + playdate.resetElapsedTime() +end + +function OverworldScene:update() + -- Get Elapsed Time to allow variable framerates + self.ElapsedTime = playdate.getElapsedTime() + playdate.resetElapsedTime() + + -- Update all objects on the overworld + self.Player:update() + + -- Reset Player Position to center + playerX, playerY = self.Player.CurrentSprite:getPosition() + self.Player.CurrentSprite:moveTo(self.DefaultPlayerX, self.DefaultPlayerY) + + -- Moves camera using the difference in player positions. + -- This code should be executed for every object, except for the player + x, y = self.Background:getPosition() + self.Background:moveTo(x + self.DefaultPlayerX - playerX, y + self.DefaultPlayerY - playerY) + + -- Update Screen + playdate.graphics.sprite.update() +end \ No newline at end of file diff --git a/source/pdxinfo b/source/pdxinfo new file mode 100644 index 0000000..aad6e1d --- /dev/null +++ b/source/pdxinfo @@ -0,0 +1,8 @@ +name=Link's Awakening Overworld (DEMO) for Playdate +author=PizzaFuel @PizzaFuelDev +description=Take a walk around the overworld from Link's Awakening on GameBoy +bundleID=com.pizzafuel.linksoverworld +version=1.0 +buildNumber=1 +imagePath=images/cards +contentWarning=This is a Fan Project and not related to Nintendo in any way. Please support the official release! \ No newline at end of file diff --git a/source/sounds/theme.wav b/source/sounds/theme.wav new file mode 100644 index 0000000..143b0ef Binary files /dev/null and b/source/sounds/theme.wav differ diff --git a/source/splashscreenscene.lua b/source/splashscreenscene.lua new file mode 100644 index 0000000..8674e99 --- /dev/null +++ b/source/splashscreenscene.lua @@ -0,0 +1,20 @@ +--[[ + The games's splash screeen. Changes to gameplay after 3 seconds. +--]] + +class("SplashScreenScene", +{ + +}).extends() + +function SplashScreenScene:init() + playdate.resetElapsedTime() + playdate.graphics.drawTextAligned("Link's Awakening Overworld Demo\n\nMade by @PizzaFuelDev", 200, 100, kTextAlignment.center) +end + +function SplashScreenScene:update() + print(playdate.getElapsedTime()) + if playdate.getElapsedTime() > 3 then + changeScene("overworld") + end +end \ No newline at end of file