commit 85225f7c2d17c2b2e08653318e0bef5508a6afbb Author: assada Date: Wed May 29 17:38:50 2024 +0300 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b205ba3 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +builds/* 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..b47c477 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,8 @@ +{ + "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, +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..6399696 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,103 @@ +{ + // 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": "&", + "windows": { + "args": [ + "${workspaceFolder}\\Build and Run (Simulator).ps1", + "-build", + "'${workspaceFolder}\\builds'", + "-source", + "'${workspaceFolder}\\source'", + "-name", + "'${workspaceFolderBasename}'" + ], + "options": { + "shell": { + "executable": "powershell.exe" + } + } + }, + "linux": { + "command": "${workspaceFolder}/build_and_run.sh", + "args": [ + "build" + ] + }, + "presentation": { + "showReuseMessage": false, + "reveal": "always", + "panel": "shared" + } + }, + { + "label": "Invoke Run script", + "type": "shell", + "command": "&", + "windows": { + "args": [ + "${workspaceFolder}\\Build and Run (Simulator).ps1", + "-build", + "'${workspaceFolder}\\builds'", + "-source", + "'${workspaceFolder}\\source'", + "-name", + "'${workspaceFolderBasename}'", + "-dontbuild" + ], + "options": { + "shell": { + "executable": "powershell.exe" + } + } + }, + "linux": { + "command": "${workspaceFolder}/build_and_run.sh", + "args": [ + "run" + ] + }, + "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/README.md b/README.md new file mode 100644 index 0000000..bad0f1d --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# FPV Run diff --git a/build_and_run.sh b/build_and_run.sh new file mode 100755 index 0000000..eed4319 --- /dev/null +++ b/build_and_run.sh @@ -0,0 +1,145 @@ +#!/usr/bin/env bash + +export PLAYDATE_SDK_PATH="/home/ut3usw/PlaydateSDK-2.5.0" + +# Check for color by variable and tput command +if [[ -z $NOCOLOR && -n $(command -v tput) ]]; then + RED=$(tput setaf 1) + CYN=$(tput setaf 6) + YEL=$(tput setaf 3) + RST=$(tput sgr0) +fi + +function display_help() { + printf "%s\n\n" "${0} build|run|-h|--help|" + printf "%-16s\n" "build: Builds the project and runs the Simulator" + printf "%-16s\n" "run : Skips building the project and runs the Simulator" + printf "\n" + printf "%s\n\n" "Set NOCOLOR=1 to disable terminal coloring" + exit 0 +} + +# We don't need fancy flags/operators for two commands +case $1 in + "build") + BUILD=1 + ;; + "run") + BUILD=0 + ;; + *) + display_help + exit 1 + ;; +esac + +# Set some paths +BUILD_DIR="./builds" +SOURCE_DIR="./source" +PDX_PATH="${BUILD_DIR}/$(basename $(pwd)).pdx" + +# Logging functions +function log() { + printf "%s\n" "${CYN}>> $1${RST}" +} +function log_warn() { + printf "%s\n" "${YEL}>! $1${RST}" +} +function log_err() { + printf "%s\n >> %s\n" "${RED}!! ERROR !!" "$1${RST}" +} + +function check_pdxinfo() { + if [[ -f ./source/pdxinfo ]]; then + if grep "com.organization.package" ./source/pdxinfo 2>&1 >/dev/null; then + log_warn "PDXINFO NOTICE:" + log_warn "Don't forget to change your unique project info in 'source/pdxinfo': 'bundleID', 'name', 'author', 'description'." + log_warn "It's critical to change your game bundleID, so there will be no collisions with other games, installed via sideload." + log_warn "Read more about pdxinfo here: https://sdk.play.date/Inside%20Playdate.html#pdxinfo" + fi + fi +} + +function chk_err() { + # Check for errors in last process and bail if needed + if [[ $? > 0 ]]; then + log_err "There was an issue with the previous command; exiting!" + exit 1 + fi +} + +function check_close_sim() { + # Check if we have 'pidof' + PIDOF=$(command -v pidof2) + + # Prefer 'pidof'; use ps if not + if [[ -n $PIDOF ]]; then + SIMPID=$($PIDOF "PlaydateSimulator") + if [[ -n $SIMPID ]]; then + log "Found existing Simulator, closing..." + kill -9 $SIMPID + chk_err + fi + else + SIMPID=$(ps aux | grep PlaydateSimulator | grep -v grep | awk '{print $2}') + if [[ -n $SIMPID ]]; then + log "Found existing Simulator, closing..." + kill -9 $SIMPID + chk_err + fi + fi +} + +# Create build dir +function make_build_dir() { + if [[ ! -d "${BUILD_DIR}" ]]; then + log "Creating build directory..." + mkdir -p "${BUILD_DIR}" + chk_err + fi +} + +# Clean build dir +function clean_build_dir() { + if [[ -d "${BUILD_DIR}" ]]; then + log "Cleaning build directory..." + rm -rfv "${BUILD_DIR}/*" + chk_err + fi +} + +# Compile the PDX +function build_pdx() { + if [[ $BUILD == 1 ]]; then + log "Building PDX with 'pdc'..." + $PLAYDATE_SDK_PATH/bin/pdc -sdkpath "${PLAYDATE_SDK_PATH}" "${SOURCE_DIR}" "${PDX_PATH}" + chk_err + fi +} + +# Run the PDX with Simulator +function run_pdx() { + if [[ -d "${PDX_PATH}" ]]; then + log "Running PDX with Simulator..." + $PLAYDATE_SDK_PATH/bin/PlaydateSimulator "${PDX_PATH}" + else + log_err "PDX doesn't exist! Please 'build' the project first!" + fi +} + + +#### MAIN SCRIPT #### +if [[ $BUILD == 1 ]]; then + log "Attempting a build and run of PDX..." + make_build_dir + clean_build_dir + check_pdxinfo + build_pdx + check_close_sim + run_pdx +else + log "Attempting to run PDX: ${PDX_PATH}..." + check_close_sim + run_pdx +fi + diff --git a/source/audio/hello.wav b/source/audio/hello.wav new file mode 100644 index 0000000..a3d64d6 Binary files /dev/null and b/source/audio/hello.wav differ diff --git a/source/audio/quad.wav b/source/audio/quad.wav new file mode 100644 index 0000000..04d6030 Binary files /dev/null and b/source/audio/quad.wav differ diff --git a/source/audio/telemko.wav b/source/audio/telemko.wav new file mode 100644 index 0000000..d33bcb3 Binary files /dev/null and b/source/audio/telemko.wav differ diff --git a/source/audio/ukr.wav b/source/audio/ukr.wav new file mode 100644 index 0000000..2207e57 Binary files /dev/null and b/source/audio/ukr.wav differ diff --git a/source/audio/war.wav b/source/audio/war.wav new file mode 100644 index 0000000..c83e8e4 Binary files /dev/null and b/source/audio/war.wav differ diff --git a/source/backgroundSprite.lua b/source/backgroundSprite.lua new file mode 100644 index 0000000..88ed9c7 --- /dev/null +++ b/source/backgroundSprite.lua @@ -0,0 +1,15 @@ +local pd = playdate +local gfx = playdate.graphics + +class("BackGround").extends() + +function BackGround:init(x, y) + gfx.sprite.setBackgroundDrawingCallback( + function (x, y, width, height) + local groundImage = playdate.graphics.image.new("sprites/bg") + gfx.setClipRect(x, y, width, height) + groundImage:draw(0,0) + gfx.clearClipRect() + end + ) +end diff --git a/source/font/Mini Sans 2X-table-18-20.png b/source/font/Mini Sans 2X-table-18-20.png new file mode 100644 index 0000000..cb26dc7 Binary files /dev/null and b/source/font/Mini Sans 2X-table-18-20.png differ diff --git a/source/font/Mini Sans 2X.fnt b/source/font/Mini Sans 2X.fnt new file mode 100644 index 0000000..cb1a5ff --- /dev/null +++ b/source/font/Mini Sans 2X.fnt @@ -0,0 +1,265 @@ +--metrics={"baseline":0,"xHeight":0,"capHeight":0,"pairs":{"Ta":[-4,1],"Tc":[-4,1],"Te":[-4,1],"To":[-4,1],"Tm":[-4,1],"Tn":[-4,1],"Tr":[-4,1],"Tu":[-4,1],"Tv":[-4,1],"Tw":[-4,1],"fB":[2,1],"fD":[2,1],"fE":[2,1],"fF":[2,1],"fH":[2,1],"fI":[2,1],"fK":[2,1],"fL":[2,1],"fM":[2,1],"fN":[2,1],"fP":[2,1],"fR":[2,1],"fW":[2,1],"fb":[2,1],"fh":[2,1],"fk":[2,1],"fC":[2,1],"fG":[2,1],"fO":[2,1],"fQ":[2,1],"fU":[2,1],"fl":[2,1],"lB":[1,1],"lD":[1,1],"lE":[1,1],"lF":[1,1],"lH":[1,1],"lI":[1,1],"lK":[1,1],"lL":[1,1],"lM":[1,1],"lN":[1,1],"lP":[1,1],"lR":[1,1],"lW":[1,1],"lb":[1,1],"lh":[1,1],"lk":[1,1],"lC":[1,1],"lG":[1,1],"lO":[1,1],"lQ":[1,1],"lU":[0,1,1,1],"ll":[0,1,1,1],"la":[0,1,1,1],"lc":[0,1,1,1],"le":[0,1,1,1],"lo":[0,1,1,1],"lm":[1,1],"ln":[1,1],"lr":[1,1],"lu":[0,1,1,1],"lv":[0,1,1,1],"lw":[0,1,1,1],"Fa":[-2,1],"Fc":[-2,1],"Fe":[-2,1],"Fo":[-2,1],"Fm":[-2,1],"Fn":[-2,1],"Fr":[-2,1],"Fu":[-2,1],"Fv":[-2,1],"Fw":[-2,1],"aT":[-4,2],"uT":[-4,2],"gT":[-4,2],"yT":[-4,2],"mT":[-4,2],"nT":[-4,2],"vT":[-4,2],"wT":[-4,2],"Td":[-4,0],"Tf":[-2,0],"Tg":[-4,0],"Tp":[-4,0],"Tq":[-4,0],"Ts":[-4,0],"Tt":[-2,0],"Tx":[-4,0],"Ty":[-4,0],"Tz":[-4,0],"TJ":[-4,0],"T�":[-4,0],"bT":[-4,0],"cT":[-4,0],"eT":[-4,0],"fA":[1,0,2,0],"fT":[2,0],"fi":[2,0],"fj":[2,0],"fY":[2,0],"fJ":[-2,0],"fS":[2,0],"fV":[2,0],"fX":[2,0],"fZ":[2,0],"hT":[-4,0],"kT":[-4,0],"lA":[1,0],"lT":[-1,0],"ld":[0,0,1,0],"lf":[-1,0],"lg":[0,0,1,0],"li":[1,0],"lj":[1,0],"lp":[1,0],"lq":[0,0,1,0],"ls":[1,0],"lt":[-1,0],"lx":[1,0],"ly":[0,0,1,0],"lz":[1,0],"lY":[-1,0],"lJ":[1,0],"lS":[1,0],"lV":[-1,0],"lX":[1,0],"lZ":[1,0],"l�":[-1,0],"oT":[-4,0],"pT":[-4,0],"qT":[-4,0],"rT":[-4,0],"rX":[-2,0],"rZ":[-2,0],"sT":[-4,0],"tT":[-2,0],"xT":[-4,0],"zT":[-4,0],"C�":[-2,0],"Ef":[-2,0],"Et":[-2,0],"E�":[-2,0],"Fd":[-2,0],"Ff":[-2,0],"Fg":[-2,0],"Fp":[-2,0],"Fq":[-2,0],"Fs":[-2,0],"Ft":[-2,0],"Fx":[-2,0],"Fy":[-2,0],"Fz":[-2,0],"FJ":[-2,0],"F�":[-2,0],"YJ":[-2,0],"Kf":[-2,0],"Kt":[-2,0],"K�":[-4,0],"LT":[-4,0],"Lf":[-2,0],"Lt":[-2,0],"LY":[-4,0],"LV":[-4,0],"L�":[-4,0],"PJ":[-2,0],"X�":[-2,0],"Z�":[-2,0],"�T":[-4,0],"�X":[-2,0],"�Z":[-2,0]},"left":["BDEFHIKLMNPRWbhk","CGOQ","Ul","aceo","mnr","uvw"],"right":["AQ","DO","HIMNWd","JU","au","gy","mn","vw"]} +tracking=2 + +0 12 +1 12 +2 12 +3 12 +4 12 +5 12 +6 12 +7 12 +8 12 +9 12 +space 8 +. 4 +A 12 +B 12 +T 12 +a 12 +b 12 +c 12 +d 12 +e 12 +f 8 +g 12 +h 12 +i 4 +j 4 +k 12 +l 5 +m 16 +n 12 +o 12 +p 12 +q 12 +r 12 +s 12 +t 8 +u 12 +v 12 +w 16 +x 12 +y 12 +z 12 +, 4 +C 12 +D 12 +E 12 +F 12 +G 12 +O 12 +H 12 +I 4 +! 4 +" 10 +' 4 +Y 12 +{ 8 +| 4 +} 8 +J 12 +K 14 +L 12 +M 14 +N 14 +P 12 +Q 12 +R 12 +S 12 +U 12 +V 14 +W 14 +X 12 +Z 12 +/ 12 +\ 12 +[ 6 +] 6 +: 4 +; 4 +^ 12 +_ 10 +` 8 +~ 16 +¥ 12 +… 16 +™ 14 +‼ 10 +© 18 +® 18 +� 16 +@ 18 +# 18 +$ 12 +% 14 +& 12 +( 6 +) 6 +* 12 ++ 12 +- 10 += 10 +? 12 +< 10 +> 10 + +Ta -4 +Tc -4 +Te -4 +To -4 +Tm -4 +Tn -4 +Tr -4 +Tu -4 +Tv -4 +Tw -4 +fB 2 +fD 2 +fE 2 +fF 2 +fH 2 +fI 2 +fK 2 +fL 2 +fM 2 +fN 2 +fP 2 +fR 2 +fW 2 +fb 2 +fh 2 +fk 2 +fC 2 +fG 2 +fO 2 +fQ 2 +fU 2 +fl 2 +lB 1 +lD 1 +lE 1 +lF 1 +lH 1 +lI 1 +lK 1 +lL 1 +lM 1 +lN 1 +lP 1 +lR 1 +lW 1 +lb 1 +lh 1 +lk 1 +lC 1 +lG 1 +lO 1 +lQ 1 +lm 1 +ln 1 +lr 1 +Fa -2 +Fc -2 +Fe -2 +Fo -2 +Fm -2 +Fn -2 +Fr -2 +Fu -2 +Fv -2 +Fw -2 +aT -4 +uT -4 +gT -4 +yT -4 +mT -4 +nT -4 +vT -4 +wT -4 +Td -4 +Tf -2 +Tg -4 +Tp -4 +Tq -4 +Ts -4 +Tt -2 +Tx -4 +Ty -4 +Tz -4 +TJ -4 +T� -4 +bT -4 +cT -4 +eT -4 +fA 1 +fT 2 +fi 2 +fj 2 +fY 2 +fJ -2 +fS 2 +fV 2 +fX 2 +fZ 2 +hT -4 +kT -4 +lA 1 +lT -1 +lf -1 +li 1 +lj 1 +lp 1 +ls 1 +lt -1 +lx 1 +lz 1 +lY -1 +lJ 1 +lS 1 +lV -1 +lX 1 +lZ 1 +l� -1 +oT -4 +pT -4 +qT -4 +rT -4 +rX -2 +rZ -2 +sT -4 +tT -2 +xT -4 +zT -4 +C� -2 +Ef -2 +Et -2 +E� -2 +Fd -2 +Ff -2 +Fg -2 +Fp -2 +Fq -2 +Fs -2 +Ft -2 +Fx -2 +Fy -2 +Fz -2 +FJ -2 +F� -2 +YJ -2 +Kf -2 +Kt -2 +K� -4 +LT -4 +Lf -2 +Lt -2 +LY -4 +LV -4 +L� -4 +PJ -2 +X� -2 +Z� -2 +�T -4 +�X -2 +�Z -2 diff --git a/source/ground.lua b/source/ground.lua new file mode 100644 index 0000000..cdc8388 --- /dev/null +++ b/source/ground.lua @@ -0,0 +1,31 @@ +class("Ground").extends(playdate.graphics.sprite) + +function Ground:init(x, y, player) + local groundImage = playdate.graphics.image.new("sprites/groundFin") + Ground.super.init(self, groundImage) + self:moveTo(x, y) + self:setZIndex(100) + self:setTag(3) + self:setCollideRect(0, 28, 800, 10) + + Ground.moveSpeed = 2 + Ground.player = player +end + +function Ground:setMoveSpeed(speed) + Ground.moveSpeed = speed +end + +function Ground:update() + if Ground.player.isMovingRight() == false then + Ground.moveSpeed = 0.2 + else + Ground.moveSpeed = 1 + end + + if self.x <= 0 then + self:moveWithCollisions(400, self.y) + end + + self:moveWithCollisions(self.x-Ground.moveSpeed, self.y) +end \ No newline at end of file diff --git a/source/level.lua b/source/level.lua new file mode 100644 index 0000000..aa1a428 --- /dev/null +++ b/source/level.lua @@ -0,0 +1,68 @@ +local pd = playdate +local gfx = playdate.graphics + +import "player" +import "ground" +import "backgroundSprite" + + +class("Level").extends() + + +-- This function relies on the use of timers, so the timer core library +-- must be imported, and updateTimers() must be called in the update loop +local function screenShake(shakeTime, shakeMagnitude) + -- Creating a value timer that goes from shakeMagnitude to 0, over + -- the course of 'shakeTime' milliseconds + local shakeTimer = playdate.timer.new(shakeTime, shakeMagnitude, 0) + -- Every frame when the timer is active, we shake the screen + shakeTimer.updateCallback = function(timer) + -- Using the timer value, so the shaking magnitude + -- gradually decreases over time + local magnitude = math.floor(timer.value) + local shakeX = math.random(-magnitude, magnitude) + local shakeY = math.random(-magnitude, magnitude) + playdate.display.setOffset(shakeX, shakeY) + end + -- Resetting the display offset at the end of the screen shake + shakeTimer.timerEndedCallback = function() + playdate.display.setOffset(0, 0) + end +end + + +function Level:init() + player = Player(30, 30) + player:add() + + ground = Ground(400, 225, player) + ground:add() + + BackGround() + + local fp = playdate.sound.fileplayer.new( "audio/war" ) + local hello = playdate.sound.fileplayer.new( "audio/hello" ) + + hello:play(1) + fp:setVolume(0.7) + fp:play(0) + + Level.telemLostSound = playdate.sound.fileplayer.new( "audio/telemko" ) + Level.telemLostSoundPlayed = false +end + +function Level:update() + if player.isDead() then + if not Level.telemLostSoundPlayed then + Level.telemLostSound:play(1) + Level.telemLostSoundPlayed = true + screenShake(500, 5) + end + gfx.drawText("Telemetry Lost", 100, 110) + end + + local bat = player.getBat() + bat = math.floor(bat/100) + gfx.drawText("Bat: " .. bat, 10, 215) + -- gfx.drawText("Dis: " .. player.dischargeRate, 100, 215) +end \ No newline at end of file diff --git a/source/lib/AnimatedSprite.lua b/source/lib/AnimatedSprite.lua new file mode 100644 index 0000000..9e7de68 --- /dev/null +++ b/source/lib/AnimatedSprite.lua @@ -0,0 +1,512 @@ +----------------------------------------------- +--- 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 + +import 'CoreLibs/object' +import 'CoreLibs/sprites' +local gfx = playdate.graphics +local function emptyFunc()end + +class("AnimatedSprite").extends(gfx.sprite) + +---@param imagetable table|string actual imagetable or path +---@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 + if (type(imagetable) == "string") then + imagetable = gfx.imagetable.new(imagetable) + end + 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 the animation without resetting +function AnimatedSprite:resumeAnimation() + self._enabled = true +end + +---Play/Pause animation based on current state +function AnimatedSprite:toggleAnimation() + if (self._enabled) then + self:pauseAnimation() + else + self:resumeAnimation() + end +end + +---Stop and reset the animation +---After calling `playAnimation` the animation will start from `defaultState` +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.firstFrameIndex = 1 + params.framesCount = #params.frames + 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 + +---Get imagetable's frame index that is currently displayed +---@return integer index 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 + +---Get the current frame's local index in the state +---I.e. 1, 2, 3, N, where N = number of frames in this state +---Also works if `frames` property was provided +---@return integer index Current frame local index +function AnimatedSprite:getCurrentFrameLocalIndex() + return self.currentFrame - self.states[self.currentState].firstFrameIndex + 1 +end + +---Get reference to the current state +---@return table state Reference to the current state +function AnimatedSprite:getCurrentState() + return self.states[self.currentState] +end + +---Get reference to the current states +---@return table states Reference to the current states +function AnimatedSprite:getLocalStates() + return self.states +end + +---Get copy of the states +---@return table states Deepcopy of the current states +function AnimatedSprite:copyLocalStates() + return table.deepcopy(self.states) +end + +---Add all states from the `states` to the current state machine (overwrites values in case of conflicts) +---@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 + +---Add new state to the state machine +---@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 + +---Change 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 + +---Change current state to an existing state and start from selected frame +---If new state is the same as current state, nothing will change +---@param name string New state name +---@param frameIndex integer Local frame index of this state. Indexing starts from 1. Default: `1` +---@param play? boolean If new animation should be played right away. Default: `True` +function AnimatedSprite:changeStateAndSelectFrame(name, frameIndex, play) + if (name == self.currentState) then + return + end + play = type(play) == "nil" and true or play + frameIndex = frameIndex or 1 + local state = self.states[name] + assert (state, "There's no state named \""..name.."\".") + self.currentState = name + self._currentFrame = state.firstFrameIndex + frameIndex - 1 + self._loopsFinished = 0 + self._currentYoyoDirection = true + state.onStateChangedEvent(self) + if (play) then + self:playAnimation() + end +end + +---Force animation state machine to switch 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 + +---Set 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 to the console for debug purposes +function AnimatedSprite:printAllStates() + printTable(self.states) +end + +---Procees the animation to the next step without redrawing the 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..5b59c7b --- /dev/null +++ b/source/main.lua @@ -0,0 +1,55 @@ +-- CoreLibs +import "CoreLibs/object" +import "CoreLibs/graphics" +import "CoreLibs/sprites" +import "CoreLibs/timer" +import "CoreLibs/crank" + +local pd = playdate +local gfx = pd.graphics + +TAGS = { + Pickup = 1, + Player = 2, + Hazard = 3 +} + +Z_INDEXES = { + Ground = 100, + Pickup = 50, + Player = 20 +} + +local font = gfx.font.new('font/Mini Sans 2X') + +gfx.setFont(font) + +-- Libraries +import "lib/AnimatedSprite" +playdate.display.setRefreshRate(50) + +-- Game +import "level" + +level = nil + +local function initialize() + -- Make it different, every time! + math.randomseed(playdate.getSecondsSinceEpoch()) + + -- Init all the things! + level = Level() + playdate.resetElapsedTime() +end + +initialize() + +function pd.update() + gfx.sprite.update() + pd.timer.updateTimers() + pd.drawFPS(10,0) + + if level then + level.update() + end +end \ No newline at end of file diff --git a/source/menu.lua b/source/menu.lua new file mode 100644 index 0000000..e69de29 diff --git a/source/pdxinfo b/source/pdxinfo new file mode 100644 index 0000000..a4e138b --- /dev/null +++ b/source/pdxinfo @@ -0,0 +1,6 @@ +name=FPVRun +author=ut3usw +description=My first game +bundleID=com.ut3usw.fpv +version=0.0.1 +buildNumber=1 \ No newline at end of file diff --git a/source/player.lua b/source/player.lua new file mode 100644 index 0000000..1c52c0d --- /dev/null +++ b/source/player.lua @@ -0,0 +1,189 @@ +local pd = playdate +local gfx = playdate.graphics + +class('Player').extends(AnimatedSprite) + +function Player:init(x, y, gameManager) + self.gameManager = gameManager + + local playerImageTable = gfx.imagetable.new("sprites/player-table-48-48") + Player.super.init(self, playerImageTable) + + self:addState("run", 8,14, {tickStep = 2}) + self:addState("up", 1,7, {tickStep = 6}) + self:addState("down", 1,7, {tickStep = 6}) + self:addState("boom", 15,21, {tickStep = 10, loop = false}) + + self:setDefaultState("down") + + self:playAnimation() + + -- Sprite properties + self:moveTo(x, y) + self:setZIndex(10) + self:setCollideRect(3, 19, 63, 33) + + self:setTag(1) + + self:changeState("down") + + -- Physics properties + self.xVelocity = 0 + self.yVelocity = 0 + self.maxXSpeed = 2 + self.maxYSpeed = 5 + + self.fallSpeed = 0.4 + + -- Player State + self.touchingGround = false + self.touchingCeiling = false + self.touchingWall = false + Player.dead = false + + Player.bat = 10000 + + Player.dischargeRate = 1 + + self.cantDown = false + + Player.moveRight = false +end + +function Player:handleGroundInput(state) + if Player.bat <= 0 or Player.dead then + return + end + + local change, acceleratedChange = playdate.getCrankChange() + + if pd.buttonJustReleased(pd.kButtonLeft) or pd.buttonJustReleased(pd.kButtonRight) then + self.cantDown = false + end + + if pd.buttonIsPressed(pd.kButtonLeft) then + self:changeToRunState("left") + self.cantDown = true + Player.moveRight = false + elseif pd.buttonIsPressed(pd.kButtonRight) then + self:changeToRunState("right") + self.cantDown = true + Player.moveRight = true + end + + if change ~= 0 then + local force = 0.01 + if change > 0 then + self:changeState("up") + else + self:changeState("down") + force = 0.05 + end + self.yVelocity = self.yVelocity - acceleratedChange * force + elseif self.cantDown == false then + self.cantDown = false + self:changeToDownState() + else + self.yVelocity = self.fallSpeed + end +end + +function Player:changeToDownState() + Player.moveRight = false + self.yVelocity = self.fallSpeed + self.xVelocity = 0 + self:changeState("down") +end + +function Player:changeToRunState(direction) + if direction == "left" then + self.xVelocity = -self.maxXSpeed + self.globalFlip = 1 + self:changeState("run") + elseif direction == "right" then + self.xVelocity = self.maxXSpeed + self.globalFlip = 0 + self:changeState("run") + end +end + +function Player:handleMovementAndCollisions() + local _, _, collisions, length = self:moveWithCollisions(self.x + self.xVelocity, self.y + self.yVelocity) + + self.touchingGround = false + self.touchingCeiling = false + self.touchingWall = false + + for i=1,length do + local collision = collisions[i] + local collisionType = collision.type + local collisionObject = collision.other + local collisionTag = collisionObject:getTag() + if collisionType == gfx.sprite.kCollisionTypeSlide then + if collision.normal.y == -1 then + self.touchingGround = true + elseif collision.normal.y == 1 then + self.touchingCeiling = true + end + + if collision.normal.x ~= 0 then + self.touchingWall = true + end + end + + if collisionTag == 3 then + Player.dead = true + self:changeState("boom") + -- elseif collisionTag == TAGS.Pickup then + -- collisionObject:pickUp(self) + end + end + +end + +function Player:isMovingRight() + return Player.moveRight +end + +function Player:isDead() + return Player.dead +end + +function Player:getBat() + return Player.bat +end + +function Player:handleDischarge(state) + if Player.dead then + return + end + if Player.bat <= 0 then + Player.bat = 0 + self.fallSpeed = 2 + self:changeToDownState() + end + + if state == "run" then + Player.dischargeRate = 5 + elseif state == "up" then + Player.dischargeRate = 10 + else + Player.dischargeRate = 1 + end + + Player.bat = Player.bat - Player.dischargeRate +end + +function Player:update() + self:updateAnimation() + if Player.dead then + return + end + + local state = self:getCurrentState()["name"] + + self:handleDischarge(state) + self:handleGroundInput(state) + + self:handleMovementAndCollisions() +end \ No newline at end of file diff --git a/source/sprites/bg.png b/source/sprites/bg.png new file mode 100644 index 0000000..3121cd2 Binary files /dev/null and b/source/sprites/bg.png differ diff --git a/source/sprites/groundDbg.png b/source/sprites/groundDbg.png new file mode 100644 index 0000000..300b536 Binary files /dev/null and b/source/sprites/groundDbg.png differ diff --git a/source/sprites/groundFin.png b/source/sprites/groundFin.png new file mode 100644 index 0000000..a9a4b15 Binary files /dev/null and b/source/sprites/groundFin.png differ diff --git a/source/sprites/player-table-64-64.png b/source/sprites/player-table-64-64.png new file mode 100644 index 0000000..0c1d467 Binary files /dev/null and b/source/sprites/player-table-64-64.png differ