├── research ├── output.png ├── low_score_rate.png ├── high_score_rate.png ├── high_score_speed.png ├── low_score_speed.png └── measure_seeds.ipynb ├── source ├── img │ ├── block.png │ ├── background.png │ ├── gameover.png │ ├── scene │ │ ├── sky.png │ │ ├── 2_lamp.png │ │ ├── 9_ufo.png │ │ ├── 9b_ufo.png │ │ ├── frame.png │ │ ├── hills.png │ │ ├── lights.png │ │ ├── 10_moon.png │ │ ├── 10b_moon.png │ │ ├── 11_planet.png │ │ ├── 12_city.png │ │ ├── 12b_city.png │ │ ├── 2b_lamp.png │ │ ├── 3_house.png │ │ ├── 4_wheel.png │ │ ├── 4b_home.png │ │ ├── 6_bridge.png │ │ ├── 7_tower.png │ │ ├── 7b_castle.png │ │ ├── 8b_tower.png │ │ ├── firework.png │ │ ├── lights2.png │ │ ├── 11b_planet.png │ │ ├── 3b_monster.png │ │ ├── 6b_turbine.png │ │ ├── background.png │ │ ├── 8_skyscraper.png │ │ └── comet-table-112-32.png │ ├── menu │ │ ├── cursor.png │ │ ├── death_msg.png │ │ ├── eyes-table-22-18.png │ │ ├── about-table-400-240.png │ │ └── home-table-400-240.png │ ├── block-outline.png │ ├── angel-table-20-29.png │ ├── bagel-table-20-20.png │ ├── dust-table-20-20.png │ ├── dust2-table-28-22.png │ ├── scores-table-19-8.png │ ├── seed-table-20-20.png │ ├── spit-table-80-80.png │ ├── beagle-table-23-21.png │ ├── leaves-table-13-10.png │ ├── player-table-22-21.png │ ├── player2-table-37-30.png │ ├── tongue-table-13-13.png │ ├── angel-outline-table-20-29.png │ ├── seed-outline-table-20-20.png │ └── fonts │ │ ├── space-harrier.fnt │ │ ├── space-harrier2.fnt │ │ └── connection_bold.fnt ├── sfx │ ├── 100_1.wav │ ├── 300_1.wav │ ├── 50_1.wav │ ├── comet.wav │ ├── pause.wav │ ├── spit.wav │ ├── start.wav │ ├── walk.wav │ ├── walk2.wav │ ├── 1000_1.wav │ ├── select.wav │ ├── tenshi.wav │ ├── unpause.wav │ ├── bean_catch.wav │ ├── menuback.wav │ ├── pyoro_die.wav │ ├── restore1.wav │ ├── restore2.wav │ ├── restore3.wav │ ├── restore4.wav │ ├── restore5.wav │ ├── tongue_out.wav │ ├── tile_destroy.wav │ ├── tongue_retract.wav │ ├── selection_reverse.wav │ └── normal_395_transition.wav ├── system │ ├── card.png │ ├── card-highlighted │ │ ├── animation.txt │ │ ├── 1.png │ │ ├── 2.png │ │ ├── 3.png │ │ ├── 4.png │ │ ├── 5.png │ │ └── 6.png │ └── launchImages │ │ ├── 1.png │ │ ├── 2.png │ │ ├── 3.png │ │ ├── 4.png │ │ ├── 5.png │ │ ├── 6.png │ │ ├── 7.png │ │ ├── 8.png │ │ ├── 9.png │ │ ├── 10.png │ │ ├── 11.png │ │ ├── 12.png │ │ ├── 13.png │ │ ├── 14.png │ │ ├── 15.png │ │ ├── 16.png │ │ ├── 17.png │ │ ├── 18.png │ │ ├── 19.png │ │ ├── 20.png │ │ ├── 21.png │ │ ├── 22.png │ │ ├── 23.png │ │ ├── 24.png │ │ ├── 25.png │ │ ├── 26.png │ │ ├── 27.png │ │ ├── 28.png │ │ └── 29.png ├── pdxinfo ├── imports.lua ├── sprites │ ├── spit.lua │ ├── block.lua │ ├── dust.lua │ ├── points.lua │ ├── leaf.lua │ ├── angel.lua │ ├── food.lua │ ├── tongue.lua │ └── player.lua ├── globals │ ├── constants.lua │ ├── score.lua │ ├── sfx.lua │ └── bgm.lua ├── lib │ ├── pool.lua │ ├── util.lua │ └── AnimatedSprite.lua ├── scenes │ ├── gameover.lua │ ├── bgscene.lua │ ├── menu.lua │ ├── level.lua │ └── stagecontrol.lua └── main.lua ├── .gitignore ├── .vscode ├── launch.json ├── settings.json ├── tasks.json └── run.js └── README.md /research/output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/research/output.png -------------------------------------------------------------------------------- /source/img/block.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/img/block.png -------------------------------------------------------------------------------- /source/sfx/100_1.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/sfx/100_1.wav -------------------------------------------------------------------------------- /source/sfx/300_1.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/sfx/300_1.wav -------------------------------------------------------------------------------- /source/sfx/50_1.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/sfx/50_1.wav -------------------------------------------------------------------------------- /source/sfx/comet.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/sfx/comet.wav -------------------------------------------------------------------------------- /source/sfx/pause.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/sfx/pause.wav -------------------------------------------------------------------------------- /source/sfx/spit.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/sfx/spit.wav -------------------------------------------------------------------------------- /source/sfx/start.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/sfx/start.wav -------------------------------------------------------------------------------- /source/sfx/walk.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/sfx/walk.wav -------------------------------------------------------------------------------- /source/sfx/walk2.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/sfx/walk2.wav -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | assets/ 2 | bin/ 3 | old/ 4 | 5 | .DS_Store 6 | 7 | research/*.mp4 8 | research/test_frames/ -------------------------------------------------------------------------------- /source/sfx/1000_1.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/sfx/1000_1.wav -------------------------------------------------------------------------------- /source/sfx/select.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/sfx/select.wav -------------------------------------------------------------------------------- /source/sfx/tenshi.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/sfx/tenshi.wav -------------------------------------------------------------------------------- /source/sfx/unpause.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/sfx/unpause.wav -------------------------------------------------------------------------------- /source/system/card.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/system/card.png -------------------------------------------------------------------------------- /source/img/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/img/background.png -------------------------------------------------------------------------------- /source/img/gameover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/img/gameover.png -------------------------------------------------------------------------------- /source/img/scene/sky.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/img/scene/sky.png -------------------------------------------------------------------------------- /source/sfx/bean_catch.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/sfx/bean_catch.wav -------------------------------------------------------------------------------- /source/sfx/menuback.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/sfx/menuback.wav -------------------------------------------------------------------------------- /source/sfx/pyoro_die.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/sfx/pyoro_die.wav -------------------------------------------------------------------------------- /source/sfx/restore1.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/sfx/restore1.wav -------------------------------------------------------------------------------- /source/sfx/restore2.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/sfx/restore2.wav -------------------------------------------------------------------------------- /source/sfx/restore3.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/sfx/restore3.wav -------------------------------------------------------------------------------- /source/sfx/restore4.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/sfx/restore4.wav -------------------------------------------------------------------------------- /source/sfx/restore5.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/sfx/restore5.wav -------------------------------------------------------------------------------- /source/sfx/tongue_out.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/sfx/tongue_out.wav -------------------------------------------------------------------------------- /source/system/card-highlighted/animation.txt: -------------------------------------------------------------------------------- 1 | frames = 1x30, 2x4, 3x4, 4x30, 5x1, 6x3, 4x30, 3x4, 2x4, 1x30 2 | -------------------------------------------------------------------------------- /research/low_score_rate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/research/low_score_rate.png -------------------------------------------------------------------------------- /source/img/menu/cursor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/img/menu/cursor.png -------------------------------------------------------------------------------- /source/img/scene/2_lamp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/img/scene/2_lamp.png -------------------------------------------------------------------------------- /source/img/scene/9_ufo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/img/scene/9_ufo.png -------------------------------------------------------------------------------- /source/img/scene/9b_ufo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/img/scene/9b_ufo.png -------------------------------------------------------------------------------- /source/img/scene/frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/img/scene/frame.png -------------------------------------------------------------------------------- /source/img/scene/hills.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/img/scene/hills.png -------------------------------------------------------------------------------- /source/img/scene/lights.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/img/scene/lights.png -------------------------------------------------------------------------------- /source/sfx/tile_destroy.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/sfx/tile_destroy.wav -------------------------------------------------------------------------------- /research/high_score_rate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/research/high_score_rate.png -------------------------------------------------------------------------------- /research/high_score_speed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/research/high_score_speed.png -------------------------------------------------------------------------------- /research/low_score_speed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/research/low_score_speed.png -------------------------------------------------------------------------------- /source/img/block-outline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/img/block-outline.png -------------------------------------------------------------------------------- /source/img/menu/death_msg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/img/menu/death_msg.png -------------------------------------------------------------------------------- /source/img/scene/10_moon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/img/scene/10_moon.png -------------------------------------------------------------------------------- /source/img/scene/10b_moon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/img/scene/10b_moon.png -------------------------------------------------------------------------------- /source/img/scene/11_planet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/img/scene/11_planet.png -------------------------------------------------------------------------------- /source/img/scene/12_city.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/img/scene/12_city.png -------------------------------------------------------------------------------- /source/img/scene/12b_city.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/img/scene/12b_city.png -------------------------------------------------------------------------------- /source/img/scene/2b_lamp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/img/scene/2b_lamp.png -------------------------------------------------------------------------------- /source/img/scene/3_house.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/img/scene/3_house.png -------------------------------------------------------------------------------- /source/img/scene/4_wheel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/img/scene/4_wheel.png -------------------------------------------------------------------------------- /source/img/scene/4b_home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/img/scene/4b_home.png -------------------------------------------------------------------------------- /source/img/scene/6_bridge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/img/scene/6_bridge.png -------------------------------------------------------------------------------- /source/img/scene/7_tower.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/img/scene/7_tower.png -------------------------------------------------------------------------------- /source/img/scene/7b_castle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/img/scene/7b_castle.png -------------------------------------------------------------------------------- /source/img/scene/8b_tower.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/img/scene/8b_tower.png -------------------------------------------------------------------------------- /source/img/scene/firework.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/img/scene/firework.png -------------------------------------------------------------------------------- /source/img/scene/lights2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/img/scene/lights2.png -------------------------------------------------------------------------------- /source/sfx/tongue_retract.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/sfx/tongue_retract.wav -------------------------------------------------------------------------------- /source/img/angel-table-20-29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/img/angel-table-20-29.png -------------------------------------------------------------------------------- /source/img/bagel-table-20-20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/img/bagel-table-20-20.png -------------------------------------------------------------------------------- /source/img/dust-table-20-20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/img/dust-table-20-20.png -------------------------------------------------------------------------------- /source/img/dust2-table-28-22.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/img/dust2-table-28-22.png -------------------------------------------------------------------------------- /source/img/scene/11b_planet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/img/scene/11b_planet.png -------------------------------------------------------------------------------- /source/img/scene/3b_monster.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/img/scene/3b_monster.png -------------------------------------------------------------------------------- /source/img/scene/6b_turbine.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/img/scene/6b_turbine.png -------------------------------------------------------------------------------- /source/img/scene/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/img/scene/background.png -------------------------------------------------------------------------------- /source/img/scores-table-19-8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/img/scores-table-19-8.png -------------------------------------------------------------------------------- /source/img/seed-table-20-20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/img/seed-table-20-20.png -------------------------------------------------------------------------------- /source/img/spit-table-80-80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/img/spit-table-80-80.png -------------------------------------------------------------------------------- /source/sfx/selection_reverse.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/sfx/selection_reverse.wav -------------------------------------------------------------------------------- /source/system/launchImages/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/system/launchImages/1.png -------------------------------------------------------------------------------- /source/system/launchImages/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/system/launchImages/2.png -------------------------------------------------------------------------------- /source/system/launchImages/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/system/launchImages/3.png -------------------------------------------------------------------------------- /source/system/launchImages/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/system/launchImages/4.png -------------------------------------------------------------------------------- /source/system/launchImages/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/system/launchImages/5.png -------------------------------------------------------------------------------- /source/system/launchImages/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/system/launchImages/6.png -------------------------------------------------------------------------------- /source/system/launchImages/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/system/launchImages/7.png -------------------------------------------------------------------------------- /source/system/launchImages/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/system/launchImages/8.png -------------------------------------------------------------------------------- /source/system/launchImages/9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/system/launchImages/9.png -------------------------------------------------------------------------------- /source/img/beagle-table-23-21.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/img/beagle-table-23-21.png -------------------------------------------------------------------------------- /source/img/leaves-table-13-10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/img/leaves-table-13-10.png -------------------------------------------------------------------------------- /source/img/player-table-22-21.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/img/player-table-22-21.png -------------------------------------------------------------------------------- /source/img/player2-table-37-30.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/img/player2-table-37-30.png -------------------------------------------------------------------------------- /source/img/scene/8_skyscraper.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/img/scene/8_skyscraper.png -------------------------------------------------------------------------------- /source/img/tongue-table-13-13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/img/tongue-table-13-13.png -------------------------------------------------------------------------------- /source/system/launchImages/10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/system/launchImages/10.png -------------------------------------------------------------------------------- /source/system/launchImages/11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/system/launchImages/11.png -------------------------------------------------------------------------------- /source/system/launchImages/12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/system/launchImages/12.png -------------------------------------------------------------------------------- /source/system/launchImages/13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/system/launchImages/13.png -------------------------------------------------------------------------------- /source/system/launchImages/14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/system/launchImages/14.png -------------------------------------------------------------------------------- /source/system/launchImages/15.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/system/launchImages/15.png -------------------------------------------------------------------------------- /source/system/launchImages/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/system/launchImages/16.png -------------------------------------------------------------------------------- /source/system/launchImages/17.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/system/launchImages/17.png -------------------------------------------------------------------------------- /source/system/launchImages/18.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/system/launchImages/18.png -------------------------------------------------------------------------------- /source/system/launchImages/19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/system/launchImages/19.png -------------------------------------------------------------------------------- /source/system/launchImages/20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/system/launchImages/20.png -------------------------------------------------------------------------------- /source/system/launchImages/21.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/system/launchImages/21.png -------------------------------------------------------------------------------- /source/system/launchImages/22.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/system/launchImages/22.png -------------------------------------------------------------------------------- /source/system/launchImages/23.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/system/launchImages/23.png -------------------------------------------------------------------------------- /source/system/launchImages/24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/system/launchImages/24.png -------------------------------------------------------------------------------- /source/system/launchImages/25.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/system/launchImages/25.png -------------------------------------------------------------------------------- /source/system/launchImages/26.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/system/launchImages/26.png -------------------------------------------------------------------------------- /source/system/launchImages/27.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/system/launchImages/27.png -------------------------------------------------------------------------------- /source/system/launchImages/28.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/system/launchImages/28.png -------------------------------------------------------------------------------- /source/system/launchImages/29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/system/launchImages/29.png -------------------------------------------------------------------------------- /source/img/menu/eyes-table-22-18.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/img/menu/eyes-table-22-18.png -------------------------------------------------------------------------------- /source/sfx/normal_395_transition.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/sfx/normal_395_transition.wav -------------------------------------------------------------------------------- /source/system/card-highlighted/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/system/card-highlighted/1.png -------------------------------------------------------------------------------- /source/system/card-highlighted/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/system/card-highlighted/2.png -------------------------------------------------------------------------------- /source/system/card-highlighted/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/system/card-highlighted/3.png -------------------------------------------------------------------------------- /source/system/card-highlighted/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/system/card-highlighted/4.png -------------------------------------------------------------------------------- /source/system/card-highlighted/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/system/card-highlighted/5.png -------------------------------------------------------------------------------- /source/system/card-highlighted/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/system/card-highlighted/6.png -------------------------------------------------------------------------------- /source/img/angel-outline-table-20-29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/img/angel-outline-table-20-29.png -------------------------------------------------------------------------------- /source/img/menu/about-table-400-240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/img/menu/about-table-400-240.png -------------------------------------------------------------------------------- /source/img/menu/home-table-400-240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/img/menu/home-table-400-240.png -------------------------------------------------------------------------------- /source/img/scene/comet-table-112-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/img/scene/comet-table-112-32.png -------------------------------------------------------------------------------- /source/img/seed-outline-table-20-20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macvogelsang/pyoro-playdate/HEAD/source/img/seed-outline-table-20-20.png -------------------------------------------------------------------------------- /source/pdxinfo: -------------------------------------------------------------------------------- 1 | name=Bird & Beans 2 | author=Mac Vogelsang 3 | description=A port of the 2009 DSiWare game Bird & Beans 4 | bundleID=com.madvogel.bird 5 | version=1.0.3 6 | buildNumber=001000003 7 | imagePath=system/ 8 | launchSoundPath=sfx/tongue_out -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [ 3 | { 4 | "type": "node", 5 | "request": "launch", 6 | "name": "Launch", 7 | "program": "${workspaceFolder}/.vscode/run.js", 8 | "preLaunchTask": "build", 9 | "internalConsoleOptions": "neverOpen", 10 | "env": { "PLAYDATE_OUTPUT" : "${config:playdate.output}" } 11 | } 12 | ] 13 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "playdate.output": "bin/BirdAndBeans.pdx", 3 | "Lua.diagnostics.globals": [ 4 | "import", 5 | "playdate", 6 | "doBoundCalculation", 7 | "globalScore", 8 | "debugHarmlessFoodOn", 9 | "debugPlayerInvincible", 10 | "game", 11 | "gfx", 12 | "tongueExtendVelocity", 13 | "playerMaxRunVelocity", 14 | "leafParticlesOn", 15 | "leafParticles", 16 | "audioSetting" 17 | ], 18 | "Lua.workspace.library": [ 19 | "/Users/mac.vogelsang/Developer/PlaydateSDK/CoreLibs" 20 | ], 21 | "Lua.runtime.nonstandardSymbol": [ 22 | "+=", 23 | "-=", 24 | "*=", 25 | "/=" 26 | ], 27 | } -------------------------------------------------------------------------------- /source/imports.lua: -------------------------------------------------------------------------------- 1 | import 'CoreLibs/object' 2 | import 'CoreLibs/graphics' 3 | import 'CoreLibs/sprites' 4 | import 'CoreLibs/frameTimer' 5 | import 'CoreLibs/timer' 6 | import 'lib/AnimatedSprite' 7 | POOL = 8 | import 'lib/pool' 9 | import 'globals/constants' 10 | import 'globals/sfx' 11 | import 'globals/bgm' 12 | import 'lib/util' 13 | import 'scenes/menu' 14 | import 'scenes/bgscene' 15 | import 'scenes/stagecontrol' 16 | import 'sprites/block' 17 | import 'sprites/spit' 18 | import 'sprites/player' 19 | import 'sprites/leaf' 20 | import 'sprites/dust' 21 | import 'sprites/food' 22 | import 'sprites/angel' 23 | import 'sprites/points' 24 | import 'scenes/level' 25 | import 'globals/score' 26 | import 'sprites/tongue' 27 | import 'scenes/gameover' 28 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build", 6 | "type": "shell", 7 | "command": "mkdir -p ${config:playdate.output}; pdc source ${config:playdate.output}", 8 | "group": { 9 | "kind": "build", 10 | "isDefault": true 11 | }, 12 | "presentation": { 13 | "echo": true, 14 | "reveal": "silent", 15 | "focus": false, 16 | "panel": "shared", 17 | "showReuseMessage": false, 18 | "clear": true, 19 | "revealProblems": "onProblem" 20 | }, 21 | "problemMatcher": { 22 | "owner": "lua", 23 | "pattern": { 24 | "regexp": "^(warning|error):\\s+(.*):(\\d+):\\s+(.*)$", 25 | "severity": 1, 26 | "file": 2, 27 | "line": 3, 28 | "message": 4 29 | } 30 | } 31 | } 32 | ] 33 | } -------------------------------------------------------------------------------- /.vscode/run.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const path = require("path"); 3 | const os = require("os"); 4 | const { exec } = require("child_process"); 5 | const process = require("process"); 6 | 7 | let sdkRoot = null; 8 | 9 | let configPath = path.resolve(os.homedir(), ".Playdate", "config"); 10 | let configText = fs.readFileSync(configPath, "utf8"); 11 | let configLines = configText.split("\n"); 12 | configLines.forEach((line) => { 13 | let components = line.split("\t"); 14 | if (components[0] == "SDKRoot") { 15 | sdkRoot = components[1]; 16 | } 17 | }); 18 | 19 | if (sdkRoot == null) { 20 | throw new Error("No SDK Root"); 21 | } 22 | 23 | let simulatorPath = path.resolve(sdkRoot, "bin", "Playdate Simulator.app"); 24 | let execPath = `/usr/bin/open -a \"${simulatorPath}\" \"${path.resolve(process.cwd(), process.env["PLAYDATE_OUTPUT"])}\"`; 25 | console.log(execPath) 26 | exec(execPath) 27 | -------------------------------------------------------------------------------- /source/sprites/spit.lua: -------------------------------------------------------------------------------- 1 | 2 | class('Spit').extends(AnimatedSprite) 3 | 4 | local spitTable= gfx.imagetable.new('img/spit') 5 | local SPIT_WIDTH = 80 6 | 7 | function Spit:init() 8 | Spit.super.init(self, spitTable) 9 | 10 | local config = {tickStep = 1, loop = false, onAnimationEndEvent = (function (self) self:setVisible(false) end)} 11 | self:addState(1, 1, 10, config) 12 | self:addState(2, 11, 20, config) 13 | self:addState(3, 21, 30, config) 14 | self:addState(4, 31, 40, config) 15 | self:setVisible(false) 16 | self:setCenter(0.5, 1) 17 | self:setZIndex(LAYERS.dust) 18 | self:add() 19 | 20 | self.inUse = false 21 | end 22 | 23 | function Spit:spawn(facing, playerPos) 24 | self.globalFlip = facing == LEFT and 0 or 1 25 | self:changeState(math.random(1,4)) 26 | self:moveTo(playerPos.x + (facing * (SPIT_WIDTH/2 + 7)), playerPos.y - 17) 27 | self:setVisible(true) 28 | self:playAnimation() 29 | self.inUse = true 30 | end -------------------------------------------------------------------------------- /source/globals/constants.lua: -------------------------------------------------------------------------------- 1 | NUM_BLOCKS = 30 2 | BLOCK_WIDTH = 10 3 | 4 | X_LOWER_BOUND = 50 5 | X_UPPER_BOUND = 350 6 | Y_LOWER_BOUND = 0 7 | Y_UPPER_BOUND = 228 8 | PLAY_AREA_WIDTH = 300 9 | 10 | Y_RANGE = Y_UPPER_BOUND - Y_LOWER_BOUND 11 | SCORE_SECTION_HEIGHT = Y_RANGE / 5 12 | 13 | LEFT, RIGHT = -1, 1 14 | 15 | -- hit constants 16 | GROUND, NONGROUND, SPIT = 1, 2, 3 17 | 18 | REFRESH_RATE = 30 19 | FRAME_TIME_SEC = 1/REFRESH_RATE 20 | FRAME_LEN = REFRESH_RATE / 30 -- how many 30 fps frames long is a frame? 21 | DT = 1/REFRESH_RATE 22 | 23 | COLLIDE_PLAYER_GROUP, COLLIDE_TONGUE_GROUP, COLLIDE_BLOCK_GROUP, COLLIDE_FOOD_GROUP, COLLIDE_NOTHING_GROUP = 1, 2, 3, 4, 5 24 | 25 | STARTING_FOOD_PARAMS = { 26 | slow = {chance=0.7, speed=25}, 27 | med = {chance=0.3, speed=33}, 28 | fast = {chance=0, speed=0}, 29 | } 30 | BNB1, BNB2 = 'bnb1', 'bnb2' 31 | 32 | BAGEL_MODE = false 33 | 34 | gfx = playdate.graphics 35 | SCORE_FONT = gfx.font.new('img/fonts/space-harrier2') 36 | -------------------------------------------------------------------------------- /source/lib/pool.lua: -------------------------------------------------------------------------------- 1 | local pool = {} 2 | local poolmt = {__index = pool} 3 | 4 | function pool.create(newObject, poolSize) 5 | poolSize = poolSize or 16 6 | assert(newObject, "A function that returns new objects for the pool is required.") 7 | local freeObjects = {} 8 | for _ = 1, poolSize do 9 | table.insert(freeObjects, newObject()) 10 | end 11 | 12 | return setmetatable({ 13 | freeObjects = freeObjects, 14 | newObject = newObject 15 | }, 16 | poolmt 17 | ) 18 | end 19 | 20 | function pool:obtain() 21 | 22 | local obj = #self.freeObjects == 0 and self.newObject() or table.remove(self.freeObjects) 23 | if #self.freeObjects == 0 then print('no free objects, creating new') end 24 | -- print('obtained. new count', #self.freeObjects) 25 | return obj 26 | end 27 | 28 | function pool:free(obj) 29 | assert(obj, "An object to be freed must be passed.") 30 | 31 | table.insert(self.freeObjects, obj) 32 | if obj.reset then obj.reset() end 33 | end 34 | 35 | function pool:clear() 36 | for k in pairs(self.freeObjects) do 37 | self.freeObjects[k] = nil 38 | end 39 | end 40 | 41 | return pool -------------------------------------------------------------------------------- /source/lib/util.lua: -------------------------------------------------------------------------------- 1 | 2 | function blockIndexToX(i) 3 | return X_LOWER_BOUND + (BLOCK_WIDTH * (i-1)) 4 | end 5 | 6 | function xToBlockIndex(x) 7 | return math.floor((x - X_LOWER_BOUND) / BLOCK_WIDTH) + 1 8 | end 9 | 10 | function enum( t ) 11 | local result = {} 12 | 13 | for index, name in pairs(t) do 14 | result[name] = index * 10 15 | end 16 | 17 | return result 18 | end 19 | 20 | function math.ring(a, min, max) 21 | if min > max then 22 | min, max = max, min 23 | end 24 | return min + (a-min)%(max-min) 25 | end 26 | 27 | function math.ring_int(a, min, max) 28 | return math.ring(a, min, max+1) 29 | end 30 | 31 | function table.random( t ) 32 | if type(t)~="table" then return nil end 33 | return t[math.ceil(math.random(#t))] 34 | end 35 | 36 | LAYERS = enum({ 37 | 'sky', 38 | 'comet', 39 | 'buildings', 40 | 'fireworks', 41 | 'hills', 42 | 'leaves', 43 | 'block', 44 | 'dust', 45 | 'angel', 46 | 'tongue', 47 | 'food', 48 | 'points', 49 | 'player', 50 | 'text', 51 | 'menu', 52 | 'cursor', 53 | 'frame', 54 | }) 55 | -------------------------------------------------------------------------------- /source/globals/score.lua: -------------------------------------------------------------------------------- 1 | class('Score').extends(gfx.sprite) 2 | 3 | local font = gfx.font.new('img/fonts/space-harrier2') 4 | 5 | function Score:init() 6 | 7 | Score.super.init(self) 8 | 9 | self.score = 0 10 | self.stage = 0 11 | self.levelDone = false 12 | 13 | self.highScore = 10000 14 | self:setZIndex(LAYERS.text) 15 | self:setIgnoresDrawOffset(true) 16 | self:setCenter(0.5, 0) 17 | self:setSize(240, 20) 18 | self:moveTo(200, 1) 19 | 20 | self.newHighScore = false 21 | self.monochromeTicker = 0 22 | end 23 | 24 | function Score:addPoints(points) 25 | self.score += points 26 | self:markDirty() 27 | 28 | if self.score > 999999 then 29 | self.score = 999999 30 | end 31 | self.stage = math.floor(self.score / 1000) 32 | if self.score > self.highScore then 33 | self.highScore = self.score 34 | self.newHighScore = true 35 | end 36 | end 37 | 38 | function Score:draw() 39 | gfx.setImageDrawMode(gfx.kDrawModeInverted) 40 | gfx.setFontTracking(-1) 41 | gfx.setFont(font) 42 | gfx.drawText(string.format("SCORE %06d HI SCORE %06d", self.score, self.highScore), 0, 0) 43 | gfx.setImageDrawMode(gfx.kDrawModeCopy) 44 | end 45 | -------------------------------------------------------------------------------- /source/sprites/block.lua: -------------------------------------------------------------------------------- 1 | class('Block').extends(gfx.sprite) 2 | 3 | local blockImg = gfx.image.new('img/block') 4 | local blockOutlineImg = gfx.image.new('img/block-outline') 5 | 6 | function Block:init(i) 7 | Block.super.init(self) 8 | self.blockIndex = i 9 | self.xPos = blockIndexToX(self.blockIndex) 10 | self.xCenter = self.xPos + BLOCK_WIDTH/2 11 | self:setCenter(0,0) 12 | self:setZIndex(LAYERS.block) 13 | self:setImage(blockImg) 14 | self:place() 15 | 16 | self.img = blockImg 17 | self.angel = Angel(self.blockIndex, self.xCenter) 18 | 19 | self:add() 20 | end 21 | 22 | -- places the block at the bottom of the screen and sets its image 23 | function Block:place() 24 | self.placed = true 25 | self:setVisible(true) 26 | self:moveTo(self.xPos, 229) 27 | 28 | local img = globalScore.monochromeTicker > 0 and blockOutlineImg or blockImg 29 | self:setImage(img) 30 | 31 | doBoundCalculation = true 32 | end 33 | 34 | -- 'destroy' a block by making it invisible 35 | function Block:destroy() 36 | SFX:play(SFX.kTileDestroy, true) 37 | self:setVisible(false) 38 | self.placed = false 39 | end 40 | 41 | function Block:monochrome() 42 | self:setImage(blockOutlineImg) 43 | end 44 | 45 | function Block:update() 46 | if self.angel.state == 2 and not self.placed then 47 | self:place() 48 | end 49 | end -------------------------------------------------------------------------------- /source/globals/sfx.lua: -------------------------------------------------------------------------------- 1 | 2 | local snd = playdate.sound 3 | local WALK_VOL = 0.8 4 | 5 | SFX = {} 6 | 7 | SFX.kCatchFood = {'bean_catch'} 8 | SFX.kWalk= {'walk', WALK_VOL} 9 | SFX.kWalk2 = {'walk2', WALK_VOL} 10 | SFX.kTongueOut = {'tongue_out'} 11 | SFX.kTongueRetract = {'tongue_retract'} 12 | SFX.kSpit = {'spit'} 13 | 14 | SFX.kTileDestroy = {'tile_destroy', 0.6} 15 | SFX.kTenshi = {'tenshi', 0.8} 16 | SFX.kRestore1 = {'restore1'} 17 | SFX.kRestore2 = {'restore2'} 18 | SFX.kRestore3 = {'restore3'} 19 | SFX.kRestore4 = {'restore4'} 20 | SFX.kRestore5 = {'restore5'} 21 | 22 | SFX.kPoints50 = {'50_1', 0.8} 23 | SFX.kPoints100 = {'100_1', 0.8} 24 | SFX.kPoints300 = {'300_1', 0.8} 25 | SFX.kPoints1000 = {'1000_1', 0.8} 26 | SFX.kComet = {'comet'} 27 | 28 | SFX.kNormal395Transition = {'normal_395_transition'} 29 | 30 | SFX.kMenuBack = {'menuback'} 31 | SFX.kMenuSelect = {'select'} 32 | SFX.kPause = {'pause'} 33 | SFX.kUnpause = {'unpause'} 34 | SFX.kCrankReturn = {'selection_reverse'} 35 | SFX.kStart = {'start'} 36 | 37 | local players = {} 38 | 39 | for _, v in pairs(SFX) do 40 | local name = v[1] 41 | local volume = v[2] or 1.0 42 | 43 | players[name] = snd.sampleplayer.new('sfx/' .. name) 44 | players[name]:setVolume(volume) 45 | end 46 | 47 | SFX.players = players 48 | 49 | function SFX:play(sfx, allowOverlap) 50 | if audioSetting == 'music' then 51 | return 52 | end 53 | local name = sfx[1] 54 | if allowOverlap then 55 | self.players[name]:play(1) 56 | elseif not self.players[name]:isPlaying() then 57 | self.players[name]:play(1) 58 | end 59 | end 60 | 61 | function SFX:stop(sfx) 62 | local name = sfx[1] 63 | self.players[name]:stop() 64 | end -------------------------------------------------------------------------------- /source/sprites/dust.lua: -------------------------------------------------------------------------------- 1 | class('Dust').extends(gfx.sprite) 2 | 3 | local LIFESPAN = 0.25 * REFRESH_RATE 4 | local DUST_FRAMES = 3 5 | local BIG_DUST_FRAMES = 8 6 | local DUST_FRAME_DUR = LIFESPAN / DUST_FRAMES 7 | local BIG_DUST_FRAME_DUR = LIFESPAN / BIG_DUST_FRAMES 8 | 9 | local DIRECTIONAL_OFFSET = 10 10 | 11 | -- contain a sprite for tongue end and a sprite to repeat for tongue segments 12 | local dustTable = gfx.imagetable.new('img/dust') 13 | local bigDustTable = gfx.imagetable.new('img/dust2') 14 | 15 | function Dust:init() 16 | Dust.super.init(self) 17 | self.direction = 0 18 | self.bigDust = false 19 | self.lifespan = LIFESPAN 20 | self:setImage(dustTable:getImage(1)) 21 | self:setZIndex(LAYERS.dust) 22 | self:add() 23 | end 24 | 25 | function Dust:despawn() 26 | self:setVisible(false) 27 | self:setUpdatesEnabled(false) 28 | end 29 | 30 | function Dust:spawn(position, direction) 31 | self.lifespan = LIFESPAN 32 | self.direction = direction or 0 33 | if self.direction ~= 0 then 34 | self:moveTo(position.x + (self.direction * DIRECTIONAL_OFFSET), position.y - DIRECTIONAL_OFFSET) 35 | else 36 | self:moveTo(position) 37 | end 38 | self:setVisible(true) 39 | self:setUpdatesEnabled(true) 40 | end 41 | 42 | function Dust:update() 43 | if self.direction ~= 0 then 44 | self:setImage(bigDustTable:getImage(BIG_DUST_FRAMES + 1 - math.ceil(self.lifespan / BIG_DUST_FRAME_DUR)), self.direction == LEFT and gfx.kImageUnflipped or gfx.kImageFlippedX) 45 | else 46 | self:setImage(dustTable:getImage(DUST_FRAMES + 1 - math.ceil(self.lifespan / DUST_FRAME_DUR))) 47 | end 48 | 49 | self.lifespan -= 1 50 | 51 | if self.lifespan <= 0 then 52 | self:despawn() 53 | end 54 | end -------------------------------------------------------------------------------- /source/img/fonts/space-harrier.fnt: -------------------------------------------------------------------------------- 1 | --metrics={"baseline":4,"xHeight":3,"capHeight":0,"pairs":{},"left":[],"right":[]} 2 | datalen=1280 3 | data=iVBORw0KGgoAAAANSUhEUgAAADgAAAAwCAYAAABE1blzAAAAAXNSR0IArs4c6QAAA3pJREFUaEPNWFty4zAMa+5/6O7EU3lpGCAgWX3szyaVLJEECNB5ffz/91k+vz++4Pt3r4/r6j3vGPDeGlaNke17jQ3vRZZQXa9J4/7k+e78mhzGxM5+769/l/ePCrlEuoTwIiyE+66KPM5l54+CqPjP9ZkEFYIuwUozpFSHFkuw0jNZP2iZBOgogX3D9rPeGc+xIiS9V58fd16ASBPsDsK1elGlEg3gawP2JwpO10JtC8yIjBIJJQIVxU6ElDojuk4E6Xons06CR+U6KrHqIqrYV6kNRDbmfGYmQOeTYKvyq0KbPeDiP3zwKcVQUJinzSTn+hb7s4JwY0MiMm0Tg0gwMVICgkkrr1U+6JT3WGcQqwcTkVlNkAmKsp6KMFPwC8LOeJWfoWx3NHEIMpHCwqv7lH2dArgjQcUC1ZusHxGtVLC2JJiISOdzKwKDNEwRpCLDVIlRrquqolmaHCqvEpaTeuHBl9elJZ8x72tJHJ2XORF0CZ8+6BBjQsFUjkm/EqBBffS9zucGsup/pOjN6O10Xk5wPlQvYz3k1t351cNl3MroEYnVJu8GCUQBEWUFYAypsd0+706QCZEaBVWCaF2V4sxOlhJExFIE1ZsFO88h2E1UrAdZIX5cZBTCqcgtiYzjvkLFSXRnEWyuTW2KjXGSOYzvzrs633JIJEWZSsD9bqpmUfSimR5k6Mw8X32x81rnw8f6jGJVKrMqj/UuQdzDXnfq3+ww/VUNWUBXbbQRhizOjTNnsgQSA0eqtwmqIbrzKawyelF3JqOgs6WU4jiqXX74ddVMKeoS2IpQQ9EDZWamLJGVCuI5tf8qO9CgWQGcOrNp5sidBV4RYB7pbGKHzbCRbMYnz71dgiuelQahrGDHnRcAFEUROVTGZN3tYT6WzJhTPaz4HsswQKYYkfbwjgQvOe1IkDU4eiOKRGIzjF3TBVBwK5W7+QwIlVKz7i0iUdFlBjhJVqNSglpi/ukopopQC465HN87kVE+hgqYJOt8rFtXVGU2dwPM2UQi+05YmJc6r8V15cesZS6oMtN2Ru7WsSjMYtwwsG29QjoOda8rnQ9hYFVNtwU9c5CT4o5KHZKut2difLTXyXpSAMeCRwE+fRgFovZLZ9bOPoYo/Gr/sXHMJaUsAc9Cf3oKxPLzidEnPqeQ+lMio1BwPqd+k2RetozE6oP/AMaRHjsrlDbdAAAAAElFTkSuQmCC 4 | width=8 5 | height=8 6 | 7 | tracking=1 8 | 9 | 0 8 10 | 1 8 11 | 2 8 12 | 3 8 13 | 4 8 14 | 5 8 15 | 6 8 16 | 7 8 17 | 8 8 18 | 9 8 19 | space 8 20 | � 8 21 | A 8 22 | B 8 23 | C 8 24 | D 8 25 | E 8 26 | F 8 27 | G 8 28 | H 8 29 | I 8 30 | J 8 31 | K 8 32 | L 8 33 | M 8 34 | N 8 35 | O 8 36 | P 8 37 | Q 8 38 | R 8 39 | T 8 40 | S 8 41 | U 8 42 | V 8 43 | W 8 44 | X 8 45 | Y 8 46 | Z 8 47 | 48 | -------------------------------------------------------------------------------- /source/img/fonts/space-harrier2.fnt: -------------------------------------------------------------------------------- 1 | --metrics={"baseline":3,"xHeight":2,"capHeight":0} 2 | datalen=1420 3 | data=iVBORw0KGgoAAAANSUhEUgAAAFAAAAAgCAYAAACFM/9sAAAAAXNSR0IArs4c6QAAA+FJREFUaEO9Wdt2IjEMK///0ewhTHJsRbKVAbYvLfXk4otkjXn8fe/nCVs94POv7fO4eM7rDnhuvFa8I3uuW/+HTt4N5/P5zOc/HmPruf8wzmfAdpnq9c3+K3jkjO1u42Jwv2J/un7u8Y0ArgOuS7FApUuoy4v1uJ9KhDpju1+oklFhEHSs0NK//xbAogKtBMT1qoJeCbhRgVWFboh87T8TPQrZxOzEF3veCUCCKYNwwOD4k1GAeGYETVUv8w8rrvm8aIhRkBNADNDWHIwLMA6cHJlsIXjLd9dhDNZNSmgpIib3GwH8tAmUJH01oqpJpe5D+Czx3AzyQROpGpTdhSsIM/kQO/AIsKCKViZgJ4cGgNtWUuknMqrSQSc67m6AkswhQe4C3Nkdiq/2YLb0v40gb5T4xmHIY6ixKp6D88fHbj2zxzM6OzsjdFrZ2V/PUB3kknbV+brOWNm786fDbuctdF5KEAbN8c+pwE6mbEJXORjxVGR4gx0LAEsA2V8WSGgilZCWCJgVblVgByPXwZMMx2Dc2f9aTztwgPfmf4S7W4FdhW1EqiDm6rAOgr+GcKhe+SpXUMxaMznw8uf2yzyFgAmxFXPxMv/VJhIRECv00yYynTjRUAMF10K2rhohJaoSe6xmbIyj3LPYucr36nwuYzbmfv+j0oliybuzKyPsOZIPzzrr3Zmfsxe7g51ApwuvLCmYHZJ8+y7dcSTaXQimEoQ5yl0KSTAU75GpzFWwRrlel4r8F50jU5iNf8lIKglpdsZhAhevqvueJDBBlI1rAF5sMqNe9CcFHE1zZvBVF+zsAS7liAv3xwbjvgg4AaymHVRHxYEjmfi2sqirgLsyR0GYBdNVEQ6ERwU543oTwhRCgQKSziIU0K237Fcwt7PYeVXCnCZSDTyPhCh25i4prt1tIoqD2Xrk9Pg5IkRpudjGVZv/hg7EmC7eZIZYNcFeSRVmQxWg/FNXONaBp0LV0V6dTvylfQMCNkqiVWU+l7OnOqgo8Y7DOrvkMDzThTj6tuCVvxvGSmSVugXS4rC5Ss3ViPyxGg8LAJMpSOyNztwSgF7D+khF+ObVBpFWIMiQEL83stQXMjhjO62QKaLjb/alEsqkJsCpSKAZII/PeEQaKoNIG4HKkBDaUlw78zTlPI6csFNiEnGf0HAWNCOUY7IJwR0FMMEtwtWotOveqTIphGLwEYLTeVGBab/idVO98SwZRsZZoyBhoME+f9RENq1YZR8IOl2wcp4FkSRzBdOkkpRgjALzA56xOHCVuCjl5UexefW1prNe3aGrjpkgtr7TuCvXwa+XH5EHZeVNwz96QqJL5WBCcQAAAABJRU5ErkJggg== 4 | width=8 5 | height=8 6 | 7 | tracking=1 8 | 9 | space 8 10 | . 4 11 | 0 8 12 | 1 7 13 | 2 8 14 | 3 8 15 | 4 8 16 | 5 8 17 | 6 8 18 | 7 8 19 | 8 8 20 | 9 8 21 | A 8 22 | B 8 23 | C 8 24 | D 8 25 | E 8 26 | F 8 27 | G 8 28 | H 8 29 | I 7 30 | J 8 31 | K 8 32 | L 8 33 | M 8 34 | N 8 35 | O 8 36 | P 8 37 | Q 8 38 | R 8 39 | S 8 40 | T 8 41 | U 8 42 | V 8 43 | W 8 44 | X 8 45 | Y 8 46 | Z 8 47 | � 8 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Public Pyoro Port for Playdate 3 | 4 | A Bird and Beans remake for Playdate. This source code is the final 1.0.3 version of the game before it was removed from itch.io. The code has had all the copyrighted music stripped, but may be helpful for anyone for learning the Playdate SDK. This was my first Playdate project and many aspects of the code are messy. 5 | 6 | # Latest release 7 | The .pdx file for the v1.0.3 version can be found [here](https://github.com/macvogelsang/pyoro/releases/tag/1.0.3) in the git releases sidebar. 8 | 9 | # Changelog 10 | 11 | ### v1.0.3 12 | - Added settings option to turn of music or sound effects 13 | - Added falling leave effects to game 2. This is a feature from the original game that I held off on implementing because I thought the extra sprites would have a performance impact, and indeed they did. So there is now an extra setting in the menu: leaf effects 'off', 'on', or 'auto'. 'Auto' keeps the effects on until there are too many sprites on screen where framerate starts to significantly drop and then turns off the effects. If anyone wants to help me improve performance on this front, feel free to reach out. 14 | - Added a more interesting loading screen transition 15 | - Added hills to the launcher card 16 | - Fixed a bug where the flashing beans would spawn too early if the screen was just cleared 17 | - Fixed yet another main menu bug 18 | - Fixed two almost-imperceptable graphical issues 19 | - Fixed some bugs you'll never know about since they were introduced in patch 1.0.3 20 | 21 | ### v1.0.2 22 | 23 | - Increased ground friction to more closely match the original 24 | - Player velocity now increases along with bean speed 25 | - Adjusted menu sound effects 26 | - Fixed another main menu button bug 27 | 28 | ### v1.0.1 29 | 30 | - Tweaked launcher animation 31 | - Fixed main menu crash 32 | - Fixed tongue persisting after death 33 | - Fixed the tongue not speeding up when bean speed increases 34 | - Tweaked scoring thresholds a tiny bit 35 | -------------------------------------------------------------------------------- /source/sprites/points.lua: -------------------------------------------------------------------------------- 1 | class('Points').extends(gfx.sprite) 2 | 3 | local imgTable = gfx.imagetable.new('img/scores') 4 | local FLASH_FRAMES = 3 5 | local OFFSET_DELAY = 0.05 * REFRESH_RATE -- time in seconds to offset points popping 6 | 7 | function Points:init(value, position, flashing, offset) 8 | Points.super.init(self) 9 | 10 | self:setCenter(0.5, 0.5) 11 | self:setZIndex(LAYERS.points) 12 | self:setImage(imgTable:getImage(1, 1)) 13 | self:add() 14 | self:despawn() 15 | end 16 | 17 | function Points:spawn(value, position, flashing, offset) 18 | 19 | self.flashing = flashing 20 | self.value = value 21 | local imgY = 1 22 | if self.value == 10 then 23 | imgY = 1 24 | elseif self.value == 50 then 25 | imgY = 2 26 | elseif self.value == 100 then 27 | imgY = 3 28 | elseif self.value == 300 then 29 | imgY = 4 30 | elseif self.value == 1000 then 31 | imgY = 5 32 | end 33 | 34 | self.startFrame = offset and (offset - 1) * OFFSET_DELAY or 0 35 | self.frame = 1 36 | self.lifespan = REFRESH_RATE * 0.5 37 | self.playedSound = false 38 | self:setImage(imgTable:getImage(1, imgY)) 39 | self:moveTo(position) 40 | self.playedSound = false 41 | 42 | self:setVisible(true) 43 | self:setUpdatesEnabled(true) 44 | end 45 | 46 | function Points:despawn() 47 | self:setVisible(false) 48 | self:setUpdatesEnabled(false) 49 | end 50 | 51 | function Points:update() 52 | self.frame += 1 53 | if self.frame >= self.startFrame then 54 | 55 | self:setVisible(true) 56 | 57 | if self.flashing and not self.playedSound then 58 | SFX:play(SFX.kPoints50, true) 59 | self.playedSound = true 60 | end 61 | 62 | if (self.value >= 300 or self.flashing) and self.lifespan % FLASH_FRAMES == 0 then 63 | self:setImage(self:getImage():invertedImage()) 64 | end 65 | 66 | self.lifespan -= 1 67 | 68 | if self.lifespan <= 0 then 69 | self:despawn() 70 | end 71 | end 72 | end -------------------------------------------------------------------------------- /source/sprites/leaf.lua: -------------------------------------------------------------------------------- 1 | class('Leaf').extends(AnimatedSprite) 2 | 3 | local Point = playdate.geometry.point 4 | local vector2D = playdate.geometry.vector2D 5 | local leavesTable = gfx.imagetable.new('img/leaves') 6 | local SMALL_LEAF, BIG_LEAF = 1, 2 7 | local AIR_RESISTANCE = 0.82 8 | function Leaf:init(size) 9 | Leaf.super.init(self, leavesTable) 10 | 11 | self.isLeaf = true 12 | self.size = size 13 | 14 | local config = {tickStep = 9, loop = true, yoyo = true, animationStartingFrame = math.random(1,3)} 15 | if self.size == BIG_LEAF then 16 | self:addState(BIG_LEAF, 1, 3, config, true) 17 | -- self.leaves = {Leaf(SMALL_LEAF), Leaf(SMALL_LEAF)} 18 | self:setCollideRect(1, 1, 11, 8) 19 | else 20 | self:addState(SMALL_LEAF, 4, 6, config) 21 | self:setCollideRect(3, 3, 7, 6) 22 | end 23 | 24 | self.position = Point.new(0, 0) 25 | self.fallSpeed = 0 26 | self.velocity = vector2D.new(0, 0) 27 | self.smallLeafOffset = vector2D.new(10, 10) 28 | self.spawned = false 29 | 30 | self:setCollidesWithGroups(COLLIDE_NOTHING_GROUP) 31 | self:setVisible(false) 32 | self:setUpdatesEnabled(false) 33 | self:setCenter(0.5, 1) 34 | self:setZIndex(LAYERS.leaves) 35 | self:add() 36 | 37 | end 38 | 39 | function Leaf:spawn(pos, fallSpeed, velocity) 40 | self.position = pos 41 | self.fallSpeed = fallSpeed 42 | self.spawned = true 43 | self:moveTo(self.position) 44 | self:setVisible(true) 45 | self:setUpdatesEnabled(true) 46 | self:playAnimation() 47 | 48 | -- random chance to fall faster 49 | if math.random() < 0.4 then 50 | self.fallSpeed *= 1.15 51 | end 52 | 53 | self.velocity.y = fallSpeed 54 | if velocity then 55 | self.velocity = velocity 56 | end 57 | end 58 | 59 | function Leaf:destroy(atGround) 60 | self:setVisible(false) 61 | self:stopAnimation() 62 | self.spawned = false 63 | 64 | if self.size == BIG_LEAF and not atGround then 65 | -- local secondLeafOffset = self.smallLeafOffset:scaledBy(math.random(-1,1)) 66 | local leaf1 = Leaf(SMALL_LEAF) 67 | leaf1:spawn(self.position, self.fallSpeed, self.velocity) 68 | -- local leaf2 = Leaf(SMALL_LEAF) 69 | -- leaf2:spawn(self.position + secondLeafOffset, self.fallSpeed, self.velocity) 70 | end 71 | 72 | self:remove() 73 | end 74 | 75 | function Leaf:update() 76 | self:updateAnimation() 77 | local velocityStep = self.velocity * DT 78 | self.position = self.position + velocityStep 79 | 80 | if self.velocity.y >= -10 then 81 | self.velocity.x = 0 82 | self.velocity.y = self.fallSpeed 83 | end 84 | if self.velocity.y < 0 then 85 | self.velocity = self.velocity * AIR_RESISTANCE 86 | end 87 | 88 | -- made it to ground level 89 | if self.position.y >= 240 or globalScore.levelDone then 90 | self:destroy(true) 91 | end 92 | 93 | self:moveTo(self.position) 94 | end -------------------------------------------------------------------------------- /source/sprites/angel.lua: -------------------------------------------------------------------------------- 1 | 2 | class('Angel').extends(gfx.sprite) 3 | 4 | -- local references 5 | local Point = playdate.geometry.point 6 | local vector2D = playdate.geometry.vector2D 7 | 8 | -- constants 9 | local FALL_VELOCITY = 360 10 | local DOWN, UP = 1, 2 11 | local FLASH_CYCLE_LEN = 4 * FRAME_LEN 12 | 13 | -- contain a sprite for tongue end and a sprite to repeat for tongue segments 14 | local imgTable = gfx.imagetable.new('img/angel') 15 | local imgOutlineTable = gfx.imagetable.new('img/angel-outline') 16 | 17 | local OFFSET_DELAY = 250 -- time in milliseconds to offset angel falling 18 | local RESTORE_SEQ = {1,2,3,4,5,4,5,4,5,4} 19 | 20 | local spawnMonochrome = false 21 | 22 | function Angel:init(blockIndex, xpos) 23 | Angel.super.init(self) 24 | 25 | self.speed = FALL_VELOCITY 26 | self.blockIndex = blockIndex 27 | 28 | self:setImage(imgTable:getImage(1, 1)) 29 | self:setZIndex(LAYERS.angel) 30 | self:setCenter(0.5, 1) 31 | 32 | self.initialPosition = Point.new(xpos, -20) 33 | self.velocity = vector2D.new(0, self.speed) 34 | self.position = Point.new(0,0) 35 | self:add() 36 | self:despawn() 37 | end 38 | 39 | function Angel:despawn() 40 | self.state = DOWN 41 | self.frame = 1 42 | self.velocity.y = self.speed 43 | self:setUpdatesEnabled(false) 44 | self:setVisible(false) 45 | self.position = self.initialPosition:copy() 46 | self:moveTo(self.initialPosition) 47 | end 48 | 49 | function Angel:spawn(offset) 50 | self.offset = offset or 1 51 | self.imgTable = spawnMonochrome and imgOutlineTable or imgTable 52 | playdate.timer.performAfterDelay(OFFSET_DELAY * (self.offset - 1), function() 53 | self:setUpdatesEnabled(true) 54 | self:setVisible(true) 55 | SFX:play(SFX.kTenshi, true) 56 | end) 57 | end 58 | 59 | function Angel:reverse() 60 | -- play sound 61 | self.state = UP 62 | self.velocity = self.velocity * -1 63 | SFX:play({'restore' .. tostring(RESTORE_SEQ[self.offset])}, true) 64 | end 65 | 66 | function Angel:update() 67 | 68 | local velocityStep = self.velocity * DT 69 | self.position = self.position + velocityStep 70 | -- don't move outside the walls of the game 71 | if self.position.y >= 238 then 72 | self:reverse() 73 | end 74 | 75 | local monochromeTicker = globalScore.monochromeTicker 76 | if monochromeTicker > 0 then 77 | spawnMonochrome = true 78 | if monochromeTicker >= self.blockIndex then 79 | self.imgTable = imgOutlineTable 80 | end 81 | else 82 | spawnMonochrome = false 83 | end 84 | 85 | self:moveTo(self.position) 86 | self:updateImage() 87 | 88 | self.frame = self.frame + 1 89 | 90 | if self.position.y <= -50 then 91 | self:despawn() 92 | end 93 | 94 | end 95 | 96 | function Angel:updateImage() 97 | local imgCol = (self.frame % FLASH_CYCLE_LEN) < FLASH_CYCLE_LEN/2 and 1 or 2 98 | self:setImage(self.imgTable:getImage(imgCol, self.state)) 99 | end 100 | 101 | 102 | -------------------------------------------------------------------------------- /source/scenes/gameover.lua: -------------------------------------------------------------------------------- 1 | 2 | class('GameOver').extends(gfx.sprite) 3 | 4 | local img = gfx.image.new('img/gameover') 5 | local Point = playdate.geometry.point 6 | local vector2D = playdate.geometry.vector2D 7 | local font = gfx.font.new('img/fonts/connection_bold') 8 | local FADE_DURATION_FRAMES = 0.8 * REFRESH_RATE 9 | local FADE_INCREMENT =1 / FADE_DURATION_FRAMES 10 | local DEATH_MSG_FRAMES = 2 * REFRESH_RATE 11 | 12 | local GOOD_SCORE_MSGS = { 13 | 'Fantastic!', 'That was amazing!', 'Rest easy now!', "You're full already!?", 'Great job!' 14 | } 15 | local BAD_SCORE_MSGS = { 16 | 'Hang in there!', 'Better luck next time.', 'See you later!', 'Ouch!', 'Take a break!', 'Practice makes perfect!' 17 | } 18 | 19 | 20 | function GameOver:init() 21 | 22 | GameOver.super.init(self) 23 | 24 | self.timer = 4 * REFRESH_RATE 25 | self.done = false 26 | 27 | self:setZIndex(LAYERS.text) 28 | self:setIgnoresDrawOffset(true) 29 | self:setCenter(0.5, 0.5) 30 | self:setImage(img) 31 | self:setImageDrawMode(gfx.kDrawModeInverted) 32 | 33 | self.position = Point.new(200, 0) 34 | self.velocity = vector2D.new(0, 60) 35 | 36 | self.fadeInVal = 0 37 | self.deathImg = gfx.image.new('img/menu/death_msg') 38 | self.deathSprite = gfx.sprite.new(self.deathImg) 39 | self.deathSprite:setZIndex(LAYERS.frame + 1) 40 | self.deathSprite:setCenter(0,0) 41 | self.deathSprite:moveTo(0,0) 42 | 43 | self:add() 44 | end 45 | 46 | function GameOver:update() 47 | 48 | if self.position.y >= 100 then 49 | if playdate.buttonJustPressed(playdate.kButtonA) and self.fadeInVal == 0 then 50 | SFX:play(SFX.kMenuBack) 51 | playdate.timer.performAfterDelay(800, function() BGM:play(BGM.kMainMenu) end) 52 | local msg = globalScore.newHighScore and globalScore.score > 10000 and table.random(GOOD_SCORE_MSGS) or table.random(BAD_SCORE_MSGS) 53 | -- pick message 54 | gfx.pushContext(self.deathImg) 55 | gfx.setFont(font) 56 | gfx.setImageDrawMode(gfx.kDrawModeFillWhite) 57 | gfx.drawTextAligned(msg, 200, 100, kTextAlignment.center) 58 | gfx.popContext() 59 | self:fadeInDeathMsg() 60 | self.deathSprite:add() 61 | elseif self.fadeInVal > 0 and self.fadeInVal < 1 then 62 | self:fadeInDeathMsg() 63 | elseif self.fadeInVal >= 1 then 64 | self.deathSprite:setImage(self.deathImg) 65 | self.fadeInVal += 1 66 | if self.fadeInVal >= DEATH_MSG_FRAMES then 67 | self.done = true 68 | end 69 | end 70 | else 71 | local velocityStep = self.velocity * DT 72 | self.position = self.position + velocityStep 73 | self:moveTo(self.position) 74 | end 75 | end 76 | 77 | function GameOver:fadeInDeathMsg() 78 | self.deathSprite:setImage(self.deathImg:fadedImage(self.fadeInVal, gfx.image.kDitherTypeBayer4x4)) 79 | self.fadeInVal += FADE_INCREMENT 80 | end 81 | 82 | function GameOver:endScene() 83 | self.deathSprite:remove() 84 | self:remove() 85 | end 86 | -------------------------------------------------------------------------------- /source/sprites/food.lua: -------------------------------------------------------------------------------- 1 | class('Food').extends(gfx.sprite) 2 | 3 | -- local references 4 | local Point = playdate.geometry.point 5 | local vector2D = playdate.geometry.vector2D 6 | 7 | -- constants 8 | local FRAME_DUR = REFRESH_RATE // 3 9 | local NUM_FRAMES = 4 10 | 11 | -- contain a sprite for tongue end and a sprite to repeat for tongue segments 12 | local foodTable = gfx.imagetable.new('img/seed') 13 | local foodOutlineTable = gfx.imagetable.new('img/seed-outline') 14 | local spawnMonochrome = false 15 | 16 | function Food:init() 17 | 18 | Food.super.init(self) 19 | 20 | self.isFood = true 21 | self.type = NORMAL 22 | self.speed = 0 23 | 24 | self.frame = 1 25 | self.animationIndex = 1 --math.random(#ANIMATION_SEQ) 26 | self.imgRow = 1 27 | self.blockIndex = 1 28 | 29 | self.imgTable = foodTable 30 | self:setImage(self.imgTable:getImage(1, self.imgRow)) 31 | self:setZIndex(LAYERS.food) 32 | self:setCenter(0.5, 0.5) 33 | 34 | self.captured = false 35 | self.capturedPosition = nil 36 | self.scored = false 37 | self.endPosition = Point.new(0,0) 38 | 39 | self.position = Point.new(0, 0) 40 | self.velocity = vector2D.new(0, 0) 41 | 42 | self.points = Points() 43 | self.dust = Dust() 44 | -- self.leaves = {Leaf(2), Leaf(2)} 45 | 46 | self:setGroups({COLLIDE_FOOD_GROUP}) 47 | self:setCollidesWithGroups({COLLIDE_PLAYER_GROUP, COLLIDE_TONGUE_GROUP}) 48 | self:add() 49 | self:setVisible(false) 50 | self:setUpdatesEnabled(false) 51 | end 52 | 53 | function Food:spawn(foodType, speed, blockIndex) 54 | self.type = foodType 55 | self.blockIndex = blockIndex 56 | self.speed = speed 57 | self.hitGround = false 58 | self.delete = false 59 | self.scored = false 60 | self.position.x = BLOCKS[self.blockIndex].xCenter + 1 61 | self.position.y = 0 62 | self.velocity.y = self.speed 63 | 64 | self.imgRow = self.type 65 | if self.imgRow > 2 then 66 | self.imgRow = 2 67 | end 68 | self.imgTable = spawnMonochrome and foodOutlineTable or foodTable 69 | self:setImage(self.imgTable:getImage(1, self.imgRow)) 70 | self:setCollideRect(4,1,12,18) 71 | if debugHarmlessFoodOn then 72 | self:setCollidesWithGroups({COLLIDE_TONGUE_GROUP}) 73 | end 74 | self:moveTo(self.position) 75 | self:setVisible(true) 76 | self:setUpdatesEnabled(true) 77 | end 78 | 79 | function Food:spawnLeaves(spitVelocity) 80 | local spitVelocityScaled = (spitVelocity * 0.25) * math.random() 81 | local leaf1 = Leaf(2) 82 | leaf1:spawn(self.position, self.speed) 83 | -- local leaf2 = Leaf(2) 84 | -- leaf2:spawn(self.position + spitVelocityScaled, self.speed) 85 | end 86 | 87 | function Food:capture(endPosition) 88 | self.velocity = vector2D.new(0, 0) 89 | self.endPosition = endPosition 90 | self.capturedPosition = self.position 91 | self.captured = true 92 | self:clearCollideRect() 93 | end 94 | 95 | function Food:hit(how, direction) 96 | if how == GROUND then 97 | self.hitGround = true 98 | end 99 | if debugHarmlessFoodOn then 100 | self.hitGround = false 101 | end 102 | 103 | if self.hitGround then 104 | BLOCKS[self.blockIndex]:destroy() 105 | end 106 | 107 | if how == SPIT then 108 | self.dust:spawn(self.position, direction) 109 | else 110 | self.dust:spawn(self.position) 111 | end 112 | self:cleanup() 113 | end 114 | 115 | function Food:cleanup() 116 | self.velocity = vector2D.new(0, 0) 117 | self.delete = true 118 | self.captured = false 119 | self.animationIndex = 1 120 | self:clearCollideRect() 121 | self:setVisible(false) 122 | self:setUpdatesEnabled(false) 123 | end 124 | 125 | function Food:update() 126 | 127 | local velocityStep = self.velocity * DT 128 | self.position = self.position + velocityStep 129 | 130 | -- made it to ground level 131 | if self.position.y >= 223 and not self.captured and BLOCKS[self.blockIndex].placed then 132 | self:hit(GROUND) 133 | end 134 | 135 | if (self.captured and (self.position.y >= self.endPosition.y - 10)) or 136 | self.position.y >= 240 then 137 | self:cleanup() 138 | end 139 | 140 | self:moveTo(self.position) 141 | self:updateImage() 142 | 143 | if self.frame % FRAME_DUR == 0 then 144 | self.animationIndex += 1 145 | if self.animationIndex > NUM_FRAMES then 146 | self.animationIndex = 1 147 | end 148 | end 149 | self.frame += 1 150 | 151 | if globalScore.monochromeTicker > 0 then 152 | -- self.imgTable = foodOutlineTable 153 | spawnMonochrome = true 154 | else 155 | self.imgTable = foodTable 156 | -- spawnMonochrome = false 157 | end 158 | end 159 | 160 | function Food:updateImage() 161 | if not self.captured then 162 | if self.type == 3 then 163 | self.imgRow = self.frame % 8 < 4 and 1 or 2 164 | end 165 | self:setImage(self.imgTable:getImage(self.animationIndex, self.imgRow)) 166 | else 167 | self:setImage(self.imgTable:getImage(1, self.imgRow)) 168 | end 169 | end 170 | 171 | -------------------------------------------------------------------------------- /source/globals/bgm.lua: -------------------------------------------------------------------------------- 1 | 2 | local snd = playdate.sound 3 | 4 | BGM = {} 5 | 6 | BGM.kNormal395 = { 7 | name='normal_395', 8 | layers={'normal_395_drums', 'normal_395_organs'}, 9 | vol=1.0, 10 | layerVol=0.8 11 | } 12 | BGM.kNormal = { 13 | name='normal', 14 | layers={'normal_drums', 'normal_organs'}, 15 | vol=1.0, 16 | layerVol=0.8, 17 | nextSong='kNormal395' 18 | } 19 | BGM.kNormalShort = { 20 | name = 'normal_short' 21 | } 22 | BGM.kSepia = { 23 | name = 'sepia' 24 | } 25 | BGM.kMonochrome = { 26 | name = 'monochrome', 27 | layers = {'monochrome_bass', 'monochrome_drums'}, 28 | vol = 1.0, 29 | layerVol = 1.0 30 | } 31 | BGM.kMonochromeIntro = { 32 | name='monochrome_intro', 33 | layers={'monochrome_bass_intro'}, 34 | vol=1.0, 35 | layerVol=1.0, 36 | nextSong='kMonochrome' 37 | } 38 | BGM.kMainMenu = { 39 | name = 'main_menu' 40 | } 41 | BGM.kGameOver = { 42 | name = 'game_over', 43 | nextSong = 'none' 44 | 45 | } 46 | BGM.kGameOver2 = { 47 | name = 'game_over2' 48 | } 49 | BGM.kPlayerDie = { 50 | name = 'pyoro_die', 51 | nextSong = 'kGameOver' 52 | } 53 | 54 | 55 | local function playSongAfter(fp, nextSongKey) 56 | -- Ignore this play if the song was stopped prematurely for any reason (aka by stopAll) 57 | if fp:getOffset() < fp:getLength() or nextSongKey == 'none' then 58 | return 59 | end 60 | 61 | local oldLayers = {table.unpack(BGM.activeLayers)} 62 | BGM:play(BGM[nextSongKey]) 63 | -- SFX:play(SFX.kNormal395Transition) 64 | for k, v in pairs(oldLayers) do 65 | BGM:addLayer(k) 66 | end 67 | end 68 | 69 | local players = {} 70 | -- set up BGM players table 71 | for _, v in pairs(BGM) do 72 | -- local name = v.name 73 | -- local layers = v.layers or {} 74 | -- local volume = v.vol or 1.0 75 | 76 | -- players[name] = snd.fileplayer.new('bgm/' .. name, 0.5) 77 | -- players[name]:setVolume(volume) 78 | -- players[name]:setStopOnUnderrun(false) 79 | -- for i, layer in ipairs(layers) do 80 | -- players[layer] = snd.fileplayer.new('bgm/' .. layer, 0.5) 81 | -- players[layer]:setVolume(0.01) 82 | -- players[layer]:setStopOnUnderrun(false) 83 | -- end 84 | 85 | -- -- some songs only play once then continue to another track 86 | -- if v.nextSong then 87 | -- players[name]:setFinishCallback(playSongAfter, v.nextSong) 88 | -- end 89 | 90 | end 91 | 92 | BGM.players = players 93 | BGM.activeLayers = {} 94 | BGM.volumes = {} 95 | 96 | function BGM:play(bgm, allowOverlap) 97 | return nil 98 | -- if not allowOverlap then 99 | -- self:stopAll() 100 | -- end 101 | 102 | -- local volume = audioSetting == 'sfx' and 0.01 or (bgm.vol or 1) 103 | -- local tracks = bgm.layers or {} 104 | -- -- infinite loop if there's no next song 105 | -- local loop = bgm.nextSong and 1 or 0 106 | 107 | -- tracks = {bgm.name, table.unpack(tracks)} 108 | -- for i, name in ipairs(tracks) do 109 | -- if i == 1 then -- only set volume for first track; rest are layers 110 | -- self.players[name]:setVolume(volume) 111 | -- end 112 | -- self.players[name]:play(loop) 113 | -- end 114 | 115 | -- self.nowPlaying = bgm 116 | end 117 | 118 | function BGM:addLayer(layer) 119 | -- if self.activeLayers[layer] == nil then 120 | -- if not self.nowPlaying.layers then 121 | -- print('!!! no layers on current track') 122 | -- return 123 | -- end 124 | -- local layerName = self.nowPlaying.layers[layer] 125 | -- local volume = self.nowPlaying.layerVol 126 | -- if audioSetting == 'sfx' then 127 | -- -- don't set player volume, but remember what it should be set to if music is turned back on 128 | -- self.volumes[layerName] = volume 129 | -- else 130 | -- self.players[layerName]:setVolume(volume) 131 | -- end 132 | -- self.activeLayers[layer] = layerName 133 | -- end 134 | end 135 | 136 | function BGM:stop() 137 | if not self.nowPlaying then 138 | return 139 | end 140 | 141 | local tracks = self.nowPlaying.layers or {} 142 | tracks = {table.unpack(tracks), self.nowPlaying.name} 143 | for i, name in ipairs(tracks) do 144 | self.players[name]:stop() 145 | end 146 | end 147 | 148 | function BGM:turnOff() 149 | for k, p in pairs(self.players) do 150 | self.volumes[k] = p:getVolume() 151 | p:setVolume(0.01) 152 | 153 | end 154 | end 155 | 156 | function BGM:turnOn() 157 | for k, p in pairs(self.players) do 158 | p:setVolume(self.volumes[k] or 1) 159 | end 160 | end 161 | 162 | function BGM:stopAll() 163 | -- stop all tracks 164 | for _, p in pairs(self.players) do 165 | p:stop() 166 | end 167 | 168 | -- reset layer volumes 169 | for l, layerName in pairs(self.activeLayers) do 170 | self.volumes[layerName] = 0.01 171 | self.players[layerName]:setVolume(0.01) 172 | end 173 | 174 | self.activeLayers = {} 175 | self.nowPlaying = nil 176 | end 177 | 178 | function BGM:skipToLoopEnd() 179 | print('skip to loop end') 180 | for i, p in pairs(self.players) do 181 | if p:isPlaying() then 182 | p:setOffset(p:getLength() - 5) 183 | end 184 | end 185 | end -------------------------------------------------------------------------------- /source/scenes/bgscene.lua: -------------------------------------------------------------------------------- 1 | class('BGScene').extends(gfx.sprite) 2 | class('Buildings').extends(gfx.sprite) 3 | local fireworkImg = gfx.image.new('img/scene/firework') 4 | local FIREWORK_HEIGHT = 112 5 | local NUM_FIREWORKS = 12 6 | local FADE_TYPE = gfx.image.kDitherTypeBayer2x2 7 | function BGScene:init() 8 | BGScene.super.init(self) 9 | 10 | self.layers = {} 11 | self:setImage(gfx.image.new('img/scene/background')) 12 | self:setCenter(0,0) 13 | self:add() 14 | 15 | local sky = gfx.sprite.new(gfx.image.new('img/scene/sky')) 16 | sky:setZIndex(LAYERS.sky) 17 | local hills = gfx.sprite.new(gfx.image.new('img/scene/hills')) 18 | hills:setZIndex(LAYERS.hills) 19 | local frame = gfx.sprite.new(gfx.image.new('img/scene/frame')) 20 | frame:setZIndex(LAYERS.frame) 21 | 22 | self.layers = {sky, hills, frame} 23 | for _, layer in ipairs(self.layers) do 24 | layer:setCenter(0,0) 25 | layer:moveTo(0,0) 26 | layer:add() 27 | end 28 | 29 | local comet = AnimatedSprite.new(gfx.imagetable.new('img/scene/comet')) 30 | comet:addState("streak", 1, nil, { 31 | tickStep = 2, 32 | loop=false, 33 | onLoopFinishedEvent = function (self) self:setVisible(false) end 34 | }) 35 | comet:setZIndex(LAYERS.comet) 36 | comet:setCenter(0.5,0.5) 37 | 38 | self.comet = comet 39 | 40 | self.buildings = Buildings() 41 | self.fireworks = {} 42 | 43 | self.yOffsets = {0, 5, 10, 13, 8, 12, 1, 17, 9, 11, 16, 6} 44 | end 45 | 46 | function BGScene:invert() 47 | self.layers[1]:setImage(self.layers[1]:getImage():invertedImage()) 48 | self.layers[2]:setImage(self.layers[2]:getImage():invertedImage()) 49 | end 50 | 51 | function BGScene:monochrome() 52 | self.layers[1]:setVisible(false) 53 | self.layers[2]:setImage(self.layers[2]:getImage():invertedImage()) 54 | self.buildings:monochrome() 55 | end 56 | 57 | function BGScene:createFireworks(n) 58 | local startPos = X_LOWER_BOUND + 10 59 | local range = 290 60 | local fireworkIncrement = range // n 61 | 62 | for i=1,n do 63 | local firework = gfx.sprite.new(fireworkImg) 64 | firework:setCenter(0,0) 65 | local ystart = 240 + (32 * self.yOffsets[i]) 66 | firework:moveTo(startPos + ((i-1) * fireworkIncrement), ystart) 67 | firework:setZIndex(LAYERS.fireworks) 68 | firework:add() 69 | table.insert(self.fireworks, firework) 70 | end 71 | end 72 | 73 | function BGScene:updateFireworks() 74 | if #self.fireworks == 0 then 75 | self:createFireworks(NUM_FIREWORKS) 76 | else 77 | for i, f in ipairs(self.fireworks) do 78 | f:moveBy(0, -16) 79 | if f.y <= - FIREWORK_HEIGHT then 80 | f: moveTo(f.x, FIREWORK_HEIGHT + 230) 81 | end 82 | end 83 | end 84 | end 85 | 86 | function BGScene:removeFireworks() 87 | for i, f in ipairs(self.fireworks) do 88 | f:remove() 89 | end 90 | end 91 | 92 | function BGScene:removeLayers() 93 | for _, layer in ipairs(self.layers) do 94 | layer:remove() 95 | end 96 | self.comet:remove() 97 | self.buildings:remove() 98 | end 99 | 100 | local BUILDING_FADE_IN_FRAMES = 20 101 | local BUILDING_FADE_STEP = 1/BUILDING_FADE_IN_FRAMES 102 | 103 | function Buildings:init() 104 | Buildings.super.init(self) 105 | 106 | self.images = {} 107 | self.monochromeMode = false 108 | self.drawMode = gfx.kDrawModeCopy 109 | self:setSize(400,240) 110 | self:setCenter(0,0) 111 | self:moveTo(0,0) 112 | self:setZIndex(LAYERS.buildings) 113 | self:setIgnoresDrawOffset(true) 114 | self:setUpdatesEnabled(false) 115 | self:add() 116 | 117 | self.frame = 1 118 | self.drawBlank = false 119 | end 120 | 121 | function Buildings:addBuilding(bld) 122 | local fade = bld.fade or 1 123 | bld.file = gfx.image.new('img/scene/' .. bld.name):fadedImage(fade, FADE_TYPE) 124 | table.insert(self.images, bld) 125 | self:markDirty() 126 | self.fadeInVal = 0 + BUILDING_FADE_STEP 127 | end 128 | 129 | function Buildings:removeBuilding(bld) 130 | self.images[bld] = nil 131 | end 132 | 133 | function Buildings:monochrome() 134 | self.images = {} 135 | local gi = game == BNB1 and 1 or 2 136 | self.monochromeMode = true 137 | self:addBuilding(BLD.kLights[gi]) 138 | self:addBuilding(BLD.k10[gi]) 139 | end 140 | 141 | function Buildings:startFlashing() 142 | self:setUpdatesEnabled(true) 143 | end 144 | 145 | function Buildings:draw() 146 | gfx.setImageDrawMode(self.drawMode) 147 | for i = #self.images, 1, -1 do 148 | local bld = self.images[i] 149 | if i == #self.images and not self.monochromeMode then 150 | bld.file:drawFaded(bld.x, bld.y, self.fadeInVal, FADE_TYPE) 151 | if self.fadeInVal < 1 then 152 | self.fadeInVal += BUILDING_FADE_STEP 153 | end 154 | else 155 | bld.file:draw(bld.x, bld.y) 156 | end 157 | end 158 | end 159 | 160 | function Buildings:update() 161 | 162 | if self.frame % 13 == 0 then 163 | self.drawMode = self.drawMode == gfx.kDrawModeInverted and gfx.kDrawModeBlackTransparent or gfx.kDrawModeInverted 164 | self:markDirty() 165 | end 166 | 167 | self.frame += 1 168 | end -------------------------------------------------------------------------------- /source/sprites/tongue.lua: -------------------------------------------------------------------------------- 1 | 2 | class('Tongue').extends(gfx.sprite) 3 | 4 | -- local references 5 | local Point = playdate.geometry.point 6 | local vector2D = playdate.geometry.vector2D 7 | 8 | -- constants 9 | local RETRACT_MULTIPLIER = -5 10 | local TONGUE_WIDTH = 11 11 | local SEGMENT_WIDTH = 10 -- lower width means more overlap between segment sprites 12 | local MAX_SEGMENTS = PLAY_AREA_WIDTH / SEGMENT_WIDTH 13 | -- local variables - these are "class local" but since we only have one tongue this isn't a problem 14 | local minXPosition = X_LOWER_BOUND + TONGUE_WIDTH/2 15 | local maxXPosition = X_UPPER_BOUND - TONGUE_WIDTH/2 16 | 17 | -- contain a sprite for tongue end and a sprite to repeat for tongue segments 18 | local tongueImages = gfx.imagetable.new('img/tongue') 19 | 20 | local function createSegment() 21 | local segment = gfx.sprite.new() 22 | segment:setImage(tongueImages:getImage(2)) 23 | segment:setZIndex(LAYERS.tongue - 1) 24 | segment:setVisible(false) 25 | segment:setCenter(0.5,0.5) 26 | segment:add() 27 | return segment 28 | end 29 | 30 | local tonguePool = POOL.create( createSegment, MAX_SEGMENTS) 31 | 32 | function Tongue:init(x, y, direction, withCrank) 33 | 34 | Tongue.super.init(self) 35 | 36 | self.direction = direction 37 | self:setImage(tongueImages:getImage(1), self.direction == RIGHT and gfx.kImageFlippedX or gfx.kImageUnflipped) 38 | self:setZIndex(LAYERS.tongue) 39 | self:setCenter(0.5, 0.5) 40 | self:setCollideRect(1,1,12,12) 41 | self:setCollidesWithGroups({COLLIDE_FOOD_GROUP}) 42 | 43 | self.withCrank = withCrank or false 44 | self.crankChange = 0 45 | self.segments = {} 46 | 47 | self.startPosition = Point.new(x,y - 2) 48 | self.position = self.startPosition:copy() 49 | self:moveTo(self.position) 50 | self.retracted = false 51 | self.retracting = false 52 | 53 | self.food = nil 54 | 55 | if direction == LEFT then 56 | self.velocity = vector2D.new(-tongueExtendVelocity,-tongueExtendVelocity) 57 | else 58 | self.velocity = vector2D.new(tongueExtendVelocity,-tongueExtendVelocity) 59 | end 60 | 61 | self:setVisible(false) 62 | self:add() 63 | if self.withCrank then 64 | self:setUpdatesEnabled(false) 65 | end 66 | 67 | SFX:play(SFX.kTongueOut) 68 | end 69 | 70 | 71 | function Tongue:reset() 72 | self.velocity = vector2D.new(0,0) 73 | end 74 | 75 | function Tongue:updateForCrank(crankPos, crankChange) 76 | local crankAmount = crankPos - 10 77 | self.position.y = self.startPosition.y - crankAmount 78 | self.position.x = self.startPosition.x + (self.direction * crankAmount) 79 | self.crankChange = crankChange 80 | self:update() 81 | end 82 | 83 | function Tongue:update() 84 | 85 | -- update tongue position based on current velocity 86 | if not self.withCrank or self.retracting then 87 | local velocityStep = self.velocity * DT 88 | self.position = self.position + velocityStep 89 | end 90 | 91 | -- only draw tongue after it extends a bit 92 | if math.abs(self.position.y - self.startPosition.y) >= 10 then 93 | self:setVisible(true) 94 | end 95 | 96 | -- handle tongue segments (aka tongue extension) 97 | local numSegmentsFromStart = (math.abs(self.position.x - self.startPosition.x))/SEGMENT_WIDTH 98 | if not self.retracting and self.crankChange >= 0 then 99 | self:drawSegmentsUntil(numSegmentsFromStart - 1) 100 | else 101 | self:removeSegmentsUntil(numSegmentsFromStart) 102 | end 103 | 104 | -- is the tongue retracted? 105 | if self.position.y > self.startPosition.y then 106 | self.retracted = true 107 | self:removeSegmentsUntil(0) 108 | self:remove() 109 | end 110 | 111 | -- is the tongue colliding? 112 | if not self.food then 113 | local collisions = self:overlappingSprites() 114 | if #collisions > 0 then 115 | self.food = table.remove(collisions) 116 | self:retract() 117 | self.food:capture(self.startPosition) 118 | SFX:play(SFX.kCatchFood) 119 | end 120 | end 121 | 122 | if self.food then 123 | self.food.position = self.position 124 | end 125 | 126 | -- don't move outside the walls of the game 127 | if self.position.x < minXPosition then 128 | self:retract() 129 | elseif self.position.x > maxXPosition then 130 | self:retract() 131 | elseif self.position.y < 0 then 132 | self:retract() 133 | end 134 | 135 | self:moveTo(self.position) 136 | 137 | end 138 | 139 | function Tongue:retract() 140 | if not self.retracting then 141 | SFX:stop(SFX.kTongueOut) 142 | SFX:play(SFX.kTongueRetract) 143 | self.velocity = self.velocity * RETRACT_MULTIPLIER 144 | self.retracting = true 145 | self:clearCollideRect() 146 | if self.withCrank then 147 | self:setUpdatesEnabled(true) 148 | end 149 | end 150 | end 151 | 152 | function Tongue:drawSegmentsUntil(numSegments) 153 | local lastDrawnX = #self.segments > 0 and self.segments[#self.segments].x or self.startPosition.x 154 | local lastDrawnY = #self.segments > 0 and self.segments[#self.segments].y or self.startPosition.y 155 | 156 | while #self.segments < numSegments do 157 | local segment = tonguePool:obtain() 158 | segment:setImage(tongueImages:getImage(2), self.direction == RIGHT and gfx.kImageFlippedX or gfx.kImageUnflipped) 159 | segment:moveTo(lastDrawnX + (self.direction * SEGMENT_WIDTH), lastDrawnY - SEGMENT_WIDTH) 160 | lastDrawnX = segment.x 161 | lastDrawnY = segment.y 162 | segment:setVisible(true) 163 | table.insert(self.segments, segment) 164 | end 165 | end 166 | 167 | function Tongue:removeSegmentsUntil(numSegments) 168 | while #self.segments > numSegments do 169 | local segment = table.remove(self.segments) 170 | segment:setVisible(false) 171 | tonguePool:free(segment) 172 | end 173 | end 174 | 175 | function Tongue:hasFood() 176 | return self.food ~= nil 177 | end 178 | 179 | 180 | -------------------------------------------------------------------------------- /source/main.lua: -------------------------------------------------------------------------------- 1 | import 'imports' 2 | 3 | -- globals 4 | globalScore = nil 5 | game = BNB1 6 | leafParticles = 'auto' 7 | audioSetting = 'sfx+music' 8 | 9 | -- debug globals 10 | debug = false 11 | debugHarmlessFoodOn = false 12 | debugPlayerInvincible = false 13 | 14 | -- scenes 15 | local save = nil 16 | local level = nil 17 | local gameover = nil 18 | local menu = nil 19 | 20 | local function loadSave() 21 | save = playdate.datastore.read() 22 | if not save then 23 | save = { 24 | bnb1 = 10000, 25 | bnb2 = -1, 26 | leafParticles = leafParticles 27 | } 28 | game = BNB1 29 | end 30 | leafParticles = save.leafParticles or 'auto' 31 | end 32 | 33 | local function initialize() 34 | -- playdate settings 35 | math.randomseed(playdate.getSecondsSinceEpoch()) 36 | playdate.display.setRefreshRate(REFRESH_RATE) 37 | -- force gc to run for at least 2ms each frame 38 | playdate.setMinimumGCTime(2) 39 | gfx.sprite.setAlwaysRedraw(true) 40 | 41 | -- start menu 42 | loadSave() 43 | menu = Menu(save) 44 | end 45 | 46 | local function writeSave() 47 | local newSave = {} 48 | if game == BNB1 then 49 | newSave.bnb1 = globalScore.highScore 50 | newSave.bnb2 = save.bnb2 51 | 52 | -- unlock bird and beans 2 53 | if newSave.bnb1 > 10000 and newSave.bnb2 < 0 then 54 | newSave.bnb2 = 0 55 | end 56 | else 57 | newSave.bnb1 = save.bnb1 58 | newSave.bnb2 = globalScore.highScore 59 | end 60 | newSave.leafParticles = leafParticles 61 | save = newSave 62 | playdate.datastore.write(newSave) 63 | end 64 | 65 | local function gameEnd() 66 | if globalScore then 67 | writeSave() 68 | end 69 | if gameover then 70 | gameover:endScene() 71 | end 72 | if level then 73 | level:endScene() 74 | end 75 | if menu then 76 | menu:endScene() 77 | end 78 | menu = nil 79 | level = nil 80 | gameover = nil 81 | 82 | menu = Menu(save) 83 | end 84 | 85 | local function startLevel() 86 | level:startScene() 87 | if menu then 88 | menu:endScene() 89 | menu = nil 90 | end 91 | end 92 | 93 | initialize() 94 | 95 | function playdate.update() 96 | gfx.sprite.update() 97 | playdate.timer.updateTimers() 98 | 99 | if menu then 100 | if menu.loading and not level then 101 | -- set high score 102 | loadSave() 103 | globalScore = Score() 104 | globalScore.highScore = save[game] 105 | -- start level 106 | level = Level() 107 | -- remove menu 108 | elseif menu.ready then 109 | startLevel() 110 | end 111 | end 112 | if level then 113 | if level.player.dead and not gameover then 114 | gameover = GameOver() 115 | end 116 | end 117 | if gameover then 118 | if gameover.done then 119 | gameover:endScene() 120 | gameEnd() 121 | end 122 | end 123 | 124 | if debug then playdate.drawFPS(0,0) end 125 | end 126 | 127 | function playdate.keyReleased(key) 128 | print(key) 129 | local numkey = tonumber(key) 130 | if numkey then 131 | local points = numkey * 1000 132 | globalScore:addPoints(points) 133 | level.stageController.stageTimeSeconds += 20 * numkey 134 | elseif key == 'h' then 135 | debugHarmlessFoodOn = not debugHarmlessFoodOn 136 | print('harmless food: ', debugHarmlessFoodOn) 137 | elseif key == 'i' then 138 | debugPlayerInvincible = not debugPlayerInvincible 139 | print('player invincible: ', debugPlayerInvincible) 140 | elseif key == 'n' then 141 | -- skip near the end of the current track(s) 142 | BGM:skipToLoopEnd() 143 | elseif key == 'k' and level then 144 | level.player:die() 145 | end 146 | end 147 | 148 | function playdate.gameWillTerminate() 149 | writeSave() 150 | end 151 | 152 | function playdate.deviceWillSleep() 153 | writeSave() 154 | end 155 | 156 | -- menu items 157 | local sysMenu = playdate.getSystemMenu() 158 | 159 | local gameEndItem, error = sysMenu:addMenuItem("main menu", function() 160 | BGM:stopAll() 161 | gameEnd() 162 | end) 163 | 164 | local particleItem, error = sysMenu:addOptionsMenuItem('audio', {'sfx+music', 'sfx', 'music'}, audioSetting, function(value) 165 | audioSetting = value 166 | if value == 'sfx' then 167 | BGM:turnOff() 168 | else 169 | BGM:turnOn() 170 | end 171 | 172 | end) 173 | 174 | local particleItem, error = sysMenu:addOptionsMenuItem('leaf FX', {'auto', 'off', 'on'}, leafParticles, function(value) 175 | leafParticles = value 176 | end) 177 | 178 | 179 | if debug then 180 | local invincibleItem, error = sysMenu:addCheckmarkMenuItem("invincibility", debugPlayerInvincible, function(value) 181 | debugPlayerInvincible = value 182 | end) 183 | 184 | local scoreItem, error = sysMenu:addOptionsMenuItem('set score', {'5k', '10k', '30k', '50k'}, '5k', function(value) 185 | if value == '5k' then 186 | globalScore.stage = 5 187 | globalScore.score = 5000 188 | end 189 | if value == '10k' then 190 | globalScore.stage = 10 191 | globalScore.score = 10000 192 | end 193 | if value == '30k' then 194 | globalScore.stage = 30 195 | globalScore.score = 30000 196 | end 197 | if value == '50k' then 198 | globalScore.stage = 50 199 | globalScore.score = 50000 200 | end 201 | end) 202 | end -------------------------------------------------------------------------------- /source/scenes/menu.lua: -------------------------------------------------------------------------------- 1 | class('Menu').extends(gfx.sprite) 2 | 3 | local homeTable = gfx.imagetable.new('img/menu/home') 4 | local aboutTable = gfx.imagetable.new('img/menu/about') 5 | local cursorTable = gfx.imagetable.new('img/player') 6 | local loading = gfx.image.new('img/scene/background') 7 | local eyesTable = gfx.imagetable.new('img/menu/eyes') 8 | local EYES_SEQUENCE = {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,3,4,4,4,4,4,4,4,4,4,4,4,4,5,6,6,4,4,4,4,4,4,4,4,4,4,5,6,4,4,4,4,5,6,4,4,4,4,4,4,4,4,4,4,4,4,4,4,3,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1} 9 | 10 | local CURSOR_Y = 185 11 | local CURSOR_X_LOCS = {58, 178, 298} 12 | local CURSOR_CYCLE_LEN = 5 13 | 14 | local LOADING_DUR_FRAMES = 0.3 * REFRESH_RATE 15 | local FADE_STEP_SIZE = 1 / LOADING_DUR_FRAMES 16 | 17 | function Menu:init(save) 18 | Menu.super.init(self) 19 | 20 | self.menuItems = 3 21 | self.cursorIndex = game == BNB1 and 1 or 2 22 | self.frame = 1 23 | self.ai = 1 24 | self.ready = false 25 | self.loading = false 26 | self.onHomePage = true 27 | 28 | BGM:play(BGM.kMainMenu, true) 29 | 30 | self.g1score = save.bnb1 31 | self.g2score = save.bnb2 32 | self.unlockedG2 = self.g2score >= 0 33 | 34 | if self.unlockedG2 then 35 | self.homeImg = homeTable:getImage(2) 36 | self.aboutImg = aboutTable:getImage(2) 37 | else 38 | self.homeImg = homeTable:getImage(1) 39 | self.aboutImg = aboutTable:getImage(1) 40 | end 41 | 42 | self:setImage(self.homeImg) 43 | self:drawScores() 44 | self:drawVersion() 45 | 46 | self:setCenter(0,0) 47 | self:setZIndex(LAYERS.menu) 48 | self:moveTo(0,0) 49 | self:add() 50 | 51 | self.cursor = gfx.sprite.new() 52 | self.cursor:setImage(cursorTable:getImage(1), gfx.kImageFlippedX) 53 | self.cursor:setCenter(0.5,1) 54 | self.cursor:moveTo(CURSOR_X_LOCS[1], CURSOR_Y) 55 | self.cursor:setZIndex(LAYERS.cursor) 56 | self.cursor:add() 57 | 58 | self.eyes = AnimatedSprite.new(eyesTable) 59 | self.eyes:addState(1, 1, nil, {tickStep = 3, frames = EYES_SEQUENCE}, true).asDefault(true) 60 | self.eyes:moveTo(36,37) 61 | self.eyes:setCenter(0,0) 62 | self.eyes:setZIndex(LAYERS.menu + 1) 63 | self.eyes:add() 64 | end 65 | 66 | function Menu:update() 67 | if not self.loading then 68 | if playdate.buttonJustPressed(playdate.kButtonRight) and self.onHomePage then 69 | self.cursorIndex = math.ring(self.cursorIndex + 1, 1, self.menuItems+1) 70 | end 71 | if playdate.buttonJustPressed(playdate.kButtonLeft) and self.onHomePage then 72 | self.cursorIndex = math.ring(self.cursorIndex - 1, 1, self.menuItems+1) 73 | end 74 | if playdate.buttonJustPressed(playdate.kButtonA) and self.onHomePage then 75 | if self.cursorIndex == 1 then 76 | game = BNB1 77 | self:nextScene() 78 | end 79 | if self.cursorIndex == 2 then 80 | if self.unlockedG2 then 81 | game = BNB2 82 | self:nextScene() 83 | else 84 | SFX:play(SFX.kPause) 85 | end 86 | end 87 | if self.cursorIndex == 3 then 88 | self:setImage(self.aboutImg) 89 | self.cursor:setVisible(false) 90 | SFX:play(SFX.kMenuSelect) 91 | self.eyes:setVisible(false) 92 | self.onHomePage = false 93 | end 94 | end 95 | if playdate.buttonJustPressed(playdate.kButtonB) and not self.onHomePage then 96 | SFX:play(SFX.kMenuBack) 97 | self.cursor:setVisible(true) 98 | self.eyes:setVisible(true) 99 | self:setImage(self.homeImg) 100 | self.onHomePage = true 101 | end 102 | else 103 | self:setImage(loading:fadedImage(1-(self.frame * FADE_STEP_SIZE), gfx.image.kDitherTypeScreen)) 104 | if self.frame >= LOADING_DUR_FRAMES then 105 | self.ready = true 106 | end 107 | end 108 | 109 | if self.frame % CURSOR_CYCLE_LEN == 0 then 110 | self.ai = self.ai == 1 and 2 or 1 111 | end 112 | self.cursor:setImage(cursorTable:getImage(self.ai), gfx.kImageFlippedX) 113 | self.cursor:moveTo(CURSOR_X_LOCS[self.cursorIndex], CURSOR_Y) 114 | self.frame += 1 115 | end 116 | 117 | function Menu:endScene() 118 | self.cursor:remove() 119 | self.eyes:remove() 120 | self:remove() 121 | end 122 | 123 | function Menu:nextScene() 124 | SFX:play(SFX.kStart) 125 | self.loading = true 126 | self.frame = 0 127 | self:setImage(loading) 128 | self.cursor:remove() 129 | self.eyes:remove() 130 | end 131 | 132 | function Menu:drawScores() 133 | local y = 209 134 | local cursorOffset = 20 135 | 136 | gfx.pushContext(self.homeImg) 137 | gfx.setFont(SCORE_FONT) 138 | gfx.setImageDrawMode(gfx.kDrawModeCopy) 139 | gfx.fillRect(0, 206, 400, 30) 140 | gfx.drawTextAligned(string.format("HS %06d", self.g1score), CURSOR_X_LOCS[1] + cursorOffset, y, kTextAlignment.center) 141 | if self.unlockedG2 then 142 | gfx.drawTextAligned(string.format("HS %06d", self.g2score), CURSOR_X_LOCS[2] + cursorOffset, y, kTextAlignment.center) 143 | end 144 | gfx.popContext() 145 | 146 | end 147 | 148 | function Menu:drawVersion() 149 | local version = playdate.metadata.version 150 | print('game version', version) 151 | gfx.pushContext(self.aboutImg) 152 | gfx.setFont(SCORE_FONT) 153 | gfx.setImageDrawMode(gfx.kDrawModeCopy) 154 | gfx.fillRect(270, 220, 130, 30) 155 | gfx.drawText("VERSION " .. version, 270, 220) 156 | gfx.popContext() 157 | end 158 | -------------------------------------------------------------------------------- /source/scenes/level.lua: -------------------------------------------------------------------------------- 1 | class('Level').extends(gfx.sprite) 2 | 3 | -- Time in seconds between seed spawns 4 | local NORMAL, HEAL, CLEAR = 1, 2, 3 5 | local PLAYER_OVERHANG_OFFSET = 4 6 | 7 | local bgImg = gfx.image.new('img/background') 8 | doBoundCalculation = false 9 | 10 | -- wait time before food spawns again after clearing all 11 | local CLEAR_ALL_RESET_TIMER = 2 12 | local foodPool = POOL.create((function() return Food() end), 32) 13 | BLOCKS = {} 14 | 15 | function Level:init() 16 | Level.super.init(self) 17 | 18 | self.player = Player() 19 | self.player:add() 20 | self.player:setUpdatesEnabled(false) 21 | globalScore:add() 22 | 23 | if #BLOCKS == 0 then 24 | for i = 1, NUM_BLOCKS do 25 | table.insert(BLOCKS, Block(i)) 26 | end 27 | else 28 | for i = 1, NUM_BLOCKS do 29 | BLOCKS[i]:place() 30 | end 31 | end 32 | 33 | self.scene = BGScene() 34 | self.stageController = StageController() 35 | self.activeFood = {} 36 | self.firstClear = false 37 | self.foodTimerInitial = 4 38 | self.spawnClearFood = 0 39 | self.foodParams = STARTING_FOOD_PARAMS 40 | self.fallSpeedModifier = 0 41 | 42 | -- self.foodTimer = 0 43 | self:resetFoodTimer() 44 | self:add() 45 | self:setUpdatesEnabled(false) 46 | end 47 | 48 | function Level:resetFoodTimer() 49 | self.foodTimer = self.foodTimerInitial * REFRESH_RATE 50 | end 51 | 52 | function Level:update() 53 | -- check active food for collisions and such 54 | for i = #self.activeFood, 1, -1 do 55 | local food = self.activeFood[i] 56 | if food.delete then 57 | if food.hitGround then doBoundCalculation = true end 58 | foodPool:free(food) 59 | table.remove(self.activeFood, i) 60 | end 61 | end 62 | 63 | -- move player and adjust its bounds 64 | self.player:moveTo(self.player.position) 65 | if doBoundCalculation then 66 | local left, right = self:getDirectionalDistsToPlayer() 67 | if #left > 0 then 68 | local leftBlock = BLOCKS[left[1].blockIndex] 69 | self.player.minXPosition = leftBlock.x + BLOCK_WIDTH + PLAYER_OVERHANG_OFFSET + 1 70 | else 71 | self.player:resetMinXPosition() 72 | end 73 | if # right > 0 then 74 | local rightBlock = BLOCKS[right[1].blockIndex] 75 | self.player.maxXPosition = rightBlock.x - PLAYER_OVERHANG_OFFSET 76 | else 77 | self.player:resetMaxXPosition() 78 | end 79 | doBoundCalculation = false 80 | end 81 | 82 | -- check for food 83 | local foods, action = self.player:getFood() 84 | for i, food in ipairs(foods) do 85 | -- score the food 86 | if not food.scored then 87 | local points = action == 'tongue' and self:calcPoints(food.capturedPosition.y) or self:calcPoints2(#foods) 88 | globalScore:addPoints(points) 89 | food.points:spawn(points, food.capturedPosition, food.type==CLEAR) 90 | if action == 'spit' then 91 | food:hit(SPIT, self.player.facing) 92 | end 93 | -- handle heal and clear foods 94 | if food.type == HEAL then 95 | -- repair one block 96 | local dists = self:getAbsoluteDistsToPlayer() 97 | if #dists > 0 then 98 | BLOCKS[dists[1].blockIndex].angel:spawn() 99 | end 100 | end 101 | if food.type == CLEAR then 102 | -- spawn up to 10 angels 103 | local dists = self:getAbsoluteDistsToPlayer() 104 | for i = 1, 10 do 105 | if dists[i] then 106 | BLOCKS[dists[i].blockIndex].angel:spawn(i) 107 | end 108 | end 109 | 110 | -- clear all food that isn't captured 111 | for i, f in ipairs(self.activeFood) do 112 | if not f.captured then 113 | f.scored = true 114 | playdate.timer.performAfterDelay(50 * (i-1), function() 115 | f:hit(NONGROUND) 116 | globalScore:addPoints(50) 117 | f.points:spawn(50, f.position, true) 118 | end) 119 | end 120 | end 121 | 122 | self.foodTimer = CLEAR_ALL_RESET_TIMER * REFRESH_RATE 123 | end 124 | 125 | food.scored = true 126 | end 127 | end 128 | 129 | local spawnFood = 0 130 | spawnFood, self.foodTimerInitial, self.foodParams, self.fallSpeedModifier = self.stageController:update(self.scene) 131 | -- build up queue of clear all foods to spawn 132 | self.spawnClearFood += spawnFood 133 | 134 | -- handle food spawning 135 | self.foodTimer -= 1 136 | if self.foodTimer <= 0 then 137 | self:resetFoodTimer() 138 | self:spawnFood() 139 | -- spawn all built up clear all foods 140 | while self.spawnClearFood > 0 do 141 | self:spawnFood(CLEAR) 142 | self.spawnClearFood -= 1 143 | end 144 | end 145 | 146 | local monochromeTicker = globalScore.monochromeTicker 147 | if monochromeTicker > 0 and monochromeTicker <= 30 then 148 | BLOCKS[monochromeTicker]:monochrome() 149 | end 150 | 151 | end 152 | 153 | function Level:spawnFood(type) 154 | local speed = nil 155 | local randy = math.random() 156 | local checkSlow = self.foodParams.slow.chance 157 | local checkMed = self.foodParams.med.chance + checkSlow 158 | if randy < checkSlow then 159 | speed = self.foodParams.slow.speed 160 | elseif randy < checkMed then 161 | speed = self.foodParams.med.speed 162 | else 163 | speed = self.foodParams.fast.speed 164 | end 165 | speed += self.fallSpeedModifier 166 | 167 | if type == nil then 168 | local randy = math.random() 169 | if randy < 0.9 then 170 | type = NORMAL 171 | else 172 | type = HEAL 173 | end 174 | end 175 | 176 | -- spawn a new food over a random block 177 | local food = foodPool:obtain() 178 | food:spawn(type, speed, math.random(1, NUM_BLOCKS)) 179 | table.insert(self.activeFood, food) 180 | end 181 | 182 | function Level:getDirectionalDistsToPlayer() 183 | local nearLeft = {} -- hold negative distances 184 | local nearRight = {} -- hold positive distances 185 | for i, b in ipairs(BLOCKS) do 186 | if not b.placed then 187 | local dist = b.xCenter - self.player.position.x 188 | if dist < 0 then 189 | table.insert(nearLeft, {dist = dist, blockIndex = i}) 190 | else 191 | table.insert(nearRight, {dist = dist, blockIndex = i}) 192 | end 193 | end 194 | end 195 | table.sort(nearLeft, function(a, b) return a.dist > b.dist end) 196 | table.sort(nearRight, function(a, b) return a.dist < b.dist end) 197 | return nearLeft, nearRight 198 | end 199 | 200 | function Level:getAbsoluteDistsToPlayer() 201 | local dists = {} 202 | 203 | for i, b in ipairs(BLOCKS) do 204 | if not b.placed then 205 | local dist = math.abs(b.xCenter - self.player.position.x) 206 | table.insert(dists, {dist = dist, blockIndex = i}) 207 | end 208 | end 209 | table.sort(dists, function(a, b) return a.dist < b.dist end) 210 | return dists 211 | end 212 | 213 | function Level:calcPoints(y) 214 | if y > 163 then 215 | return 10 216 | elseif y > 113 then 217 | return 50 218 | elseif y > 77 then 219 | return 100 220 | elseif y > 42 then 221 | return 300 222 | else 223 | return 1000 224 | end 225 | end 226 | 227 | function Level:calcPoints2(numFood) 228 | if numFood == 1 then 229 | SFX:play(SFX.kPoints50, true) 230 | return 50 231 | elseif numFood == 2 then 232 | SFX:play(SFX.kPoints100, true) 233 | return 100 234 | elseif numFood == 3 then 235 | SFX:play(SFX.kPoints300, true) 236 | return 300 237 | elseif numFood >= 4 then 238 | SFX:play(SFX.kPoints1000, true) 239 | return 1000 240 | else 241 | return 0 242 | end 243 | end 244 | 245 | function Level:startScene() 246 | self:setUpdatesEnabled(true) 247 | self.player:setUpdatesEnabled(true) 248 | BGM:play(BGM.kNormal) 249 | end 250 | 251 | function Level:endScene() 252 | for i = #self.activeFood, 1, -1 do 253 | local food = self.activeFood[i] 254 | food:cleanup() 255 | foodPool:free(food) 256 | table.remove(self.activeFood, i) 257 | end 258 | self:setUpdatesEnabled(false) 259 | self.scene:removeLayers() 260 | self.scene:remove() 261 | self.player:remove() 262 | globalScore.levelDone = true 263 | globalScore:remove() 264 | self.stageController = nil 265 | end -------------------------------------------------------------------------------- /source/sprites/player.lua: -------------------------------------------------------------------------------- 1 | 2 | class('Player').extends(gfx.sprite) 3 | 4 | -- local references 5 | local Point = playdate.geometry.point 6 | local vector2D = playdate.geometry.vector2D 7 | local min, max, abs, floor = math.min, math.max, math.abs, math.floor 8 | 9 | -- constants 10 | 11 | local GROUND_FRICTION = 0.1 12 | local SPIT_VELOCITY = 100 13 | local STAND, LOWER, CATCH, MUNCH, TURN, JUMP, CROUCH = 1, 2, 3, 4, 5, 6, 7 14 | local MUNCH_TIME_SEC = 0.5 15 | 16 | local WALK_CYCLE_LEN = 4 * FRAME_LEN 17 | local MUNCH_CYCLE_LEN = 6 * FRAME_LEN -- how many frames long each munch cycle is 18 | local DEATH_CYCLE_LEN = 10 * FRAME_LEN 19 | local PEAK_SPIT = 5 20 | local CRANK_DEAD_ZONE = 10 21 | local MAX_Y_DIST = 222 22 | local MAX_CRANK = MAX_Y_DIST + 10 23 | 24 | local INIT_X = X_LOWER_BOUND + (PLAY_AREA_WIDTH / 2) 25 | local INIT_Y = 229 26 | 27 | -- local variables - these are "class local" but since we only have one player this isn't a problem 28 | local playerWidth = 19 29 | local LEFT_WALL = X_LOWER_BOUND + playerWidth/2 30 | local RIGHT_WALL = X_UPPER_BOUND - playerWidth/2 31 | 32 | 33 | local PLAYER_ACCELERATION = 80 34 | local COLOR, MONO = 1, 2 35 | 36 | local p1ImgTable = gfx.imagetable.new('img/player') 37 | local p2ImgTable = gfx.imagetable.new('img/player2') 38 | local beagleImgTable = gfx.imagetable.new('img/beagle') 39 | 40 | function Player:init() 41 | 42 | Player.super.init(self) 43 | 44 | self.tongueMode = game == BNB1 45 | self.imgTable = self.tongueMode and p1ImgTable or p2ImgTable 46 | self:setImage(self.imgTable:getImage(1,1)) 47 | self:setZIndex(LAYERS.player) 48 | 49 | self:setCenter(0.5, 1) 50 | self:moveTo(INIT_X, INIT_Y) 51 | self:setGroups({COLLIDE_PLAYER_GROUP}) 52 | self:setCollidesWithGroups({COLLIDE_FOOD_GROUP}) 53 | 54 | if self.tongueMode then 55 | self:setCollideRect(2,1,18,18) 56 | else 57 | self:setCollideRect(9,11,19,18) 58 | end 59 | 60 | self.color = COLOR 61 | self.collisionResponse = gfx.sprite.kCollisionTypeOverlap 62 | self.animationIndex = 1 63 | self.frame = 1 64 | self.facing = LEFT 65 | self.flip = gfx.kImageUnflipped 66 | 67 | self.canDoCrank = true 68 | self.action = nil 69 | self.spitTimer = 0 70 | self.munchTimer = 0 71 | self.spitPool = {Spit(), Spit(), Spit(), Spit()} 72 | self.spitVelocity = vector2D.new(SPIT_VELOCITY, -SPIT_VELOCITY) 73 | self.minXPosition = LEFT_WALL 74 | self.maxXPosition = RIGHT_WALL 75 | 76 | self.position = Point.new(INIT_X, INIT_Y) 77 | self.velocity = vector2D.new(0,0) 78 | 79 | end 80 | 81 | function Player:reset() 82 | self.position = Point.new(INIT_X, INIT_Y) 83 | self.velocity = vector2D.new(0,0) 84 | end 85 | 86 | function Player:resetMinXPosition() 87 | self.minXPosition = LEFT_WALL 88 | end 89 | 90 | function Player:resetMaxXPosition() 91 | self.maxXPosition = RIGHT_WALL 92 | end 93 | 94 | -- called every frame, handles new input and does simple physics simulation 95 | function Player:update() 96 | 97 | if self.action then 98 | -- reset action if tongue retracted 99 | if type(self.action) == 'table' and self.action.retracted then 100 | if self:hasFoodOnTongue() then 101 | self.munchTimer = MUNCH_TIME_SEC * REFRESH_RATE 102 | end 103 | self.action = nil 104 | end 105 | -- reset action if done spitting 106 | if type(self.action) == 'boolean' and self.spitTimer == 0 then 107 | self.action = nil 108 | end 109 | end 110 | 111 | -- set control events 112 | if playdate.buttonIsPressed("left") and not self.action and not self.dead then 113 | self:runLeft() 114 | elseif playdate.buttonIsPressed("right") and not self.action and not self.dead then 115 | self:runRight() 116 | end 117 | 118 | if (playdate.buttonJustReleased('left') or playdate.buttonJustReleased('right')) and not self.dead and not self.action then 119 | SFX:play(SFX.kWalk2, true) 120 | end 121 | 122 | if playdate.buttonJustPressed(playdate.kButtonA) and not self.action and not self.dead then 123 | self.velocity.x = 0 124 | 125 | if self.tongueMode then 126 | self.action = Tongue(self.position.x, self.position.y, self.facing) 127 | else -- the spit action does not need a table 128 | self.action = true 129 | self.spitTimer = PEAK_SPIT 130 | SFX:play(SFX.kSpit) 131 | for i, spit in ipairs(self.spitPool) do 132 | if not spit:isVisible() then 133 | spit:spawn(self.facing, self.position) 134 | break 135 | end 136 | end 137 | end 138 | elseif self.tongueMode and not self.dead and not playdate.isCrankDocked() then 139 | local crankPos = playdate.getCrankPosition() 140 | local crankChange, _ = playdate.getCrankChange() 141 | if (crankPos > CRANK_DEAD_ZONE and crankPos < 360 - CRANK_DEAD_ZONE) then 142 | if crankPos > CRANK_DEAD_ZONE and crankPos < MAX_CRANK then 143 | if self.canDoCrank and crankChange > 0 then 144 | self.velocity.x = 0 145 | self.action = Tongue(self.position.x, self.position.y, self.facing, true) 146 | self.canDoCrank = false 147 | elseif not self.canDoCrank and self.action and self.action.withCrank and not self.action.retracting then 148 | self.action:updateForCrank(crankPos, crankChange ) 149 | end 150 | end 151 | else 152 | -- only allow crank again if it is returned to deadzone 153 | if not self.canDoCrank then 154 | self.canDoCrank = true 155 | SFX:play(SFX.kCrankReturn) 156 | end 157 | if self.action and self.action.withCrank then 158 | self.action:retract() 159 | end 160 | end 161 | end 162 | 163 | if playdate.buttonJustReleased(playdate.kButtonA) and self.action and not self.dead and self.tongueMode then 164 | self.action:retract() 165 | end 166 | 167 | if (playdate.buttonIsPressed("left") == false and playdate.buttonIsPressed("right") == false) then 168 | self.velocity.x = self.velocity.x * GROUND_FRICTION 169 | end 170 | 171 | -- collision check 172 | local collisions = self:overlappingSprites() 173 | if #collisions > 0 and not self.dead then 174 | local food = table.remove(collisions) 175 | food:hit(NONGROUND) 176 | if not debugPlayerInvincible then 177 | self:die() 178 | end 179 | end 180 | 181 | 182 | -- update frame counter to control various animation timings 183 | self.frame += 1 184 | 185 | 186 | -- if not stopped, walking alternates between img 1 and 2 187 | if abs(self.velocity.x) < 10 then 188 | self.velocity.x = 0 189 | else 190 | local animateState = (self.frame % WALK_CYCLE_LEN) < WALK_CYCLE_LEN/2 191 | self.animationIndex = animateState and 1 or 2 192 | end 193 | 194 | -- spit lasts four frames atm 195 | if self.spitTimer > 0 then 196 | self.animationIndex = self.spitTimer + 1 197 | if self.spitTimer <= 1 then 198 | self.animationIndex = 1 199 | end 200 | self.spitTimer -= 1 201 | end 202 | -- munching alternates between 1 and 4 203 | if self.munchTimer > 0 then 204 | local animateState = (self.frame % MUNCH_CYCLE_LEN) < MUNCH_CYCLE_LEN/2 205 | self.animationIndex = animateState and 4 or 1 206 | self.munchTimer -= 1 207 | end 208 | -- death alternates between 5 and 6 209 | if self.dead then 210 | local animateState = (self.frame % DEATH_CYCLE_LEN) < DEATH_CYCLE_LEN/2 211 | self.animationIndex = animateState and 5 or 6 212 | self.imgTable = p1ImgTable 213 | end 214 | 215 | -- update Player position based on current velocity 216 | local velocityStep = self.velocity * DT 217 | self.position = self.position + velocityStep 218 | 219 | -- don't move outside the walls of the game 220 | if self.position.x < self.minXPosition then 221 | self.velocity.x = 0 222 | self.position.x = self.minXPosition 223 | elseif self.position.x > self.maxXPosition then 224 | self.velocity.x = 0 225 | self.position.x = self.maxXPosition 226 | end 227 | 228 | if globalScore.monochromeTicker > 0 then 229 | self.color = MONO 230 | else 231 | self.color = COLOR 232 | end 233 | 234 | self:updateImage() 235 | 236 | end 237 | 238 | -- sets the appropriate sprite image for Player based on the current conditions 239 | function Player:updateImage() 240 | local ai = self:hasTongue() and CATCH or floor(self.animationIndex) 241 | if math.abs(self.velocity.x) > 0 then 242 | if ai == 1 then 243 | SFX:play(SFX.kWalk) 244 | end 245 | if ai == 2 then 246 | SFX:play(SFX.kWalk2) 247 | end 248 | end 249 | self:setImage(self.imgTable:getImage(ai, self.color), self.flip) 250 | end 251 | 252 | function Player:hasTongue() 253 | return type(self.action) == 'table' and self.spitTimer == 0 254 | end 255 | 256 | function Player:hasFoodOnTongue() 257 | if self:hasTongue() then 258 | return self.action:hasFood() 259 | else 260 | return false 261 | end 262 | end 263 | 264 | function Player:getFood() 265 | if self:hasFoodOnTongue() then 266 | return {self.action.food}, 'tongue' 267 | elseif self.spitTimer == PEAK_SPIT - 1 then 268 | return self:getSpitHits(), 'spit' 269 | else 270 | return {}, nil 271 | end 272 | end 273 | 274 | function Player:getSpitHits() 275 | local spitOrigin = Point.new(self.position.x, self.position.y - 8) 276 | local xmax = self.facing == LEFT and X_LOWER_BOUND - spitOrigin.x or X_UPPER_BOUND - spitOrigin.x 277 | local ymax = spitOrigin.y - 0 278 | local dxdy = math.min(math.abs(ymax), math.abs(xmax)) 279 | 280 | local hits = gfx.sprite.querySpritesAlongLine(spitOrigin.x, spitOrigin.y, spitOrigin.x + (self.facing * dxdy), spitOrigin.y - dxdy) 281 | local foods = {} 282 | for i, f in ipairs(hits) do 283 | if f.isFood then 284 | if (globalScore.stage < 20 and leafParticles == 'auto') or leafParticles == 'on' then 285 | f:spawnLeaves(self.spitVelocity) 286 | end 287 | f:capture(self.position) 288 | table.insert(foods, f) 289 | end 290 | 291 | if f.isLeaf then 292 | f.velocity = self.spitVelocity:copy() 293 | if f.size == 2 and math.random() < 0.5 then 294 | f:destroy(false) 295 | end 296 | end 297 | end 298 | return foods 299 | end 300 | 301 | function Player:runLeft() 302 | self.facing = LEFT 303 | self.flip = gfx.kImageUnflipped 304 | self.velocity.x = -playerMaxRunVelocity --max(self.velocity.x - PLAYER_ACCELERATION, -playerMaxRunVelocity) 305 | self.spitVelocity.x = -SPIT_VELOCITY 306 | self.munchTimer = 0 307 | end 308 | 309 | function Player:runRight() 310 | self.facing = RIGHT 311 | self.flip = gfx.kImageFlippedX 312 | self.velocity.x = playerMaxRunVelocity --min(self.velocity.x + PLAYER_ACCELERATION, playerMaxRunVelocity) 313 | self.spitVelocity.x = SPIT_VELOCITY 314 | self.munchTimer = 0 315 | end 316 | 317 | function Player:die() 318 | BGM:play(BGM.kPlayerDie) 319 | self.dead = true 320 | if self:hasTongue() then 321 | self.action:retract() 322 | end 323 | self.velocity =vector2D.new(0, 30) 324 | end 325 | -------------------------------------------------------------------------------- /source/scenes/stagecontrol.lua: -------------------------------------------------------------------------------- 1 | class('StageController').extends() 2 | local FADE1 = 0.8 3 | local FADE2 = 1 4 | BLD = {} 5 | BLD.k1 = {{name = '2_lamp',x = 269,y = 174}, {name = '2b_lamp',x = 267,y = 177}} 6 | BLD.k2 = {{name = '2_lamp',x = 223,y = 185}, {name = '2b_lamp',x = 215,y = 166}} 7 | BLD.k3 = {{name = '3_house',x = 151,y = 161}, {name = '3b_monster',x = 154,y = 154}} 8 | BLD.k4 = {{name = '4_wheel',x = 112,y = 130}, {name = '4b_home',x = 122,y = 94, fade=FADE1}} 9 | BLD.k6 = {{name = '6_bridge',x = 193,y = 118, fade=FADE1}, {name = '6b_turbine',x = 273,y = 99, fade=FADE1}} 10 | BLD.k7 = {{name = '7_tower',x = 151,y = 60, fade=FADE1}, {name = '7b_castle',x = 146,y = 46, fade=FADE1}} 11 | BLD.k8 = {{name = '8_skyscraper',x = 97,y = 32}, {name = '8b_tower',x = 94,y = 16, fade=FADE1}} 12 | BLD.k9 = {{name = '9_ufo',x = 145,y = 26 }, {name = '9b_ufo',x = 139,y = 15, fade=FADE2}} 13 | BLD.k10 = {{name = '10_moon',x = 216,y = 71 }, {name = '10b_moon',x = 218,y = 67}} 14 | BLD.k11 = {{name = '11_planet',x = 52,y = 7 }, {name = '11b_planet',x = 47,y = 3, fade=FADE2}} 15 | BLD.k12 = {{name = '12_city',x = 262,y = 8}, {name = '12b_city',x = 274,y = -7, fade=FADE2}} 16 | BLD.kLights = {{name = 'lights',x = 0,y = 0}, {name = 'lights2',x = 0,y = 0}} 17 | 18 | -- stage where we start late game 19 | LATE_GAME_STAGE = 50 20 | -- number of stages to continually scale the spawn rates in late game 21 | LATE_GAME_STAGE_COUNT = 50 22 | -- amount to decrease food timer by each stage in the late game 23 | FOOD_TIMER_LATE_GAME_MODIFIER = -0.001 24 | -- how much to change fall speed each stage in the lage game 25 | FALL_SPEED_LATE_GAME_MODIFIER = 1 26 | 27 | -- hard-coded function that determines how the current stage (index) affects the food timer (value) 28 | FOOD_TIMER_STAGE_DATA = {2, 1, 0.8, 0.7, 0.6, 0.5, 0.45, 0.4, 0.35, 0.32, 0.3, 0.28, {minStage=12, nextThreshold=20, val=0.25}, {minStage = 20, nextThreshold=30, val=0.23}, {minStage = 30, nextThreshold=1000, val = 0.2}} 29 | 30 | -- fall speed distributions of food 31 | FOOD_PARAM_FUNC = { 32 | -- time stage 0 and 1 33 | STARTING_FOOD_PARAMS, 34 | -- time stage 2 35 | { 36 | slow = {chance=0.6, speed=33}, 37 | med = {chance=0.4, speed=43}, 38 | fast = {chance=0, speed=0}, 39 | }, 40 | -- time stage 3 41 | { 42 | slow = {chance=0.4, speed=33}, 43 | med = {chance=0.3, speed=43}, 44 | fast = {chance=0.3, speed=55}, 45 | }, 46 | -- time stage 4 47 | { 48 | slow = {chance=0.5, speed=43}, 49 | med = {chance=0.5, speed=55}, 50 | fast = {chance=0, speed=0}, 51 | }, 52 | -- time stage 5 53 | { 54 | slow = {chance=0.3, speed=43}, 55 | med = {chance=0.35, speed=55}, 56 | fast = {chance=0.35, speed=65}, 57 | }, 58 | -- time stage 6 59 | { 60 | slow = {chance=0.35, speed=55}, 61 | med = {chance=0.35, speed=65}, 62 | fast = {chance=0.30, speed=80}, 63 | }, 64 | -- time stage 7 65 | { 66 | slow = {chance=0.30, speed=55}, 67 | med = {chance=0.30, speed=65}, 68 | fast = {chance=0.40, speed=80}, 69 | }, 70 | -- time stage 8 71 | { 72 | slow = {chance=0.35, speed=65}, 73 | med = {chance=0.35, speed=80}, 74 | fast = {chance=0.30, speed=95}, 75 | } 76 | } 77 | -- average time in seconds between food spawn 78 | STARTING_FOOD_TIMER = 2 79 | -- seconds between each time based stage increase 80 | STAGE_TIME_INTERVAL = 20 81 | -- time stage before stage_timer kicks in 82 | STAGE_TIME = 2 83 | -- starting tongue velocity 84 | STARTING_TONGUE_VELOCITY = 200 85 | tongueExtendVelocity = STARTING_TONGUE_VELOCITY 86 | -- how much to increment tongue velocity for each food speed increase 87 | TONGUE_VELOCITY_UNIT = STARTING_TONGUE_VELOCITY / STARTING_FOOD_PARAMS.slow.speed * 0.25 88 | 89 | STARTING_MAX_RUN_VELOCITY = 80 90 | RUN_VELOCITY_UNIT = STARTING_MAX_RUN_VELOCITY / STARTING_FOOD_PARAMS.slow.speed * 0.6 91 | OVERALL_MAX_RUN_VELOCITY = 200 92 | 93 | function StageController:init() 94 | StageController.super.init(self) 95 | self.fallSpeedPicker = 1 96 | self.stage = 0 97 | self.prevStage = 0 98 | self.stageLog = {} 99 | self.stageTimeSeconds = 0 100 | self.timeStage = 0 101 | self.prevTimeStage = 0 102 | self.gi = game == BNB1 and 1 or 2 103 | 104 | self.foodTimer = STARTING_FOOD_TIMER 105 | self.foodParams = STARTING_FOOD_PARAMS 106 | self.fallSpeedModifier = 0 107 | self.foodTimerModifier = 0 108 | self.slowestFoodSpeed = STARTING_FOOD_PARAMS.slow.speed 109 | 110 | tongueExtendVelocity = STARTING_TONGUE_VELOCITY 111 | playerMaxRunVelocity = STARTING_MAX_RUN_VELOCITY 112 | self:setStageData(globalScore.stage) 113 | end 114 | 115 | function StageController:update(scene) 116 | local spawnFoodCount = 0 117 | 118 | self:setStageData(globalScore.stage) 119 | 120 | if self:reachedStage(1) then 121 | scene.buildings:addBuilding(BLD.k1[self.gi]) 122 | end 123 | 124 | if self:reachedStage(2) then 125 | scene.buildings:addBuilding(BLD.k2[self.gi]) 126 | end 127 | 128 | if self:reachedStage(3) then 129 | scene.buildings:addBuilding(BLD.k3[self.gi]) 130 | end 131 | 132 | if self:reachedStage(4) then 133 | scene.buildings:addBuilding(BLD.k4[self.gi]) 134 | end 135 | 136 | if self:reachedStage(5) then 137 | spawnFoodCount+=1 138 | BGM:addLayer(1) 139 | end 140 | 141 | if self:reachedStage(6) then 142 | scene.buildings:addBuilding(BLD.k6[self.gi]) 143 | end 144 | 145 | if self:reachedStage(7) then 146 | spawnFoodCount+=1 147 | scene.buildings:addBuilding(BLD.k7[self.gi]) 148 | end 149 | 150 | if self:reachedStage(8) then 151 | scene.buildings:addBuilding(BLD.k8[self.gi]) 152 | end 153 | 154 | if self:reachedStage(9) then 155 | spawnFoodCount+=1 156 | scene.buildings:addBuilding(BLD.k9[self.gi]) 157 | end 158 | 159 | if self:reachedStage(10) then 160 | scene.buildings:addBuilding(BLD.k10[self.gi]) 161 | BGM:addLayer(2) 162 | end 163 | 164 | if self:reachedStage(11) then 165 | scene.buildings:addBuilding(BLD.k11[self.gi]) 166 | end 167 | 168 | if self:reachedStage(12) then 169 | scene.buildings:addBuilding(BLD.k12[self.gi]) 170 | end 171 | 172 | if self:reachedStage(20) then 173 | BGM:play(BGM.kSepia) 174 | scene:invert() 175 | end 176 | 177 | local monochromeTicker = globalScore.monochromeTicker 178 | if monochromeTicker >= 1 and monochromeTicker < 30 then 179 | globalScore.monochromeTicker += 1 180 | end 181 | 182 | if self:reachedStage(30) then 183 | BGM:play(BGM.kMonochromeIntro) 184 | BGM:addLayer(1) 185 | scene:monochrome() 186 | globalScore.monochromeTicker = 1 187 | end 188 | 189 | 190 | if self:reachedStage(40) and not playdate.getReduceFlashing() then 191 | scene.buildings:startFlashing() 192 | end 193 | 194 | if self:reachedStage(50) then 195 | if BGM.nowPlaying == BGM.kMonochrome then 196 | BGM:addLayer(2) 197 | end 198 | end 199 | 200 | if self.stage ~= self.prevStage then 201 | self:recalculateFoodParams(false) 202 | if self.stage > 9 then 203 | spawnFoodCount += 1 204 | end 205 | if self.stage >= 13 and self.stage < 20 then 206 | SFX:play(SFX.kComet, true) 207 | scene.comet:moveTo(math.random(180,220), math.random(40,60)) 208 | scene.comet:setVisible(true) 209 | scene.comet:playAnimation() 210 | end 211 | end 212 | 213 | if self.stage >= 50 then 214 | scene:updateFireworks() 215 | end 216 | 217 | if self.timeStage ~= self.prevTimeStage then 218 | self:recalculateFoodParams(true) 219 | end 220 | self.stageTimeSeconds += FRAME_TIME_SEC 221 | return spawnFoodCount, self.foodTimer, self.foodParams, self.fallSpeedModifier 222 | end 223 | 224 | function StageController:reachedStage(stage) 225 | -- return true if query stage <= current stage ANDj 226 | -- only only the first time this happens 227 | local firstTimeReached = stage <= self.stage and self.stageLog[stage] == nil 228 | if firstTimeReached then 229 | self.stageLog[stage] = true 230 | end 231 | return firstTimeReached 232 | 233 | end 234 | 235 | function StageController:setStageData(stage) 236 | self.prevStage = self.stage 237 | self.stage = stage 238 | 239 | local timeStage = self.stageTimeSeconds // STAGE_TIME_INTERVAL 240 | self.prevTimeStage = self.timeStage 241 | self.timeStage = timeStage 242 | end 243 | 244 | -- use stage and timeStage to determine food spawn rate and fall speed 245 | function StageController:recalculateFoodParams(newTimeStage) 246 | -- food timers 247 | local timeBasedFt = 1.91 * math.exp(-0.02 * self.stageTimeSeconds + 0.0366) + 0.65 248 | local stageBasedFt = 2 249 | for i, data in ipairs(FOOD_TIMER_STAGE_DATA) do 250 | local minStage = i - 1 251 | local ft = data 252 | if type(data) == 'table' then 253 | minStage = data.minStage 254 | ft = data.val 255 | end 256 | if self.stage < minStage then 257 | break 258 | else 259 | stageBasedFt = ft 260 | end 261 | end 262 | self.foodTimer = math.min(timeBasedFt, stageBasedFt) 263 | 264 | self.fallSpeedPicker = self.timeStage 265 | -- if the score grows too fast, we may need to simulate a later time stage for faster fall speed 266 | if self.stage > self.fallSpeedPicker + 5 then 267 | self.fallSpeedPicker = self.stage - 5 268 | print('bumped up time stage', self.fallSpeedPicker) 269 | end 270 | if self.fallSpeedPicker > #FOOD_PARAM_FUNC then 271 | self.fallSpeedPicker = #FOOD_PARAM_FUNC 272 | end 273 | if self.fallSpeedPicker <= 0 then 274 | self.fallSpeedPicker = 1 275 | end 276 | self.foodParams = FOOD_PARAM_FUNC[self.fallSpeedPicker] 277 | 278 | -- player is getting too good, lets ramp up the challenge in the LATE GAME 279 | local lateGameProgress = self.stage - LATE_GAME_STAGE + 1 280 | if lateGameProgress > 0 and lateGameProgress <= LATE_GAME_STAGE_COUNT then 281 | self.fallSpeedModifier = lateGameProgress * FALL_SPEED_LATE_GAME_MODIFIER 282 | self.foodTimerModifier = lateGameProgress * FOOD_TIMER_LATE_GAME_MODIFIER 283 | 284 | self.foodTimer += self.foodTimerModifier 285 | end 286 | 287 | local currentLowSpeed = self.foodParams.slow.speed + self.fallSpeedModifier 288 | if currentLowSpeed > self.slowestFoodSpeed then 289 | local tongeVelocityIncr = (currentLowSpeed - self.slowestFoodSpeed) * TONGUE_VELOCITY_UNIT 290 | tongueExtendVelocity += tongeVelocityIncr 291 | local runVelocityIncr = (currentLowSpeed - self.slowestFoodSpeed) * RUN_VELOCITY_UNIT 292 | playerMaxRunVelocity = math.min(playerMaxRunVelocity + runVelocityIncr, OVERALL_MAX_RUN_VELOCITY) 293 | self.slowestFoodSpeed = currentLowSpeed 294 | end 295 | 296 | if newTimeStage and debug then 297 | print('reached time stage', self.timeStage) 298 | else 299 | print('reached stage', self.stage) 300 | end 301 | 302 | end -------------------------------------------------------------------------------- /source/img/fonts/connection_bold.fnt: -------------------------------------------------------------------------------- 1 | --metrics={"baseline":22,"xHeight":-1,"capHeight":-1} 2 | datalen=11556 3 | data=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 4 | width=33 5 | height=30 6 | 7 | tracking=2 8 | 9 | space 3 10 | ! 6 11 | " 11 12 | # 30 13 | $ 17 14 | % 33 15 | & 19 16 | ' 6 17 | ( 11 18 | ) 11 19 | * 11 20 | + 11 21 | , 9 22 | - 14 23 | . 6 24 | / 22 25 | 0 17 26 | 1 11 27 | 2 17 28 | 3 17 29 | 4 17 30 | 5 17 31 | 6 17 32 | 7 17 33 | 8 17 34 | 9 17 35 | : 6 36 | ; 9 37 | < 11 38 | = 14 39 | > 11 40 | ? 17 41 | @ 22 42 | A 17 43 | B 17 44 | C 17 45 | D 17 46 | E 17 47 | F 17 48 | G 17 49 | H 17 50 | I 6 51 | J 17 52 | K 17 53 | L 14 54 | M 22 55 | N 17 56 | O 17 57 | P 17 58 | Q 19 59 | R 17 60 | S 17 61 | T 17 62 | U 17 63 | V 17 64 | W 27 65 | X 17 66 | Y 17 67 | Z 17 68 | [ 11 69 | \ 22 70 | ] 11 71 | ^ 17 72 | _ 17 73 | ` 9 74 | a 19 75 | b 17 76 | c 17 77 | d 17 78 | e 17 79 | f 14 80 | g 17 81 | h 17 82 | i 6 83 | j 9 84 | k 14 85 | l 6 86 | m 25 87 | n 17 88 | o 17 89 | p 17 90 | q 17 91 | r 14 92 | s 17 93 | t 14 94 | u 17 95 | v 17 96 | w 27 97 | x 17 98 | y 17 99 | z 17 100 | { 14 101 | | 6 102 | } 14 103 | ~ 14 104 | £ 19 105 | € 19 106 | ₽ 19 107 |  6 108 |  17 109 |  11 110 | -------------------------------------------------------------------------------- /research/measure_seeds.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import cv2\n", 10 | "import numpy as np\n", 11 | "import matplotlib.pyplot as plt\n", 12 | "from skimage import measure, color\n", 13 | "import pandas as pd\n", 14 | "from plotnine import *\n", 15 | "import scipy.optimize as opt\n" 16 | ] 17 | }, 18 | { 19 | "cell_type": "code", 20 | "execution_count": null, 21 | "metadata": {}, 22 | "outputs": [], 23 | "source": [ 24 | "low_score = 'low_score.mp4'\n", 25 | "high_score = 'high_score.mp4'\n", 26 | "def get_video_stats(filename):\n", 27 | " video=cv2.VideoCapture(filename)\n", 28 | " count=int(video.get(cv2.CAP_PROP_FRAME_COUNT))\n", 29 | " fps = int(video.get(cv2.CAP_PROP_FPS))\n", 30 | " reso = int(video.get(cv2.CAP_PROP_FRAME_WIDTH))\n", 31 | " video.release()\n", 32 | " print(f'frames {count}, fps {fps}, resolution {reso}')\n", 33 | " \n", 34 | "\n", 35 | "get_video_stats(low_score)\n", 36 | "get_video_stats(high_score)" 37 | ] 38 | }, 39 | { 40 | "cell_type": "code", 41 | "execution_count": null, 42 | "metadata": {}, 43 | "outputs": [], 44 | "source": [ 45 | "target_color = np.array([153, 118, 37])\n", 46 | "target_color2 = np.array([209, 165, 55])\n", 47 | "FALL_DIST = 50\n", 48 | "\n", 49 | "def get_frame(filename,index):\n", 50 | " frame = None\n", 51 | " video=cv2.VideoCapture(filename)\n", 52 | " video.set(cv2.CAP_PROP_POS_FRAMES, index)\n", 53 | " res, frame = video.read()\n", 54 | " if res:\n", 55 | " frame = np.flip(frame, -1)\n", 56 | " video.release()\n", 57 | " return frame\n", 58 | "\n", 59 | "def frame_to_pixel_strip(frame, vert_offset=0):\n", 60 | " col_start = 287\n", 61 | " row_start = 84\n", 62 | " px_screen_width = 225\n", 63 | " px_width = 6\n", 64 | "\n", 65 | " if frame.shape[0] == 720:\n", 66 | " col_start = 191\n", 67 | " row_start = 57\n", 68 | " px_width = 4\n", 69 | " row_start = row_start + (vert_offset * px_width)\n", 70 | " bean_table = np.zeros(px_screen_width)\n", 71 | " crop = frame[row_start:row_start+px_width, col_start:col_start+(px_screen_width * px_width)] \n", 72 | " pixel_blocks = np.array(np.hsplit(crop, px_screen_width))\n", 73 | " avg_color = np.round(np.mean(pixel_blocks, axis=(1,2))).astype(int)\n", 74 | " return avg_color, bean_table\n", 75 | "\n", 76 | "def show_pixel_strip(strip, target_i=None):\n", 77 | " if len(target_i):\n", 78 | " strip[target_i-1] = (255, 0, 0)\n", 79 | " strip[target_i+1] = (255, 0, 0)\n", 80 | " img_format = strip.reshape((1, -1, 3))\n", 81 | " plt.figure(figsize = (30,3))\n", 82 | " plt.imshow(img_format) \n", 83 | "\n", 84 | "def count_close_colors(pixels, mode=''):\n", 85 | "\n", 86 | " distances = []\n", 87 | " distances2 = []\n", 88 | " threshold = 60\n", 89 | " if mode == 'deltae':\n", 90 | " newPixels = color.rgb2lab(pixels / 255)\n", 91 | " newTarget = color.rgb2lab(target_color / 255) \n", 92 | " distances = np.sqrt(np.sum((newPixels-newTarget)**2,axis=1)) \n", 93 | " distances2 = np.ones((newPixels.shape[0], 1)) * 100\n", 94 | " threshold = 30\n", 95 | " else:\n", 96 | " distances = np.sqrt(np.sum((pixels-target_color)**2,axis=1))\n", 97 | " distances2 = np.sqrt(np.sum((pixels-target_color2)**2,axis=1))\n", 98 | "\n", 99 | " browns = np.logical_or(distances<=threshold,distances2<=30)\n", 100 | " groups = measure.label(browns)\n", 101 | " group_count = np.max(groups) #np.sum(np.diff(browns, prepend=False, append=False)) // 2\n", 102 | " bean_count = group_count\n", 103 | " bean_locs = []\n", 104 | " for i in range(bean_count):\n", 105 | " bean_locs.append(np.argmax(groups > i))\n", 106 | " return bean_count, np.array(bean_locs)" 107 | ] 108 | }, 109 | { 110 | "cell_type": "code", 111 | "execution_count": null, 112 | "metadata": {}, 113 | "outputs": [], 114 | "source": [ 115 | "\n", 116 | "strip, _ = frame_to_pixel_strip(get_frame(high_score, 6326), vert_offset=0 )\n", 117 | "count, bean_loc = count_close_colors(strip, mode='deltae')\n", 118 | "print(bean_loc)\n", 119 | "show_pixel_strip(strip, bean_loc)\n", 120 | "\n" 121 | ] 122 | }, 123 | { 124 | "cell_type": "code", 125 | "execution_count": null, 126 | "metadata": {}, 127 | "outputs": [], 128 | "source": [ 129 | "def count_beans_by_frame(filename, debug=False):\n", 130 | " frame_num=0\n", 131 | " last_spawned_frame = 0\n", 132 | " counts = []\n", 133 | " speeds = []\n", 134 | " fall_history = []\n", 135 | " video=cv2.VideoCapture(filename)\n", 136 | " while video.isOpened():\n", 137 | " rete,frame=video.read()\n", 138 | " if rete:\n", 139 | " # count the beans here\n", 140 | " frame = np.flip(frame, -1)\n", 141 | " strip, bean_table = frame_to_pixel_strip(frame)\n", 142 | " if len(fall_history) == 0:\n", 143 | " fall_history = bean_table\n", 144 | " count, bean_loc = count_close_colors(strip, mode='deltae')\n", 145 | " # If no beans were found, check lower strip\n", 146 | " if count == 0:\n", 147 | " lower_strip, bean_table = frame_to_pixel_strip(frame, vert_offset=1)\n", 148 | " count, bean_loc = count_close_colors(lower_strip, mode='deltae')\n", 149 | " if count > 0:\n", 150 | " frame_diff = frame_num - last_spawned_frame\n", 151 | " counts.append((frame_num, frame_diff, count))\n", 152 | " last_spawned_frame = frame_num\n", 153 | " if frame_diff > 4:\n", 154 | " fall_history[bean_loc] = frame_num\n", 155 | " if debug:\n", 156 | " print(frame_num, frame_diff, count)\n", 157 | " show_pixel_strip(strip, bean_loc)\n", 158 | " \n", 159 | " # check bottom of screen to measure fall speed\n", 160 | " finish_strip, _ = frame_to_pixel_strip(frame, vert_offset=FALL_DIST) \n", 161 | " finish_count, finish_loc = count_close_colors(finish_strip, mode='deltae')\n", 162 | " if finish_count == 0:\n", 163 | " finish_strip, _ = frame_to_pixel_strip(frame, vert_offset=FALL_DIST+1) \n", 164 | " finish_count, finish_loc = count_close_colors(finish_strip, mode='deltae')\n", 165 | " # if there was a bean in this column earlier\n", 166 | " for loc in finish_loc:\n", 167 | " if fall_history[loc] > 0:\n", 168 | " fall_time = frame_num - fall_history[loc]\n", 169 | " speeds.append((frame_num, fall_time, finish_count))\n", 170 | " fall_history[loc] = 0\n", 171 | " \n", 172 | " frame_num +=1\n", 173 | " else:\n", 174 | " break\n", 175 | " video.release()\n", 176 | " \n", 177 | " return counts, speeds\n", 178 | "\n", 179 | "def make_dataframe(counts, fps, mode='rate'):\n", 180 | " frame, frame_diffs, spawn_count = list(zip(*counts))\n", 181 | " data = pd.DataFrame({'frame':frame, 'interval':frame_diffs, 'count':spawn_count})\n", 182 | " data = data[(data.interval >= 5)]\n", 183 | " \n", 184 | " data['interval_sec'] = data.interval / fps\n", 185 | " data['game_sec'] = data.frame / fps\n", 186 | " data['label'] = ''\n", 187 | " #data.loc[data.spawn_interval_sec >= 1.5, 'label'] = data.frame\n", 188 | " if mode == 'speed':\n", 189 | " data.interval = data.interval / FALL_DIST # convert to frames/pixel\n", 190 | " data['pixels_per_frame'] = 1 / data.interval\n", 191 | " data['pixels_per_sec'] = data.pixels_per_frame * fps\n", 192 | " data = data[(data.pixels_per_sec <= 500) & (data.pixels_per_sec >= 20)]\n", 193 | " data['y'] = data.pixels_per_sec\n", 194 | " else:\n", 195 | " data['y'] = data.interval_sec\n", 196 | " \n", 197 | " data['x'] = data.game_sec\n", 198 | " return data\n", 199 | "\n", 200 | "def encode_stage_by_frame(data, frame_coding):\n", 201 | " data['stage'] = 0\n", 202 | " for stage, frame_num in enumerate(frame_coding):\n", 203 | " data.loc[data.frame >= frame_num, 'stage'] = stage\n" 204 | ] 205 | }, 206 | { 207 | "cell_type": "code", 208 | "execution_count": null, 209 | "metadata": {}, 210 | "outputs": [], 211 | "source": [ 212 | "def logf(x, a, b, c, d):\n", 213 | " #return a / (1. + np.exp(-c * (x - d))) + b\n", 214 | " return a * np.exp(c * (-x + d)) + b\n", 215 | "\n", 216 | "def print_logf(a, b, c, d):\n", 217 | " s = f'f(x) = {a}e^(-{c}x+{d*c}) + {b}'\n", 218 | " print(s)\n", 219 | " return s \n", 220 | "\n", 221 | "def log_fit_curve(data, xs = []):\n", 222 | " if len(xs) == 0:\n", 223 | " xs = np.arange(0, data.x.max())\n", 224 | " popt, pcov = opt.curve_fit(logf, data.x, data.y, method='trf')\n", 225 | " ypred = logf(xs, *popt)\n", 226 | " function_str = print_logf(*np.round(popt,2))\n", 227 | "\n", 228 | " return pd.DataFrame({'x':xs, 'y':ypred}), function_str\n", 229 | "\n", 230 | "def plot_curve(data, model=None, title='', f_str=''):\n", 231 | " plot = (ggplot(data) \n", 232 | " + aes(x='x', y='y', color='factor(stage)')\n", 233 | " + labs(x='Game Time (sec)', y='Spawn Interval (sec)', title=title, color=\"Score (thousands)\")\n", 234 | " + geom_point()\n", 235 | " + geom_text(aes(label='label'), color='black')\n", 236 | " + scale_x_continuous(breaks=np.arange(0, data.x.max(), 10))\n", 237 | " + scale_y_continuous(breaks=np.arange(0, data.y.max(), 5))\n", 238 | " )\n", 239 | "\n", 240 | " if model is not None:\n", 241 | " plot += geom_line(aes(x='x', y='y'), data=model, color='black', size=1.5)\n", 242 | " plot += annotate('text', x=model.x.max()-55, y=model.y.max(), label=f_str)\n", 243 | " return plot" 244 | ] 245 | }, 246 | { 247 | "cell_type": "code", 248 | "execution_count": null, 249 | "metadata": {}, 250 | "outputs": [], 251 | "source": [ 252 | "low_score_counts = count_beans_by_frame(low_score)\n", 253 | "high_score_counts = count_beans_by_frame(high_score)" 254 | ] 255 | }, 256 | { 257 | "cell_type": "code", 258 | "execution_count": null, 259 | "metadata": {}, 260 | "outputs": [], 261 | "source": [ 262 | "def do_analysis(filename, counts, speeds):\n", 263 | " fps = 30\n", 264 | " stage_coding = [0]\n", 265 | " if filename == high_score:\n", 266 | " fps = 60\n", 267 | " stage_coding = [0, 270, 491, 631, 824, 954, 1057, 1252, 1424, 1578, 1655, 1825, 1847]\n", 268 | " \n", 269 | " rate_data = make_dataframe(counts, fps)\n", 270 | " speed_data = make_dataframe(speeds, fps, mode='speed')\n", 271 | "\n", 272 | " encode_stage_by_frame(rate_data, stage_coding)\n", 273 | " encode_stage_by_frame(speed_data, stage_coding)\n", 274 | "\n", 275 | " rate_model, f_str1 = log_fit_curve(rate_data)\n", 276 | " rate_plot = plot_curve(rate_data, rate_model, title=f'Bean Spawn Rate {filename}', f_str=f_str1)\n", 277 | " \n", 278 | " speed_plot = plot_curve(speed_data, title=f'Bean Fall Speed {filename}')\n", 279 | " speed_plot += labs(y='Fall Speed (pixels/sec)')\n", 280 | "\n", 281 | " display(rate_plot)\n", 282 | " display(speed_plot)\n", 283 | " return (rate_data, speed_data), (rate_plot, speed_plot)" 284 | ] 285 | }, 286 | { 287 | "cell_type": "code", 288 | "execution_count": null, 289 | "metadata": {}, 290 | "outputs": [], 291 | "source": [ 292 | "data1, plots = do_analysis(low_score, *low_score_counts)" 293 | ] 294 | }, 295 | { 296 | "cell_type": "code", 297 | "execution_count": null, 298 | "metadata": {}, 299 | "outputs": [], 300 | "source": [ 301 | "data2, plots = do_analysis(high_score, *high_score_counts)" 302 | ] 303 | }, 304 | { 305 | "cell_type": "code", 306 | "execution_count": null, 307 | "metadata": {}, 308 | "outputs": [], 309 | "source": [ 310 | "combined_speed = pd.concat((data1[1], data2[1]))\n", 311 | "combined_plot = plot_curve(combined_speed, title=f'Bean Fall Speed Combined')\n", 312 | "combined_plot += labs(y='Fall Speed (pixels/sec)')\n", 313 | "display(combined_plot)" 314 | ] 315 | }, 316 | { 317 | "cell_type": "code", 318 | "execution_count": null, 319 | "metadata": {}, 320 | "outputs": [], 321 | "source": [ 322 | "for i in range(0, 0):\n", 323 | " group_loc = np.argmax(groups > i)\n", 324 | " if group_loc == 0:\n", 325 | " continue\n", 326 | " # only count this bean if it is the first one seen in this group position \n", 327 | " if beans[group_loc] == 0:\n", 328 | " bean_count += 1 # first spotting of new bean!\n", 329 | " if beans[group_loc] < 3: # This is within the first 3 frames of this beans existenance \n", 330 | " beans[group_loc] += 1\n", 331 | " else: # we have already counted this bean for 3 frames, so clear it\n", 332 | " beans[group_loc] = 0" 333 | ] 334 | } 335 | ], 336 | "metadata": { 337 | "interpreter": { 338 | "hash": "31f2aee4e71d21fbe5cf8b01ff0e069b9275f58929596ceb00d14d90e3e16cd6" 339 | }, 340 | "kernelspec": { 341 | "display_name": "Python 3.8.9 64-bit", 342 | "language": "python", 343 | "name": "python3" 344 | }, 345 | "language_info": { 346 | "codemirror_mode": { 347 | "name": "ipython", 348 | "version": 3 349 | }, 350 | "file_extension": ".py", 351 | "mimetype": "text/x-python", 352 | "name": "python", 353 | "nbconvert_exporter": "python", 354 | "pygments_lexer": "ipython3", 355 | "version": "3.8.8" 356 | }, 357 | "orig_nbformat": 4 358 | }, 359 | "nbformat": 4, 360 | "nbformat_minor": 2 361 | } 362 | -------------------------------------------------------------------------------- /source/lib/AnimatedSprite.lua: -------------------------------------------------------------------------------- 1 | ----------------------------------------------- 2 | --- Sprite class extension with support of --- 3 | --- imagetables and finite state machine, --- 4 | --- with json configuration and autoplay. --- 5 | --- By @Whitebrim git.brim.ml --- 6 | ----------------------------------------------- 7 | 8 | -- You can find examples and docs at https://github.com/Whitebrim/AnimatedSprite/wiki 9 | -- Comments use EmmyLua style 10 | 11 | import 'CoreLibs/object' 12 | import 'CoreLibs/sprites' 13 | local gfx = playdate.graphics 14 | local function emptyFunc()end 15 | 16 | class("AnimatedSprite").extends(gfx.sprite) 17 | 18 | ---@param imagetable table 19 | ---@param states? table If provided, calls `setStates(states)` after initialisation 20 | ---@param animate? boolean If `True`, then the animation of default state will start after initialisation. Default: `False` 21 | function AnimatedSprite.new(imagetable, states, animate) 22 | return AnimatedSprite(imagetable, states, animate) 23 | end 24 | 25 | function AnimatedSprite:init(imagetable, states, animate) 26 | AnimatedSprite.super.init(self) 27 | 28 | ---@type table 29 | self.imagetable = imagetable 30 | assert(self.imagetable, "Imagetable is nil. Check if it was loaded correctly.") 31 | 32 | self:add() 33 | 34 | self.globalFlip = gfx.kImageUnflipped 35 | self.defaultState = "default" 36 | self.states = { 37 | default = { 38 | name = "default", 39 | ---@type integer|string 40 | firstFrameIndex = 1, 41 | framesCount = #self.imagetable, 42 | animationStartingFrame = 1, 43 | tickStep = 1, 44 | frameStep = 1, 45 | reverse = false, 46 | ---@type boolean|integer 47 | loop = true, 48 | yoyo = false, 49 | flip = gfx.kImageUnflipped, 50 | xScale = 1, 51 | yScale = 1, 52 | nextAnimation = nil, 53 | 54 | onFrameChangedEvent = emptyFunc, 55 | onStateChangedEvent = emptyFunc, 56 | onLoopFinishedEvent = emptyFunc, 57 | onAnimationEndEvent = emptyFunc 58 | } 59 | } 60 | 61 | self._enabled = false 62 | self._currentFrame = 0 -- purposely 63 | self._ticks = 1 64 | self._previousTicks = 1 65 | self._loopsFinished = 0 66 | self._currentYoyoDirection = true 67 | 68 | if (states) then 69 | self:setStates(states) 70 | end 71 | 72 | if (animate) then 73 | self:playAnimation() 74 | end 75 | end 76 | 77 | local function drawFrame(self) 78 | local state = self.states[self.currentState] 79 | self:setImage(self._image, state.flip ~ self.globalFlip, state.xScale, state.yScale) 80 | end 81 | 82 | local function setImage(self) 83 | local frames = self.states[self.currentState].frames 84 | if (frames) then 85 | self._image = self.imagetable[frames[self._currentFrame]] 86 | else 87 | self._image = self.imagetable[self._currentFrame] 88 | end 89 | end 90 | 91 | ---Start/resume the animation 92 | ---If `currentState` is nil then `defaultState` will be choosen as current 93 | function AnimatedSprite:playAnimation() 94 | 95 | local state = self.states[self.currentState] 96 | 97 | if (type(self.currentState) == 'nil') then 98 | self.currentState = self.defaultState 99 | state = self.states[self.currentState] 100 | self._currentFrame = state.animationStartingFrame + state.firstFrameIndex - 1 101 | end 102 | 103 | if (self._currentFrame == 0) then 104 | self._currentFrame = state.animationStartingFrame + state.firstFrameIndex - 1 105 | end 106 | 107 | self._enabled = true 108 | self._previousTicks = self._ticks 109 | setImage(self) 110 | drawFrame(self) 111 | if (state.framesCount == 1) then 112 | self._loopsFinished += 1 113 | state.onFrameChangedEvent(self) 114 | state.onLoopFinishedEvent(self) 115 | else 116 | state.onFrameChangedEvent(self) 117 | end 118 | end 119 | 120 | ---Stop the animation without resetting 121 | function AnimatedSprite:pauseAnimation() 122 | self._enabled = false 123 | end 124 | 125 | ---Play/Pause animation based on current state 126 | function AnimatedSprite:toggleAnimation() 127 | if (self._enabled) then 128 | self:pauseAnimation() 129 | else 130 | self:playAnimation() 131 | end 132 | end 133 | 134 | ---Stop and reset the animation 135 | ---After calling `playAnimation` `defaulState` will be played 136 | function AnimatedSprite:stopAnimation() 137 | self:pauseAnimation() 138 | self.currentState = nil 139 | self._currentFrame = 0 -- purposely 140 | self._ticks = 1 141 | self._previousTicks = self._ticks 142 | self._loopsFinished = 0 143 | self._currentYoyoDirection = true 144 | end 145 | 146 | local function addState(self, params) 147 | assert(params.name, "The animation state is unnamed!") 148 | if (self.defaultState == "default") then 149 | self.defaultState = params.name -- Init first added state as default 150 | end 151 | 152 | self.states[params.name] = {} 153 | local state = self.states[params.name] 154 | setmetatable(state, {__index = self.states.default}) 155 | 156 | params = params or {} 157 | 158 | state.name = params.name 159 | if (params.frames ~= nil) then 160 | state["frames"] = params.frames -- Custom animation for non-sequential frames from the imagetable 161 | params.framesCount = params.framesCount or #params.frames 162 | if (type(params.firstFrameIndex) ~= "string") then 163 | params.firstFrameIndex = params.firstFrameIndex or 1 164 | end 165 | end 166 | if (type(params.firstFrameIndex) == "string") then 167 | local thatState = self.states[params.firstFrameIndex] 168 | state["firstFrameIndex"] = thatState.firstFrameIndex + thatState.framesCount 169 | else 170 | state["firstFrameIndex"] = params.firstFrameIndex -- index in the imagetable for the firstFrame 171 | end 172 | state["framesCount"] = params.framesCount and params.framesCount or (self.states.default.framesCount - state.firstFrameIndex + 1) -- This state frames count 173 | state["nextAnimation"] = params.nextAnimation -- Animation to switch to after this finishes 174 | if (params.nextAnimation == nil) then 175 | state["loop"] = params.loop -- You can put in number of loops or true for endless loop 176 | else 177 | state["loop"] = params.loop or false 178 | end 179 | state["reverse"] = params.reverse -- You can reverse animation sequence 180 | state["animationStartingFrame"] = params.animationStartingFrame or (state.reverse and state.framesCount or 1) -- Frame to start the animation from 181 | state["tickStep"] = params.tickStep -- Speed of animation (2 = every second frame) 182 | state["frameStep"] = params.frameStep -- Number of images to skip on next frame 183 | state["yoyo"] = params.yoyo -- Ping-pong animation (from 1 to n to 1 to n) 184 | state["flip"] = params.flip -- You can set up flip mode, read Playdate SDK Docs for more info 185 | state["xScale"] = params.xScale -- Optional scale for horizontal axis 186 | state["yScale"] = params.yScale -- Optional scale for vertical axis 187 | 188 | state["onFrameChangedEvent"] = params.onFrameChangedEvent -- Event that will be raised when animation moves to the next frame 189 | state["onStateChangedEvent"] = params.onStateChangedEvent -- Event that will be raised when animation state changes 190 | state["onLoopFinishedEvent"] = params.onLoopFinishedEvent -- Event that will be raised when animation changes to the final frame 191 | state["onAnimationEndEvent"] = params.onAnimationEndEvent -- Event that will be raised after animation in this state ends 192 | 193 | return state 194 | end 195 | 196 | ---Parse `json` file with animation configuration 197 | ---@param path string Path to the file 198 | ---@return table config You can use it in `setStates(states)` 199 | function AnimatedSprite.loadStates(path) 200 | return assert(json.decodeFile(path), "Requested JSON parse failed. Path: " .. path) 201 | end 202 | 203 | ---Returns imagetable frame index that is currently displayed 204 | ---@return integer Current frame index 205 | function AnimatedSprite:getCurrentFrameIndex() 206 | if (self.currentState and self.states[self.currentState].frames) then 207 | return self.states[self.currentState].frames[self._currentFrame] 208 | else 209 | return self._currentFrame 210 | end 211 | end 212 | 213 | ---Returns reference to the current states 214 | ---@return table states Reference to the current states 215 | function AnimatedSprite:getLocalStates() 216 | return self.states 217 | end 218 | 219 | ---Copies states 220 | ---@return table states Deepcopy of the current states 221 | function AnimatedSprite:copyLocalStates() 222 | return table.deepcopy(self.states) 223 | end 224 | 225 | ---All states from the `states` will be added to the current state machine (overwrites values in case of conflict) 226 | ---@param states table State machine state list, you can get one by calling `loadStates` 227 | ---@param animate? boolean If `True`, then the animation of default/current state will start immediately after. Default: `False` 228 | ---@param defaultState? string If provided, changes default state 229 | function AnimatedSprite:setStates(states, animate, defaultState) 230 | local statesCount = #states 231 | 232 | local function proceedState(state) 233 | if (state.name ~= "default") then 234 | addState(self, state) 235 | else 236 | local default = self.states.default 237 | for key, value in pairs(state) do 238 | default[key] = value 239 | end 240 | end 241 | end 242 | 243 | if (statesCount == 0) then 244 | proceedState(states) 245 | if (defaultState) then 246 | self.defaultState = defaultState 247 | end 248 | if (animate) then 249 | self:playAnimation() 250 | end 251 | return 252 | end 253 | 254 | for i = 1, statesCount do 255 | proceedState(states[i]) 256 | end 257 | if (defaultState) then 258 | self.defaultState = defaultState 259 | end 260 | if (animate) then 261 | self:playAnimation() 262 | end 263 | end 264 | 265 | ---You can add new states to the state machine using this function 266 | ---@param name string Name of the state, should be unique, used as id 267 | ---@param startFrame? integer Index of the first frame in the imagetable (starts from 1). Default: `1` (from states.default) 268 | ---@param endFrame? integer Index of the last frame in the imagetable. Default: last frame (from states.default) 269 | ---@param params? table See examples 270 | ---@param animate? boolean If `True`, then the animation of this state will start immediately after. Default: `False` 271 | function AnimatedSprite:addState(name, startFrame, endFrame, params, animate) 272 | params = params or {} 273 | params.firstFrameIndex = startFrame or 1 274 | params.framesCount = endFrame and (endFrame - params.firstFrameIndex + 1) or nil 275 | params.name = name 276 | 277 | addState(self, params) 278 | 279 | if (animate) then 280 | self.currentState = name 281 | self:playAnimation() 282 | end 283 | 284 | return { 285 | asDefault = function () 286 | self.defaultState = name 287 | end 288 | } 289 | end 290 | 291 | ---Changes current state to an existing state 292 | ---@param name string New state name 293 | ---@param play? boolean If new animation should be played right away. Default: `True` 294 | function AnimatedSprite:changeState(name, play) 295 | if (name == self.currentState) then 296 | return 297 | end 298 | local play = type(play) == "nil" and true or play 299 | local state = self.states[name] 300 | assert (state, "There's no state named \""..name.."\".") 301 | self.currentState = name 302 | self._currentFrame = 0 -- purposely 303 | self._loopsFinished = 0 304 | self._currentYoyoDirection = true 305 | state.onStateChangedEvent(self) 306 | if (play) then 307 | self:playAnimation() 308 | end 309 | end 310 | 311 | ---Force to move animation state machine to the next state 312 | ---@param instant? boolean If `False` change will be performed after the final frame of this loop iteration. Default: `True` 313 | ---@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. 314 | function AnimatedSprite:forceNextAnimation(instant, state) 315 | local instant = type(instant) == "nil" and true or instant 316 | local currentState = self.states[self.currentState] 317 | self.forcedState = state 318 | 319 | if (instant) then 320 | self.forcedSwitchOnLoop = nil 321 | currentState.onAnimationEndEvent(self) 322 | if (currentState.name == self.currentState) then -- If state was not changed during the event then proceed 323 | if (type(self.forcedState) == "string") then 324 | self:changeState(self.forcedState) 325 | self.forcedState = nil 326 | elseif (currentState.nextAnimation) then 327 | self:changeState(currentState.nextAnimation) 328 | else 329 | self:stopAnimation() 330 | end 331 | end 332 | else 333 | self.forcedSwitchOnLoop = self._loopsFinished + 1 334 | end 335 | end 336 | 337 | ---Sets default state. 338 | ---@param name string Name of an existing state 339 | function AnimatedSprite:setDefaultState(name) 340 | assert (self.states[name], "State name is nil.") 341 | self.defaultState = name 342 | end 343 | 344 | ---Print all states from this state machine table to the console 345 | function AnimatedSprite:printAllStates() 346 | printTable(self.states) 347 | end 348 | 349 | ---Function that will procees the animation to the next step without redrawing sprite 350 | local function processAnimation(self) 351 | local state = self.states[self.currentState] 352 | 353 | local function changeFrame(value) 354 | value += state.firstFrameIndex 355 | self._currentFrame = value 356 | state.onFrameChangedEvent(self) 357 | end 358 | 359 | local reverse = state.reverse 360 | local frame = self._currentFrame - state.firstFrameIndex 361 | local framesCount = state.framesCount 362 | local frameStep = state.frameStep 363 | 364 | if (self._currentFrame == 0) then -- true only after changing state 365 | self._currentFrame = state.animationStartingFrame + state.firstFrameIndex - 1 366 | if (framesCount == 1) then 367 | self._loopsFinished += 1 368 | state.onFrameChangedEvent(self) 369 | state.onLoopFinishedEvent(self) 370 | return 371 | else 372 | state.onFrameChangedEvent(self) 373 | end 374 | setImage(self) 375 | return 376 | end 377 | 378 | if (framesCount == 1) then -- if this state is only 1 frame long 379 | self._loopsFinished += 1 380 | state.onFrameChangedEvent(self) 381 | state.onLoopFinishedEvent(self) 382 | return 383 | end 384 | 385 | if (state.yoyo) then 386 | if (reverse ~= self._currentYoyoDirection) then 387 | if (frame + frameStep + 1 < framesCount) then 388 | changeFrame(frame + frameStep) 389 | else 390 | if (frame ~= framesCount - 1) then 391 | self._loopsFinished += 1 392 | changeFrame(2 * framesCount - frame - frameStep - 2) 393 | state.onLoopFinishedEvent(self) 394 | else 395 | changeFrame(2 * framesCount - frame - frameStep - 2) 396 | end 397 | self._currentYoyoDirection = not self._currentYoyoDirection 398 | end 399 | else 400 | if (frame - frameStep > 0) then 401 | changeFrame(frame - frameStep) 402 | else 403 | if (frame ~= 0) then 404 | self._loopsFinished += 1 405 | changeFrame(frameStep - frame) 406 | state.onLoopFinishedEvent(self) 407 | else 408 | changeFrame(frameStep - frame) 409 | end 410 | self._currentYoyoDirection = not self._currentYoyoDirection 411 | end 412 | end 413 | else 414 | if (reverse) then 415 | if (frame - frameStep > 0) then 416 | changeFrame(frame - frameStep) 417 | else 418 | if (frame ~= 0) then 419 | self._loopsFinished += 1 420 | changeFrame((frame - frameStep) % framesCount) 421 | state.onLoopFinishedEvent(self) 422 | else 423 | changeFrame((frame - frameStep) % framesCount) 424 | end 425 | end 426 | else 427 | if (frame + frameStep + 1 < framesCount) then 428 | changeFrame(frame + frameStep) 429 | else 430 | if (frame ~= framesCount - 1) then 431 | self._loopsFinished += 1 432 | changeFrame((frame + frameStep) % framesCount) 433 | state.onLoopFinishedEvent(self) 434 | else 435 | changeFrame((frame + frameStep) % framesCount) 436 | end 437 | end 438 | end 439 | end 440 | 441 | setImage(self) 442 | end 443 | 444 | ---Called by default in the `:update()` function. 445 | ---Must be called once per frame if you overwrite `:update()`. 446 | ---Invoke manually to move the animation to the next frame. 447 | function AnimatedSprite:updateAnimation() 448 | if (self._enabled) then 449 | self._ticks += 1 450 | if ((self._ticks - self._previousTicks) >= self.states[self.currentState].tickStep) then 451 | local state = self.states[self.currentState] 452 | local loop = state.loop 453 | local loopsFinished = self._loopsFinished 454 | if (type(loop) == "number" and loop <= loopsFinished or 455 | type(loop) == "boolean" and not loop and loopsFinished >= 1 or 456 | self.forcedSwitchOnLoop == loopsFinished) then 457 | self:forceNextAnimation(true) 458 | return 459 | end 460 | processAnimation(self) 461 | drawFrame(self) 462 | self._previousTicks += state.tickStep 463 | end 464 | end 465 | end 466 | 467 | function AnimatedSprite:update() 468 | self:updateAnimation() 469 | end --------------------------------------------------------------------------------