From 98d754c9888942920ecd83fb27b4796fc484be4b Mon Sep 17 00:00:00 2001 From: DebrisHauler Date: Sun, 22 Sep 2024 22:49:34 -0400 Subject: [PATCH] Squashed commit of the following: commit 66951160a864c0d227481a8cd1510706da81197f Author: gelakinetic Date: Sun Sep 22 14:15:53 2024 -0400 Sokobokobanaban first merge (#296) commit 0e137f6e3ef38f56e40610d76236b44a42083cd9 Author: JVeg199X <97848253+JVeg199X@users.noreply.github.com> Date: Sun Sep 22 07:29:50 2024 -0400 Pango (initial pull request) (#295) commit 1da56447cbce8fb99526c503bf4b3cf1ea7130b3 Author: Dylan Whichard Date: Sun Sep 22 03:58:48 2024 -0700 MIDI Registered/Non-registered Parameter Support, default instrument changes (#293) Add documentation too commit 576902abebc8cf24aea0ce87cf8314de5a80a0aa Author: Dylan Whichard Date: Sun Sep 22 03:52:55 2024 -0700 Add FreeDesktop config files for native support in graphical desktop environments (#294) commit 2298b2b23b77155dced96eb6c11739c45c844823 Author: johnnywycliffe Date: Sun Sep 22 06:43:26 2024 -0400 Palette wsg (#292) Add ability to palette-swap WSGs when drawing commit e6fde30f918137f0334c5430a4d2abcabdbf89c9 Author: Dylan Whichard Date: Sat Sep 21 01:15:52 2024 -0700 Fix broken markdown (#291) commit 8e3ef72bdc9432d09ff0fc3df873c31cfd2023a8 Author: Dylan Whichard Date: Fri Sep 20 17:17:22 2024 -0700 Add MIDI technical documentation (#289) commit 674cb11eddeb1e7add9bab570da5b5c2ac91a3eb Author: cnlohr Date: Thu Sep 19 15:48:56 2024 -0700 Update demo code for usb hid demo (#287) commit 69615ad90c758d4de1755edcbc83568ec72a7a8c Author: gelakinetic Date: Thu Sep 19 06:29:38 2024 -0400 Add pinball to the attic But get a few nice things along the way commit 9c9db123829aefbc2bbc4a7b99046684a7d922bd Author: johnnywycliffe Date: Thu Sep 19 06:00:36 2024 -0400 Add 2048 to main (#285) commit 1a16767aef54ebe300a16de35a9ba7d75ecbf410 Author: Dylan Whichard Date: Wed Sep 18 02:41:54 2024 -0700 Emulator MIDI File Opening (#283) Emulator can now play MIDI files from a computer directly * Add emulator utility to inject fake NVS data * Add emulator utility to inject fake CNFS data * Add --midi-file argument to emulator * Add MIDI file docs to EMULATOR.md --- .../workflows/build-firmware-and-emulator.yml | 10 + .gitignore | 2 + .vscode/tasks.json | 4 +- Doxyfile | 1 + assets/2048/Sounds/sndBounce.mid | Bin 0 -> 152 bytes assets/2048/pngs/NewTiles/New_Dot.png | Bin 0 -> 1014 bytes assets/2048/pngs/NewTiles/New_Med_Star.png | Bin 0 -> 1102 bytes assets/2048/pngs/NewTiles/New_Small_Star.png | Bin 0 -> 1046 bytes assets/2048/pngs/Sparkles/Sparkle_Blue.png | Bin 0 -> 1008 bytes assets/2048/pngs/Sparkles/Sparkle_Cyan.png | Bin 0 -> 1008 bytes assets/2048/pngs/Sparkles/Sparkle_Green.png | Bin 0 -> 1008 bytes assets/2048/pngs/Sparkles/Sparkle_Orange.png | Bin 0 -> 1008 bytes assets/2048/pngs/Sparkles/Sparkle_Pink.png | Bin 0 -> 1008 bytes assets/2048/pngs/Sparkles/Sparkle_Purple.png | Bin 0 -> 1008 bytes assets/2048/pngs/Sparkles/Sparkle_Red.png | Bin 0 -> 1008 bytes assets/2048/pngs/Sparkles/Sparkle_Yellow.png | Bin 0 -> 1008 bytes assets/2048/pngs/Tiles/Tile-Blue-Diamond.png | Bin 0 -> 1201 bytes assets/2048/pngs/Tiles/Tile-Blue-Square.png | Bin 0 -> 1093 bytes assets/2048/pngs/Tiles/Tile-Cyan-Legs.png | Bin 0 -> 1126 bytes assets/2048/pngs/Tiles/Tile-Green-Diamond.png | Bin 0 -> 1205 bytes assets/2048/pngs/Tiles/Tile-Green-Octo.png | Bin 0 -> 1129 bytes assets/2048/pngs/Tiles/Tile-Green-Square.png | Bin 0 -> 1093 bytes assets/2048/pngs/Tiles/Tile-Mauve-Legs.png | Bin 0 -> 1126 bytes assets/2048/pngs/Tiles/Tile-Orange-Legs.png | Bin 0 -> 1126 bytes assets/2048/pngs/Tiles/Tile-Pink-Diamond.png | Bin 0 -> 1202 bytes assets/2048/pngs/Tiles/Tile-Pink-Octo.png | Bin 0 -> 1128 bytes assets/2048/pngs/Tiles/Tile-Pink-Square.png | Bin 0 -> 1092 bytes assets/2048/pngs/Tiles/Tile-Purple-Legs.png | Bin 0 -> 1127 bytes assets/2048/pngs/Tiles/Tile-Red-Octo.png | Bin 0 -> 1127 bytes assets/2048/pngs/Tiles/Tile-Red-Square.png | Bin 0 -> 1092 bytes .../2048/pngs/Tiles/Tile-Yellow-Diamond.png | Bin 0 -> 1204 bytes assets/2048/pngs/Tiles/Tile-Yellow-Octo.png | Bin 0 -> 1128 bytes assets/fonts/pango-fw.font.png | Bin 0 -> 1294 bytes assets/pango/levels/mockup.bin | Bin 0 -> 274 bytes assets/pango/levels/preset.bin | Bin 0 -> 274 bytes assets/pango/sounds/bgmCastle.mid | Bin 0 -> 2217 bytes assets/pango/sounds/bgmDeMAGio.mid | Bin 0 -> 2217 bytes assets/pango/sounds/bgmGameOver.mid | Bin 0 -> 209 bytes assets/pango/sounds/bgmGameStart.mid | Bin 0 -> 200 bytes assets/pango/sounds/bgmIntro.mid | Bin 0 -> 233 bytes assets/pango/sounds/bgmNameEntry.mid | Bin 0 -> 648 bytes assets/pango/sounds/bgmSmooth.mid | Bin 0 -> 2889 bytes assets/pango/sounds/bgmUnderground.mid | Bin 0 -> 4569 bytes assets/pango/sounds/snd1up.mid | Bin 0 -> 152 bytes assets/pango/sounds/sndBlockStop.mid | Bin 0 -> 168 bytes assets/pango/sounds/sndCheckpoint.mid | Bin 0 -> 144 bytes assets/pango/sounds/sndCoin.mid | Bin 0 -> 144 bytes assets/pango/sounds/sndDie.mid | Bin 0 -> 184 bytes assets/pango/sounds/sndHit.mid | Bin 0 -> 144 bytes assets/pango/sounds/sndHurt.mid | Bin 0 -> 176 bytes assets/pango/sounds/sndJump1.mid | Bin 0 -> 144 bytes assets/pango/sounds/sndJump2.mid | Bin 0 -> 144 bytes assets/pango/sounds/sndJump3.mid | Bin 0 -> 144 bytes assets/pango/sounds/sndLevelClearA.mid | Bin 0 -> 209 bytes assets/pango/sounds/sndLevelClearB.mid | Bin 0 -> 185 bytes assets/pango/sounds/sndLevelClearC.mid | Bin 0 -> 161 bytes assets/pango/sounds/sndLevelClearD.mid | Bin 0 -> 161 bytes assets/pango/sounds/sndLevelClearS.mid | Bin 0 -> 241 bytes assets/pango/sounds/sndMenuConfirm.mid | Bin 0 -> 144 bytes assets/pango/sounds/sndMenuDeny.mid | Bin 0 -> 144 bytes assets/pango/sounds/sndMenuSelect.mid | Bin 0 -> 144 bytes assets/pango/sounds/sndOutOfTime.mid | Bin 0 -> 248 bytes assets/pango/sounds/sndPause.mid | Bin 0 -> 160 bytes assets/pango/sounds/sndPowerUp.mid | Bin 0 -> 184 bytes assets/pango/sounds/sndSlide.mid | Bin 0 -> 176 bytes assets/pango/sounds/sndSpawn.mid | Bin 0 -> 176 bytes assets/pango/sounds/sndSquish.mid | Bin 0 -> 152 bytes assets/pango/sounds/sndWarp.mid | Bin 0 -> 200 bytes assets/pango/sounds/sndWaveBall.mid | Bin 0 -> 160 bytes assets/pango/sprites/blockfragment.png | Bin 0 -> 998 bytes assets/pango/sprites/break-000.png | Bin 0 -> 1019 bytes assets/pango/sprites/break-001.png | Bin 0 -> 1028 bytes assets/pango/sprites/break-002.png | Bin 0 -> 1034 bytes assets/pango/sprites/break-003.png | Bin 0 -> 1034 bytes assets/pango/sprites/kr-000.png | Bin 0 -> 1079 bytes assets/pango/sprites/kr-001.png | Bin 0 -> 1077 bytes assets/pango/sprites/kr-002.png | Bin 0 -> 1066 bytes assets/pango/sprites/kr-003.png | Bin 0 -> 1061 bytes assets/pango/sprites/kr-004.png | Bin 0 -> 1058 bytes assets/pango/sprites/kr-005.png | Bin 0 -> 1071 bytes assets/pango/sprites/kr-006.png | Bin 0 -> 1061 bytes assets/pango/sprites/kr-007.png | Bin 0 -> 1080 bytes assets/pango/sprites/kr-008.png | Bin 0 -> 1085 bytes assets/pango/sprites/kr-009.png | Bin 0 -> 1065 bytes assets/pango/sprites/kr-010.png | Bin 0 -> 1079 bytes assets/pango/sprites/kr-011.png | Bin 0 -> 1065 bytes assets/pango/sprites/kr-012.png | Bin 0 -> 1066 bytes assets/pango/sprites/kr-013.png | Bin 0 -> 1087 bytes assets/pango/sprites/kr-014.png | Bin 0 -> 1070 bytes assets/pango/sprites/kr-015.png | Bin 0 -> 1042 bytes assets/pango/sprites/pa-000.png | Bin 0 -> 1081 bytes assets/pango/sprites/pa-001.png | Bin 0 -> 1079 bytes assets/pango/sprites/pa-002.png | Bin 0 -> 1084 bytes assets/pango/sprites/pa-003.png | Bin 0 -> 1082 bytes assets/pango/sprites/pa-004.png | Bin 0 -> 1067 bytes assets/pango/sprites/pa-005.png | Bin 0 -> 1075 bytes assets/pango/sprites/pa-006.png | Bin 0 -> 1071 bytes assets/pango/sprites/pa-007.png | Bin 0 -> 1079 bytes assets/pango/sprites/pa-008.png | Bin 0 -> 1077 bytes assets/pango/sprites/pa-009.png | Bin 0 -> 1091 bytes assets/pango/sprites/pa-010.png | Bin 0 -> 1084 bytes assets/pango/sprites/pa-011.png | Bin 0 -> 1081 bytes assets/pango/sprites/pa-012.png | Bin 0 -> 1070 bytes assets/pango/sprites/pa-013.png | Bin 0 -> 1083 bytes assets/pango/sprites/pa-014.png | Bin 0 -> 1082 bytes assets/pango/sprites/pa-015.png | Bin 0 -> 1047 bytes assets/pango/sprites/pa-100.png | Bin 0 -> 314 bytes assets/pango/sprites/pa-101.png | Bin 0 -> 323 bytes assets/pango/sprites/pa-102.png | Bin 0 -> 308 bytes assets/pango/sprites/pa-103.png | Bin 0 -> 324 bytes assets/pango/sprites/pa-104.png | Bin 0 -> 290 bytes assets/pango/sprites/pa-105.png | Bin 0 -> 310 bytes assets/pango/sprites/pa-106.png | Bin 0 -> 305 bytes assets/pango/sprites/pa-107.png | Bin 0 -> 338 bytes assets/pango/sprites/pa-108.png | Bin 0 -> 324 bytes assets/pango/sprites/pa-109.png | Bin 0 -> 331 bytes assets/pango/sprites/pa-110.png | Bin 0 -> 310 bytes assets/pango/sprites/pa-111.png | Bin 0 -> 322 bytes assets/pango/sprites/pa-112.png | Bin 0 -> 318 bytes assets/pango/sprites/pa-113.png | Bin 0 -> 325 bytes assets/pango/sprites/pa-114.png | Bin 0 -> 319 bytes assets/pango/sprites/pa-en-000.png | Bin 0 -> 1077 bytes assets/pango/sprites/pa-en-001.png | Bin 0 -> 1081 bytes assets/pango/sprites/pa-en-002.png | Bin 0 -> 1079 bytes assets/pango/sprites/pa-en-003.png | Bin 0 -> 1077 bytes assets/pango/sprites/pa-en-004.png | Bin 0 -> 1073 bytes assets/pango/sprites/pa-en-005.png | Bin 0 -> 1070 bytes assets/pango/sprites/pa-en-006.png | Bin 0 -> 1069 bytes assets/pango/sprites/pa-en-007.png | Bin 0 -> 1073 bytes assets/pango/sprites/pa-en-008.png | Bin 0 -> 1062 bytes assets/pango/sprites/po-000.png | Bin 0 -> 349 bytes assets/pango/sprites/po-001.png | Bin 0 -> 359 bytes assets/pango/sprites/po-002.png | Bin 0 -> 283 bytes assets/pango/sprites/po-003.png | Bin 0 -> 300 bytes assets/pango/sprites/po-004.png | Bin 0 -> 317 bytes assets/pango/sprites/po-005.png | Bin 0 -> 343 bytes assets/pango/sprites/po-006.png | Bin 0 -> 318 bytes assets/pango/sprites/po-007.png | Bin 0 -> 361 bytes assets/pango/sprites/po-008.png | Bin 0 -> 350 bytes assets/pango/sprites/po-009.png | Bin 0 -> 279 bytes assets/pango/sprites/po-010.png | Bin 0 -> 295 bytes assets/pango/sprites/po-011.png | Bin 0 -> 358 bytes assets/pango/sprites/po-012.png | Bin 0 -> 338 bytes assets/pango/sprites/po-013.png | Bin 0 -> 353 bytes assets/pango/sprites/po-014.png | Bin 0 -> 318 bytes assets/pango/sprites/px-000.png | Bin 0 -> 287 bytes assets/pango/sprites/px-001.png | Bin 0 -> 288 bytes assets/pango/sprites/px-002.png | Bin 0 -> 303 bytes assets/pango/sprites/px-003.png | Bin 0 -> 292 bytes assets/pango/sprites/px-004.png | Bin 0 -> 291 bytes assets/pango/sprites/px-005.png | Bin 0 -> 316 bytes assets/pango/sprites/px-006.png | Bin 0 -> 304 bytes assets/pango/sprites/px-007.png | Bin 0 -> 341 bytes assets/pango/sprites/px-008.png | Bin 0 -> 321 bytes assets/pango/sprites/px-009.png | Bin 0 -> 303 bytes assets/pango/sprites/px-010.png | Bin 0 -> 293 bytes assets/pango/sprites/px-011.png | Bin 0 -> 317 bytes assets/pango/sprites/px-012.png | Bin 0 -> 327 bytes assets/pango/sprites/px-013.png | Bin 0 -> 331 bytes assets/pango/sprites/px-014.png | Bin 0 -> 308 bytes assets/pango/tiles/.DS_Store | Bin 0 -> 6148 bytes assets/pango/tiles/pa-tile-000.png | Bin 0 -> 975 bytes assets/pango/tiles/pa-tile-001.png | Bin 0 -> 1005 bytes assets/pango/tiles/pa-tile-002.png | Bin 0 -> 992 bytes assets/pango/tiles/pa-tile-003.png | Bin 0 -> 1009 bytes assets/pango/tiles/pa-tile-004.png | Bin 0 -> 984 bytes assets/pango/tiles/pa-tile-005.png | Bin 0 -> 1011 bytes assets/pango/tiles/pa-tile-006.png | Bin 0 -> 1016 bytes assets/pango/tiles/pa-tile-007.png | Bin 0 -> 992 bytes assets/pango/tiles/pa-tile-008.png | Bin 0 -> 985 bytes assets/pango/tiles/pa-tile-009.png | Bin 0 -> 1022 bytes assets/pango/tiles/pa-tile-010.png | Bin 0 -> 1021 bytes assets/pango/tiles/pa-tile-011.png | Bin 0 -> 1022 bytes assets/pango/tiles/pa-tile-012.png | Bin 0 -> 1023 bytes assets/pango/tiles/pa-tile-013.png | Bin 0 -> 1030 bytes assets/pango/tiles/pa-tile-014.png | Bin 0 -> 1031 bytes assets/pango/tiles/pa-tile-015.png | Bin 0 -> 1031 bytes assets/soko/levels/SK_LEVEL_LIST.txt | 22 + assets/soko/levels/classic/sck_c_threes.tmx | 26 + assets/soko/levels/classic/sk_c_alignthat.tmx | 27 + assets/soko/levels/classic/sk_c_cosms.tmx | 30 + assets/soko/levels/classic/sk_c_files.tmx | 23 + assets/soko/levels/classic/sk_c_fours.tmx | 27 + assets/soko/levels/classic/sk_c_plus.tmx | 23 + assets/soko/levels/classic/sk_c_test2.tmx | 29 + assets/soko/levels/euler/sk_e_a-frame.tmx | 25 + assets/soko/levels/euler/sk_e_alkatraz.tmx | 38 + assets/soko/levels/euler/sk_e_apollo.tmx | 34 + assets/soko/levels/euler/sk_e_camera.tmx | 25 + assets/soko/levels/euler/sk_e_casette.tmx | 34 + assets/soko/levels/euler/sk_e_copymachine.tmx | 50 + assets/soko/levels/euler/sk_e_curlingiron.tmx | 26 + assets/soko/levels/euler/sk_e_doubleblock.tmx | 25 + assets/soko/levels/euler/sk_e_feint.tmx | 23 + assets/soko/levels/euler/sk_e_groundwork.tmx | 23 + assets/soko/levels/euler/sk_e_harmonica.tmx | 25 + assets/soko/levels/euler/sk_e_mouse.tmx | 22 + assets/soko/levels/euler/sk_e_spine.tmx | 28 + assets/soko/levels/euler/sk_e_spiral.tmx | 23 + assets/soko/levels/euler/sk_e_start.tmx | 23 + .../soko/levels/euler/sk_e_steeringwheel.tmx | 21 + assets/soko/levels/euler/sk_e_threestep.tmx | 42 + assets/soko/levels/euler/sk_e_threestep2.tmx | 38 + assets/soko/levels/euler/sk_e_throughput.tmx | 34 + assets/soko/levels/euler/sk_e_tunnels.tmx | 23 + assets/soko/levels/euler/sk_e_waterwheel.tmx | 69 + assets/soko/levels/sk_e_overworld.tmx | 154 ++ assets/soko/levels/sk_overworld.tmx | 134 ++ assets/soko/sprites/pango/sk_pango_back1.png | Bin 0 -> 183 bytes assets/soko/sprites/pango/sk_pango_back2.png | Bin 0 -> 181 bytes assets/soko/sprites/pango/sk_pango_fwd1.png | Bin 0 -> 207 bytes assets/soko/sprites/pango/sk_pango_fwd2.png | Bin 0 -> 208 bytes assets/soko/sprites/pango/sk_pango_side1.png | Bin 0 -> 211 bytes assets/soko/sprites/pango/sk_pango_side2.png | Bin 0 -> 284 bytes assets/soko/sprites/pixel/sk_pixel_back.png | Bin 0 -> 257 bytes assets/soko/sprites/pixel/sk_pixel_front.png | Bin 0 -> 297 bytes assets/soko/sprites/pixel/sk_pixel_left.png | Bin 0 -> 276 bytes assets/soko/sprites/pixel/sk_pixel_right.png | Bin 0 -> 370 bytes assets/soko/sprites/sk_crate.png | Bin 0 -> 1476 bytes assets/soko/sprites/sk_crate_2.png | Bin 0 -> 1448 bytes assets/soko/sprites/sk_crate_ongoal.png | Bin 0 -> 218 bytes assets/soko/sprites/sk_dog.png | Bin 0 -> 1515 bytes assets/soko/sprites/sk_e_crate.png | Bin 0 -> 183 bytes assets/soko/sprites/sk_goal.png | Bin 0 -> 421 bytes assets/soko/sprites/sk_grass.png | Bin 0 -> 1776 bytes assets/soko/sprites/sk_portal_complete.png | Bin 0 -> 141 bytes assets/soko/sprites/sk_portal_incomplete.png | Bin 0 -> 143 bytes assets/soko/sprites/sk_ring.png | Bin 0 -> 1529 bytes assets/soko/sprites/sk_sticky_crate.png | Bin 0 -> 268 bytes assets/soko/sprites/sk_sticky_trail_crate.png | Bin 0 -> 248 bytes .../games => attic}/pinball/mode_pinball.c | 97 +- attic/pinball/mode_pinball.h | 28 + attic/pinball/pinball_circle.c | 80 + attic/pinball/pinball_circle.h | 10 + attic/pinball/pinball_draw.c | 153 ++ attic/pinball/pinball_draw.h | 7 + attic/pinball/pinball_flipper.c | 106 + attic/pinball/pinball_flipper.h | 9 + attic/pinball/pinball_game.c | 500 +++++ attic/pinball/pinball_game.h | 19 + attic/pinball/pinball_line.c | 140 ++ attic/pinball/pinball_line.h | 8 + attic/pinball/pinball_physics.c | 461 +++++ attic/pinball/pinball_physics.h | 6 + attic/pinball/pinball_point.c | 25 + attic/pinball/pinball_point.h | 6 + attic/pinball/pinball_rectangle.c | 65 + attic/pinball/pinball_rectangle.h | 9 + attic/pinball/pinball_triangle.c | 45 + attic/pinball/pinball_triangle.h | 7 + attic/pinball/pinball_typedef.h | 165 ++ docs/EMULATOR.md | 22 +- docs/MIDI.md | 102 + docs/soko/readme.md | 43 + docs/soko/soko_levels.md | 96 + emulator/resources/SwadgeEmulator.desktop | 11 + emulator/resources/install.sh | 57 + emulator/src/components/hdw-nvs/hdw-nvs.c | 170 ++ emulator/src/components/hdw-nvs/hdw-nvs_emu.h | 7 + emulator/src/emu_cnfs.c | 161 ++ emulator/src/emu_cnfs.h | 7 + emulator/src/extensions/emu_args.c | 40 +- emulator/src/extensions/emu_args.h | 3 + emulator/src/extensions/emu_ext.c | 3 +- emulator/src/extensions/midi/ext_midi.c | 59 + emulator/src/extensions/midi/ext_midi.h | 10 + emulator/src/extensions/modes/ext_modes.c | 10 +- emulator/src/idf/midi_device.c | 5 + main/CMakeLists.txt | 44 +- main/display/wsg.c | 14 +- main/display/wsg.h | 7 +- main/display/wsgPalette.c | 404 ++++ main/display/wsgPalette.h | 91 + main/menu/menuManiaRenderer.c | 4 +- main/midi/midiFileParser.c | 54 +- main/midi/midiPlayer.c | 493 ++++- main/midi/midiPlayer.h | 39 + main/modes/games/2048/2048_game.c | 1017 ++++++++++ main/modes/games/2048/2048_game.h | 18 + main/modes/games/2048/2048_menus.c | 159 ++ main/modes/games/2048/2048_menus.h | 18 + main/modes/games/2048/mode_2048.c | 498 +++++ main/modes/games/2048/mode_2048.h | 147 ++ main/modes/games/pango/paEntity.c | 1547 +++++++++++++++ main/modes/games/pango/paEntity.h | 191 ++ main/modes/games/pango/paEntityManager.c | 439 ++++ main/modes/games/pango/paEntityManager.h | 68 + main/modes/games/pango/paGameData.c | 282 +++ main/modes/games/pango/paGameData.h | 86 + main/modes/games/pango/paLeveldef.h | 19 + main/modes/games/pango/paSoundManager.c | 97 + main/modes/games/pango/paSoundManager.h | 65 + main/modes/games/pango/paSprite.h | 20 + main/modes/games/pango/paTables.h | 69 + main/modes/games/pango/paTilemap.c | 429 ++++ main/modes/games/pango/paTilemap.h | 111 ++ main/modes/games/pango/paWsgManager.c | 295 +++ main/modes/games/pango/paWsgManager.h | 147 ++ main/modes/games/pango/pango.c | 1757 +++++++++++++++++ main/modes/games/pango/pango.h | 47 + main/modes/games/pango/pango_typedef.h | 71 + main/modes/games/pinball/mode_pinball.h | 111 -- main/modes/games/pinball/pinball_draw.c | 134 -- main/modes/games/pinball/pinball_draw.h | 6 - main/modes/games/pinball/pinball_physics.c | 771 -------- main/modes/games/pinball/pinball_physics.h | 16 - main/modes/games/pinball/pinball_test.c | 316 --- main/modes/games/pinball/pinball_test.h | 9 - main/modes/games/pinball/pinball_zones.c | 178 -- main/modes/games/pinball/pinball_zones.h | 9 - main/modes/games/soko/soko.c | 364 ++++ main/modes/games/soko/soko.h | 279 +++ main/modes/games/soko/soko_consts.h | 13 + main/modes/games/soko/soko_game.c | 313 +++ main/modes/games/soko/soko_game.h | 12 + main/modes/games/soko/soko_gamerules.c | 1240 ++++++++++++ main/modes/games/soko/soko_gamerules.h | 49 + main/modes/games/soko/soko_input.c | 179 ++ main/modes/games/soko/soko_input.h | 40 + main/modes/games/soko/soko_save.c | 680 +++++++ main/modes/games/soko/soko_save.h | 5 + main/modes/games/soko/soko_undo.c | 124 ++ main/modes/games/soko/soko_undo.h | 17 + main/modes/music/jukebox/jukebox.c | 1 - main/modes/music/usbsynth/mode_synth.c | 88 +- main/modes/system/mainMenu/mainMenu.c | 20 +- main/swadge2024.c | 3 + main/utils/fl_math/vectorFl2d.c | 17 +- main/utils/fl_math/vectorFl2d.h | 1 + main/utils/macros.h | 29 +- makefile | 9 +- tools/pango_editor/mockup.bin | Bin 0 -> 274 bytes tools/pango_editor/mockup.tmx | 23 + tools/pango_editor/pa-tileset.png | Bin 0 -> 1209 bytes tools/pango_editor/pa-tileset.tsx | 4 + tools/pango_editor/pango-editor.js | 111 ++ tools/pango_editor/pango-tiles.tsx | 4 + tools/pango_editor/pango.tiled-project | 11 + tools/pango_editor/pango.tiled-session | 65 + tools/pango_editor/preset.tmx | 23 + tools/sandbox_test/usbhid_test/sandbox.c | 2 +- tools/sandbox_test/usbhid_test/test/Makefile | 12 +- tools/sandbox_test/usbhid_test/test/hidtest.c | 21 +- .../usbhid_test/test/winbuild.bat | 1 + tools/soko/plugin/sokoban_tiled_importer.js | 21 + .../sokobon_binary_conversion_script.lua | 91 + tools/soko/soko_tmx_preprocessor.py | 68 + tools/soko/templateTiledProject/README.md | 62 + .../entitySprites/button16.png | Bin 0 -> 192 bytes .../entitySprites/crate16.png | Bin 0 -> 181 bytes .../entitySprites/ghostblock16.png | Bin 0 -> 176 bytes .../entitySprites/laser90Right16.png | Bin 0 -> 252 bytes .../entitySprites/laserEmitUp16.png | Bin 0 -> 184 bytes .../entitySprites/laserReceiveOmni16.png | Bin 0 -> 242 bytes .../entitySprites/laserReceiveUp16.png | Bin 0 -> 317 bytes .../entitySprites/player16.png | Bin 0 -> 255 bytes .../entitySprites/warpexternal16.png | Bin 0 -> 220 bytes .../entitySprites/warpinternal16.png | Bin 0 -> 241 bytes .../entitySprites/warpinternalexit16.png | Bin 0 -> 257 bytes .../extensions/export-to-soko.js | 391 ++++ tools/soko/templateTiledProject/objLayers.tsx | 79 + .../templateTiledProject/sk_binOverworld.bin | Bin 0 -> 107 bytes .../templateTiledProject/sk_binOverworld.tmx | 55 + .../templateTiledProject/sk_laserTest.bin | Bin 0 -> 123 bytes .../templateTiledProject/sk_laserTest.tmx | 63 + .../templateTiledProject/soko_entities.tsx | 79 + .../soko/templateTiledProject/templateMap.bin | Bin 0 -> 99 bytes .../soko/templateTiledProject/templateMap.tmx | 71 + .../templateProject.tiled-project | 14 + .../templateProject.tiled-session | 42 + .../tileSprites/tilesheet.png | Bin 0 -> 792 bytes tools/soko/templateTiledProject/tilesheet.tsx | 8 + tools/soko/templateTiledProject/warehouse.bin | Bin 0 -> 106 bytes tools/soko/templateTiledProject/warehouse.tmx | 37 + tools/soko/tmx_to_binary.py | 222 +++ tools/svg-to-pinball/.gitignore | 4 + tools/svg-to-pinball/.vscode/launch.json | 15 + tools/svg-to-pinball/.vscode/settings.json | 7 + tools/svg-to-pinball/README.md | 55 + tools/svg-to-pinball/gentable.sh | 8 + tools/svg-to-pinball/pinball.svg | 778 ++++++++ tools/svg-to-pinball/requirements.txt | 1 + tools/svg-to-pinball/svg-to-pinball.py | 487 +++++ 383 files changed, 18904 insertions(+), 1726 deletions(-) create mode 100644 assets/2048/Sounds/sndBounce.mid create mode 100644 assets/2048/pngs/NewTiles/New_Dot.png create mode 100644 assets/2048/pngs/NewTiles/New_Med_Star.png create mode 100644 assets/2048/pngs/NewTiles/New_Small_Star.png create mode 100644 assets/2048/pngs/Sparkles/Sparkle_Blue.png create mode 100644 assets/2048/pngs/Sparkles/Sparkle_Cyan.png create mode 100644 assets/2048/pngs/Sparkles/Sparkle_Green.png create mode 100644 assets/2048/pngs/Sparkles/Sparkle_Orange.png create mode 100644 assets/2048/pngs/Sparkles/Sparkle_Pink.png create mode 100644 assets/2048/pngs/Sparkles/Sparkle_Purple.png create mode 100644 assets/2048/pngs/Sparkles/Sparkle_Red.png create mode 100644 assets/2048/pngs/Sparkles/Sparkle_Yellow.png create mode 100644 assets/2048/pngs/Tiles/Tile-Blue-Diamond.png create mode 100644 assets/2048/pngs/Tiles/Tile-Blue-Square.png create mode 100644 assets/2048/pngs/Tiles/Tile-Cyan-Legs.png create mode 100644 assets/2048/pngs/Tiles/Tile-Green-Diamond.png create mode 100644 assets/2048/pngs/Tiles/Tile-Green-Octo.png create mode 100644 assets/2048/pngs/Tiles/Tile-Green-Square.png create mode 100644 assets/2048/pngs/Tiles/Tile-Mauve-Legs.png create mode 100644 assets/2048/pngs/Tiles/Tile-Orange-Legs.png create mode 100644 assets/2048/pngs/Tiles/Tile-Pink-Diamond.png create mode 100644 assets/2048/pngs/Tiles/Tile-Pink-Octo.png create mode 100644 assets/2048/pngs/Tiles/Tile-Pink-Square.png create mode 100644 assets/2048/pngs/Tiles/Tile-Purple-Legs.png create mode 100644 assets/2048/pngs/Tiles/Tile-Red-Octo.png create mode 100644 assets/2048/pngs/Tiles/Tile-Red-Square.png create mode 100644 assets/2048/pngs/Tiles/Tile-Yellow-Diamond.png create mode 100644 assets/2048/pngs/Tiles/Tile-Yellow-Octo.png create mode 100644 assets/fonts/pango-fw.font.png create mode 100644 assets/pango/levels/mockup.bin create mode 100644 assets/pango/levels/preset.bin create mode 100644 assets/pango/sounds/bgmCastle.mid create mode 100644 assets/pango/sounds/bgmDeMAGio.mid create mode 100644 assets/pango/sounds/bgmGameOver.mid create mode 100644 assets/pango/sounds/bgmGameStart.mid create mode 100644 assets/pango/sounds/bgmIntro.mid create mode 100644 assets/pango/sounds/bgmNameEntry.mid create mode 100644 assets/pango/sounds/bgmSmooth.mid create mode 100644 assets/pango/sounds/bgmUnderground.mid create mode 100644 assets/pango/sounds/snd1up.mid create mode 100644 assets/pango/sounds/sndBlockStop.mid create mode 100644 assets/pango/sounds/sndCheckpoint.mid create mode 100644 assets/pango/sounds/sndCoin.mid create mode 100644 assets/pango/sounds/sndDie.mid create mode 100644 assets/pango/sounds/sndHit.mid create mode 100644 assets/pango/sounds/sndHurt.mid create mode 100644 assets/pango/sounds/sndJump1.mid create mode 100644 assets/pango/sounds/sndJump2.mid create mode 100644 assets/pango/sounds/sndJump3.mid create mode 100644 assets/pango/sounds/sndLevelClearA.mid create mode 100644 assets/pango/sounds/sndLevelClearB.mid create mode 100644 assets/pango/sounds/sndLevelClearC.mid create mode 100644 assets/pango/sounds/sndLevelClearD.mid create mode 100644 assets/pango/sounds/sndLevelClearS.mid create mode 100644 assets/pango/sounds/sndMenuConfirm.mid create mode 100644 assets/pango/sounds/sndMenuDeny.mid create mode 100644 assets/pango/sounds/sndMenuSelect.mid create mode 100644 assets/pango/sounds/sndOutOfTime.mid create mode 100644 assets/pango/sounds/sndPause.mid create mode 100644 assets/pango/sounds/sndPowerUp.mid create mode 100644 assets/pango/sounds/sndSlide.mid create mode 100644 assets/pango/sounds/sndSpawn.mid create mode 100644 assets/pango/sounds/sndSquish.mid create mode 100644 assets/pango/sounds/sndWarp.mid create mode 100644 assets/pango/sounds/sndWaveBall.mid create mode 100644 assets/pango/sprites/blockfragment.png create mode 100644 assets/pango/sprites/break-000.png create mode 100644 assets/pango/sprites/break-001.png create mode 100644 assets/pango/sprites/break-002.png create mode 100644 assets/pango/sprites/break-003.png create mode 100644 assets/pango/sprites/kr-000.png create mode 100644 assets/pango/sprites/kr-001.png create mode 100644 assets/pango/sprites/kr-002.png create mode 100644 assets/pango/sprites/kr-003.png create mode 100644 assets/pango/sprites/kr-004.png create mode 100644 assets/pango/sprites/kr-005.png create mode 100644 assets/pango/sprites/kr-006.png create mode 100644 assets/pango/sprites/kr-007.png create mode 100644 assets/pango/sprites/kr-008.png create mode 100644 assets/pango/sprites/kr-009.png create mode 100644 assets/pango/sprites/kr-010.png create mode 100644 assets/pango/sprites/kr-011.png create mode 100644 assets/pango/sprites/kr-012.png create mode 100644 assets/pango/sprites/kr-013.png create mode 100644 assets/pango/sprites/kr-014.png create mode 100644 assets/pango/sprites/kr-015.png create mode 100644 assets/pango/sprites/pa-000.png create mode 100644 assets/pango/sprites/pa-001.png create mode 100644 assets/pango/sprites/pa-002.png create mode 100644 assets/pango/sprites/pa-003.png create mode 100644 assets/pango/sprites/pa-004.png create mode 100644 assets/pango/sprites/pa-005.png create mode 100644 assets/pango/sprites/pa-006.png create mode 100644 assets/pango/sprites/pa-007.png create mode 100644 assets/pango/sprites/pa-008.png create mode 100644 assets/pango/sprites/pa-009.png create mode 100644 assets/pango/sprites/pa-010.png create mode 100644 assets/pango/sprites/pa-011.png create mode 100644 assets/pango/sprites/pa-012.png create mode 100644 assets/pango/sprites/pa-013.png create mode 100644 assets/pango/sprites/pa-014.png create mode 100644 assets/pango/sprites/pa-015.png create mode 100644 assets/pango/sprites/pa-100.png create mode 100644 assets/pango/sprites/pa-101.png create mode 100644 assets/pango/sprites/pa-102.png create mode 100644 assets/pango/sprites/pa-103.png create mode 100644 assets/pango/sprites/pa-104.png create mode 100644 assets/pango/sprites/pa-105.png create mode 100644 assets/pango/sprites/pa-106.png create mode 100644 assets/pango/sprites/pa-107.png create mode 100644 assets/pango/sprites/pa-108.png create mode 100644 assets/pango/sprites/pa-109.png create mode 100644 assets/pango/sprites/pa-110.png create mode 100644 assets/pango/sprites/pa-111.png create mode 100644 assets/pango/sprites/pa-112.png create mode 100644 assets/pango/sprites/pa-113.png create mode 100644 assets/pango/sprites/pa-114.png create mode 100644 assets/pango/sprites/pa-en-000.png create mode 100644 assets/pango/sprites/pa-en-001.png create mode 100644 assets/pango/sprites/pa-en-002.png create mode 100644 assets/pango/sprites/pa-en-003.png create mode 100644 assets/pango/sprites/pa-en-004.png create mode 100644 assets/pango/sprites/pa-en-005.png create mode 100644 assets/pango/sprites/pa-en-006.png create mode 100644 assets/pango/sprites/pa-en-007.png create mode 100644 assets/pango/sprites/pa-en-008.png create mode 100644 assets/pango/sprites/po-000.png create mode 100644 assets/pango/sprites/po-001.png create mode 100644 assets/pango/sprites/po-002.png create mode 100644 assets/pango/sprites/po-003.png create mode 100644 assets/pango/sprites/po-004.png create mode 100644 assets/pango/sprites/po-005.png create mode 100644 assets/pango/sprites/po-006.png create mode 100644 assets/pango/sprites/po-007.png create mode 100644 assets/pango/sprites/po-008.png create mode 100644 assets/pango/sprites/po-009.png create mode 100644 assets/pango/sprites/po-010.png create mode 100644 assets/pango/sprites/po-011.png create mode 100644 assets/pango/sprites/po-012.png create mode 100644 assets/pango/sprites/po-013.png create mode 100644 assets/pango/sprites/po-014.png create mode 100644 assets/pango/sprites/px-000.png create mode 100644 assets/pango/sprites/px-001.png create mode 100644 assets/pango/sprites/px-002.png create mode 100644 assets/pango/sprites/px-003.png create mode 100644 assets/pango/sprites/px-004.png create mode 100644 assets/pango/sprites/px-005.png create mode 100644 assets/pango/sprites/px-006.png create mode 100644 assets/pango/sprites/px-007.png create mode 100644 assets/pango/sprites/px-008.png create mode 100644 assets/pango/sprites/px-009.png create mode 100644 assets/pango/sprites/px-010.png create mode 100644 assets/pango/sprites/px-011.png create mode 100644 assets/pango/sprites/px-012.png create mode 100644 assets/pango/sprites/px-013.png create mode 100644 assets/pango/sprites/px-014.png create mode 100644 assets/pango/tiles/.DS_Store create mode 100644 assets/pango/tiles/pa-tile-000.png create mode 100644 assets/pango/tiles/pa-tile-001.png create mode 100644 assets/pango/tiles/pa-tile-002.png create mode 100644 assets/pango/tiles/pa-tile-003.png create mode 100644 assets/pango/tiles/pa-tile-004.png create mode 100644 assets/pango/tiles/pa-tile-005.png create mode 100644 assets/pango/tiles/pa-tile-006.png create mode 100644 assets/pango/tiles/pa-tile-007.png create mode 100644 assets/pango/tiles/pa-tile-008.png create mode 100644 assets/pango/tiles/pa-tile-009.png create mode 100644 assets/pango/tiles/pa-tile-010.png create mode 100644 assets/pango/tiles/pa-tile-011.png create mode 100644 assets/pango/tiles/pa-tile-012.png create mode 100644 assets/pango/tiles/pa-tile-013.png create mode 100644 assets/pango/tiles/pa-tile-014.png create mode 100644 assets/pango/tiles/pa-tile-015.png create mode 100644 assets/soko/levels/SK_LEVEL_LIST.txt create mode 100644 assets/soko/levels/classic/sck_c_threes.tmx create mode 100644 assets/soko/levels/classic/sk_c_alignthat.tmx create mode 100644 assets/soko/levels/classic/sk_c_cosms.tmx create mode 100644 assets/soko/levels/classic/sk_c_files.tmx create mode 100644 assets/soko/levels/classic/sk_c_fours.tmx create mode 100644 assets/soko/levels/classic/sk_c_plus.tmx create mode 100644 assets/soko/levels/classic/sk_c_test2.tmx create mode 100644 assets/soko/levels/euler/sk_e_a-frame.tmx create mode 100644 assets/soko/levels/euler/sk_e_alkatraz.tmx create mode 100644 assets/soko/levels/euler/sk_e_apollo.tmx create mode 100644 assets/soko/levels/euler/sk_e_camera.tmx create mode 100644 assets/soko/levels/euler/sk_e_casette.tmx create mode 100644 assets/soko/levels/euler/sk_e_copymachine.tmx create mode 100644 assets/soko/levels/euler/sk_e_curlingiron.tmx create mode 100644 assets/soko/levels/euler/sk_e_doubleblock.tmx create mode 100644 assets/soko/levels/euler/sk_e_feint.tmx create mode 100644 assets/soko/levels/euler/sk_e_groundwork.tmx create mode 100644 assets/soko/levels/euler/sk_e_harmonica.tmx create mode 100644 assets/soko/levels/euler/sk_e_mouse.tmx create mode 100644 assets/soko/levels/euler/sk_e_spine.tmx create mode 100644 assets/soko/levels/euler/sk_e_spiral.tmx create mode 100644 assets/soko/levels/euler/sk_e_start.tmx create mode 100644 assets/soko/levels/euler/sk_e_steeringwheel.tmx create mode 100644 assets/soko/levels/euler/sk_e_threestep.tmx create mode 100644 assets/soko/levels/euler/sk_e_threestep2.tmx create mode 100644 assets/soko/levels/euler/sk_e_throughput.tmx create mode 100644 assets/soko/levels/euler/sk_e_tunnels.tmx create mode 100644 assets/soko/levels/euler/sk_e_waterwheel.tmx create mode 100644 assets/soko/levels/sk_e_overworld.tmx create mode 100644 assets/soko/levels/sk_overworld.tmx create mode 100644 assets/soko/sprites/pango/sk_pango_back1.png create mode 100644 assets/soko/sprites/pango/sk_pango_back2.png create mode 100644 assets/soko/sprites/pango/sk_pango_fwd1.png create mode 100644 assets/soko/sprites/pango/sk_pango_fwd2.png create mode 100644 assets/soko/sprites/pango/sk_pango_side1.png create mode 100644 assets/soko/sprites/pango/sk_pango_side2.png create mode 100644 assets/soko/sprites/pixel/sk_pixel_back.png create mode 100644 assets/soko/sprites/pixel/sk_pixel_front.png create mode 100644 assets/soko/sprites/pixel/sk_pixel_left.png create mode 100644 assets/soko/sprites/pixel/sk_pixel_right.png create mode 100644 assets/soko/sprites/sk_crate.png create mode 100644 assets/soko/sprites/sk_crate_2.png create mode 100644 assets/soko/sprites/sk_crate_ongoal.png create mode 100644 assets/soko/sprites/sk_dog.png create mode 100644 assets/soko/sprites/sk_e_crate.png create mode 100644 assets/soko/sprites/sk_goal.png create mode 100644 assets/soko/sprites/sk_grass.png create mode 100644 assets/soko/sprites/sk_portal_complete.png create mode 100644 assets/soko/sprites/sk_portal_incomplete.png create mode 100644 assets/soko/sprites/sk_ring.png create mode 100644 assets/soko/sprites/sk_sticky_crate.png create mode 100644 assets/soko/sprites/sk_sticky_trail_crate.png rename {main/modes/games => attic}/pinball/mode_pinball.c (53%) create mode 100644 attic/pinball/mode_pinball.h create mode 100644 attic/pinball/pinball_circle.c create mode 100644 attic/pinball/pinball_circle.h create mode 100644 attic/pinball/pinball_draw.c create mode 100644 attic/pinball/pinball_draw.h create mode 100644 attic/pinball/pinball_flipper.c create mode 100644 attic/pinball/pinball_flipper.h create mode 100644 attic/pinball/pinball_game.c create mode 100644 attic/pinball/pinball_game.h create mode 100644 attic/pinball/pinball_line.c create mode 100644 attic/pinball/pinball_line.h create mode 100644 attic/pinball/pinball_physics.c create mode 100644 attic/pinball/pinball_physics.h create mode 100644 attic/pinball/pinball_point.c create mode 100644 attic/pinball/pinball_point.h create mode 100644 attic/pinball/pinball_rectangle.c create mode 100644 attic/pinball/pinball_rectangle.h create mode 100644 attic/pinball/pinball_triangle.c create mode 100644 attic/pinball/pinball_triangle.h create mode 100644 attic/pinball/pinball_typedef.h create mode 100644 docs/MIDI.md create mode 100644 docs/soko/readme.md create mode 100644 docs/soko/soko_levels.md create mode 100644 emulator/resources/SwadgeEmulator.desktop create mode 100755 emulator/resources/install.sh create mode 100644 emulator/src/components/hdw-nvs/hdw-nvs_emu.h create mode 100644 emulator/src/emu_cnfs.c create mode 100644 emulator/src/emu_cnfs.h create mode 100644 emulator/src/extensions/midi/ext_midi.c create mode 100644 emulator/src/extensions/midi/ext_midi.h create mode 100644 main/display/wsgPalette.c create mode 100644 main/display/wsgPalette.h create mode 100644 main/modes/games/2048/2048_game.c create mode 100644 main/modes/games/2048/2048_game.h create mode 100644 main/modes/games/2048/2048_menus.c create mode 100644 main/modes/games/2048/2048_menus.h create mode 100644 main/modes/games/2048/mode_2048.c create mode 100644 main/modes/games/2048/mode_2048.h create mode 100644 main/modes/games/pango/paEntity.c create mode 100644 main/modes/games/pango/paEntity.h create mode 100644 main/modes/games/pango/paEntityManager.c create mode 100644 main/modes/games/pango/paEntityManager.h create mode 100644 main/modes/games/pango/paGameData.c create mode 100644 main/modes/games/pango/paGameData.h create mode 100644 main/modes/games/pango/paLeveldef.h create mode 100644 main/modes/games/pango/paSoundManager.c create mode 100644 main/modes/games/pango/paSoundManager.h create mode 100644 main/modes/games/pango/paSprite.h create mode 100644 main/modes/games/pango/paTables.h create mode 100644 main/modes/games/pango/paTilemap.c create mode 100644 main/modes/games/pango/paTilemap.h create mode 100644 main/modes/games/pango/paWsgManager.c create mode 100644 main/modes/games/pango/paWsgManager.h create mode 100644 main/modes/games/pango/pango.c create mode 100644 main/modes/games/pango/pango.h create mode 100644 main/modes/games/pango/pango_typedef.h delete mode 100644 main/modes/games/pinball/mode_pinball.h delete mode 100644 main/modes/games/pinball/pinball_draw.c delete mode 100644 main/modes/games/pinball/pinball_draw.h delete mode 100644 main/modes/games/pinball/pinball_physics.c delete mode 100644 main/modes/games/pinball/pinball_physics.h delete mode 100644 main/modes/games/pinball/pinball_test.c delete mode 100644 main/modes/games/pinball/pinball_test.h delete mode 100644 main/modes/games/pinball/pinball_zones.c delete mode 100644 main/modes/games/pinball/pinball_zones.h create mode 100644 main/modes/games/soko/soko.c create mode 100644 main/modes/games/soko/soko.h create mode 100644 main/modes/games/soko/soko_consts.h create mode 100644 main/modes/games/soko/soko_game.c create mode 100644 main/modes/games/soko/soko_game.h create mode 100644 main/modes/games/soko/soko_gamerules.c create mode 100644 main/modes/games/soko/soko_gamerules.h create mode 100644 main/modes/games/soko/soko_input.c create mode 100644 main/modes/games/soko/soko_input.h create mode 100644 main/modes/games/soko/soko_save.c create mode 100644 main/modes/games/soko/soko_save.h create mode 100644 main/modes/games/soko/soko_undo.c create mode 100644 main/modes/games/soko/soko_undo.h create mode 100644 tools/pango_editor/mockup.bin create mode 100644 tools/pango_editor/mockup.tmx create mode 100644 tools/pango_editor/pa-tileset.png create mode 100644 tools/pango_editor/pa-tileset.tsx create mode 100644 tools/pango_editor/pango-editor.js create mode 100644 tools/pango_editor/pango-tiles.tsx create mode 100644 tools/pango_editor/pango.tiled-project create mode 100644 tools/pango_editor/pango.tiled-session create mode 100644 tools/pango_editor/preset.tmx create mode 100644 tools/sandbox_test/usbhid_test/test/winbuild.bat create mode 100644 tools/soko/plugin/sokoban_tiled_importer.js create mode 100644 tools/soko/plugin/sokobon_binary_conversion_script.lua create mode 100644 tools/soko/soko_tmx_preprocessor.py create mode 100644 tools/soko/templateTiledProject/README.md create mode 100644 tools/soko/templateTiledProject/entitySprites/button16.png create mode 100644 tools/soko/templateTiledProject/entitySprites/crate16.png create mode 100644 tools/soko/templateTiledProject/entitySprites/ghostblock16.png create mode 100644 tools/soko/templateTiledProject/entitySprites/laser90Right16.png create mode 100644 tools/soko/templateTiledProject/entitySprites/laserEmitUp16.png create mode 100644 tools/soko/templateTiledProject/entitySprites/laserReceiveOmni16.png create mode 100644 tools/soko/templateTiledProject/entitySprites/laserReceiveUp16.png create mode 100644 tools/soko/templateTiledProject/entitySprites/player16.png create mode 100644 tools/soko/templateTiledProject/entitySprites/warpexternal16.png create mode 100644 tools/soko/templateTiledProject/entitySprites/warpinternal16.png create mode 100644 tools/soko/templateTiledProject/entitySprites/warpinternalexit16.png create mode 100644 tools/soko/templateTiledProject/extensions/export-to-soko.js create mode 100644 tools/soko/templateTiledProject/objLayers.tsx create mode 100644 tools/soko/templateTiledProject/sk_binOverworld.bin create mode 100644 tools/soko/templateTiledProject/sk_binOverworld.tmx create mode 100644 tools/soko/templateTiledProject/sk_laserTest.bin create mode 100644 tools/soko/templateTiledProject/sk_laserTest.tmx create mode 100644 tools/soko/templateTiledProject/soko_entities.tsx create mode 100644 tools/soko/templateTiledProject/templateMap.bin create mode 100644 tools/soko/templateTiledProject/templateMap.tmx create mode 100644 tools/soko/templateTiledProject/templateProject.tiled-project create mode 100644 tools/soko/templateTiledProject/templateProject.tiled-session create mode 100644 tools/soko/templateTiledProject/tileSprites/tilesheet.png create mode 100644 tools/soko/templateTiledProject/tilesheet.tsx create mode 100644 tools/soko/templateTiledProject/warehouse.bin create mode 100644 tools/soko/templateTiledProject/warehouse.tmx create mode 100644 tools/soko/tmx_to_binary.py create mode 100755 tools/svg-to-pinball/.gitignore create mode 100755 tools/svg-to-pinball/.vscode/launch.json create mode 100755 tools/svg-to-pinball/.vscode/settings.json create mode 100755 tools/svg-to-pinball/README.md create mode 100755 tools/svg-to-pinball/gentable.sh create mode 100755 tools/svg-to-pinball/pinball.svg create mode 100755 tools/svg-to-pinball/requirements.txt create mode 100755 tools/svg-to-pinball/svg-to-pinball.py diff --git a/.github/workflows/build-firmware-and-emulator.yml b/.github/workflows/build-firmware-and-emulator.yml index 850739ef8..4f42db1a7 100644 --- a/.github/workflows/build-firmware-and-emulator.yml +++ b/.github/workflows/build-firmware-and-emulator.yml @@ -55,6 +55,9 @@ jobs: emu_artifacts: - swadge_emulator - version.txt + - install.sh + - icon.png + - SwadgeEmulator.desktop idf_install: ~/.espressif runs-on: ${{ matrix.runner }} @@ -169,6 +172,13 @@ jobs: run: | make SwadgeEmulator.app + - name: Create Linux FreeDesktop files + if: matrix.emulator && matrix.family == 'linux' + run: | + cp emulator/resources/install.sh . + cp emulator/resources/icon.png . + cp emulator/resources/SwadgeEmulator.desktop . + - name: Create Emulator zip if: matrix.emulator && matrix.family != 'windows' run: | diff --git a/.gitignore b/.gitignore index e11e95fa8..24767414f 100644 --- a/.gitignore +++ b/.gitignore @@ -91,3 +91,5 @@ tools/font_maker/*.ttf main/utils/cnfs_image.c main/utils/cnfs_image.h tools/cnfs/cnfs_gen + +_old/* \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json index db3d404a3..cc564de79 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -1,6 +1,4 @@ { - // See https://go.microsoft.com/fwlink/?LinkId=733558 - // for the documentation about the tasks.json format "version": "2.0.0", "tasks": [ { @@ -26,6 +24,6 @@ "clean" ], "group": "build" - }, + } ] } \ No newline at end of file diff --git a/Doxyfile b/Doxyfile index b403154b7..a564bf95b 100644 --- a/Doxyfile +++ b/Doxyfile @@ -956,6 +956,7 @@ INPUT = ./main \ ./docs/CODE_OF_CONDUCT.md \ ./docs/PORTING.md \ ./docs/EMULATOR.md \ + ./docs/MIDI.md \ ./docs/SERIAL_DEBUG.md # This tag can be used to specify the character encoding of the source files diff --git a/assets/2048/Sounds/sndBounce.mid b/assets/2048/Sounds/sndBounce.mid new file mode 100644 index 0000000000000000000000000000000000000000..c79d0d87d89183bf5fe10a8a7e8b85f61ce5e210 GIT binary patch literal 152 zcmeYb$w*;fU|?flWME=^;2Tnu4djV4{AXrh_#eT-!X&}L@IR25eW3!we|-k1bTY$+ zN}x1Q$p$V5h7If~3>&!X88)N>`6)~c8`ywiU^XL!tp*ea^MN!Q55ofnpgbc3!+`*X a2?_Q54G9bk6Fh*l2aq-Z(gr}I^%(%X&?T<` literal 0 HcmV?d00001 diff --git a/assets/2048/pngs/NewTiles/New_Dot.png b/assets/2048/pngs/NewTiles/New_Dot.png new file mode 100644 index 0000000000000000000000000000000000000000..1d09be065214e2ec0decc70602e74487faf46310 GIT binary patch literal 1014 zcmds#J!_Ov5QUGX36TX6t6-sx5Fub33)?Ikk_B19CZw>@LdIGYOy^g%XlpT8h*(*e z#!3q-5sSgtrW6}3tb}dHC-XDBki6VGGiT1n&bB9;gQevqNrUm$Xv+7>@!#SC|5twO zKjm}c-gI-QgKy`4ODCUgU*Fx}(Il5bN~!v8CO3sCP1R*{xhq`hs_wRsha!}r>S0TH zDpHxM#-OS9_5+}$nQ(yf{YWe)2v|cCUJzk3&|m|`H0{)R;ABL#l_+E}_spnv7QyhKoE6nZ6AL+D zJWgXRUSb|@VGn*_0E&?aX|P}oO?W|s%|L?<7}K;<=Ydn58CIf@#oVhSB!2=YVIn0% zLL((&AqR}dX{^Od?0_Z1_<;c^Mk1uaf;BYZ1ras_4K`p*(@vcS4!^sLJ2!suIl4cc z?9lLkp1t}08$@0mkJfj$W`DlF9!QG|>+_qZzdSyFcXfXFa5&SWSC>9~J^OOyv!8mm icIM~hJD>jE*0tsL4=%j@_+srj^BQkVMhC-NPyPWhVn;au literal 0 HcmV?d00001 diff --git a/assets/2048/pngs/NewTiles/New_Med_Star.png b/assets/2048/pngs/NewTiles/New_Med_Star.png new file mode 100644 index 0000000000000000000000000000000000000000..8f60375afe9c718c05dff12423a59224063cc343 GIT binary patch literal 1102 zcmds#%}do$5QZlWt6Nf58>P^UgfK}Ph*|{sg^F>}tGKX5tp+NH$O17)D1}6+MG+0v zs$3>*+Li__ii7k4LaPuGY7x1Rs7=%3`7=78KhBw%cixXX)mQAQEYuW4q_U^Gqr~^f z`rqbq{vTW(y~$_8NU5t$<`(yS7b%CVWFC~w z)m@WoWLk7vQcs$aMrO&dg?eZTjZ7L5u!bhQAi`!=fejebv{UDS<7UY$vO*Sfcemsg zSui{(d$N=)vVLgM?O`b_vK_dEJ@|nEC`KZh5CLmw!V4m71{!R@n5LaN51iy4*-8|$ zn0rbd*;xd`gK`RwY&5Zu1IFVt*5W1R;THDb2L_-RiI4^h*3g6(MA!^8*nlxjJ9Qp7 zku$?e6tb9m1z)6@$$&k=UiCD-1<8c~m@e(^=$uNFk0E&?aX|P}oO?W|s%|L?< z7}K;<=YhlTu3=#K8=uctO2q*h{?9Y?YxD^sXL>qX2fL?Mmii0xb=%7OevU7UJ*up3 zfBd4SVzMP_hn{^XU2i@=J6U~U^m1Wo_34(y)vr5emftpA`c(CPt!)3*x?4W+=Ew2r z9ZmBuk8S$B>(6}awdW^W9#n0t8Ec<-eQ%}r;i0{=ueSH!J+b@CyY8cRDtbr$)^9zx pZ?aSzZa6UhvGM*|edC$Q!?(*%UmR$vU1)}uNKa?6W3KJ&jekzNZH52< literal 0 HcmV?d00001 diff --git a/assets/2048/pngs/NewTiles/New_Small_Star.png b/assets/2048/pngs/NewTiles/New_Small_Star.png new file mode 100644 index 0000000000000000000000000000000000000000..e9d8bdaee94adef94e2f8a61956702d6722c77dc GIT binary patch literal 1046 zcmds#&r8%{5XL7Zk|n#;AtbQVf>4(xsE)g=>4L1fE>U*rGz1-uFpLU2Nl+e)8iF9Y zFX5rPorfss5*?z8fW#=h6uBK9Q+3r)&05UeOx-k0vv^Q8S9cB9ELwD1 zsE0;q7E6XL)l(xii!>r&4NZ7Kgw3o18!)D6r_KY%&B828A&a@YTexKjh6iO2i?A&H z(4yPZA}z}f+`=CGzyK5@5lx7IH8kM`5jF!2HegKCPMrr%xMx|3LKbt6@GNH$3=hhY zo@F$#kORi!G}huJ=HV9h;0Fev7>SSu3)awt7ev?$G}wSKO*?fSI9ZutB??*0Ju5=; zCvXxbQZghoQX&>|z<8X-TD-&#STc+s7=U6VLK-YsLla&QVKdNR1I9G%)Oq0WyK62j z|KjuQZojug!~c1%?XSK={w^<&!e5)%iXOn&Hl&ZfA375y4tz1 zKmT~NHMM;5#rw|_$3`yh?B2ezd4F;3!?XE&FHfIn&aE80Z4b9!Z$AIge(>N`Q7E*ru-Ik_3MRkuHF2tEb;&4@yR)# z!K=m2woZR;UP`x)=Fj#fJeuTENGVm{&E%#qrK!4XE_a11UDe$d@=$~_R6T4dPem$I z)mT>YQk1e(4Q&F6QF&H zJ@|nEC`KZh5CLmw!V4m71{!R@n5LaN51fpswi1Ob=AIeV&LS8dl(VARXksA;jK^uL z#Y@b?E$qP$3_vjwAq^I+p$RXDuo-Bu0b`nW>O63&Gs8+0vY2~ygyc`)Buu1aNNA)) zEaZUkIE}SeC*4u+2(L?V<2fby*HWjy>|Y$e1-oH-@g0C z=hESPbF7muqulZdNFr*3%7Ku=0VxRA}qtIY0>Rzk(O!IEE)E) zD9f^H(ujaHG~opiHnR$Bz?i0;IuD$1PxoqtEao2J8D1?I9+V?J)2sDEi|#1T@@hM9 z3w!Vb15k`aG$8`k(1aI6*bFq-fH6%wbsjhwQEep(SIli7z)6@$$&k=U ziCD-1<8c~m@e(^=$uNFk0E&?aX|P}oO?W|s%|L?<7}K;<=YhlT?$-9+FFxO&&S%>+ zT+H*}-?KM}ygi+)?<~ioeLcUqwz61!I{Uo#;mOhCmA^}`hd+ND4ep*EkFFmaFD*V^ Z{WH>d_s;6s*Ts7#HQks^PR9E${sWZ&MrHs2 literal 0 HcmV?d00001 diff --git a/assets/2048/pngs/Sparkles/Sparkle_Green.png b/assets/2048/pngs/Sparkles/Sparkle_Green.png new file mode 100644 index 0000000000000000000000000000000000000000..62b0e41938193c120309d4e4807da2274683bd5d GIT binary patch literal 1008 zcmds#y-w6o5XH|*v4pt23Wb#g1)WU^){zAiRK; z7FH8G3rUP5Hk#0C?8YDS7~TZ_xp!vHoR2-3&vr*E4^||NrccHTzISi`mY4Yd{QTkz zpTWz;?zS#Jul zOjTo9$xBhnQZ=**Bu43_$nEyHS(v3+^O62VqS{InvY2~jR6C1ccu>xYYNLsT955cI zu@)~e54W%fKQI8rNQ5+4u!bhQAi`##!3KTfr9z>Ng$2zN6jYMktecq4W+4_<7?XJcEVRUegiu*dXJVxi zUVx6mYGPtxVMi&6HL)9i%wup9_~+i4IdeYtWIme=*B-7(8cv^&7JTpB{;e+a|Mj=C zUwj6Ki^;Cee{5b$OQ-V}`(qwWaw(*gs_$lUQ<&0JT{f4y!j-P-ZVP!RLK&(awv?wL zm8ohhD|snOS*nIMfy5}i6uI3VHw&{gt6t3A-NG&1s(DcMun5brYFc!ATBK!KHA{xQ zEXuO1nlvI{4NZ7Kgw3o18!)D6r_KW>+|#{UA&a?3c!pOCh6m+H&-7~j(4srav%K04 z+`=CGzyK5@5lx7IH8kM`5jF!2HegKCPMrr%MpRpgLKbt+jA~~Q3=hg#QEfD_kORi! zG}huJ=HV9h;0Fev7>SSu3)awt7ev?$G}wSKO*?fSIMtb9B??*0y*fhjCvXxbQZgho zQX&>|z<8X-TD-&#STc+s7=U6VLK-YsLla&QVKdNR1I9G%)Oq0WyL+JOjaZx^#Y z8t&$KdGqcwBDbcaXZx%1=0Haq+xM@oj=x+F&o&=ze7_j@^84*im-DTsE8_=W2mk&a Z?W|~L{qe`+$bsfT6s9y)m(As_aHXrd+d>|SP=>09E#;|5 zWvUv>N?wXmma3smATdfWMQ)eJ&B83rsuy#2w{T0hY95q5EW$FZnik!j7HOGQ&5~g+ zi?S@MCXEPKLla&QVKb}128?Oisq?@I_jIpT$YSmhp5fJk;XyglGrd|rwCIlVEU&f$ zx3C94FaX6!L=z%l4NZ7Kgv~&M4H(n3Q|Ez`5!F_rkj30HquN;n!-H~GR2xk!A`pM^fe?Hr$ z;r~1j{~o+W{d?CQnMwUx!<^QTv9@AeL#t^8SfGy3uUaCq`Kv%kd){NH{1 z?i-(T2eXYeoqQermd?M}yt}>5qe(7>lv4HGOl}HOnySm@a#y(0Ro!hN4@D?L)x(zZ zRHQOhjb$Y-MJY?w&?b-=rI#YNv*TuAmS)w9xw~7qrCT)*${rSB8CFe;ZcmG}Osi(e zu$M(ymQ|BR1gxP6FNm<2RbT_gH0{)R;Dmd+S1V*O_XyAMYQgZJ9O;=}tsh!+M|qZ4 z+ksoygC7`xVkDvo5wM0Pydc76puq-=Y1*mtz{!YeD^bW|?wL{TEP~-dIV-A-CKhtQ zc$~&syu>`*!XEs<02Ct;(qO?Fn(%@Mn}G%!Fs5mz&I6}9Gps}*i@8@vNd5#)!bD1j zghoolLJk;@(^!j_*a1t1@dE=;j6_I-1#4)+3nFX=8f?Irrky$u9Da8h;fWo-8q`$@+ABvbOW=6j)|NbN~PV literal 0 HcmV?d00001 diff --git a/assets/2048/pngs/Sparkles/Sparkle_Red.png b/assets/2048/pngs/Sparkles/Sparkle_Red.png new file mode 100644 index 0000000000000000000000000000000000000000..f442967bf38030fb5e42b204925bd399eade6381 GIT binary patch literal 1008 zcmds$F>4cW5XE0wu%(#2wH=%~D0I1@NGD4|4M+%Sad4^#_XTuNaH*R(Io>9?Y3Ubm zbLe!qIk zOjTo9$xBhnQZ=**BwDE@Nwhd_7G`NyJ($?t!Y$pZd64$72+Oc)T2y;lq-9z)OPakb z%CfAQG$LRPO?W|s$&3OUFuExx&jTmi)4f_DgNY+N!>a|ugLI^4dbNINQ61%3UTp_% zVGn*_0E&@_CPcs*n(%@MlYs^sFuExx&jTkTs;xvJgNZYv+F1m{gLGC@8_inS1IFVt z*5W1R;THDb2L_-RiI4^h*3g6(M3@XT*nrVZIe8v9)jPvV6f&5&dWGztg|lH+$_`l? zTVgHj0poERYw;30U`aE6U;v7d2x+ij4NZ7KgvmgI4H(^&ljniM?{53>@lUSr&*#%a z3jWXY=+BD}h}@Zs?jNnj(_@`%?yg^4+_-x=Jl(pzdH#9e%P)7|f0^yvSs8D99Q?gH a*;~=8AM5X~f8SgDg*6#ZM`!!Tum1rAYeQWC literal 0 HcmV?d00001 diff --git a/assets/2048/pngs/Sparkles/Sparkle_Yellow.png b/assets/2048/pngs/Sparkles/Sparkle_Yellow.png new file mode 100644 index 0000000000000000000000000000000000000000..7d7be3baaaf9661e388f5ed79cde70e455b6b60d GIT binary patch literal 1008 zcmds#KTFkN6vw|e2SvQQrG!&MgAiwMa8mBYF2v<>m4+rGIB%c^9c>7tS8%qVt;C^K zr-G(KTSHEc1x^jocKUc;Mi1zZ=Q-zm&-c$golWaw}q6|QtucU#Cq5z0{Yu%$c| zsZ3R4S;?iOz8R?UO5hecS1Rnwx|(;_X?s#!AZ zWl@%8)ua&tYiPm?B5Y4a2L_-RiD*Iutf2`nh_D%GumNM5cIrHEGNRf_6tb9mW>hy zkrJ_x1IFVt*5W00z>;D7zyK5@5z=768k+Ee2%CWh8!)D6r_KY1-`#`#XFvJ;cr~By z)9`Y!iDNztQWiLUxuIPrTt70W(Nii4|6c!kPm=WH(+k+Dr zL?CKl&9jTj@=i1WkBind~`SS!wBDJR;Aable~53K|wM(L%+IfX6=5CC(MmsMU9+ZvM)@tYVLyK#LpTs+z~TbGs{-jus*qWRCU>CzQy{fn8cp${D!Ym)YcK3%=pHQHGp8S5Mf zmV{H2dq3JWi${CXC$F796zm>ph&)@Hmyy+KGXs5H+rHaLW0Ov3o9!mhW!9wA-AMP;F(;QrQsyL_X0 literal 0 HcmV?d00001 diff --git a/assets/2048/pngs/Tiles/Tile-Blue-Square.png b/assets/2048/pngs/Tiles/Tile-Blue-Square.png new file mode 100644 index 0000000000000000000000000000000000000000..4157e129ff5230f151b2f527b4d33f8df18b5e92 GIT binary patch literal 1093 zcmds#%}do$5Qax{VTRG7C@m-q43Q|DO{+jrRFI2Y#jH&cHDKF>grG$d`2|53G(tqU zEbgjpAW;!AR4c(vAQUALp^G9Rrl<2~bU=TcGc)hJA9r@R(%;)g9E))zFq(R z8h7!(V`FNXPu=xue~;AO9oZDApC29??c-5JOvFV(B+Fe@OvP0~B}-XN%*0K?Bulrt zn2WoFOO{~`u@Dc5kSti5Vkw>yDOsRZKw^|$iroGkQ+3r)&9a!gnYwA1X5m5ET-`NX zv(Td3LOnD>v#?~?Qav?Nvyesvtf2`nh_IPeU<1Z9?bLbTxLKHGQOIKM?iOxY1jB=} zhecQx{m`P@(;_X49k_)(_<;c^Mk1OJ0c&W&3nFX=8f?Irrky$uoN&)#B??*0J;Jj% zi(q(Aj`S=>6AL+DJWgXRUSb|@VGn*_0E&?aX|P}oO?W|s%|L?<7}K;<=Yf+YGps}* zi@9fsko*apgo%_435}G9g&Z&*r?D0+P2;|B(y7>SSu3)awt7ev?$G}wSKO*?fS zIQ;I~M=pHjvp!j^jL`6Zp0S@(%ZQvF?Cl;MnEk%?qGjb+W5e*Ti<>*6dsohE^e<1` zKYsOW?ZJu34@-S-pLe!BTJ3r^{^nJ8`$E%`JBNRCx1O9{dN|iL)-d$)(5dyAd%qXA zHY*=@7QWm(XtlP!x&0m6N4F==UpZI3we@-J>11`hW#9bc1J_Q!-!mrPX6pRU=<3|3 a-8br67bh>@UF&=e9U1Ja^wxSV-Tn)Wd2gBk literal 0 HcmV?d00001 diff --git a/assets/2048/pngs/Tiles/Tile-Cyan-Legs.png b/assets/2048/pngs/Tiles/Tile-Cyan-Legs.png new file mode 100644 index 0000000000000000000000000000000000000000..24f10ce252ac02c7bbfdddf1be8ee0fabf6cd7d7 GIT binary patch literal 1126 zcmds#%}do$5QfKWk&=>7kdjbgTF}O3AtFTC6}_M$Dh6pFEl!&tA{R!qQ3)(Cw4i3y zA~#V=L@7pKnNSxFf)GlJqQKA~>0=Q^)bx1%j1K6Jb7tn9_u~$?wKf(NmKBOfQMM`5 z&iBy9-Q<9rVkXhV)38PfQ7lS~;t5M#f{VFC7f;YCATdfWMQ$4>sG4e2^|Y9K2x>uN zP)|H42U9bRrk-fg9im#)7}XO?hJ&lQMpsXy5dmvx!V4m7W);|gF-<#l9yk_448fB^ z7IQZX(Sj$z@Stol#27s3hZf!DA$ss+2X0{xeqaEKk%%Tlz#5wHf(V;|1{*M@X{XKu zCz^S(5``@09;1137QyhK9Njz_O)TVq@i>jOc!_zqg+2Iz0VqZyq``tUG~opiHUkYd zU`*3aod=Gm%&-!LEavVhLh>hY5++hIBs5YY7IMINoW@$b#12?8j2{?)VkANuELcMm zUJzk3&|m|`H0{)R;PAVv>^QN;=S^RGYX=Sg=jr;f-s`TQ?s$ zJoDvT+26I*q4&MbgDvBI?~31!^xbTlzVUsi`^xplFM95sUTUo?DXAMB*|FYJQ(8W? zH}k~i3hvs#RO#(w!&Q&IZ5tV$&i_?iz1Y3;+QG_|g51TK$^Cg_71^N_J*7D54Xvy-#_Q96>ZG-as>HMqQpHmNx?|U}eGf{qWb>c!@P51AI Udum_Ky@s!38(K4y^}PfC09M6>P5=M^ literal 0 HcmV?d00001 diff --git a/assets/2048/pngs/Tiles/Tile-Green-Diamond.png b/assets/2048/pngs/Tiles/Tile-Green-Diamond.png new file mode 100644 index 0000000000000000000000000000000000000000..bd145764f30db32f03537e714f5939731fc8988c GIT binary patch literal 1205 zcmds#%S+Wk6vvNNzHWx1RAz;WT__}~K_aXq@v0Z=Vk#yHr6^Mpw6GQ;uF6ac3f$ym zK`7WFv&SlGQAuRd7(r!9MTwENDSC;RFnydqqXGRfbI$pm@8fosMT!#>)+C5XVz{KR zobR^9zqlp*-#FQPnorb;^5Oy+e6jYcNc8!#y_F#zm53HGB38utyHcW+7$sJT^JS$) zYcX1^7U#Q_5pBd6u|}L9R#vnXW5rr=9!ojVPK*=l#Cd2XATdfWMQ)2nD`k|jN;$un zyVlBRWwmmi2W4%PG0Ga{JT1DlRmLi7mGdka)=n9xtW(aDMg**(2``ASnN?r|#x(8J zdEgkWt=7&fWHEPRv^Cm!!SJALthQD=uOC`;8>g+)&f9@o*n=M!fMO(~2@$Y{CcGfR zW}v|ajA`1b^T4sjIBz8iSpaG*TiKa=>_;##+3@4p=gb z9~gjQBtjZ2SVI$D5MeXWU<1Z9?bLbT@Vg6E9Gu}Zak4y8LBs!fs%D$}5cw-yxTmtD z^Xk~+wfl7?C6GA*E7O*QSugL&Nzwp>5dI}n{-lQ9+lac)SJjK>6?7Z$vkE-ib|+SAtkWnb@@YZ$)$EN<)N zhNr7j(pq=?=*}Ox@Hu}XZK3DJ&`3ek-PtQS2acZk8&x;8;`RPV3m=d#;ZUS-u%PzT EKl`GessI20 literal 0 HcmV?d00001 diff --git a/assets/2048/pngs/Tiles/Tile-Green-Octo.png b/assets/2048/pngs/Tiles/Tile-Green-Octo.png new file mode 100644 index 0000000000000000000000000000000000000000..396c26793b71dc0c6f455540d5ed4bc29a65c636 GIT binary patch literal 1129 zcmds#%S+W!5XYyqx?Q{>)Iy?75=KOzjs-1>62+^CiH{N&5iQOl!oXZe3-LuIU0bvW zM<^4Es7R?nLwWT4K-J2DWTw}hj zjc@hlU#5uvhrafY@Y!;;t)W(Cm-c-V*?OzBxuc#(6$v6H5=A`SRVApHN>uSw)*vB> z1&KjCb*o7*F_UQGX;`C#C>A9~@r0!=!NpvnizjFmkQk+xBDc*GR82LidRojq1ht?s zs3#tjgQ=NDQ%|($4pA*?jOvLc!@<>DqpK&`*!XEs<02Ct;(qO?Fn(%@Mn}G%! zFs5mz&I89&W>|?r7IXI$A^8(H2@@$95*jHH3prpsPGc=zVh1c4#t#fYF%lsS7ObHO zFNm-iXs`iens(|uaQNL-w0E!ZS?OzQX{X`;JZFFR&mi(~W8LYF{Mh%8Z*qf`nQhBG zt7RwVJX@G~KDj45^<`mXhugVpgQIQdZ%$NRY+oF|e0O8)`1--&@t;#`8_Vy1J>69^ zJ6KWrdbGUeRPpxgrvvjYnOy16(CV>@uD%bKuII}qEkCf(ReP`L*}KmBMOCkk T=*nX63n)uteM{YJ?S^Wbb4f1l5*bTFEF^_^NMvXgkQk+xBDa-es!7#VBQNG|rpeSzBlDna zNmEi!8krW|=9*mHH8M+vEi{FCXk^lefHgGX1rav03T(ibrky$uoMh%^krlF-drIcX zA`6BG<>cmWk@Z81?iA)>k?p`O?7F1=gp;|&iZ4oCpHW`TiQDQv1MX(a_7;)Y-45V z#+BvU-F0KXzD>O^zdU_pVRvb~c)cOEkKHPq+VOqU;o~z0nlIk0dc53!Z{6<~gKHmM i-F$7n`@zHF-4o42i*-+49jttSj&u~d+NN6v&;J7(xN>X& literal 0 HcmV?d00001 diff --git a/assets/2048/pngs/Tiles/Tile-Mauve-Legs.png b/assets/2048/pngs/Tiles/Tile-Mauve-Legs.png new file mode 100644 index 0000000000000000000000000000000000000000..b67d0a36d60c553220f4064992cf67d6707c3b62 GIT binary patch literal 1126 zcmds#%S+W!5XYwnp^_pNsnJs?v^M1wwFyx)?+r0iX%Q_7>PQ5Jc}rUbJr+JTx`<8@ zLK*~u1w{}9K@=s7VGuErn`I-oz!nVIi=AGfcixv{WdZGngsW;6Az ze5)7#@|N+x>SOO^K1;e=8|!4^^}25&O9xt-+8cOOksuO9OvKY&Rf0-XF%?f`4HAOH zAQr?^w?+w3Vib$wX;@8yNi;DNPgv>_T%wD)c!E{|iBWnfa$7t>HL9Abr^Vbu&=}N$ zdg4JjM2%4`swY}>2h(V3rk+?b99*NTxq2dv2v|cCUJzk3tH1_~Y1*mtz=+g2!;n`^XSQFVj%~N$7!s^OU%P9?7_4Hm4S2``AS8ECKp zW14pAJa9Z^hLtE}F?UZ9l0SiyFp-iWp^*}?kORi!G}huJcEFNh{J;PdBN5VI!5W(I zf(V;|1{*M@X{XKuhu_`Swj*Epe8{ynx6$x_p2NR-M-e%et*>p*^v%DkExNiRZ~63w z8|(K~z8vbmlWW_*s;79*)JW;y-CZ*?Wp7SRelH)I)dwB97dAY2^7iz(8nevm;>S}Z zllO0KS=jrd=j?F)!Ic%)d#g`eT(PRB|MRuu&(6P^9q8Pg`MIriEHhN`bol*9*XfR? z=Mz`9SLHs9&;4n+H-2lVUZWQ7wuS z)y4@I!n6ny+=Mi>x(P0_58R9p6xgP~h=|kU{WH3tKkhx}yyyLxQX${l)Nr6dM4EDa z*&^T0t-qc1{Lj=Tr}%8UR_yJT%5uv$k-F)^g~1*kgNTZlNEGpO4?#4D1&KjCm4k|^ zm`YUf)E!LJ#7v@zr{NGqqga#}#S>O=Q5SQGE}o!+fW#=h6uE634Z%W)A$VHMT{W17 zsKFBt%4)$ZL<^p1(XBC93^4{zEE!f0<{^6UL>dvWh9s_a&+@#G_jBa#^W^B;w9$c7WUu=2A~*;kOm9Z(1aI6*bFq- zfH6%wbsjjLGQ&y~vY5N42+5zoNtj5=w`fFy4^Nn?Qa)bRl6GQtZC^= z^s zCcezC|0z75e`3wQDqT~rv$Y-DmlmrZ58v#l&Ntt^T-lsi__=c0T95yDvHw*6vvK%J Mt|yBeKWqL(uJGk;w*Pev$$X@HVx&+hG%|k zm?kV6hWRyGQaVi5tXyQ7wTz2yGKL+m&Y$6I_1QVk^Sqz;k8eXsyl`?#T8fBFE{a7< z`R0!OC6DKS=3s3-pK*Ii3nS9{cFtFkgrg;!%GdK~M68I4I1wN2#)vhdMw}5J%EpSd zqE?(0AG(c-RZ$hE;={0UVx6cH=fnprVvZgh6iQm)d!=Ag&Z&*r?D0S3KYbGYK+K+D-BH=lP*@2qWG`D|oT>irjOxhG3Tf2H&}S_x7IA~p0;i9!2ZQq@yxv2b!eBO_3>zHWP9B| DoSvl< literal 0 HcmV?d00001 diff --git a/assets/2048/pngs/Tiles/Tile-Pink-Octo.png b/assets/2048/pngs/Tiles/Tile-Pink-Octo.png new file mode 100644 index 0000000000000000000000000000000000000000..4c81a03b6cbe6c9dfbc7d51c9e486212f4a0eeff GIT binary patch literal 1128 zcmds#%S+W!5XZ-?yoy#R6!aiVx@m5-DT-EQiV7m4)}kOJ;SjCLTDXw%5!9t@;iEWd zXi-MB2?_+6KAu0L1N!5fnfcE5adRE*&1EH z@7|TaOdOrxopdSc0NaE-3!>WMTWU=2-pL4?h$0vj- z?lA_7!INNkP>vSNf+zjZqC0vp51#D6E$qP$3_vjw(S!(CLla&QVKdNR1I9G%)Op}o zjGnASA&a@2MNiHm7#@_(qbH+@g&Z&*r?D0LOBF>$ZnfJg^AnqPW^cSZ)>rjv*U8g!OSzt^<=uz2?CX2|xNx*~ zrg(K-K2x?~Xz0!U+_|>U?R<+TW$+y}y?8Q_~L?md5W5Og>qEd2#CL{FMVE zm80bo^}}`JEu&TCUrNrbDrl>2C~WMRc{y6V@#C{QJI1bs?AZ4DTOE()zn`D{Du-VT U{`h;f;x>$>wW+;vwxMVEAKSxtX8-^I literal 0 HcmV?d00001 diff --git a/assets/2048/pngs/Tiles/Tile-Pink-Square.png b/assets/2048/pngs/Tiles/Tile-Pink-Square.png new file mode 100644 index 0000000000000000000000000000000000000000..5bc36e9a3f11814ac14148343f00b0241dd26d34 GIT binary patch literal 1092 zcmds#-)Glh6vxjh*``IbXo}gzs7UQl6su-y-zi_SWf!|CrPJst6-&EXCR@?67AK}O z>LPYAks>DPrekyy-NaT=w5yaNPOm+GMi1M^^PF?u=l$b5-dpW%sBfwlk%pd1XCL3T z)xX-c{NJ%Oe2q`d#lG$inOofUO=QiD-s2~_cvKM+agh+oa#s~oag|WXQdSc)ag#8~ z(ycD$;x6HmWmrQj#6uz^3znu>il;40!4C{TF%lsS7ObHOFNm-iXs`iens(|uaI$2E zl_+E}_bd^TKY^1lk&+>ykrJ_x1IFVt*5W00z>;D7zyK5@5z=768k+Ee2%CWh8!)D6 zr_KY1-(73};8#8$F7;LWY4|_Sz^~z1M2_}!9yw7NUwQv>>q2wwy58=^-TS8w?y3HM zH+1iF#c!SeQn&r7ZE9PYs63wEu8f_lb(|Xg2eZ;{(f|Me literal 0 HcmV?d00001 diff --git a/assets/2048/pngs/Tiles/Tile-Purple-Legs.png b/assets/2048/pngs/Tiles/Tile-Purple-Legs.png new file mode 100644 index 0000000000000000000000000000000000000000..f3137d78f0285cab5d110a196a24dc07cf4f9cc1 GIT binary patch literal 1127 zcmds#%}do$5Qe90VUio6g<)itLbWKHC_)I5D|#!;R17l9MI8wuO2ed8BoQ~U>{AXT zB;r>3gbHaB3F6Q$A*d{hO2|bJF2b4~&!5o&{c+CByz_qCTtj_rVZo{b5h={pWg7Wz zUHr?>CZS&^lE4f~pFcvO)f5=BhJ(_K}9N>niwPh|}fg2W&e z#8bCM2~lDci{fcmO@c`@F%wT%>JnU{i@A7$Rso4odMR>SJV7<8nyRP8+(Xb9)Pj2A zK{-T?Q7x(`T671~XlkaOSTY=3qpP`kB8>=GLla&QVKb}128?Oisq?^zAy^2W6tb9m zjKN~?Bp4o)qXo0zNk6pcjvmZ|Cp&Npd+-ATP>e)0Ap+LWgcn5E3^dq)F-<#l9yk`G zCo56NV(w^F1So9L;8SH`nFnrXLi(DqXRxVSZ+I ze930cki!$*j3zGS9GuLdRfhtg~9Q^{jF79 zW!>d%+2Kt`SI%5M-!^Bro}L>mE~zNkQJOzpKG8DUxAw^G3k#i3I>v{teVts8UsI!l Tc{9&9Ls+sk^_j8i_KW`jj_85} literal 0 HcmV?d00001 diff --git a/assets/2048/pngs/Tiles/Tile-Red-Octo.png b/assets/2048/pngs/Tiles/Tile-Red-Octo.png new file mode 100644 index 0000000000000000000000000000000000000000..84b566789fba5c280c442b35eaba13612d900a78 GIT binary patch literal 1127 zcmds#%S+W!5XZ*{YLb;nkT<~=H%k$UhLRScUay)Yc2Rq-tl(5cL}Vc?B6^XuYUe-@ zQ5S+xXyd|_LAW|S2n>QYl1MFz78zvF^zr-|9nc@=%*=Pbk2_u%=&h;FREtPW-+}xf z-`%T!Rb~8dUKqQ@r{wZrZ%(G(G<+2)y7VW~@SF_-A#30egtM(L%WMTWU=2-pL4?h$0vj- z?q(rc@FW->lr4rBgD3saqT4(~51#D6E$qP$3_vjw(S!(CLla&QVKdNR1I9G%)Op}U zGf!5ckj30%G*8YV7#@_Pnu&6-Dxban zY{eh_Xt*%{v~X;CymI|_Yu?|HYu=gf-~8hFV$aos)2|*b*S+jnIW)TW?BcbK{efrbYwy$q;d3*csmg9|o-?yD9uDMvz znc24Ebot4q2btdN+@^Qsr4xnQ758R4E^Vo6ojJFsD_i@zKiht)_3=P){&UOQlF6B+ TLb376PY6q2_dtFsH*)hI^1*aB literal 0 HcmV?d00001 diff --git a/assets/2048/pngs/Tiles/Tile-Red-Square.png b/assets/2048/pngs/Tiles/Tile-Red-Square.png new file mode 100644 index 0000000000000000000000000000000000000000..f395737279d1574b179c625ca1305247e04d8771 GIT binary patch literal 1092 zcmds#&r8%{5XMLIpq3C}lo}Ww6p1Kyj-6awbU_wb7c);CWTI}7kPviG0$q(dSTuq} z1YHVG(Y1)EkeBKtc!`3dBqH=s6vXuL{WE$&zr63vJoEh6h0)4TTWe>lh_sc52djJw zyMN7l_}~9=W{yw8rRq>o*4`Z15@}o<9jTRgR1p(#kt7oNt}3SDDoG`BSxwBuO_E9E zZgnvicS$ahhc$^M@gylpWGoG_5D!Tqk)c&UVw7Hr+;)$tx@uC5yqLS0x@j_v%!9JI zx@&TcOp9(y>Pb`5$SfJQP!CO^kx3&0*3g6(MA*zKumNM5cIrHE+$@)%R?v~sl z3x)?}PnMEJ)( za%NbGLKbt693lA=I0+Le84?;P5eqqBJWgXRUSbC<8O9F`Krs>_4Hm4S2``AS8ECKp zW14pAJaG8k^^Q$`<+C|mt&Gv|f1dH5GxrfWTOJ&!4KIA(ICbFJ(dMSnbA7A7o_4)z z>AwD;e0#a$$*sQWcPpjWkB{}Nt`{CnynHdxyVQE`TGx+(&f{|{cj|@lrjh4|Pi)TL z_`STnReAqs>C2Tvw$@XsxA*UK@0>p~b-H?W`}4-b>FPws{>8foFPwb4cU->BH~8P$ cdi_(&<;I&Aiy!t)PPM~E%B9NSTJh}cKSQBx(*OVf literal 0 HcmV?d00001 diff --git a/assets/2048/pngs/Tiles/Tile-Yellow-Diamond.png b/assets/2048/pngs/Tiles/Tile-Yellow-Diamond.png new file mode 100644 index 0000000000000000000000000000000000000000..6a41dc775d9ae7b746d6fedda6d6875c3dbd05e5 GIT binary patch literal 1204 zcmds#?`zFb9LGOnlNK4vk0j>qUYeBLqm`0I({{6Ft|cuG=<@L^vPuBLtsk48jAtcVlw;ckqm5o^R5@u6%~ zRK=<|6(72d6}4imI4eF38z<_-I&n^Xz%pLci}m8X_yBDLBu43_$ZhPXG1eGoj1P;s zs~W4usqujaWwpjyVsf-P_|B;Qy=t0i*D=HdG)~#+`=CGzyK5@5lx7IH8kM`5jF!2 zHegKCPMrsiv(^VIQOIKM&RHLvMKC-lJ8yk3npnsI<8c~m@e=cJ3w!Vb15k`aNP`7y zXu=C3Yz7)^z?i0;Iu9HlGQ&y~vY5LM5t2WFlQ5BzA)%2Hv5*7C<22UdC3e7)Vf?@V z6eAJRV8I%i@PY`Nfd(5erfH|n1Bc(;f(;GBeBK>Q)^4ET|2!K-gvN+{m}5Gat3=>mArWthZB}F5XUE zDN0^wc~-LgXC@KX!tz*ZAQz1{^zUzN&rDf0R60#Fmzs2X$F2PGdlk=%AAdi!<9znj zjnicf2d_`rn(NrsUr{lC+Thpx=CfVvzpZTCe5W|wSeiK2wXA!z&qZ>nA1`-jc5c(H zLwOShruQBG6#KL&d#fPcGt$0fMf1tM*=$X;{^;ZUyD#r2GP~YKcmG*;XwsRsqH76l zTiP*rx%k|#&M(z}JG%#;JX~~jadhFHH!sqsHa?g)n){u*87c2Uy(HGuR`#vla^fF* C=A}9S literal 0 HcmV?d00001 diff --git a/assets/2048/pngs/Tiles/Tile-Yellow-Octo.png b/assets/2048/pngs/Tiles/Tile-Yellow-Octo.png new file mode 100644 index 0000000000000000000000000000000000000000..b84a6fb17c240f5c8392be21b0e0b534a0c624bc GIT binary patch literal 1128 zcmds#%S+W!5XVPTF+)jIFp;1ZfKULE-FC=(P~11AZ%f)GNrn>NVC9G zw1_04g|y5T8Z89n7;X}R3fx3R?Ru&^D@ zJ-Gc>xr6_$8zW)u`%eF?TZ!rqR?B z56Tua1dTyG(W2X;hNv;BCzcGGYjBOOo=77C*3g6(MA*zKumNM5cIrHEf>|{4q>#nj zL$DaklVEsI4$)#XPx_%ncW{etp6tLa?7q-02Fq(m&_fblquwRnjguw)oNFaX6!gfv*Nh9`c z{_Hi)J?9R$)*oFgj*VUa@}Oq2u(GSRuKP#Z{M4s=2Qn8ovoEK%>K<1&&7Xfiy40{% zRytD{uekH(*v7-`k(P$(edC$?g@idHDbU literal 0 HcmV?d00001 diff --git a/assets/fonts/pango-fw.font.png b/assets/fonts/pango-fw.font.png new file mode 100644 index 0000000000000000000000000000000000000000..c3de0ffb5f9a292a32c86b572d735cc8660648b2 GIT binary patch literal 1294 zcmV+p1@ZccP)Px#2~bQ_MF0Q*00030|Nn#C&(Z(@00VSVPE!E?|3pyMApigbq)9|URA_{P*#%eYAsq>rbvJvr0LI)6%qOS^>F=M7JB8sLEwXT_FF3g;;mht zMi`3-cZ{d-UQE9PS$)Oe00IF?Lk@3^KfyRr}gwlLzR@#oXe8-p`63GtFfdf}_vXCJ^a%dirsM|X*-4TE! zybvuTEQ@-p1X9~9j6i{mh_iW;xkHeMEGsXkPGmk31@eMgBy8Zt-%^F&%- ziNc!x;8%?(6!jR_Kn(4qHz?&)4E`ib_iH<{jU*I5_8)Z(3yKIj*^Nj^5QeA;3mb{| zXQB}L_Jwu*s0w4YQ3(}5$4U@Hlbra@c|F7NP8DV?ZaQoA4qeQ{;-Rz;7oe!_G{6Ss z2sN76YGZ#Sx)IJ9q!fh0X!abAbqi%WwTxkHq))PO>WwcFB^ir>_s70BMSAXXqXHJVlcj_;OmkW zt3MWEtAr9ml&3;BB-3%^HIKnk>tD>?K~cbg^g=eWOArl^zleJ$WUVBS?JV(Eq9CK$X#!BMewAnxZ#45IFraI4@y@F?!6*tmg;egYCvOZk zU0qHoeXH5rADrZrC6!#Zxt^#zor%Kjyfsq@CCpxzhPR?% zG~+}Re6+%bJR4^yOe_k@Na;@sB`?=1|7fghMb24-Hla^Fcaite{iD&%V5Xg!En!lz zkVx|ix#V#&_Zxo|24Hl?a55=RHWaNor9+>_Mih!c#PH5YF?KT)_3OjPebcmnW(s1q zk;7|}M@YuO`cxE5*>3V1kP^OsA_^7wTcUsiiJ}lG#zSlk&E4p+C^*De&76RmYCqIl zI8Eo{{nQ=}$M-0d4g!M=-t)DpS2yF?5O&DwNYh~XZ{xPEh}?Ch%<+CMxWg+2Ngrja zOgH3rnNdBOUM~+qY8)&-g^iHf$c+c4Ij0^7nQpoLFCYTH7looh04ur>JMH^U6iDIA zSA@%=sucON6Lhem0kr0RiWF=?xezlPlBu{o2kh!-GhL(6H+NuUFor2Ci=O$PBTr#i zXZ0^8@teLPoHI|8Jw)~nMZ?Ajyy%l!PML8z1_LSp9is!JlO7h{>TGld@8x$CTWw#H z--37acqLI31_BD6gRc+Ide*c4()s}{dA85?**@F&2PCRd0S9uv;Q9^q8 z^-b(IuA}XiZCu)3ef-9mIdf*lqrvQbEFuT;OezuuvrivTHCevd-`{hpvaHM6_DYtM z%JaA1wx;SwSyrln-}7IOzkHU(%Z@Bw#PVI>JCOx>7YDVS?CD`#6yqg_9Ve~FUQ&l= z@C=^76L<`d;VC?YNARfgm3)7WYw&pk|8sc0iGMooU!CX2PU3eY9l!w`!XXS`07q~H zLm0vdoWMJH2j_8=%wf5{IO#oc_ry(OH<@n-_iVP0?RblE2%&Lv*bHf+PJ4oM%;k3Z=lTIXke`Mi0( z>p7O!&hnne!vnN^7^BDNDV##PhxMsXTpzpDGeT=0vpv#! zbiVGUU*vtn!?;`4~t@=8HvsK-hzD8f8fA4oW|2}zi z{!^&?VfqSvh0f2xKHt^9M;_hF5b8T&x`o#7pY>-u&vO1<^5~q$Q1{$)6Rq=pN*C`( zxjvSiQ`tV2_4m2Iz9)02zbB?|&^Ku7`*ttO`g`Pe_?`3ESBLjwwXX&L5B3V|`k&(m DP=c9* literal 0 HcmV?d00001 diff --git a/assets/pango/sounds/bgmDeMAGio.mid b/assets/pango/sounds/bgmDeMAGio.mid new file mode 100644 index 0000000000000000000000000000000000000000..e50939584ba30457e31616f74a444c1927eb3ce7 GIT binary patch literal 2217 zcmd6pyGjFL5QWDJBIpDBosEb_ml&1AOVk*zfus;DEG;YqZS)ED5%y7h!}x{ui*Z3v zNMUuD%Rh7G%xuWu?&&ci7Gfr5<6&_3_KdX>!?}pz{rvpw>f#sP&Mm#Z$FLq{+Tu$T z*&yXvMA7S}ya)E1Der@gPRbkLWh>>Ek%~?`r!z!T7SC4R;G#QXH^fg@gf;2yXRu7lU$HSr7Z0{a|12jw;Gx#YJ?pWTW6 z@{*@{>Lnj}JC}JG^)lwj+RNZFSnfle)J;9r+q%}#U$xCME-&+}TeZzA`={}~F~+xg_}p`unHfyr2B)D?jtTe^*g>U;53r&$Rkme~WpyCgyEpx3M?DO;Dcd>zwXI ze#ZaX{=I+qukH7s-?`kobLp?@Zyo*SRsGhP_WrAV{@UOD%TGP7*!vZGxBd^L&!X88)OcFl6a`fV2Y=?F{5Q0~NyLp?sjz0QChpPXGV_ literal 0 HcmV?d00001 diff --git a/assets/pango/sounds/bgmIntro.mid b/assets/pango/sounds/bgmIntro.mid new file mode 100644 index 0000000000000000000000000000000000000000..8c79b83c185cbd987ff1919da6577964ab08332a GIT binary patch literal 233 zcmYk%u?m7v6vpu{m1h1X747Ius4jBq0$N4bcOfN4T%h-aFa{1~vUS zoImGs>;0t#FvbW5ntK1Za+;wxf&O4w#$x(&YffHs^Z~xv8@gC4DF7WFZ3*G%n8q}v zDa~3X8TZ9)=*qK_lJl~$W%=K49QL-(c0*qthpy_BRP6J+lALDGVW09|$~kE~Noag< W`7U&HawVcc=Se{Qd?h}=65s>ji8yiq literal 0 HcmV?d00001 diff --git a/assets/pango/sounds/bgmNameEntry.mid b/assets/pango/sounds/bgmNameEntry.mid new file mode 100644 index 0000000000000000000000000000000000000000..817d7e476ca1e42a74c61997530c3c6c0ba4ef40 GIT binary patch literal 648 zcmb`FJ4ypl7=_PF!X!i&U}qyCxxzw@8@&%H3)-9sTMQKVRR+uhS6bg8l?mACbJeAWEbn_BDbOJ%A!_S`+k{Tp%{a>xB<-_w2n?Vq!E&R&1#JstcGzI&JO y61MN$U;cpnpmJa57WfN%&&Ti>w(s}2=T!aP->0unpWmCo8QepA=+8a*rM>|$X_is| literal 0 HcmV?d00001 diff --git a/assets/pango/sounds/bgmSmooth.mid b/assets/pango/sounds/bgmSmooth.mid new file mode 100644 index 0000000000000000000000000000000000000000..a56157d7ea53b1ab8e1a3bc4e97f56db11bbaa2d GIT binary patch literal 2889 zcmd^>zit{q5Qo>sCKg66Qe73LNbQ_$A!Cf?!(hY-pD_p<=u&K?Na>=uzenIvDDx;O z^Csk5&OdNgL9!xqO{z~b`)6i$W@k4UezC@wqS-P9^C=mA`-)UH^YPBkcCcsWAI+0) z-^}}k-5)Z_)*7?I{v)Aoh&kzy!ab;?t4Whw5h&`q6}X7~5hxh9l_u+&#R9L+Jhueb|RR=>99W@D|>{8`y>JzY;@NmmTQp b?;2jiCUkXoWp~r7Xlu>?b$gB8{fU1Aw|G*7 literal 0 HcmV?d00001 diff --git a/assets/pango/sounds/bgmUnderground.mid b/assets/pango/sounds/bgmUnderground.mid new file mode 100644 index 0000000000000000000000000000000000000000..92aa5f2734fa7e2943d0f614f7b6a9c96983e756 GIT binary patch literal 4569 zcmc(j!A{#i5Qdjx0is-5<&a~~T#7(Zq69&ROB8~lC{YAOh^Plws_3n1?>s^tpbwEp zs`?`7zji;_UfYTtsCrucyPkh%X8+k8c{KYH7-M$Kmf1F+Mze2Ug$~T>qvyS<@0rzm z^LOsGSxvTg_kYJzZ+@E9_TK2~!_~X-kMCx=-!;qK!2B{AKO3_Y?{ep$)coQ!D6%T(&V$3JbB(@6KY<6rgPeK3JJb4^)-N<(XuQ;TnZQHgVa_~uO|3Vz-qzUG$UK;R z;CI(gWOtHdUlb3#te?vMCF1Xir^q70!NdQCdYkv`s5%EL%@BiWtH&guppU)btrqV@H+y_2lxfPgCnvbOSH` zz|$6w)!nuBz1F@b!d3Ky9Xv&O`Pcb%bp>|yM;>~L)9RJ{UJBc~B`@R`ow>S0SMEA| z9r0N`uj50P@PWxIevnu4JPy22xAz+Fb#D3C+d$V|sOvlN-iddl(ds;ny@c+|`4QKJ z7uLmtcpt<&(P--=jlB)(#tZ9aF21?=rW$P>rLmW&tGs=S{m9R+S)}vtEjTak7jy&W zoVm~Q{ml9u*KN6cqmygp!PfPy##@c~*xR7)%kknzzK))+tLpbko}QoY$ENz`p5i?O zXYZ{%J!f?i-)rUc9p8K9bcMd%dk>7h!RSA~&a?Fv-tg&jQPiF>=D_n@Koprt;irIEf1zrd{XZeRybOFY(osC|dp_gMH? zxFOyoKY53bb^ODwFP^@5*vA+4g^$F0BwqSU;>Axe>+oP#lo#@ZJ@?|v=Qrx-xF524 zj`B!8*X5hO0p0MudoJ{CeciV^VpE7QfTAuy4ZN_xFy}5bD5BdvV`)yKxE-!UhHqYe$|D9j8daZh%)ZtY5wS6J=;_?*fA*#>) z#baO=ww52e)OUpE1(!jgcPHdZ{P+wWz+*er4C!ogeQY_pEcy1^-jF3g+!U Fnm&!X88)N>`6)~c8`ywiU^XL!tp*ea^MN!Q55ofnpgbc3!+`*X c2^sY^4H*m!6T*OW7?2LGuV@Gc8m-R&0L+jk7LMf_?y7ZS^F3BXZ1KQO%F$y%yJ>d6 id8hr9Wmqzz-oXc6ZQMLt*~muLvX+&sWGPF%>i7Z?Bq&t? literal 0 HcmV?d00001 diff --git a/assets/pango/sounds/sndCheckpoint.mid b/assets/pango/sounds/sndCheckpoint.mid new file mode 100644 index 0000000000000000000000000000000000000000..866cec1414b61c6e58029acee80cbffcc8ed23b4 GIT binary patch literal 144 zcmeYb$w*;fU|?flWME=^;2Tnu4djV4{AXrh_#eT-!X&}L@IR25Q~dzLe|-k1bTq?; zN}x1Q$p$V5h7If~3>&!X88)N>`6)~c8`ywiU^XL!tp*ea^MN!Q55ofnpgbc3!+`*X X2_f}14IvB+69Vfi8Ulev>N5ZUsT?F> literal 0 HcmV?d00001 diff --git a/assets/pango/sounds/sndCoin.mid b/assets/pango/sounds/sndCoin.mid new file mode 100644 index 0000000000000000000000000000000000000000..10fb39a7def6dd3eb13f092fb308d77fd6bf59ee GIT binary patch literal 144 zcmeYb$w*;fU|?flWME=^;2Tnu4djV4{AXrh_#eT-!X&}L@IR25O{#(6zdi$0I+|fa zB~TiuWCNE2!v^*gh7H{H3>#8`{1hgJ4QxO$Fq;v=Rs)KI`9PYDhv5MOP@a*2;Q#}} Vgy4FchF~BaR$tK&1~gKi0RVz2Bgp^& literal 0 HcmV?d00001 diff --git a/assets/pango/sounds/sndDie.mid b/assets/pango/sounds/sndDie.mid new file mode 100644 index 0000000000000000000000000000000000000000..dba1e7513b0a133499e46e602898612844455009 GIT binary patch literal 184 zcmXZVK?;IU6oujcsl1|^7cHQvD^Np3aNr#8`{1hgJ4QxO$Fq;v=Rs)KI`9PYDhv5MOP@a*2;Q#}} U1dn=~1`i-@1Eg($M(Q&F0DVa#LjV8( literal 0 HcmV?d00001 diff --git a/assets/pango/sounds/sndHurt.mid b/assets/pango/sounds/sndHurt.mid new file mode 100644 index 0000000000000000000000000000000000000000..530006503534c6470a06c712372daed648cc408c GIT binary patch literal 176 zcmXZVu?@m75QX92PGk_!gBXA|3pk0(G%1i$Fo2Pe=;#ndG6JJ`1V+gw<|$Bp_wJo^ z{cwwb!(h$a50AUjo?Jk_l%+ka$>+iyCvwHVevzK4b&@uLw2GwdNV+OVOLE#Rv-!;@ i?UyXwjv4g^KJX5spMq>#8`{1hgJ4QxO$Fq;v=Rs)KI`9PYDhv5MOP@a*2;Q#}} U1dn=~1`i#8`{1hgJ4QxO$Fq;v=Rs)KI`9PYDhv5MOP@a*2;Q#}} U1fP1F1|J~p52XEpM(Q&F0Dk!+aR2}S literal 0 HcmV?d00001 diff --git a/assets/pango/sounds/sndJump3.mid b/assets/pango/sounds/sndJump3.mid new file mode 100644 index 0000000000000000000000000000000000000000..941b35664039fc6833f822a29505818fc92a1010 GIT binary patch literal 144 zcmeYb$w*;fU|?flWME=^;2Tnu4djV4{AXrh_#eT-!X&}L@IR25O{#(6zdi$0I+|fa zB~TiuWCNE2!v^*gh7H{H3>#8`{1hgJ4QxO$Fq;v=Rs)KI`9PYDhv5MOP@a*2;Q#}} U1pj)Q27e$O0;EHLM(Q&F0Dq|?fdBvi literal 0 HcmV?d00001 diff --git a/assets/pango/sounds/sndLevelClearA.mid b/assets/pango/sounds/sndLevelClearA.mid new file mode 100644 index 0000000000000000000000000000000000000000..5faca34edb0907e3ae0dd2c07c2530339e1ca25e GIT binary patch literal 209 zcmYk0F$%&!6hvo}u!`C%9>6viNU@M2AtXd0g$Gz6U?Wyq?RtY8z(f2ac$C~^9ioEO zn>X`|-*mkNprH_{vFWa7+8QAS!ZAsr%^cwn>#avf@tvQLJ7=EA!a!ygvUHFW-9`}2 zZp!MDckCx8^$H`nE8w-qkhz=BfWeT4%*p)EOL|GK$(sCmK`-bPS&^-KTzt!)QoI3? Cr#Wi? literal 0 HcmV?d00001 diff --git a/assets/pango/sounds/sndLevelClearB.mid b/assets/pango/sounds/sndLevelClearB.mid new file mode 100644 index 0000000000000000000000000000000000000000..f56147fbee0fb9f731177be247d2aa37393b01c5 GIT binary patch literal 185 zcmYk0F$%&!6hz-9VHLGkJb+XgBcM(dX^0%b_pCLM+Ad&z;=KE3pz?`?!1x{qhEU(J_7i literal 0 HcmV?d00001 diff --git a/assets/pango/sounds/sndLevelClearC.mid b/assets/pango/sounds/sndLevelClearC.mid new file mode 100644 index 0000000000000000000000000000000000000000..887708f9508c0a1875c37c1625a42e50d2a1212b GIT binary patch literal 161 zcmXYqF$%&!6hz-{!YXR7cmUg6VIfV51Qb$ufCz#Xl1i(MH^@=;2p%OjS%+x#X6AkP zO?&MChrybuY5SYnmXtv{=egZ2NvF*12U5Xb--$12oy3(VE;`~m5HISTB|2?$Hott( gK6lY=n2pSr!t&_N_hzn0#2jWHDTcXo8XY#8`{1hgJ4QxO$Fq;v=Rs)KI`9PYDhv5MOP@a*2;Q#}} Ugphihh7cg_0i->EM(Q&F0Dp-ia{vGU literal 0 HcmV?d00001 diff --git a/assets/pango/sounds/sndMenuDeny.mid b/assets/pango/sounds/sndMenuDeny.mid new file mode 100644 index 0000000000000000000000000000000000000000..586cc825a2bec9fa6d36925590d86e20ad035aee GIT binary patch literal 144 zcmeYb$w*;fU|?flWME=^;2Tnu4djV4{AXrh_#eT-!X&}L@IR25O{#(6zdi$0I+|fa zB~TiuWCNE2!v^*gh7H{H3>#8`{1hgJ4QxO$Fq;v=Rs)KI`9PYDhv5MOP@a*2;Q#}} Q1cQ2;1_K5gC=Id#0DM^^Hvj+t literal 0 HcmV?d00001 diff --git a/assets/pango/sounds/sndMenuSelect.mid b/assets/pango/sounds/sndMenuSelect.mid new file mode 100644 index 0000000000000000000000000000000000000000..2be02c48a2acb3c0765500cf01d54165f3a05df3 GIT binary patch literal 144 zcmeYb$w*;fU|?flWME=^;2Tnu4djV4{AXrh_#eT-!X&}L@IR25O{#(6zdi$0I+|fa zB~TiuWCNE2!v^*gh7H{H3>#8`{1hgJ4QxO$Fq;v=Rs)KI`9PYDhv5MOP@a*2;Q#}} U1dn=~1`i-@1Eg($M(Q&F0DVa#LjV8( literal 0 HcmV?d00001 diff --git a/assets/pango/sounds/sndOutOfTime.mid b/assets/pango/sounds/sndOutOfTime.mid new file mode 100644 index 0000000000000000000000000000000000000000..31b1d5b93b6ea1c9abc96f02997a5ba744021b6b GIT binary patch literal 248 zcmZ{fK?=e!6a^<~h@$TG1a7^87bqwMDqTkq+_|c&_6P+pk|PLSBsYn_HFc${_urc# zkkVZ|U{PolOLx0UTayY%Cu8(xPC6Fm5lK5Tei811MG1>NVde?TmT;EtHNna@-s*F2 sWgokwT@ewk;CX0z*EXGlzLKmYYss2nXJVOuo$O66+5h*>uYLH+8_8ZtM*si- literal 0 HcmV?d00001 diff --git a/assets/pango/sounds/sndPause.mid b/assets/pango/sounds/sndPause.mid new file mode 100644 index 0000000000000000000000000000000000000000..5712c1e37813a032f44e20f46fbd03a3e4321404 GIT binary patch literal 160 zcmeYb$w*;fU|?flWME=^;2Tnu4djV4{AXrh_#eT-!X&}L@IR25O{#(6zdi$0I-6lb zB~TiuWCNE2!v^*gh7H{H3>#8`{1hgJ4QxO$Fq;v=Rs)KI`9PYDhv5MOP@a*2;Q#}} Xgy?#khG+(x2@ya#0!X9sK~@3){LLw0 literal 0 HcmV?d00001 diff --git a/assets/pango/sounds/sndPowerUp.mid b/assets/pango/sounds/sndPowerUp.mid new file mode 100644 index 0000000000000000000000000000000000000000..4763e03dfbdfd5742aaee903d9bc662259106843 GIT binary patch literal 184 zcmYk0F$%&!6hz-9VHLGkJb{kKj>qlXZxKR&VCb zFMivdBj9mlZfv{jMQu&WAsve%+ssLa+;0ccp6~p??wWOCi-64{w(PN!ddx5{H)Z$9 jJNc=Le#MA(2QPS*{cR}4O02{n2C)|FKi{Y~Kd)EbH(fBc literal 0 HcmV?d00001 diff --git a/assets/pango/sounds/sndSlide.mid b/assets/pango/sounds/sndSlide.mid new file mode 100644 index 0000000000000000000000000000000000000000..ed1d66270f894043d6259285832069ae128e8eb2 GIT binary patch literal 176 zcmXZVF$%&!5QX7?lduuB*GjBxa|KhT2tf!zNReg*!Ol)B^ax(T8_W?rN^Y_~qSZHV z-oVuDIRG9<;>NnYUX)hE6yh<g~M;B4R2w8y*JOZO+6LSg_RPWy1 zmww|e0Vpt7Gd1pZm2F5Fq*I>T?Sgd7%GZum^PBI)L90iQDXsKX~6~{Ksb1JO-#Ib^8_VO$}hoE=wzKBr#Cb2=U4qR z0s)72bJf3JYDZEA>6YjIRFK*%oJUg0s_)oGS|_%>Vw;HV2JE38Jr=Yr+5PiH`_jd* XXGY$^7p8hRjJ4Q^jo68uUYGm;p?4)` literal 0 HcmV?d00001 diff --git a/assets/pango/sounds/sndWarp.mid b/assets/pango/sounds/sndWarp.mid new file mode 100644 index 0000000000000000000000000000000000000000..f4a5e6e66729ee34419152ee493896b20f39d680 GIT binary patch literal 200 zcmeYb$w*;fU|?flWME=^;2Tnu4djV4{AXrh_#eT-!X&}L@IR25O{#(6zdi$0dNRX? zN}x1Q$p$V5h7If~3>&!X88)N>`6)~c8`ywiU^XL!tp*ea^MN!Q55ofnpgbc3!vO|{ Z310O!4PHRn8Av+=X$K(fK)N{4X#gwGGtK}2 literal 0 HcmV?d00001 diff --git a/assets/pango/sounds/sndWaveBall.mid b/assets/pango/sounds/sndWaveBall.mid new file mode 100644 index 0000000000000000000000000000000000000000..f04eb8b265a82dd8ece8d19517d5be819535fe46 GIT binary patch literal 160 zcmXYqF$%&!6hz-9VIyj(NNAM`Q$vQ-jKD+wtc-MlR@|duQg%`PiSkldXlp%YmeY(e`l4_vypm{A2##9-aQ>GxvGA zwV|`$%h%GQ!`<=0CXXh$6jDmncQd&uOlhhvo6B9{N>_Eag*+6Y3{?+X%2ScbR5g~B zycDG@RYRLVVw7Hr+#Zgbg;|$d>7}K;<=YbRM>0Ygn#oQx2!>a|ugL0&2dbNIN(H-SkUTp_%VGn*_ z0E&@_CPcs*n(%@Mn}G%!Fs5mz&I2bSs;xvJi@9e;wX+C@2j#4&Hkw$-0poERYw;5E za0`3z0|QWuL`Z`LYiPm?B5Vd4Y`~bNojMPk>ddebg)HV?9U=J>I0+Le84?;P5eqqB zJWgXRUSbC<8O9F`Krs>_4Hm4S2``AS8ECKpW14pAJaG8kz1shH#pmM7bh1yw|9L*# zeLF$q+GzOpU_L(0X2*K+?9I*k^Rf1Rz24EEmBIMkDBt7MR$~E zd9@w5g+2Iz0VqZynh*hNXu=C3Yz7)^z?i0;IuD$TsJ0S?EasjW)y^Uq9+b19+Gt`S z2aLyQti?;r!!7K=4-7yt5+My1tf2`nh_D%GumNM5cIrHEsx!k%6tb9mb%f+k;3Q0> zWJqYFL@eZh@i>jOc!?dbWEej%0L4gzG+3~PCcGfRW}v|ajA`1b^T6SEcXE3DC!cS3 zC*vs%|L3{(=fN99o*xdbtS`Oz_;hEV&MwUSc(ill?d@Bazutei_rk)v6Ql3%H}B0I rJhyf1cpm+9Q?t)r|9yV(@TJYqXO>=#?)-ka@c|GGSH^?w<&CX>SWrft literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/break-001.png b/assets/pango/sprites/break-001.png new file mode 100644 index 0000000000000000000000000000000000000000..421f27d67cf23a48b5106d522a7fbc04a9f7545a GIT binary patch literal 1028 zcmds#J!_Ov5QUEl8Wut#2#TbzG8PKrG=d;x-H@#4vh1$n2S`AeKcE3C3qe6GtRhUK zg``cLRu!Wt4t!S&kN*JHqpW%h%<=&Y&b3XRvaIiMtIo**o-(T;I_%0s&%^l(Y zxeE(pK8GHS*1Fo6E&Y@ZzaHM0tnz4*OChCH?QSMFg(*$d%I0!cxYAXv+d>|SP==}v zTgp?B%2YKhD|snOS*iwY0*O(2DRMhFZWd-~R&6nNcMG?4tHy(}hecS1Rij0>r$t(( zRb$Dpmql5YRU?fESVI$D5MeW`zy^$I+Ntxv3HNlbrjW(lBRs>a35EydNYC_Y`k_U4 zlxKN0J8%no@B;%-j6^gc0@l!k7ev?$G}wSKO*?fSI2lpRN))n~duCK~7QyhKoE6oK zCKhtQc$~&syu>`*!XEs<02Ct;(qO?Fn(%@Mn}G%!Fs5mz&I6}fW>|?r7IUu_A^8(H z2@@$95*jHH3prpsPGc=zVh1c4#t#fYF%lsS7ObHOFNm-iXs`iens(|uaQNMw9pC%G z=j-FqU`)gRd2auC@&S?Ees5)RYU}x%m5wgX?ft5S?^mvUdNJF3b!=;Le&hHwZ|&cG zdvpKG$!`xgmp<=)JahTZ^54a))2GAxCoa94?QTApE)Rac-#&UBH1$^py`ApHvwzI$ BPJRFY literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/break-002.png b/assets/pango/sprites/break-002.png new file mode 100644 index 0000000000000000000000000000000000000000..dab3fc1d0523afabbe56f36ec58fcbeff17e787e GIT binary patch literal 1034 zcmds#PfOHc5XWb;z$7W6Ai{HW5yW`~$%-z-58YiN>LO-ysDpy5U+cJHem(c_I<9TN0JKsP1YN=V6oVY$AB9ooPc9-v^)4%aC z{@av}mg=dInnfBBu!bhQAi`!=fejebv{UDS<7Q!&rI5wk-7Va*1jB=}hecSH zerVC{X_1y?2X0{xeqaEKk%%Tlz#5wHf(V;|1{*M@X{XKuC)~5FL?Mg0M|hUA2!;pc zNY65wSjYk6aT;s!67z5id+-ATP>e)Kg9U47!V4m71{!R@n5LaN51g#buo8tV=AIQH z`4cz^6Db)I8YvMAIbb|aV=Z1{2P_%J4-7yt5+My1tf2`nh_D%GumNM5cIrHE_}$&; z-TT95__W*fX!t+Ros;LE5xLlD-|Am`|8Vp0yv&S`t>?MjX8A??(dgjug|qLj{@m}! zaQ5=h=^d5sYEOiHY5RA!fvpz62U1~P%&w>6GbbHgH=!q z3-b#EwXhq6Ei42@gCGVAEyOb8llwEgki6VGGiT1n-dP?jbUTMTlDbQa{UP6jyMO!k z@c-oL10z0D*M|#p+I)XxTbjPRe12t~N0VF%DWz(6Gr1{DX{uH>m%GB1u4>&D@=$~_ zRBhN&o{Chas$p5lOHs;FHE0t^jM7Vy+wO6*FiW#)i@Cd7xTRY)9+W*S!ZNHHExJ7| z(lV_YONPBH%Cf8)X+*#pn(%@Mn^^@mU`*3aod-_1r+YPpEao2J8D32=JSazcrdQJs zExMyT%d6ReTiAmi7=U6Vq6rbOh9=kORi!G}huJ=HV9h;0Fev7>SSu3)awt7ev?$G}wSKO*?fSIMp)4N))n~d$kD3 zpTJ3&NXd}UNQqd;0poERYw;30V979kU;v7d2x+ij4NZ7Kgv~&M4H(n3Q|E!h@9xCt z;txI_Zwv<`8vf67;qTg0L@qA%XIFX;uRNIQ=-B@0Uv>1)*~xI`RB!wHJAJtI^33Y` zi&yu*OvayPHwO2%c78wE+IaKr__ep)Lsy^m&OKk-S$#BlJ^s0OrZfJ0^YPcqU}|Z8 K(BGW9bo(D1l~khu literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/kr-000.png b/assets/pango/sprites/kr-000.png new file mode 100644 index 0000000000000000000000000000000000000000..1cfc09624873b009cc18d9e36529fda92150efa9 GIT binary patch literal 1079 zcmds#%}dl_5XWb&Ko<-{4-&l?QFNa=L|~{Zwh*$c5@isb1|#Z_C7n8~r>MX~K@)W< zItZ;0bn6ggMleX8B6=e!D7sY?beTRqe?|}JkLQ`0?|fhOa$|g~ySlk5BHi^vwI<&l z{`x!D@qg*kwLw0WndaEAv{$x#73sLrI6OJZql%b_i-bs)yQ-LqtAt9HvYMEQn}kW0 zZgnvicL|p)!x~~C9ugs0ur$R|JS9@HK&ybnD7_T9{X3@Ws-c=?F?Ta{(=g4#gR;50 zYq(~iMYn}|XoO~A$*`q*YNTc%jR;sn6J8KuGpoP`jA`1b^T2VlFw3Hl#oXO3+_DIU z2W1b7uq^taMYpF#S{6HS3w!Vb15k`aG$8`k(1aI6*bFq-fH6%wbsjk3p2bQOvY2~> zXK@z6@Sq&&S&SwYa=>_;##+3@Jlw(_{J;PdBN5VI!5W(If(V;|1{*M@X{XKuCrf5n zi9!~0&k`Z|6F3PIDH#$PDG>`fU_4G^EnZ>=EE&cR3_vjwAq^I+p$RXDuo-Bu0b`nW z>O64x-R+*3`oib+`R4cp4gcpk^6TPVL|&-Zh9>)OoS9$U^kkrOneA@O?r0drB@u@K$Rm4PGBt){rs$wdx5-M5JYGNjC z5++%y)x})gC0w#JYlww-NQ7j;(iBValt{?}tpXCQ)RH9ncTCk)Lp94_Vl#EqFwMe) zw7I%#xMrb6wS{_Ugl1t$v!!}!q-G(F2v|cCUJzk2qre7?Zpz8?z;UxM%c79M#O@Yu zSp>s_w1-7l7X8qo+S4K}iygRyJ@|nEC`KZh5CLmw!V4ly1{!R@=%$=J51eq%VkHV0 zOdR1^oJBA^NJn}Wqge}kz<8X-TD-(O+`=CGzyK5@5z=768k+Ee2$O*Z8!);lC(i>X z%g(S8g$yRnvO@OH!r3q@Wrr+{EwL8%fblquwRnjgu%sD3FaX6!gfv*Nh9h9E^p95OcfUATYrmW6dOYx8xwn4*&+Nw8=DC;69hEIN zDuY)JE$;mKt#7fmZf5$|wYNjpZ@*p`JaFmM%$wTw$q$_qpNAKwzOQfm?%p+fcImQJ P`1gu5$6EEy=$ZMy(|}&n literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/kr-002.png b/assets/pango/sprites/kr-002.png new file mode 100644 index 0000000000000000000000000000000000000000..1a7b6d967c5cc79062f46840ed85d9beef75ff66 GIT binary patch literal 1066 zcmds#y=&895XUc7u!IK1#iACR9I`mvCZcUfX;USXq{bJdbUKUTREL5n;-tiif)2O1 zh`1G<99+aYh#ZBggB1k9K?N5x6bIq_`1~0@(EfOyyZhevCAVhVQ?=^es)*ED)6EXw zUHpv>Z{z>#Yb!_jRF*nZ4Ov~^|3hS`KYLZW0ug$HGG zb=PpsLW^z-_0R~-!jfT2_0&ksLK+dUh9W?_~^A&a@YTexKr z3=hg47GYWRLyK-ti?l3u;1>4a2L_-RiD*Iutf2`nh_D%GumNM5cIrHE!aa+XC}c7B z2+!gyg5g0q(z6&%EaZUkIE}SpaG*TiKa=>_;##+3@4p=gb9~gjQBtjZ2SVI$D5MeXWU<1Z9 z?bLbT@Vh(MJ^P){=H*ViOT+(pdRxnn5V_cDPRxxySh=%NdpSPbICFDiGoIC-oPT%m z)BQtNs}=d!=pT8rTsgIU?Rsr+>}P#pUi!<=*B%~z y`>o@Do>qF(pX*y2W8F(LBVTIc@Ar+pxNv&U$ydGG^Itz4XQ#9#+s)O+!i~RxIADeV literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/kr-003.png b/assets/pango/sprites/kr-003.png new file mode 100644 index 0000000000000000000000000000000000000000..7ffd299518bfb8da8462d1e4e88b1f5a4b0ad32e GIT binary patch literal 1061 zcmds#J8P6t5QZlSNLY<{2^hp8Hd+}2gEr!#%f`4OOWch@v=N0Nb_OkkK!_>4purRh z4i<_wBACL)!X`ox#qolLAXauJMFhcsHsj;@86HSJ&Y78a-pgJeX%6>PwpK)>r*XL6 z;=6&rzV0smFHX%?`E*=p4cBD;{kA2M&Kn~~M~8S+5fgEd5Xo{^6;pAQP{~qO6Eks> zFv-%bF6QDc;gV%oLoCEYA|wlzrdW!nL`oKD6_6OEmm;@+$5dT4RI@DRZl-P;rdfDU zHdl8I*DSQ?wong^&@3z&wp34z)GVYC0c&W&3nFZ071)3=O*?fSIBphZSroFEySs&3 z7QyhK>|qg>ML)FY_OwXLVh3(v4}M?(ijjyWM8F!F@PY`Nfd(5erfH|n11H?GScyUw zbC2*W&LS8dlp{Th(ZoUy7?0Cfi`P%83zqk%x AdjJ3c literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/kr-004.png b/assets/pango/sprites/kr-004.png new file mode 100644 index 0000000000000000000000000000000000000000..06c303ea35be57970b890118b02243eb13c89709 GIT binary patch literal 1058 zcmds#&xh1;7{{M!$h0+-6=O@sB9?gWF=NK=Fvgf&$r5{d=vr;6Lux6ti4G-?s6*L^ zmgv}_Wk@_a?5R~+me?*?9Y%+EdiD7;`mp=;`8?0_e%?Rk?w~(YpO~5uk$QKwJ>+|k zzecUf|IzjPr} z;hKdO-4^Ph5t@Z1!-Gb9c9J%OV&a zlszoMvgn5v-JTX{S?s_q?7WJqYFL@eZh@i>jOc!?dbWEej%0L4gzG+3~PCcGfRW}v|ajA`1b z^T6SEcYJPPhtKAvVSkQ>|MQ#~UwMwm#csPf-*~k2VEyp>Q?=H)JI&2_bMn}M)#;bJ zlk1Ob`(8IHPj-4!=eI`9<+GC;^~)c|E4v%lp1wG=w0f&uJ@Kn^;r8dBqw$Tcy!rOq oyUN3>wO4z;KAZjW=g4S(?_+hLGMN7UdwZK*((UxyYpuoQzv%m5*#H0l literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/kr-005.png b/assets/pango/sprites/kr-005.png new file mode 100644 index 0000000000000000000000000000000000000000..03cc9a66b28f02d41746a6993b48f6b528301294 GIT binary patch literal 1071 zcmds#%}dl_5XWcrvKUgAK(`Lkp-w?CV7EY7*`%~(6%W-$P|QK-(wlp!gAf=MLFHk> zTL*6&9ipI%frns+JV!4O5g24b2QLQGr{~Y;0sZkjGxMGA%ie8HHwFi410pgwd9vQ( zyN|!2YCr$iZ$CJ}r*f^;7?8CCjjeScr#2NER$ju@p~H zg5g2g!y+t;erVC{X_1!24&1^X{J;PdBN0uAfHgGX1ras_4K`p*(@vcSPPk{W5``@0 z9^qM>MKC-lM|u{ciG>_69;dMuFEJ0dum?Xd0L4gzG+3~PCcGfRW}v|ajA`1b^T5fH z8CIf@#oV(*Nd5#)!bD1jghoolLJk;@(^!j_*a1t1@dE=;j6_I-1#4)+3nFX=8f?Ir zrky$u9Da9)XU=Z&`Eb28JwwC)dFFOH&k%WKvOYFDw0z^?+sfLpYG3=^*ysB%YD=|S zPa5aCwWZ-neKZ|-!@ug15Z{j0y)mED_Xc33Qv6VvtX_{GJ) D83|{o literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/kr-006.png b/assets/pango/sprites/kr-006.png new file mode 100644 index 0000000000000000000000000000000000000000..e90a0a0adb50511c706e86994e9dc019c98bf4ba GIT binary patch literal 1061 zcmds#y=&895XVm)ER6*_2th;;r4A}8hm&BnB*du1kXDC4i&D7J7h2K94jGhElq!fE zxD^+N48@^1l_&+v;ou^IvnYa_os{$A^Jn-#`{Q};?t9;tEVf&Xk>S0=A~MoEU+eJQ z&0lqB2mjwKJUYW?+gztHA^i{gzKd*MYG0bF^Qa;w;vylE<*q8G;wqt%rK~1q;wE8| zrCVLh#a+TB%dm!6h=)W-7A#G%6io*JoHNFxH)(1aI6*vu-h0b`nW>O64VEX=YfWHEPl3%4wS z;X&EMA}ou3XwmIyk(R{{+`=CGzyK5@5lx7IH8kM`5jF!2HegKCPMrr%xM#5vg)HVC z;aQwTFgz$ndKROJg&Z&*r?D0ejs#M9wyA=ccO9rkCH3%E_U(AD1gHb+h~Uz>&e_ z{U;vG-Dq~(zgE}n_`}}T+2^;vyqdXwv-};Gq*1I^m tcJ%(AgI6}zD~F$K%=Xs?zVr8kxI%(Ehwf@Al-d{jNUDN;o literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/kr-007.png b/assets/pango/sprites/kr-007.png new file mode 100644 index 0000000000000000000000000000000000000000..0de9825c5cea305ffac7cfba72b4ccd6ef3b40f1 GIT binary patch literal 1080 zcmds#%S+W!5XUDYFfW27fmomrxex?thzd$lG!^1@@r4CO5DYD(7DgmgWDDs*50HdG z+SEEBsJ69m5jh0WqFNM4EsAgvQOl-J=g;VX{y1l5zVm(D>$U1&V?%3$h%^ol^^fpv z;;*HCBmY+}-|FGBVP<5oPnKWr_$E?!vvy=`fJYTE5f=%OEO%8g6;}zBEM+w@6E_Ky zEZyp2F76U8S%x*lLOdiwvS4Y7rFcrDWPw%ziBWnfa{G5o)m1|^%VO?k>ZW0ug$HGG zb=PpsLW^z-_0R~-!jfT2_0&ksLK+dUh9W?_~^A&a@YTexKr z3=hg47GYWRLyK-ti?l3u;1>4a2L_-RiD*Iutf2`nh_D%GumNM5cIrHE!aa+XC}c7B z2+!gyg5g0q(z6&%EaZUkIE}SpaG*TiKa=>_;##+3@4p=gb9~gjQBtjZ2SVI$D5MeXWU<1Z9 z?bLbT@Vo06J^q!?hjSyxQy_I`}Voh{8> zZL>dGYn7qHYp-tq`L%0yrtQ$v%2diTrFbD?8yda`|K z@3kl2YwKtC&!1lTd4FQ=^uqk9t}VOwJXn0!y!8C@`kl$u#Y*MM&VA3S?LC*apBO(g RzUeItWO$(3zub57>R(-eWM=>X literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/kr-008.png b/assets/pango/sprites/kr-008.png new file mode 100644 index 0000000000000000000000000000000000000000..9513601ecfed84a107303b5e0ee3f29211b96f25 GIT binary patch literal 1085 zcmds#%S+Z#5QfLZ5J4!SC=qcHE~20sCS8n=HhT)m#n^P{wcC)Zt!Temq!&b5f=%OEO%8g6;}zBEM+w@6E_Ky zEZyp2F76U8S%x*lLOdiwvS4Y7rFcrDWPw%ziBWnfa{G5o)m1|^%VO?k>ZW0ug$HGG zb=PpsLW^z-_0R~-!jfT2_0&ksLK+dUh9W?_~^A&a@YTexKr z3=hg47GYWRLyK-ti?l3u;1>4a2L_-RiD*Iutf2`nh_D%GumNM5cIrHE!aa+XC}c7B z2+!gyg5g0q(z6&%EaZUkIE}SpaG*TiKa=>_;##+3@4p=gb9~gjQBtjZ2SVI$D5MeXWU<1Z9 z?bLbT@Vjdp8QtLXd2)DQgogj~9RGcF1(8#gp3dsdr3(vlTVJ)-cb~k~`Fz*J((VT@ zPP~8e{nvp*-)0`I^)0rZK2o#y!`+7Zwz}oDPeU{PXMP+zxNml3~W6hf%ug^c5ZLhA6w_I78oZfS1`^T3LFV&5mUwnIc^vk_JZ)#e$-P{;@y4>-# Y_WIDd!#gVXhoB*q-hrOguJLPs0b~zmssI20 literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/kr-009.png b/assets/pango/sprites/kr-009.png new file mode 100644 index 0000000000000000000000000000000000000000..6aaf31c3535573779cc3a45e6e3bd4925bb4030f GIT binary patch literal 1065 zcmds%KWh_U5XLW!&_7U7R4Ae-v?xeH;XqsztSM=vgpia%>(W8aj)IDI5p+`tx+t9J zAfh-(a8eu!f+C`aag&ZBNc97Vbvb{1KZY+PFZbTvbI{##+3@4p`ER9~gjQBtjZ2SVI$D5MeUVU;{=s z<>Yzb@Vh(ESzPB@yV!1aDEL3m{O>D|5qYjrJ2^LYw|l3u_09gF>PBnd$Nt~vyB<87 zJTt%JRR8J3{l?6~@Uh`99|r$yU#YBCmnR0lu8r?py0qSXbb9v4__u2J&`SUE*{Sz0 zq`u|lwaM4@-sdMH<-H3>7r)=!82I(1b8qo@ZvH7qW4c-EO)Xvj2NBa< Aw*UYD literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/kr-010.png b/assets/pango/sprites/kr-010.png new file mode 100644 index 0000000000000000000000000000000000000000..377c86f745ba34ff0a81ddc7f3d8b87a40b93782 GIT binary patch literal 1079 zcmds%%}W$u5XQ&ETm&KNvh+#N2StZsvP&RnYqk`Ns0DTMkVEtVK^h?yWT2yh@w-M4iQX0-#?=lc3UFxbPRikOItgh-ZHRZPWILM2ODP0Yki z!X!(zx|oZ*giDrY4Y3dpiI6N~wHd8kZ(=0ql zo2$EqYZh8mTd0RdXcm?~7(f zMKC-_dsu{J(GM-EJuT9**nwNvgC7`xVkDvo5wM0Pydc74puq-=Zpz8?zzO#(R-%x> z#1WpwSp>s_bfjl7nzgV8jK^uL#Y@b?E$qP$3_vjwAq^I+p$RXDFd1mD0i&C8@;q>| z>Zk z!04u&JP#axcbf)|eCPT&(%%@M;Qu^_exG}Y$kBSOYp{Cv)U8YHwe^)fho4+N`|{@K zo#u_-ZXf&Fa(u_c8@wn#;4Y9-_-fT2R|?X#YjXGB47=NYI2&fA?2x6gCDy_oFdnC|7B8^_mNerB2A~*;kOm9Z(1aI6m<%-7 zfYD7kc^)|Y?nY+MZg8#LsG74B{GaDcZ(#+I^Yz+zYxv>S?$WOIzU}?pu?ORC=HB0z zOV4}D$A3Nj(>~q!Hu`#S?ckGY>*(UZv+l9#+Q!Y*6OYDjPxt)Dfqb!lPpiGWFn{lg zeLcGVedJ5~)2sClkI!9P?Yup7`16jxgTGJB{p_pGH!l0i%gw*J)F+#@&cubqf0spL A1ONa4 literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/kr-012.png b/assets/pango/sprites/kr-012.png new file mode 100644 index 0000000000000000000000000000000000000000..858714176ac3775aa3e31280eed98b9cb1aeb285 GIT binary patch literal 1066 zcmds#%}dl_5XZ+BLn_kAW*1lU_sNT=g;T?{qa0A^PTU@F3q$WeZ51yBGT8Ks<-)8 z`5Wk|@c-4N>xcPt&9xg7vhuFBDYEU_%(2cSk1Ap!E)pVH?y6!ct`aI)%4%XJZW1P0 zy4A&8+$CJH3~Pvmcu0g~!O|2<@svo(0<8iPqx4eb_V1XgtA=Wp#oW!*O~W(`56b51 zuHl-67Tp%=p%I#eCBv5Lsgat6G$LRPO?W|s&8z|&Fs5mz&I8BI!YqqI7ISyEaLXbX z9+W*S!m{Xx7Tul}X<6*RE$qP$3_vjw(S!(CLla&QVKdNR1I9G%)Op~9dloBE$YSmh zp2b-N!-I09XEB;s$N}SV8f)q-02Fq(m&_fblquwRnjguw)oNFaX6!gfv*Nh9fnI{K;Z%^w4Ve==}8e>Yp7)I&+t&1{?L^ z-|I{32hZJ^e*U_9`OW6-1CMJ%pZAZC?HwHX^tCen?fCqow`Xs$Q<{^l`pU%VtA7D* C5nz4* literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/kr-013.png b/assets/pango/sprites/kr-013.png new file mode 100644 index 0000000000000000000000000000000000000000..e3d0017b6de6cf8ae52ce4c726827fb385d2c614 GIT binary patch literal 1087 zcmds#y-U?$6vxjs2$3vnib{xrz*?d+L{MU)+|;p42S_2HR&m5mGgo16Z$ZTfZg@cEfW z!~3?+kGyKyEbX-eyFRT9^pDGinU=|=g|SnwM;jNreol>CU%XY{{itiYqrG5gi48m=;csiZxlgu zHHc2zp(yGmNDvG{m);1vbf{woqq(ai%#wD($r$-$go?rY5JycvKM+agh+oa#s~oag|WXQdSc)ag#8~ z(ycD$;x6HmWmrQj#6uz^3znu>il;`PmR+_PATLKbt6 z@GQuH6W{oJJlkxvX!t+R(VyoYBJxzdHZt9JfBw!=b-2H~a{Tetg{QaHduuCS z2M6A6UAygFL(eOdTUw_(+XsfP&yCz@&s=`izw^VT+1^dR9xX4QZ!X@o7aM=v+x7d< zlY#wvKOOE|Ti^Ys>-~dk+unSc&D9HYCkNlHyj literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/kr-015.png b/assets/pango/sprites/kr-015.png new file mode 100644 index 0000000000000000000000000000000000000000..60a3c832219b123043e00e9f3081cee26b2b637b GIT binary patch literal 1042 zcmds#ziZQB5XP_SP>P|olSn7U;v$H*Ikh3R1~h~uD3*!^^`>^x!P!YkK@bFQuHYaP z97J#saVQ9fAc#_MC=L>*x~P+$pWdI*7uqlHdw0)0Kk}g4S!_%kn-Gyk`$DV7cWUo% z{2>2Nou2IT8N1zEY|7?`nI9qthTV&U1s+wzL|i09vhJ#4Dy|YLS!Fdb6E_KytZsEN z7k3GltYHnY5D$rvEG$j26iD>JM_A&a?Z zMM(YxPQpY=hJ;2+#6k`jkJDI-m)HSIhVcUfP>e)Kg9U47!V4m71{!R@n5LaN4;+4X zv;CzJpY1!nPM?PTc`pCHyMf4VyEQ*J`grZ($0Kjgj5n{{pWj}8vGm>_E&u)bZn$&x z*Y(lsn^QZ-cZY{pE^RbEoj5nMI(T++?Cg!*(HnU=yLG6u^>psv_g7b@zkYji{`2Im U)aPH8r~iO0?S)Qjv$=BbKM~wmD*ylh literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/pa-000.png b/assets/pango/sprites/pa-000.png new file mode 100644 index 0000000000000000000000000000000000000000..d9ba2a2a3c5473fdb622fb1f5d5563758b0fc7ae GIT binary patch literal 1081 zcmds#zf0F)5XUbKl5a@Xl9+DgP)l)cQK%^<#20^IAcawMLbw?z4IvUyXd*_$VM|D> zLw`VFxMY_?&=9nU4v7%7WKGwr=g;T?{qQ_@_rCX!?~}f2Pkp7SA|my@m%9e|Ht^S2 zSIhsk2Tw2ZIWRHM(DmGjWqJ$yg>k?Y-@` zrqh+3wUfKc9lsa)_jgAxoM?Er*uHjXY2(@6`uOOp$6H74wM?#FpBVmj?tI(Mv8!+A Pp&-58)vlG!8#8|aGg4}O literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/pa-001.png b/assets/pango/sprites/pa-001.png new file mode 100644 index 0000000000000000000000000000000000000000..7566fd22e44d6a7f9f9d0566bb01d5aca5b76d4a GIT binary patch literal 1079 zcmds#&r8%{5XNWgBF2K95(%M8bW(7lTY3n8LQhfcQ8T*`tnS|lVAq==%34inwN zLl>(<6haW@A!tYo*JoHNFxH)(1aI6*vu-h0b`nW>O64VEX=YfWHEPl3%4wS z;X&EMA}ou3XwmIyk(R{{+`=CGzyK5@5lx7IH8kM`5jF!2HegKCPMrr%xM#5vg)HVC z;aQwTFgz$ndKROJg&Z&*r?D0`6NhGx7V7PQONLXZweIrHGf+VP+jv#~}hiC|q5Gn(Q9FB;7 zIXH>XBvCY2G+aeOLC_YImQXYlA#}ZZ{)`^b56^RV?|c9FUhVJeX>8cqAR>*`LtO)W zH}lt2spJ3R)La6E!w+g3zsulFAr>gG{JOvFV(B+Fe@OvP0~B}-XN%*0K? zBulrtn2WoFOO{~`u@Dc5kSti5Vkw>yDOsRZKw^|$iroGkQ+3r)&9a!gnYwA1X5m5E zT-`NXv(Td3LOnD>v#?~?Qav?Nvyesvtf2`nh_IPeU<1Z9?bLbTxLKHGQOIKM?iOxY z1jB=}hecQx{m`P@(;_X49k_)(_<;c^Mk1OJ0c&W&3nFX=8f?Irrky$uoN&)#B??*0 zJ;Jj%i(q(Aj`S=>6AL+DJWgXRUSb|@VGn*_0E&?aX|P}oO?W|s%|L?<7}K;<=Yf+Y zGps}*i@9fsko*apgo%_435}G9g&Z&*r?D0+P2;|B(y7>SSu3)awt7ev?$G}wSK zO*?fSIQ;Hf28WmVydNFt8>Hd?JjZ@se1OPH)vg0WP17S&Z?-($UOD*N=2xFjOb%A> zE$T&oleaRTjr8z3qEzhVSg$xaq_4WW$c5qswj0FUQu-_}Ye%XD3d7-SwpY>fL0t**G5Eux;Y%+V0c<~BXk$Wwul|jrR$cP?X0)wIl5z!(P z#%+th$VF`;T0{tKCZT06qSeJHC?cW$YHzjFD`K|UMKHO59{dA09@$i~}K_30{)Dq;hKdO-4^Ph5t@Z1!-Gb9c9J z%OV&alszoMvgn5v-JTX{S?s_q?7WJqYFL@eZh@i>jOc!?dbWEej%0L4gzG+3~PCcGfRW}v|a zjA`1b^T6SEx4U_KozL6zjmahr|K~aSy>%avt?|*r)4lVj@4ngkV&~?O->+vrEp%^x zFwuG7^V<5&p336KUjy}ymyc%749zdw!?RZ|4)$ML?AUj2;M=8o&)V?w{@O}k-<~I5 zYn$G+u1{3kQzz!yOQ(+f96q*K-!@dAZGY)Mx$tM{$7=ii?Ea47U9B6C3pc1}o&W#< literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/pa-004.png b/assets/pango/sprites/pa-004.png new file mode 100644 index 0000000000000000000000000000000000000000..9c00bde30ebd9f4522e8b783bce65761fb5644c6 GIT binary patch literal 1067 zcmds#y-U?$6vt0iP}+rPD6yg;P#PNI5N(EbFLp_8cD)2S>EN+wNouI&4hlhKP|$@A z5>(P8IJGnscW69B5Dn2P*$cIZEe)qn&!5o)`r~=dIp6brxpyWSW0l^4UJ=4-HY#iH4t*2pxi>N1s`02ICgLI?lI5-{rs680lBKLBX5uDc zlBHW+%*9>8CCjjeScr#2NER$ju@p~H zg5g2g!y+t;erVC{X_1!24&1^X{J;PdBN0uAfHgGX1ras_4K`p*(@vcSPPk{W5``@0 z9^qM>MKC-lM|u{ciG>_69;dMuFEJ0dum?Xd0L4gzG+3~PCcGfRW}v|ajA`1b^T5fH z8CIf@#oV(*Nd5#)!bD1jghoolLJk;@(^!j_*a1t1@dE=;j6_I-1#4)+3nFX=8f?Ir zrky$u9Da9$lT%;$e7ezWOw#awo{PV4t|0PCy*kqB@60~9uuo3xIJ5a=@zwIdU-SEq z&#tfDdbFooJ8*m8?oe(0WM|{Y={Ljvv-{^oWoF_0<8#k1Kb?EO@_lCWlz;xxIl6Id zb+Eg&Tee>B9Dlgf_p%3P7Y4?;6YAWYDf}gNfTnr4w?eLm0gyr1`vxzcWpRtMJ(ib!>=IWo?7 z6@RsX<@|qjuD6R%Womr1F0=2}e-l}DwY|5~;88_P#6?0R%UxAW#Z^KjOIc0K#7)8^ zOSih1i@StNmSGLC5D$rvELfUiDV`E3S)f%wVw7Hr-2NR?b=6SKvY5M>x@nkZ;X&D4 z-8EdZ(4yNyJv2hIuw>X$JvCCZkVXWop$RXDu$fh01I9G%)Op~zS(s%}$YSp97H(Mt z!-KMiMOYU7(4yPZA}xy@xP?9VfdME+BAO5ZYiPm?B5Vd4Y`~bNojMPkaL-~T3R%oO z!m~JwV0ciD^ejdb3prpsPGc=zVjgZ`4}M?(ijfFuuwV^MctM2CK!Xh!)3j6Pfs-XO ztVAJ;xo3%x{0W?diIfZpjg*Lm955cIu@*0}1C|Wq2L_-RiI4^h*3g6(MA!^8*nlxj zJ9Qp7{O&eS9Q?{>;pBL0f`S#3`D6Zw_WR$D@0;21 zX=wHI>y2Av_`RQBGe7+D%UomX=%!=gOWhTX`Ac{0PwnEF%Jtm(`EqaD4VKGTqct*H HKXl*n2M`}N|v&kn2DQ& zNtSMPF&B3Umn_2?Vj&(9Az83A#Zo*aQnEm+fW#=h6uJF7rs}Gpnq@I}Gj-E2&BBAS zxw>n(W}!v5g?ea&W?{*&rFv?lW+9CTSVI$D5MeW`zy^$I+NtxvakDVXqL9Vh-7Va* z2!;n`4~wuY`k_U)r$t&8J8%no@B;%-j6^gc0@l!k7ev?$G}wSKO*?fSIN_efN))n~ zdxU3k7QyhK9O+q%CKhtQc$~&syu>`*!XEs<02Ct;(qO?Fn(%@Mn}G%!Fs5mz&I2b) zW>|?r7IV)MA^8(H2@@$95*jHH3prpsPGc=zVh1c4#t#fYF%lsS7ObHOFNm-iXs`ie zns(|uaQNK~PF~vL^JTWxoTTCZJQufbFC+5Sc>T;&|MTgm?>pMV-CcKX4FB4Au>bI@ z>YmG6LnC`P+Y2vu|2V$cs2%IOzw!3q>7&b)Z-*)ewioyHF0EGQKJ}ldF3L=M_1R$M z^~l6^H1m6d0X1)9r)e3{`ttM>h*gMSuEp?W__i0 GW&STm7GM4V literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/pa-007.png b/assets/pango/sprites/pa-007.png new file mode 100644 index 0000000000000000000000000000000000000000..5fc066b581effd50f9fea1412ca1ba4eab129663 GIT binary patch literal 1079 zcmds#zf0F)5XVm?FyRY@SSl5iU_-w-1&0t6MZZ!aUoAq4q~NGl6OzE7MI@vetd53; zprF8^p~c0*w#%u-g0>)`r3NSKdiDGnJ)j?+=kDJ3{_$NH8y%`NwKs`Kr8+!Nz%CI)#_5fgEd5Xo{^6;pAQP{~qO6Eks>Fv-%b zF6QDc;gV%oLoCEYA|wlzrdW!nL`oKD6_6OEmm;@+$5dT4RI@DRZl-P;rdfDUHdl8I z*DSQ?wong^&@3z&wp34z)GVYC0c&W&3nFZ071)3=O*?fSIBphZSroFEySs&37QyhK z>|qg>ML)FY_OwXLVh3(v4}M?(ijjyWM8F!F@PY`Nfd(5erfH|n11H?GScyUwbC2*W z&LS8dlp{Th(ZoUy7?0Cfi@M#pLRKhOECTh9?WQyu7=XkD7Q|E02eva#X!$EmN2mA03s>${dl zT0X7x^wr72x0e0G!|QWvdp-YZYzI~zl(bln@H(s22 zdhKdw$DyOMZ#K@pZSQ;aY~|2cbU{&M}})pIM|+b0e@={mmq%KOc2 O2uO8sbYQuE>h53QrD3-K literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/pa-008.png b/assets/pango/sprites/pa-008.png new file mode 100644 index 0000000000000000000000000000000000000000..0e6ee046c4c3fa15f01f7db115f78f906b747337 GIT binary patch literal 1077 zcmds#%S+W!5Xa}*<{}B|CW!)}h;|WUxhs-uX30hL(!wHyU|PA+Bt?iY5GcfOB1KUnW;Y2Mi^A}#$VYD0Wm z`Rk}|q-02Fq(m&_fblquwRnjguw)oNFaX6!gfv*Nh9inHwlS{XsSf_PW8!s+=e|l)Gdwq89&8^3|)IRpEvLz?~T-n=weEPuIJyWwUSug#) K_1a?3=#TDzM6&#q3l^Jy5Vb$7_z($*D`Ra5=FgIzqTh>5sJh-A5|imA9tsAMUtiJ7=b zm}Kcz7jto!aLF>PAr|5x5t0Q-fT2R|?X#YjXGB47wXAuk!%8{PMXksA;jK^uL#Y@b?E$qP$3_vjwAq^I+p$RXDuo-Bu0b`nW>O63= zWQLU}WHI+F5t2WFlQ5BzA)%2Hv5*7C<22UdC3e7)Vf?@V6eAJRV8I%i@PY`Nfd(5e zrfH|n1Bc(;u7TlYKJQ0s)d3p*&vWAU#e0aH=;=H%*fewY=F7^$_Qs=Emg*m;D~GD* z+xurnVWdf)w1eG`wys!wXWe_n4{+Pi18GQKdrt#xF!=X3qs dyG9>dzASIHv_1Ipz4HKMq^GOeIoDCY_7`iwYmEQ^ literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/pa-010.png b/assets/pango/sprites/pa-010.png new file mode 100644 index 0000000000000000000000000000000000000000..3dcdad74040a5f6a9ab32f951badff3fedcb2f37 GIT binary patch literal 1084 zcmds#-)qld6vxlD#JXZ><-RhVDZh{d_m_ z*Ho$F|Eo*m2l&*S@9%1p>4ojzMQU&K9UkoDQAJF|MM5OYT~$oQRYE08SxwBuO~NEg zx4M{%yM#-YVGXem4~dX0Sejxfo)RfppjAL(lwOM5{vA_w)lki{n7f&}X_#i=LD^j0 zHC(gMqT50}G(xkmWY|(YHBz&XMg**(2``ASnN?r|#x(8JdEmHNm}ODOV(#u1ZdnAw zgR+N3SQh=zqTACVEsGtvg+2Iz0VqZynh*hNXu=C3Yz7)^z?i0;IuD$1&tfGCS>igNoH$5MYPMlePcj@%v>dv<{M_Si@^sbCder&EAdOCIP_Li}Uh2<~1?$7_O ztURl}8>t=bdwl%sgKe8e+s7+As*6vCSNBy$H?%x#x;(dh>cwRB^UIdX+=;2@(lfJv U-LcJ|?!iL3J9|5(+fH8n3-PdSh5!Hn literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/pa-011.png b/assets/pango/sprites/pa-011.png new file mode 100644 index 0000000000000000000000000000000000000000..b4379d94750e5f700de15471289a8bff437725b2 GIT binary patch literal 1081 zcmds#OH0*Z5XL7(FhMX-Fo`-)Nv(>8q=iMAtSb|VYKi`kh3;O4MXXcsba;}XG*V@~9+C-#%=vcMR zw}Zd#)@J@MUAcLP&xWabZ9o>^Z}~3LbYtZB*dUK8Vj?aQB3bULVk)i@Dp|^EVkT}9 zCRw`G#a!GaT(S&nh=q7agk-_e6ie}xNXY`N0urP2QslONOx0CGHOpe|X6mM4nuP~t zb9L8n%|eTA3-!Q^?1gxP6FNm-iXs`iens(|uaKb%{l_+E} z_XyA8EP~-dInuKjO)TVq@i>jOc!_zqg+2Iz0VqZyq``tUG~opiHUkYdU`*3aod-^q z%&-!LEasjiLh>hY5++hIBs5YY7IMINoW@$b#12?8j2{?)VkANuELcMmUJzk3&|m|` zH0{)R;PAWa9c_H$^Z9&zc$9|!^Ng=vyobo?q3V&b?%6Z9KW=KSv>yFy50}4O-F$z? zuD2(iue_;MUN>@k=bol>{^a-B*~3efwafBhX5*i`a}DX}-`dqOHu0kC(a&mAXV1>@ zU-PFQ>!dB;X?gWPcAq@xr(XV;>$|WvGw^ib`hn?NU;CdeT>7-UGPT;f&ENG+?%hAX P4+=6gI9y#En3(woc>!oR literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/pa-012.png b/assets/pango/sprites/pa-012.png new file mode 100644 index 0000000000000000000000000000000000000000..35878a50e3a40006566e2f31e62a88a394258328 GIT binary patch literal 1070 zcmds#-)qis6vxl@q-J?UT&yP-qqrGS=f({&n|VrWJ$txN%7spn8;vp-ERx4~|q+BRm>_RKGl;h>|XZY0i?ejV3ywCf`b7^RBpxRmM6p?CuPhW%Y z3jVq}D*S(O=E`aO9Mg%;fw>Y)*ug(bt5>Zy^Mg)|~y4NZ7Kgw3o18!)D6r_KY%&B82;LKbs(w{XiM z7#@^8EW)zrhZfzQ7HL`Rz%A^-4-7yt648VRSVI$D5MeXWU<1Z9?bLbTgnJe%QOIKM z5uU|a1jB=Jq-QajSjYk6aT;s!67z5id+-ATP>e)Kg9U47!V4m71{!R@n5LaN51cHS zVI>M#%soqlDl;YvSb9>6Nm!qcVDb%jpNtd%i8} zp1wG)x0WouxvhQt=ZE(%KX1NXJ9@1)v#UFQADR2I@MqPl>b?1ytp}Hn?Os3qwmCVq z>D}?$7h0>o)+#-Z_SNSfF;E57#ZWO^$4QGyCN1VfISBf3UCB Hd+6L>x%g+p literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/pa-013.png b/assets/pango/sprites/pa-013.png new file mode 100644 index 0000000000000000000000000000000000000000..98dbe63d8e25052dc0e6a4654927816f6a9df4e7 GIT binary patch literal 1083 zcmds#-)Glh6vxjtGGjD~$ymFINUT$ANo;MlC10~mzLgQpA*q|9Ey`rlOf~6;6J3?* zBI_bWrHe?S(^N~-O&4ACsff&QK$ zzN`6buCL?&^D`HA@~JsK)YmQZ3oYM8YG(%bjP&xTA|~P@A(G{;DyHHpp^~MnCT8L$ zVUnd=UChN@!X?YFhFFM)L`W7aO|cYDiIgnRDj+dRFGX(uj;Xq8sAgHr-AvsyOtbKy zY_9Gau32c&ZJ{0-p;=fmY^k0asaZ%P0@l!k7ev_1DzE`#ns(|uaNI1+vM6LRcXtc7 zEP~-d*~20%i+*U)?P-yg#SYxU9{j)n6eAH$h=4UT;RO*k0}VD{Ow&%C2Tr(Wu@Z$W z<{sf$oJBA^C`Wo0qltwaFdnC|7B4Xmx3C94FaX6!gfv*Nh9e)Kg9U47!V4m71{!R@ zn5LaN4;+4X8;19PC{?N+4H=n96J4RdPzFr(%YTsRZb8^+#<8xOQ51jw}b9(ER2bCR*<5O!V zAAMMJdsA2QoohYEw$1f#oa%g1eLHsP$n?bPdn+efmtQn3-Tm@w@#*y&tsOIm|I9Ae XY<}1NAffg*Ig) zw5nB+ijE#6(;qM6%pf#Z+7+RI-%S#7x{I zOtN&Vi@CTk1tdo4rO55yF;!O$)hvs-o2i?IX%-%o z&DCAQH481eE!0CJGz&|HE!9&aH4AA(z#5wHf(V;g1vX$z(@vcSj+=#97KJS4?r!0h zMKC-ldsu{J(GM-UJuT9**nwNvgC7`xVkDvo5wM0Pydc76puq-=Y1*mtzzO#(R-%x_ z+#@`Tvj~O<Yckj{&{A{rHM}~9UYaax95gOZoNCPbMfQt`j>k%hn5fSZ+j&R500OD z@n(1L``@ci&R%}@e4_p5+JQY|=a()_Um5p5Q#TI(T4-zJv^|`By*|I(xp=jyUe%X_ Rqbq}OkZO0WGTU`#>@U7WZ`PmR4?Z^D&yBTCt?*f`z$^X}RC;o9Dj z7@eM3d$IN8;Bs?wVf*;WUq2pSZJpWN`rLXmy!K%>9(}*_<;e4$*OQ&qzb95M-hcIS b-<6546Vq>}hPP(d53@kJonCvRdHMD~5)W4y literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/pa-100.png b/assets/pango/sprites/pa-100.png new file mode 100644 index 0000000000000000000000000000000000000000..9eff408b95feb5eb0a38f954c9c007ed9a12cebc GIT binary patch literal 314 zcmV-A0mc4_P)Px#^hrcPR5*>TRND>2FbFeIH@eKk%~WQ}PTWVE6iP_@ah58jV2ni`gb9?Pvp5MT z#NHNXBJ`$cC06dL#JQHu^_r4dd+I#aF4I(1);h!rR2rn=08KA{dpo}D-*7kJ7~;Jz zQH5#?>kefo38?fPc(U$_Pi|ffwim5l?2m?B7H#fxQ8r*dgGPx#{YgYYR5*>LRKbnJFbHI<>*!n)TT`gXThrVl!Dq0YjkHLS9GHQD;D~%_SUIV} zCD>+s$%Gwwhr=h*oxubpOM~unjWOqx?08zapF^aPwEH|zid1w^>HRnU>pR-{?A|Xd zRruqFToggC;a695cP22Nm zphw@(XDuL&1E>HhmI|^8m@{s9g` VgR1FtPx#?ny*JR5*>5RKbzNFbI>}(rr!jbkyXYbdRhP7I1Q_nam_ulEDxY4P;Okr-LQ- zU-8a_oruE}SW4uMEczDxB>H~$#AD6C z8s^Q~=m@uM>^6`=(v+s<)2Px#{z*hZR5*>Tl+kU*APj`>P)Ev4>{QRhPS=M7Vr=tERck3yLcX(oI0X1(uv%1y zO4u^rqTnPxajZn_bq?9y01aF#E8`_u4FCw#sp7H%)|*&;Kz=iQV$ZfH zih^n)8&_4Q0U8}JOVtBMrq;V7%{)BYKZi6V4QX@%kotSg=Ns#qEWLPZkJqY}EPRj2 z+++N0kXQL1c`dwc-!UW%sT_4mk|)CLoLt;A$M0Z?V}wJO5|n?VP<7)!R`?+NxA*~D WUbBIiE*bU!0000Px#+(|@1R5*>5R7)1aAP5}tdZT(KdZv0N??g9I0l^wpdFXtcpoE}=A-+cy6zvV0 zPlzbn2mr*hEp_5FiDA=38Q2qCJ9`n>3tfJJSU@ZxFsiYugBSI0n~hId++N;Nszhaw znG7^o+aBLa)q~9uLMP0*qf!BYMu;cJ@Em~q3rp3SsoOyc)tMK9Xij1W{p>meW=P{j zXg`VF!6wXC2!3}!oPiPb4UuMT_Bps+=m3p_zg6pDh<2A#(1AyHBwILr7xPQs oM1Xmzo~H~&j0`b literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/pa-105.png b/assets/pango/sprites/pa-105.png new file mode 100644 index 0000000000000000000000000000000000000000..974d38649752944ba6ba4882de9d9db1cd6dc64b GIT binary patch literal 310 zcmV-60m=S}P)Px#@JU2LR5*>5Q^^s-FbG^c=_sm+)MVA9p2$lQpe5t*^2qGLB_Lr6%Wu*lESEn1 zLnKuq0Eq5q>l^1u0-GnQz?tCsWsiZg(3O9P8bpl(S*)xHKGg5AA3p7Hdu2s{_WE|J85~NWPE>10tpk9pCO#=6a{y@yNAzB)$3>#*ss{nv6YgNv?t7pL8+r&G zn;2B414m>w0?8tnz);RT;vx|eK1NmGokr;Q;T{laaSB1X=sqK(7qiHw{j`n1E_o9S zv+4p_f9B!6LSi~&Hurq7qlttP{Ev!w<Px#>q$gGR5*>5lU){sAP9w_(~ib9F>A^-*_!S=bSMxdho?ySxctcUVi1D$xe_~n z&y0-%Rv!2tjvKnT<#o=8^)Lgk1X8- zuEgPKcm3MmB>tzM@GzJK;hEoOOKD7l`$YL$SOES2pu2R)H{WH+00000NkvXXu0mjf DHWr8j literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/pa-107.png b/assets/pango/sprites/pa-107.png new file mode 100644 index 0000000000000000000000000000000000000000..516e4482983a356b6dd81869921671c822ccb329 GIT binary patch literal 338 zcmV-Y0j>UtP)Px$3`s;mR5*>5R6(}HAP7tP=0?Mr?3v1$yc2)XqNuc;pN)zV0-{8K6&Mwa>$vZG zZl{79Civ#;d`tSY`35S;*%(P!I-luy)YDlA?v$`tBZ+l-RFILl8YlUrjzfn=@GL2AFnl7x^yDRuPaJT kqDHJK%z3`xe>F((2MGs%1nqDh!2kdN07*qoM6N<$f~*vc&j0`b literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/pa-108.png b/assets/pango/sprites/pa-108.png new file mode 100644 index 0000000000000000000000000000000000000000..617dd368d424d50d68cf0a52f82b96e1a03a1d9d GIT binary patch literal 324 zcmV-K0lWT*P)Px#{z*hZR5*>DR8ba$AP5|K-6+q*o=MJRXJS8E2@SQke3Y6q&LBisgfcW*CjkMt z9{&Js{suIQhXPMReDkS1#gc$R3y~6HH4<-Qsvsh%YAVSVf^%t+xd6G`3RrFLK028M zP%R)z&>SmLhHef({S`IwNsR@=8Jm{t>nYcQ{NBtrNnJx%NCvKPx{XPog&zc!q)B2F zBkp=CoIjwqKwCH6}Cu|E_5P*j1q z;~oG3)r1ozY6wh;YnHGxUBzA&Qrp%|yUxlRK8Y#*H>_e=i7OTFPxX0nYF{ask$nN7 Wu!SP4YZf~I0000Px$1xZ9fR5*>DRMD-(FbK1w8!0n!GaWOvGx0vsL=;Hx^pN^Nij7S~003r(nH`27 z!`Cwc$1^^IAU{l_{ zM0{Zy0I(}PN$ix)L>d^KVR5!Yyw$~n-5{$0 zE(=lCQ>4Y){%|Tbit;Y}C(0M;QnYU2X^2C?VafX?kpNij%f!%w*TYc(;O^e7G^_6D dtY7A$_74UBph}(xvylJ*002ovPDHLkV1jiaj^zLV literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/pa-110.png b/assets/pango/sprites/pa-110.png new file mode 100644 index 0000000000000000000000000000000000000000..6e7a7f81626c2604a1da81dd5a78c80ed266c8ac GIT binary patch literal 310 zcmV-60m=S}P)Px#@JU2LR5*>LR9gPP^#6DV50;JM5q)Mq{8^{azGgyJzp(R-| zKLI=O&V-eSz4lO9^HIQwn2CuKIp9`Ft4fRFhqe~%LpSxgr3HD_Gap_~R5ucPdFVwT7%Adh*1{CF3UzX<$p z_$rEBsc+*ivAvD9h~*h@Tt*f+u>b%EnGF2oF01E!06G+%mW5Fx761SM07*qo IM6N<$f_^1}>i_@% literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/pa-111.png b/assets/pango/sprites/pa-111.png new file mode 100644 index 0000000000000000000000000000000000000000..5ab490066b20588cb5f75511b147084bc4fc01bb GIT binary patch literal 322 zcmV-I0lof-P)Px#{7FPXR5*>Llu>d7AqYgfQ#q1nB4=98WX{z2n25UI?50AMAH%>CS`_%F@$^Fm zrC`tTM?hFtfaEiLz`|4RXw$VOuaL5W0ULlSdDutgqzN?LCPr-XEIq&4{a8V1kAAK7 z8^CAmG(>dX_D%<+4zd9NS*d?l1=)5$RG@w-&z7NP8Ria{T^fw!t(C&qJVD=Y;kgR; zeUOfzq5N0dt10e9umDH~Nqt`ishia2jHfw}U8^$s)L}qM4d@dVEqqS_*&OP>B8K;H zZNsXg{1e~+N`UIo0QOT_{c#skX(qs+SGREoG|s^|%*FHhpYiLv(CU}#9`GjQ2OJcb U@7{BR2mk;807*qoM6N<$f_vYOYXATM literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/pa-112.png b/assets/pango/sprites/pa-112.png new file mode 100644 index 0000000000000000000000000000000000000000..e75ee5ea72298b5cc5c7ea591384e2d8d48e9023 GIT binary patch literal 318 zcmV-E0m1%>P)Px#_(?=TR5*>LR6!DhFbG^6e`MZ7-(=oI-$V~hA{dCBDJKo=vV>LO&!AOO89D)~ z$52*m3s<>G!BouGtJbT$lWZ%A8Q8`|a3@)T0bqB9O+K-L8qlf;n_1vp_UL}fb?_=q z=@v;&yHW9ENVd>bxYPx#|4BqaR5*>Llv}RDFbG6vtS-rGa@SPWS~dkm)Gk0q|xHh01Bt0DI)Dcg#WWE4VUqD9{-MEeu|S3W2FX*h=MVZr|De z4$%Q^*2txkJ(_Yg``R_Xh#sOkkL+Y$r-FSObwH0y&kl?cQ;Ebrk#Ml6I^*iS1?;kA znh9pWZ)IE;(F9ebG1aNGhGoBmSiC0i?Cxy;ntN`$U^vQ^;S7j$nT~lGumbG;?BD&a zPhu|BwY?em<9lFbmw-Fw3eqrD^9}RDU(eYFFtNMN*|Px#`AI}UR5*>DR6!2IAPjR*e>C2dKb1FSZ}JW+5FlmMEVUvuc1#)}_yH;y?Ms0G z_`d!jw)hEYhsA*x5$$>`t63?K@CcC;Q8p2OjID!6q`H}V(a2PgwD05BR}3RzWG!^u zRTjx0ZCTF+Q;SZEZ3XZR71DawB@3;$UUC6VJF}Edyoz-P!7*qG$pw-NX4Kp+@GeW0 zDnip`zd5;*#$R`n4Au20u2L*AlcqR%g5+slRpa$bfzw6@GpOJ|s#VW%9ufkyzu|ka zYhp;=;cOEsf8Pgnkd7xwJMnav@Ps@F4CHAfmN>jnjANGX&*obS6Mn(ve*jn-r-3&L RefIzW002ovPDHLkV1j-te|G=? literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/pa-en-000.png b/assets/pango/sprites/pa-en-000.png new file mode 100644 index 0000000000000000000000000000000000000000..9e2dc8fe5ae0fb585ad67867672372a93a21ea55 GIT binary patch literal 1077 zcmds#-)qis6vxkFlX*0)?ZR4=3wA?Er=+we*7LNj*3;UCtkjTGF4P!tLs?1EiiSE@ zF1T>B=R)E_nv1#7Nh`II8|7kLkPC4k9WS3h!>6`ypU*kxecnHwn`6zP&T3y(L^?-? z>*IX8`0MRxGq zHwlw0-Rfd4?h-CphBd@OJS0N0U}=h_cuJ&XfmQ*DQF$d>7}K;<=Yiv9VU|T9i@Cd7 zxMdLx56T`EVOjJ;i*8Sgv@CYu7WUu=2A~*;XhH<6p$RXDuo-Bu0b`nW>O645J&Tnn zWHI*$&*ChC;XyglvlvY*|z<8X-TD-&#STc+s7=U6VLK-YsLla&QVKdNR z1I9G%)Oq0WyX&8r`poD3h4JPD4gcpk^7HaTM9z-X4^8&oyMDW}`PKH0wm(OgR`S<@ zU9ZpX-g|6kU$bjwapA{?_SM#tEsrm@*G6~T-&PquDZjg}y{L9ie|&m(&(fp8p8e|| zbkzHozxQ;{RaS>jw4H7YOigWk^YY5!=hK&3-_E}~{_NnsPiyl76Saj?i<|DOoV)S$ R%-RRm%SfYHZw*e*{RLAfVtW7p literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/pa-en-001.png b/assets/pango/sprites/pa-en-001.png new file mode 100644 index 0000000000000000000000000000000000000000..a3775116fa6172a9b15e6fa28f79ccdecf03b2ff GIT binary patch literal 1081 zcmds#%}do$5QayukcfsWyNE%w2waa?i;9waF*8KeER-OGhH52b)T)IPWFHWv!9dcY zO|+1R3W_3w7KVeZleT^BT8UCHJ)J+J1N!5fnR(~^xO2lp{cSCsEh5r3aHKZEx1GOg zbA|sOuG~Jzr{Tg#f1fO^b^R1+yg7Vqtj?o~n23vnNS3>*n2M`}N|v&kn2DQ&NtSMP zF&B3Umn_2?Vj&(9Az83A#Zo*aQnEm+fW#=h6uJF7rs}Gpnq@I}Gj-E2&BBASxw>n( zW}!v5g?ea&W?{*&rFv?lW+9CTSVI$D5MeW`zy^$I+NtxvakDVXqL9Vh-7Va*2!;n` z4~wuY`k_U)r$t&8J8%no@B;%-j6^gc0@l!k7ev?$G}wSKO*?fSIN_efN))n~dxU3k z7QyhK9O+q%CKhtQc$~&syu>`*!XEs<02Ct;(qO?Fn(%@Mn}G%!Fs5mz&I2b)W>|?r z7IV)MA^8(H2@@$95*jHH3prpsPGc=zVh1c4#t#fYF%lsS7ObHOFNm-iXs`iens(|u zaQNNr8J+mS=kv_S&?pW6=NaF){0Nbk25N`LstecdG;Ckq)m+&+@oqVPFTOaldUER7 z`JM;$_fPI^Zo9s5^wW;%nWpvH%Etc2nY|sut-ZhA%vPJ`?|%7=-qj&mG-V7MAyZ7uhgVKQlJOql%b_i-bs)yQ-LqtAt9HvYMEQn}kW0 zZgnvicL|p)!x~~C9ugs0ur$R|JS9@HK&ybnD7_T9{X3@Ws-c=?F?Ta{(=g4#gR;50 zYq(~iMYn}|XoO~A$*`q*YNTc%jR;sn6J8KuGpoP`jA`1b^T2VlFw3Hl#oXO3+_DIU z2W1b7uq^taMYpF#S{6HS3w!Vb15k`aG$8`k(1aI6*bFq-fH6%wbsjk3p2bQOvY2~> zXK@z6@Sq&&S&SwYa=>_;##+3@Jlw(_{J;PdBN5VI!5W(If(V;|1{*M@X{XKuCrf5n zi9!~0&k`Z|6F3PIDH#$PDG>`fU_4G^EnZ>=EE&cR3_vjwAq^I+p$RXDuo-Bu0b`nW z>O64x-5nggu+C@oR--mb!~c2C{l5DWk+(-GC&zl8zMB28z1i13xH{AQ?*7LkTRyG) z+1Y<~-`vE~?C{m&ZToNRXgu1}wYK%dmuu6TPrqK&H_yI3@9G_IwW>ev$YkIA(Yf}A z^@~4yS8V;#ME(4x_13RgZoZ%D^#18XR~DxZf37sEYvY%%9~gYwz3c0X$@yCAR22d; MGE}Q93`{)u3ti@BMF0Q* literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/pa-en-003.png b/assets/pango/sprites/pa-en-003.png new file mode 100644 index 0000000000000000000000000000000000000000..3a8531bed94950b881e72bdfb749668b2e60795e GIT binary patch literal 1077 zcmds#zf0F)5Xa9P3dvGJ5_5il;`PmR+_PATLKbt6 z@GQpT1X z=l!*>OP#Y1-fq3PI@8zGdF{&h)UL_8$<7!1_V0dkW^R4u^s|wb->ZK0t=&O4z^(VK)!%OXtI$qzrvpB!;Q1#REwb>uFr^DNuw~d@Tb?ew+ O)=Ragzk8u_V(KrWZEEWP literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/pa-en-004.png b/assets/pango/sprites/pa-en-004.png new file mode 100644 index 0000000000000000000000000000000000000000..8106e846285606c62928ca3deb886c7ed1d00d5c GIT binary patch literal 1073 zcmds#%}do$5Qe8T5aW^%ftUrl&>#?Dprxo?F(XXwl@_gpU~pGafo6*3s-y@hXm&*? zv`DCB6fLqAF}SFpY|$4KwP@?2g6Zk}86D6c=giDI@5jA7Hac8sZfh2i%E+N=jc*Ho ztxXO5Uz@wy$7jo#+VG&PuDAaX*}6D(WPFH66)_PP36U&!RWTJ;36(5mH8B%636m_{ z>S8YL5-wSWHN-+ZBto)aX^N$IN~C0gRso4odMR@IcTCk)Lp94{?q=$yVVZ>pWpj1c zaLq!CZVUC$2+hKhVN3PYNXnXfblquwRnknxP?9VfdME+BBa5BH8kM`5jF!2HegKCPMrr%mdvmc zg)HWtB|`Eia1tg`G9)xoA{KJMc$~&syu=PzGK?P>fMO&<8Z1~t6J8KuGtgiI#x(8J zdEoH7>#86B&S&FnZM06q|9OsWPTfW1$&u>7cj*^e_cEA z=)=J5x0c@rUo^a4c|O(NcK%7%!{vAP`;XpUx$&p|dGCX{H#;VKrh9uD7f!QUMutYK JtAi7l{sLQPWSRf~ literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/pa-en-005.png b/assets/pango/sprites/pa-en-005.png new file mode 100644 index 0000000000000000000000000000000000000000..59a33a43afa4478509d1ba2c143fadfcb9551204 GIT binary patch literal 1070 zcmds#OH0*Z5XQ#~WWs?60+AxlqM-EzL5nEyk|0Mpo{A(Q3ML8{ffWW;>ZU@XT13o3 zI^9;0-=L5Rl9)=uMHk%0=>yazYWnqlj9$<`?>jTkJePB0tTt5X?(G(l%J9MdI^RwF z^>lUc|Miud`}nj?)rb0I>HXI4BJB%fBaHzbRm4PGBt){@RmD_XB~-GM)x=EPBuuh& ztBbj~OSohi)({KvkO;|wr74!;DUp%|S_LFV>7~f+-!WBJ4b?1*xtpn*hG`Zal+D#$ z!!-*nx-Ha0BQy(3hAq`oBQ*) z6tb9mmI%q8z)6@$$&k=UiCD-1<8c~m@e(^=$uNFk0E&?aX|P}oO?W|s%|L?<7}K;< z=YhlTZs+(!i_hot_1ZWM|K~aUbNUe?Cx`nFGD1-fSKl7ajb8jPyJvO7uI1-%r~kZb4t{<4MW$BH9Bu5qw~f6rJW%Uj>N_$2 E7rU%w5C8xG literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/pa-en-006.png b/assets/pango/sprites/pa-en-006.png new file mode 100644 index 0000000000000000000000000000000000000000..3ac23f5d4031bd901fd7931e522fb0c243d10aa7 GIT binary patch literal 1069 zcmds#%}dl_5XZ-aGQ+k4!K6cWNW!HK16^u`Y|R#8+jQyBC7~J!4+YyHiaK~63eCPYJ_gl@Wf&Srs5gBNlu21vb z#oth0FaO`)oge4Z)0v){knV?lKSj1YXq{o*JoHNFxH)(1aI6*vu-h0b`nW>O64VEX=YfWHEPl3%4wS z;X&EMA}ou3XwmIyk(R{{+`=CGzyK5@5lx7IH8kM`5jF!2HegKCPMrr%xM#5vg)HVC z;aQwTFgz$ndKROJg&Z&*r?D03FF8tv0>Be+(hKB$1od0$6IU?s8^%L!(rTK*&JJ)G4>x;GJE8Wq>yH!Nmh6ZaR z+&lSnRJZZ{eE!B!u9ov71AWqb-T6tRvNU{Zte-~}F%cIDku0&Qn2M`}N|v;mn2DQ& zNtSALF&B3Umn_X1Vj&(9Az83A#Zo*aQnEm+fJ7^`B#HhVQ+3r)%`%wSOx-k0v+y8o zuI?JHS!hvhp&lBcSy<9+sh%3CSx6%S*3g6(M3~GdumPi+a`HTI+$_wpC}c3PyMKrs>_4Hm4S2``8+8ECKp zqnmQ_JaG8kb&Za1a&63w)JG}!KhNpk7w;kR+)(ZKSjWnRn@er7w|Zi=egE90^`}3! z&mXwe*nCv2%-sDjeg9eSo#vat-a`wQZ_i#kI5^O?Ftt{hd2#kjqi3S~&+^yqj}NZ4 zukHEv_VB)*<&AgmpG+SEex6w5e=0K6 LU#~U$8dv@TnsH}D literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/pa-en-008.png b/assets/pango/sprites/pa-en-008.png new file mode 100644 index 0000000000000000000000000000000000000000..f6b2b2ad7b974339ee4908cea5f62290b65ff63f GIT binary patch literal 1062 zcmds#ziZQB5XP^mlu`-mpj2@YOQsIOfs+W0DK%JQO+y7i;vk1gHytd3wIYI+>f~^n z=pu?49Gw&tm!ni1v>-Gnif$s}rsv1^&+vuz%lqElbI*_5oM}x}M|O>fNVR#m(dN6A zztQ1A{=dAka*)r!h4xfkdaJv?iBy(nraO~7s)&iWNQh*)tBR?(N~mNhtBIMoNtk5m zRu^+|mvG54tRWWSArX=VOH(YxQz9h`vEsJ1y zQ1-A0%c37zbbDH)Ww8Udum?Xd0L4f|6Cz*@O?W|s%|L?<7}K;<=YbRMS*%1Mi@8U5 z7H1I*56Y3A#b{z72aLyQti?;r!!7K=4-7yt5+My1tf2`nh_D%GumNM5cIrHEvSfyp zC}c7BED@4Ffs-(ik|Cjy60wj2#^W^B;w5&#l41P702Ct;(qO?Fn(%@Mn}G%!Fs5mz z&I5(T>6o@q8FI-_@A-CnB7p5ejucl|x`P$Ou6GOGfUn>2vO=Cwp```b(wQtAT v!p_ZC=eLb-^tJx|z{f*BUQCZaU3_$8%bPo$`Fq{&Ird3&veoF-Pha~BHN0PN literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/po-000.png b/assets/pango/sprites/po-000.png new file mode 100644 index 0000000000000000000000000000000000000000..83a43918638b81307306b0ff7d1c2c7783e69968 GIT binary patch literal 349 zcmV-j0iyniP)Px$7fD1xR5*>5R6(}HAP5V6y^(i)6Mr#e zX69rp6(2$XzMJuo%{!kgJnNBnRRlAGs>ZjfPT!k_3Pc1m+u{|ag8yJE;8LSw?-5rv z-WB}V%mbh81V5#W@)+Nck1>KO**%NwqvS=lJ&5sL<{+ZiZXGyqG_S$>Tj+aP)JufiFB*NL(`Yu`Cx00000NkvXXu0mjf;Q5sz literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/po-001.png b/assets/pango/sprites/po-001.png new file mode 100644 index 0000000000000000000000000000000000000000..be3c8da018f52d0994969c6b6babd64dcbb90814 GIT binary patch literal 359 zcmV-t0hs=YP)Px$AxT6*R5*>5RMD-(FbFeGw^4Q?Gr>;G#QW$$xrAJGNR>*8jh%!NO89^Sz#N=w zVv04$K=J;!eyrdwkKiGm1d&8XF@@^hc?AQwq~yXdh-r2W5+92qyB5?Yy&8Yu0$^s$ zMupm>RFEo$-36*(?)|IkzKOvYVE42M5rK%bWSa#5aCZ<Px#)k#D_R5*>TRKX3zAPjS=Zp2P7Q?e5?TXA8go(BOdP+#45i-%k*UBa_#DCOyU~I)8J*mG}dHQ|7Z` zo;GS&+VOWPg_KzVfJzy}oDxc{gMsHi9-}pxrTh65lv!H>;N~W_6e2S0+i!DfU|(md hpjO-0xZ&@M^Z`w#Znr`9yE^~?002ovPDHLkV1n}zc8~x7 literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/po-003.png b/assets/pango/sprites/po-003.png new file mode 100644 index 0000000000000000000000000000000000000000..c6f90fff98ac4933313469b7199a32e145568b14 GIT binary patch literal 300 zcmV+{0n`48P)Px#=1D|BR5*>LR6!EMAP8JK{gHoyH{qXr6FnptVkEKc&N$HwyR4uR0<6HW0BRtg zzg7r$=mN~#vs2qfwvMr>11ks7O77sDk>huqM^Ie2HEneoEQJ98wbss~F(?z6k%5~U zIANTRnATv0_~>92ml^=)K1YoEPX%@ub1z&PJ8k0%+)Wqwh8#uyEuzIO*zV*ptH%OA zw2_(3RJV+(hk*awD-HnAGQUA2LZ%85p;H%3)c`jES~{!&Td`@29QPx#_en%SR5*>DRMD}+FbEUxx{-H+nP4Z+q<*xfjzi+y(@*Ri2oVtk6SO(!yTeey zn&$bhj3@%DcM%~6g6Jl9gcSq;*l>_;@Xw)jc#m6kr;4h|Q*?od5LM;vH+Vww+6B3L zp3Tm#JXRngNfr^=`j%j=Iwu3bZ40w}lH_bR*}gfnYSg_ zsVfPpl5AgU%X3FuDxZ;^2Jei`F(l!u=KVkUG4 z-C@1|s|AQznOxcUnyO|SUsVSypH66k4P*m%3+9q_U31dw*vI=|B^-DIPtkM(;Gct< P00000NkvXXu0mjf&P;}X literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/po-005.png b/assets/pango/sprites/po-005.png new file mode 100644 index 0000000000000000000000000000000000000000..81920aa4b6fb94c9d59077bc7d13a7b8cad188d2 GIT binary patch literal 343 zcmV-d0jU0oP)Px$5lKWrR5*>5RKc|aAqbq=myYZSYJ#4ui5wD*y14#7mtCGP1A-6>q*!#LR9N-Z z1+1Wm1OU8lrw3>eNI@65RtJOrj9Q>Ywq)*30zeFmT?zL2*_6?UxgNCIPH=aMNZd;J zFOHXJX8m7Pr!#lwpEDrY$=tn{ek90LNBgmaG5v^W-}o|ts`nCBg&n9$AZcSmYv-6n z4<>uuk>HeFN{|J&Oh$D8xWX*L>~Z>*GA7@vT}B4?`x{tEK96b0;7izT5t*`3Rahx` z7a9?V;R!Qntyy*imO-K_MI>UJ|0H;d=}HB$TOLhq>`vPe(P)Px#_(?=TR5*>DRM8cKFbF*Qx)D3UOt4dCq90X^Bx>6GiHUFrc#%*bBGK=yc@0S@ z3*N(N;~}V$C`&%SYQ8t6OUWARP8l4GQ|SsjHr%GIC<~@)bq;qJTKEO-PU8?KFgw~& z)&DCXq9k*7S|+HfdoqBD4+{G7BVu1;W13gcE8Kxp>9K`cekRk`Lxa+j2aFk{hg&Jk zqCWt>jueq|;x-E+LYO5`ShZ3ZiZAHk7#mW{N` QzW@LL07*qoM6N<$g7c_@^#A|> literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/po-007.png b/assets/pango/sprites/po-007.png new file mode 100644 index 0000000000000000000000000000000000000000..a9054c7e42f897096e6944734193376902ca9444 GIT binary patch literal 361 zcmV-v0ha!WP)Px$BS}O-R5*=|RM8d0AP5V6w~;%+Ot2F(aUZRoBJDLl^g%IQ1^{V%OR$jTKr{q7z>Ql8~W{3vh!G}vJee#Bx zftevymbbC&MlLOw88I_db<0&%pgGWd^uG~gCHPx$7)eAyR5*=|RMD}-AP8JC*O5J8P0*7y@jeV5DxPm=k`aX!781e>W;RtNW;WlG zrZ|Bb2ta>DSb^2T!_G)5WUQaNA<|hWb2C51xTWG(%eQiPAHTYK+ z092KLL<0a2kka1A{NoP zI$nV%k-ZDkrr&xiK~*WX(Ru~vNmSMCd=Q-a?;Y?AGLivi9(6Lat%&FDlkp74jKG8a znMOHdTuYErK=0Z_x&~IdfVWTi)qs3vs8*T?9JTOptgWRazJ7r3?#cZ!PWCW4q5AKHP=-tR_H2?qr07*qoM6N<$f?FewQvd(} literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/po-009.png b/assets/pango/sprites/po-009.png new file mode 100644 index 0000000000000000000000000000000000000000..ea7e5c247c6169e237ba643f676ec16b9919806e GIT binary patch literal 279 zcmV+y0qFjTP)Px#(Md!>R5*>@lu-`DAPhqd>W#QldZutE&Xj$uVWD*?llC=Bl?t*S76JeOrfQ~Y zSk&M@ehh$_s#(GOJNc7C0l>;ygVa918vuyNd)Vp{!2PvWY!iBK#O7QV01GmZD<4&S zFNPJYT*ofYkg-+P$d)&bBb d8^0sW`vOi-i@)}@8}9%B002ovPDHLkV1j1Ba)|%{ literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/po-010.png b/assets/pango/sprites/po-010.png new file mode 100644 index 0000000000000000000000000000000000000000..8dd9d1ca1b82e814a1db58e421e3e9977d0c149c GIT binary patch literal 295 zcmV+?0oeYDP)Px#;Ymb6R5*>bRND>2APjS=Zj_y1CbCmz;y!3-pgLuO<=04d1Y z+Y`bHLjltA&0>!o073}Gx(Y~v%qJ0OWXT(<}tVpA=x|(JysVrxV8~_yi zjer-qgOP!MZQ~ozsB>0|w>}j3vIdu3c#38h-jcn@Wf4W@=U_BuC71)cb8}d)`I`&S zwP$GZx17=$x%QOZ<=7s7A7x t?eBA`@96J)U!H$2NI@3?Kj`CG&JXE3fKN-Q0wDkZ002ovPDHLkV1nz(cJcrK literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/po-011.png b/assets/pango/sprites/po-011.png new file mode 100644 index 0000000000000000000000000000000000000000..b79b1184daa861bbac1d6d2c60ebb2be5e66bab5 GIT binary patch literal 358 zcmV-s0h#`ZP)Px$AW1|)R5*=|RKXF%AP8J;t|NPbnxH3Z;vNP?qnGTAGcmfbiwfZYFK_^m!nZZ8 z*1x}}02yY3z|4@sJqSH12vBY0RY_OCMF*i&Ajy=%;4-U;V4q#KAf2pu8Flzx@(7Tj zq86Z3Pz`mB99bn>TeIA(U?#6P7Ll1JTdLZ!i}bmOygSAWGlRQ>hyZ{-Z;6q4MOWb3 zypzbyR>%hccaQx**u2OwI+)p+*#nI~gYxEo7P1NepsNUvMO0P5>{tW1s^<~EyYyr) zTiGg(06Q{9eS+-F?2`o&Ay;*&^%nfPiN4IoP0SA}9@3_86ZtW~3Hfc@P9c-c>~zc& zPy2X`S`Av@Pip2OR#0>z-87k174WhrwuZCEap7%{A1sUDU_>eUB>(^b07*qoM6N<$ Ef}OFLX#fBK literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/po-012.png b/assets/pango/sprites/po-012.png new file mode 100644 index 0000000000000000000000000000000000000000..777b9486dd5a607e893997af0be52cb87e132699 GIT binary patch literal 338 zcmV-Y0j>UtP)Px$3`s;mR5*=|RND>2FbFfJ+bBE1Ot4dC;yxN$^6XivTBWfufk0Tm3w*VnA%y_| zloz?^O;pH{Y-RvJTLF{`t+tIE_-w<}GHuRi4xSVuZ-G9$HZB5GhZI5!0Jtbn>d65C zz<2eNnE~v{8I!<8?xFgmCDK=Ce~1eZQ787dz!)|12ZlxpF!R$;L&QO8?I1@ufkfyv zh0*>^rmF>xk#Z^{!sR63s!k$ZId^3=GlHwS>vf6R`+k_&JGB4I?BVW1B9zE`n1NdK zxJW?jgAgGR@@99{k*hQ4s*qBd2vTcapBpKUvC|yL(I*H0kVm{Wl9|zIs4V9}CdYRc k$#UHpP+fQCYRt#pAF=0=7#2ZT+5i9m07*qoM6N<$f}d%QUjP6A literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/po-013.png b/assets/pango/sprites/po-013.png new file mode 100644 index 0000000000000000000000000000000000000000..50a17a28c854d87b7503a5602d1219a4a4d82ee1 GIT binary patch literal 353 zcmV-n0iOPeP)Px$8%ab#R5*>5RN<|LFbF)18?h741Uq#m-j7N}TK#;Nn3R@-D@A|;XLx)f;v(We zUpvFAKUH$94;BD`nL$LL%`Q^Acw3EFAtJYc6}zxHxWUY*9-3Jwn*xZ4qcKQWyKHVZ znQs`u8e?A`d_iLXXA!fC|B$U6w8bKyt149W(axN$ZUl&EBCUKMpV8e%cB1Kl1~ve2 zRd?-mn6jGPg3Xya(E+%tx_9qxFL1VY@Ia^YfQVcT5As(;l2M(I zmJxT(nJXS&q4vRp3fi;rq@P)Px#_(?=TR5*>LR6!C0AqWed{>VS!o8V9Tru5L!MReWmG$$P-guo~WU;*(EazFs! z^)%s<=vM``=$b%_eUr-pJA9k$?mc%VxFE@uyMiXW!p6u(32>S?c#u`>;O+td?_4#) z%pf8liCQb%y`LNY9^)+m$*yKNTWB|4RF$gAJFFBC0IU=M82Zc*k#2rFbViz?9_tA- z=OZQ$WK&sHr&X60aQCUY*|}V?rKqG6WagN>Bbi!Q^|>RR9yc|s#{iHA@Y5!y(Q+b) zUR><~ld|(aSQxoN_UN(&cNZ&VX)4Xw*1reyf7m_@nh`#>6JxSUj*sr*AJ%`efr07f QoB#j-07*qoM6N<$g1A+NT>t<8 literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/px-000.png b/assets/pango/sprites/px-000.png new file mode 100644 index 0000000000000000000000000000000000000000..5eded032a41093f28e57e9be7b93ff90b36f7618 GIT binary patch literal 287 zcmV+)0pR|LP)Px#*-1n}R5*>Tl*2p0m8_Dv81 zF?oX2>&=TC3^!{lC7_GNW!+js zw$zC{gaUn3AyU9<#4Wt{x#IO)+rn2J+;-vI10YNxvD1)85ChPx#+DSw~R5*>LRNDcAAPl^?lJ3-*x)aWHeiZRYg4i~H;61_-1pX}31wr;7&F=OA zU{Qd<%tW*9Q;0z{cPL$Bm*CdHs-d`@L=W`B+}Jt^R=%4cBIgeRGjb-fq=+uMWG$5o z0H1g&(JT-3R^VyH4Bq=a;MWeOT{!nt007ARIxR`F&!7@*kGeXY+Ql`>&UGeI zuh+f`(LXYhYyx_|%w3&zDkHLthE_CIHq-PP!~b9dh)IZEM8HEbFqlLSYWu~)5y!8w m*cn#+4ULcwiX>6MEZ_t54QsZo4BJWo0000Px#=}AOER5*>LR9g;%FbuQNZZs~-oywWKkG3>TQZ}(jNDyM@Q8$D!L9*Wj>3!Wl z-lItfm<|jWW|=+chLDzR%A5ha&sMV8{{+FpB^V{PG4*3x@g(-!4$)Td$d&}A*JlLQ z?oT3S_G0q0h&uR2vI4IwSjmc!WST+G%rh3&eX&IkE(5H$CNKbKpSAE>6dQmm8!ara zQ(`7|0|2S@^$SUh>6pKzJcig9$-0H?Fo8Neg0yjf86xRKve)~7z;01Iis1GH4eY~oYG+6)u002ovPDHLkV1g@O BezyPs literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/px-003.png b/assets/pango/sprites/px-003.png new file mode 100644 index 0000000000000000000000000000000000000000..d4fdb71cd850b4c8bf369f77307183387775c580 GIT binary patch literal 292 zcmV+<0o(qGP)Px#-bqA3R5*>DRKXF$APkdw-6-tTnYt61NqsaBfq}H~C&8R#$ruO=LGm?0_PLTj z|6@o9m;wwKjw<({1tFcSsXPLfoGW?wK0z>Z3Pz4L8b5BOC-Jp6(G}c^DS>&tN8qtO ziCDQGroI<(4z5UM;JJd?STPa}d%!&k8|4B)GY1Cf!R@>ZHkjAPr*wL(3YOlxU?v~{ zatrFdD+Svz(j=e1)iiL@{6UdaZcFS#s^fjpAV^0uB}jVb?>a-ZfW=?8kvoChb}0}P q>Gd|gwl!(Uc!dPwcvTwo*BXZa0000Px#-AP12R5*>5RND>2APlpsZgk!0X6jCKCf`S2AsBj%gaigVCPWY^1SzfwGUF$< z&wWVHc^Gk?IdfD$4qOBPK)G=Mz*1+b)QtUFEZGNu$&m?yr6v0$hU|zv7xRO6002ovPDHLkV1j_CckBQF literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/px-005.png b/assets/pango/sprites/px-005.png new file mode 100644 index 0000000000000000000000000000000000000000..8c8fde498f4208125d8d916256716297c2533130 GIT binary patch literal 316 zcmV-C0mJ@@P)Px#_DMuRR5*>5RKXF$APkdoY*cpYOxcOdq&^zJ7(-g$aR)Y%g)tD$1j()m()+9L zJC99*>Soxv$0W;sZ)gAjKPx#>PbXFR5*>5l-(JFFbGA@?dL{1Q#(_4iZjuNHU=VzmL~-cT=FLpGtB(Ika~ah zDJxLx7*><<-=16guD^r>0H8EK0Gn5LEk71>@&O(rSO}Ffe9jxd8JMY9lE4|ZkPs}4 zAf#DmNnnFD@Xkc<{ZPLPy!RY@Z3+r*Dfv%aJBhuD+I?CejMA;Eq;UZ=vp%BIer&Z+ zPx$4@pEpR5*==RNJk^APlpsv(aUz&eWagOuiow2;osnl?uUmIY2lF5<&X4x0}uO z3GzaK86$b;baK$n1rS_1W!w-X=4rdzlfXjTA3}x%Ek9(0zAW=!*FO?VL)T3ddKcur zLL7qZng~`k$uUVmgOlGePoWnXinS7E6`s7W|NiTaD|=f)i*zUOPGeT?e{#S(t@X9& z@w{KS8HeU!M6*?uNp}~#_JT(F>Q^U}hL(Yv%ii5MaRXA%{Ay&NH|RtT_D>a7Q0o3K zx=|y^ujrnPx#`$YWLZ3jNfRU+6J!(< zP6l8Ia_dIO2rz4e#|;Vui6Bq{Rr{kH016~%`1<(}8v!$tw1nyx0p}g9J543XC98rY zpsOE~n3-6gSkh*{6pXtdU8a1#Imvq)2o?thfY;^|zxS!I)Pt(*C~5Zd-vUiV?|c~j zCt9OL5MYVjZ^m-l=$s9}9aB{~MwYaI6TA6kXQCiIzc4L9f$lO}V{ehaw)Z9W;I8$%w4c}?FU_+w T{q03^00000NkvXXu0mjf^P+@& literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/px-009.png b/assets/pango/sprites/px-009.png new file mode 100644 index 0000000000000000000000000000000000000000..c2307dba1dd0a2479aaf0f2911f4342426c153f5 GIT binary patch literal 303 zcmV+~0nq-5P)Px#=}AOER5*=|R9muzFbGSWv!m<&S5ViK`(ag{R(?9A2?Xc>0MImiG>c~>lH`i) z@Xisa1Su5CjAtWBOSG3r4#4NQfCM4Px#-$_J4R5*>LQ&ASfAP7tPb|ZVIo@sZoXJQ`}hah#Y<4+(VLrQ*C literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/px-011.png b/assets/pango/sprites/px-011.png new file mode 100644 index 0000000000000000000000000000000000000000..5c18daf2c002e5d4a0314e74c3ab564b44694260 GIT binary patch literal 317 zcmV-D0mA-?P)Px#_en%SR5*>Llj{w`FbIV2qHbjGl$pAdJJa)E|YkOx5X&MrWd@yt@@Jjy~#GFM=kg<`+a6Q~A<3-J`-SFt~I@mNog;kc0I zo4{sg>&qC8ARvGw2y%NWTs6G5QHD%%ZteRMi}!2BJP6x P00000NkvXXu0mjfk*$NG literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/px-012.png b/assets/pango/sprites/px-012.png new file mode 100644 index 0000000000000000000000000000000000000000..ff16dba3b5ef21e1155ac6c10dfdc8da9f416cde GIT binary patch literal 327 zcmV-N0l5B&P)Px$0ZBwbR5*>5l+lrdKnO%%Z*4}J)5UZblg8Bdq3poujI~r*Is#2YqcAXIk9^rQ zbKZdnsh$VH5jc83WH6$x?F?O+1N0z~=(XYYEH24$vEh6!zFFc^at3(M`cCNcrXQ=H z2E@JM89NVV&?AztjnrZvRp58O?MgqZ0A9D=FKo8Tc1*8h5!Gh^Eg6amxqmoiteI#3 zDNK7i^aC$5C=#7dpKk#0;<^((KyR1in?kRkWo-W1urlpwk;eEy6@u+PI9O=c=d*5Q zwxc^Qey*PW2}K1j20=)eX8`S=pj7uFW1k?7{U2@2T!lk#Y1biNT*(T9e#?q4&t=0g Zhkqu-)Ey&vXKVlf002ovPDHLkV1n*jikbib literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/px-013.png b/assets/pango/sprites/px-013.png new file mode 100644 index 0000000000000000000000000000000000000000..768fb507a3abde86d3cf7cd9b8e9b0d3290f12e6 GIT binary patch literal 331 zcmV-R0kr;!P)Px$1xZ9fR5*>5RNE26AP8JC*HP@rnyx2P)7}Tc0BVxknM?#>d58iD1exWm^Z3k^ z1St>@9Q3vpi0FUZ2#zMt2nC`nIfAsH*1f_`Xo2Twpw;a^)2|&}7OM{p1HrU}Cs+dk zU|Xbt5G%_La{@s7`y!t};8UYjLxjWfHY%$zz0yNxTg1cHsc8rO+xfz)!Fy4emr z$Ps3Hc3LDEO6Z5WD#BB1Z(P&z3}h0?qKq?rcNc(gAG*P4IT_c0f^=K|aR$JnT|(U* z!3*a=QumkvL9S{Ka3$!2(m?JUyPU|Zf#!Iv&(u9ho_EX839u1RLtlScKUaS2Px#?ny*JR5*>5R9g%`%G@(JXOebGB-d2olbkK=O=He~ z8Y}f}uho}#-i=*-pcVGr1AR73lW8;zY`)49b+MZ2F7P)c?59S$(2Do~00006G?Wc0*F)$pG!s>%RsUjPaSAw{$U?0yJlo`bi8@6H}l)s{0L zgwS1Of41Xa+Gqvl9BgMUS>)~Xtbx5~J64K8HRRmewjy)S9)aej7w z%%e#zg_KftY$i8_DNWUBbGa*A>8h@_kcT3aq3UK!c`8zws>ZUCm!g!VYG@Nkv{Flw z=2Y4)-x%d%?Hh=4UT;RO*UGYV|L z=%$=J51epM_iBX&HJ@|nEC`KZh5CLmw!V4ly z1{!R@=%$=J51fpswi1O5CeDm%XAuk!(pgb$G;3iG7?0Cfih($ literal 0 HcmV?d00001 diff --git a/assets/pango/tiles/pa-tile-001.png b/assets/pango/tiles/pa-tile-001.png new file mode 100644 index 0000000000000000000000000000000000000000..e16af24ce1063e0f0a570ea71cb0b3ace00dbb21 GIT binary patch literal 1005 zcmds#y=s(E5QblWimJMYha9u`OA(fyI6@%+Vf$#-Y-w{?sE2T!(Fd~Up7 z9v$l9>fs;h=9l5+@r*~4TnZ_r>bsfT6s9y)m(As_aHXrd+d>|SP=>09E#;|5WvUv> zN?wXmma3smATdfWMQ)qpW?`0Q)r+~iTeziLH4n-j7GW7yO^a?%i?mFuX34OZMOl_r zlSTxrp$RXDu$fh01I9G%)Op~9d%9OEWHI*$&+ux&@Sq&&nO?0QT69NwmRH+>TiAmi z7=U6Vq6rbOh9_;##+3@ zJlw(_{J;PdBN5VI!5W(If(V;|1{*M@X{XKur#dsNL?Mg0S4T+x1Wv+4N`{0+O2k4A z7?0CfiQL=5+7V%%e#zg_KhD-Arx@Q<|#F=5klK(pBAUArD0;L)F8U@>HZURgGmO zFGVR!)zBu87^RmYx7Bg8FiW%Q#oXO3+|sR@2W1b7uneoFMYpF#TBcRAWZ281EX%4% zBLddYgcn5E%qp+}W14pAJaEE2-K!O{n0th0c(q`7P>%FWuhtJOx}!YHtL?xo?7i$%6ki+KTEc1adwS$0E;Q~_Z+D=n-P*=o7Jy|uR`X?t-nTk{>={0(pOfAaMHhR?0H z>;0+De?R&w4Zf^i9nN_)$)%7|s=k}aO<_t?b=h3*3Rk+SyDj9Q2xX{x*ixQ~RHmx2 ztmLIAWvLq41QMh4Qsj1X+$_w}ta>qbcMG?4tL8!3!y+uhs%g>fX_1y`)hrqIvM9^4 zYSM^+H8kM`5jL|5Y`~bNojMPka8LJYg)HVC;Tc{n7#@@(J=3f8LyPVx&+=+Ja0`3z z0|QWuL^L4+*3g6(MA!^8*nlxjJ9Qp78BuK|3R%oOGpe0MFgz${MYYkyLJk;@(^!j_ zn1@@~gC7`xVkANuELcMmUJzk3&|m|`H0{)R;8bUZl_+E}_v#4ApTJ3&NXd}UNQqd; z0poERYw;30V979kU;v7d2x+ij4NZ7Kgv~&M4H(n3Q|E!h@9xRw_z$0}ckAVbhX3;% zUBCa1$mfgM%fsETuTOTzFYXOjccz~-AO09jK7SibPRAD?pIu&PXZmpXbELD!ccou% UMmrzI4=x$iV!oW6Pft$&0jsY(9RL6T literal 0 HcmV?d00001 diff --git a/assets/pango/tiles/pa-tile-004.png b/assets/pango/tiles/pa-tile-004.png new file mode 100644 index 0000000000000000000000000000000000000000..dbe2cc0fffce656e278b599a706bd79234a101ff GIT binary patch literal 984 zcmds#y-M6s5XFz$h#=Z1B9bN`Ha5-^L{`=XS!7pIXd{U80!d+=q!DY2DaA?)>%2g^ zLDD9jmF5M~jz8xyyr6&XotZP|V=oWK!^MU51xbse{lSFq^4s719RIhrm!^DXzD$O@ zdU)LUC(T|R9vtoQXp&1IrBr=4lbgbnrs}e}+!d~LRd-v+LlMeQ^{}Np6{$>BV_C^d zQOZ&^v7~f+?YLQ(rCIf2?(Py|+tVT~)2dlA>}64wW!0n+ z0c&W&3nFZ071)3=O*?fSIN_e|)e2e6J;F1*S};5)M|!4L>xUNIQJ&@1cHkEF;0Fev z7>Q^?1gxP6FNm-iXs`iens(|ua5AFWN))n~duCKSi(q(A&WdWIiG>_69;dMuFEJ0d zum?Xd0L4gzG+3~PCcGfRW}v|ajA`1b^T4Uj3@cH{V(!%ul0SiyFp-iWp^*}?kORi! zG}huJcEFNh{J;PdBN5VI!5W(If(V;|1{*M@X{XKuhu_`i^!SC(pRbegl!o_tKL5YC uMda#e@absn`t0Y2zrUC7SMGml?&FE{biQ+Q_v86H9UAS82M@ca-(CTC&^X@! literal 0 HcmV?d00001 diff --git a/assets/pango/tiles/pa-tile-005.png b/assets/pango/tiles/pa-tile-005.png new file mode 100644 index 0000000000000000000000000000000000000000..20e4f663523cd80b3e58bf76e6808d687e2d665b GIT binary patch literal 1011 zcmds$KZ_Gk5X2`ZNC+MVIYC6wN^C4-YYWeWTtGrdf`XO*SZ%Ddu+&(IwZ(RV{o$t& zi$%1!!rnqVD@)hk?Z@yz^4Pb#voo{Fo8@A*J32p-v^zhTthmR=pWz9UE`^j*b!;X#g(*$dX>++NTDBt7MRk;Cd9@w5g+2Iz z0VqZynh*hNXu=C3Oa>Zk!04u&JP(|VsJ0S?3?|NuYG)A)57JptZ8U3P4;YWrSc{jK zhg;Z#9~gjQBtjZ2SVI$D5MeUVU;{=s<>YzbRPPKcQOIE8>J_qo7S4uQDLZ6oY>Bn7 z2aLyQti?<0fF;fNfdME+BBa5BH8kM`5heo-Hehs9PM!x2zq`xpyFa678n=;*=A!T93z{ov-Mz0b36+fR?b?rfKjUM)YKIk`D| a>NS7lAHTUL{CH<;KIYLRmqJRZ`fesSg(*$dWplYJTc!mME!@(rng?YMi?9rMOvm+vt-!I zqAbg*Nh1Q*(1aI6*vu-h0b`nW>O645J>9DnvY2~>XLz+>cuU-^96n{Ll& z_&?9B-w$3Q^6F%?Hb49H?fW+aT^ug2ec!+EcYp8alk0~|N7ruej&HtRES{ZtzVl+` j?x`D_i(kWcuYUabtgVm3)Ax?P%t6p(ygfQt-+lNG0d7dU literal 0 HcmV?d00001 diff --git a/assets/pango/tiles/pa-tile-007.png b/assets/pango/tiles/pa-tile-007.png new file mode 100644 index 0000000000000000000000000000000000000000..3a5038eb186cd12b734c1ee242001c08ca1cb2fd GIT binary patch literal 992 zcmds$F-ja@5QSe9LLAe$9kkY(95DXfIRTtEw}6oL>5*jY>yi0Q3U zdVoQ+^agr?w3Bb=82+&PGylxIdGF2c++NTlZdN8rOg;SCzyK5@5z=768k+Ee2$O*Z8!);lC(i?i-`&pa=$Y&3bUL0<@H)@e ze`kLXxjhj%e^ym=6vkS;b5UNeSKO|r~jZg;(PJrZ|V&H zZ{5By=5uOwv@oZgpWQ#w=~u&tlX)IZaw(*gs@=`xrZA3!3ZW`<`5Bkoa@As6|QtuS6j$K5z0_? zv!y&0sZ3R4S;o9yM40!4C{TF%lsS7ObHOFNiQ1Xs`jJn{x6zaH@BPl_+E|arFw>KMQBW ztdt$HG`7TA*aODnG}huJcEFNm{J;PdBN5VI!5W(If(Vm=1{*NCDJRbZhu__y@ulxv zU$2iA#}xdZ=fcK~r-(c`9Lz57een9;%k4V4MAo_#WzeanAN8qO~c*5@wY`UlfhOmP4J literal 0 HcmV?d00001 diff --git a/assets/pango/tiles/pa-tile-011.png b/assets/pango/tiles/pa-tile-011.png new file mode 100644 index 0000000000000000000000000000000000000000..b06a00e8695f2107ad02d6a8d62e4ee82cb763ce GIT binary patch literal 1022 zcmds#ziO0G5XDcvz!E}&+KE~$7LrC0rnOkZhGZdGmfa??cV^C8jRkArD0;L)C^Y z<*7(zsv4G+ycDG@Rf9Hx#3;QKxt$+33$rwdL(4sra zv%H!exP?9VfdME+BAO5ZYiPm?B5Vd4Y`~bNojMPkjHqTM3R%oOGpad@V0ci@ifTp^ z3prpsPGc=zVjgZ`4}M?(ijfFuuwV^MctM2CK!Xh!)3j6Pfm1CrtVAJ;xmSyj{0W?d ziIfZpjg*Lm955cIu@*0}1C|Wq2L_-RiI4^h*3g6(MA!^8*nlxjJ9Qp7{O)ed?)~6% z{CGN^(eQts!@p17Bl6B@cz1sJ)&0fd_MqDsEMC4)|5CrsPJiuP?;Wq*+V5XI=)eBb q^&WJ+qi*nK@cHf8SAG99IeGVI<>QC>@8#^_2{;<<<{yivPfm!p2xSKDj@`3(3p9GjryA?2FCO`a=IwU(&+xZa3z8{^alUDgG~C zKR4ks_hh`js>7qp-=*Hm&5i9f9!+v7q?D@N&E%#qrKwukTXLvQi@Sq&&nO;plwCIlV zEU#tBdS@6LKbt+jB3sz7#@_fqMFgf zLJk;@(^!j_n1@@~gC7`xVkANuELcMmUJzk3&|m|`H0{)R;8e>DD^bW|?$shBe*!0A zA|*pYBPC)X2aLyQti?<0fF;BDfdME+BBa5BH8kM`5jF!2HegKCPMrr1zq_T${cn7} z?ulcF}$$*=iZq)b3XE9V>apcPV^-8r#D7(zDE!K*1G&Z ze{rzj)44yNtZVP<$$jb2(~X6jDmn?q+gRn9@|OY%X_&D_zyPE##pHWvJS) zr92g>OjX0Ol9!^CrE1V7kQk+xBDaI%W?`0Q)fRJiw{T0hYCI@=ScGL*HCl9gTBK!K zHI@u}S(Ig2HPVQHH8kM`5jL|5Y`~bNojMPka8LJY3R%oO!ZW;@V0ciD^h~d&A6j%r zd6rkR1GlgTKQI8rNJJANU=2-pL4?gfgAEwdv{UDSlM&UdL?Mg0XGS$=5eyH?Sy9bs zVj%~N$7!s^OU%P9?7_4Hm4S2``AS8ECKpW14pAJaDRIhLtE}G52Z_l0Siy zFp-iWp^*}?kORi!G}huJcEFNh{J;PdBN5VI!5W(If(V;|1{*M@X{XKuhu__~#qHmG zes0fa3mX2fi6+X*!;b_SQEa{{wub BN@f56 literal 0 HcmV?d00001 diff --git a/assets/pango/tiles/pa-tile-014.png b/assets/pango/tiles/pa-tile-014.png new file mode 100644 index 0000000000000000000000000000000000000000..75362619c1f5b71dcfad4b268608564e9675eb25 GIT binary patch literal 1031 zcmds#y=v4^5XDan!6mXP*r-^DMM$&NG$IJP?2@d=5_W?{u&{9o!A31aw6TjyQUx-d zAhuiN305OwF=%aN8(+XGGX8QO!wbouduQg%`Pl7^@mhEJ^0K7vXnio@d;a9Fv&jF| z-nl8Cg{PCXzGmOA{FY9=-MG6s9erK?)Eg*+6Y3{@Mp zl&2zlZ+G6hR7H;WQjR$29i?9rYh94a2L_-RiD*Iutf2`nh_D%GumNM5cIrHEGNPK5C}c7B%&6upg5g0qE2paG*TiKa=>_;##+3@4p=gb9~gjQBtjZ2SVI$D5MeXWU<1Z9?bLbT@Vi@?ZXNOY z`D`+t((r$tdw-sPK;&>VxV?FC@9~dMU0v-g&S&#GJKw%sTe{GDxxTylX7AzGv+tj5 zpSgZx+PSoP`o;du`J-{%+By1oxbGeDl$H~Kf?>j%e^ym=6vk>%4F1CIKLpNJ6;}4`JOub>&)}N z*FQPqGxvBpTGGz<3%{fzuUBrb4tX@mrI1ppb~lro!jz_JWplYJT+P2;|B(y7>SSu3)awt7ev?$G}wSKO*?fSIQ;GwXKVX> zem$?voadTsT`}5k1t-Zf@b + + + + + +0,0,1,1,1,1,1,1, +0,1,1,2,2,2,2,1, +0,1,2,2,2,3,2,1, +1,1,2,2,3,3,1,1, +1,2,2,2,2,2,1,0, +1,2,2,2,1,1,1,0, +1,1,1,1,1,0,0,0 + + + + + + + + + + + + + diff --git a/assets/soko/levels/classic/sk_c_alignthat.tmx b/assets/soko/levels/classic/sk_c_alignthat.tmx new file mode 100644 index 000000000..fc50e7c08 --- /dev/null +++ b/assets/soko/levels/classic/sk_c_alignthat.tmx @@ -0,0 +1,27 @@ + + + + + + +0,0,1,1,1,1,0,0,0, +0,1,1,2,2,1,0,0,0, +0,1,2,2,2,1,1,1,1, +1,1,2,2,3,3,3,3,1, +1,2,2,2,2,1,2,2,1, +1,2,2,2,2,1,2,2,1, +1,1,1,1,1,1,1,1,1 + + + + + + + + + + + + + + diff --git a/assets/soko/levels/classic/sk_c_cosms.tmx b/assets/soko/levels/classic/sk_c_cosms.tmx new file mode 100644 index 000000000..f5acc76db --- /dev/null +++ b/assets/soko/levels/classic/sk_c_cosms.tmx @@ -0,0 +1,30 @@ + + + + + + +0,1,1,1,1,1,1,1,0, +0,1,2,2,1,2,2,1,0, +0,1,2,3,2,3,2,1,0, +0,1,2,3,1,3,2,1,0, +1,1,2,3,1,3,2,1,1, +1,2,2,2,2,2,2,2,1, +1,2,2,2,1,2,2,2,1, +1,1,1,1,1,1,1,1,1 + + + + + + + + + + + + + + + + diff --git a/assets/soko/levels/classic/sk_c_files.tmx b/assets/soko/levels/classic/sk_c_files.tmx new file mode 100644 index 000000000..adc216733 --- /dev/null +++ b/assets/soko/levels/classic/sk_c_files.tmx @@ -0,0 +1,23 @@ + + + + + + +0,1,1,1,1,1,1, +0,1,2,2,1,2,1, +0,1,2,3,2,2,1, +0,1,2,3,1,2,1, +1,1,2,3,1,2,1, +1,2,2,2,2,2,1, +1,2,2,2,2,1,1, +1,1,1,1,1,1,0 + + + + + + + + + diff --git a/assets/soko/levels/classic/sk_c_fours.tmx b/assets/soko/levels/classic/sk_c_fours.tmx new file mode 100644 index 000000000..21b6f2ce1 --- /dev/null +++ b/assets/soko/levels/classic/sk_c_fours.tmx @@ -0,0 +1,27 @@ + + + + + + +0,0,0,0,0,0,0,1,1,1,1,0,0,0, +1,1,1,1,1,1,1,1,2,2,1,0,0,0, +1,2,2,2,2,2,2,2,2,2,2,1,1,1, +1,2,2,2,2,2,1,1,2,2,2,3,3,1, +1,2,2,2,2,2,2,1,1,2,2,3,3,1, +1,2,2,2,2,2,2,2,2,2,1,1,1,1, +1,1,1,1,1,1,1,1,1,1,1,0,0,0 + + + + + + + + + + + + + + diff --git a/assets/soko/levels/classic/sk_c_plus.tmx b/assets/soko/levels/classic/sk_c_plus.tmx new file mode 100644 index 000000000..e4dd0a82b --- /dev/null +++ b/assets/soko/levels/classic/sk_c_plus.tmx @@ -0,0 +1,23 @@ + + + + + + +2,1,1,1,1,1,1,1, +1,2,2,2,2,2,2,1, +1,2,2,3,2,3,2,1, +1,1,2,2,2,2,2,1, +1,1,2,3,2,3,2,1, +1,1,2,2,2,2,2,1, +1,1,1,1,1,1,1,1 + + + + + + + + + + diff --git a/assets/soko/levels/classic/sk_c_test2.tmx b/assets/soko/levels/classic/sk_c_test2.tmx new file mode 100644 index 000000000..50b640c3f --- /dev/null +++ b/assets/soko/levels/classic/sk_c_test2.tmx @@ -0,0 +1,29 @@ + + + + + + +0,0,0,0,0,0,0,0,0,0, +0,12,12,12,12,12,12,12,12,0, +0,12,14,12,12,12,12,12,12,0, +0,12,13,14,12,13,13,13,12,0, +0,12,13,13,12,13,13,13,12,0, +0,12,13,13,12,13,13,13,12,0, +0,12,13,13,14,13,13,13,12,0, +0,12,13,13,13,13,13,13,12,0, +0,12,12,12,12,12,12,12,12,0, +0,0,0,0,0,0,0,0,0,0 + + + + + + + + + + + + + diff --git a/assets/soko/levels/euler/sk_e_a-frame.tmx b/assets/soko/levels/euler/sk_e_a-frame.tmx new file mode 100644 index 000000000..a26061b99 --- /dev/null +++ b/assets/soko/levels/euler/sk_e_a-frame.tmx @@ -0,0 +1,25 @@ + + + + + + +2,2,2,2,2, +2,2,0,2,2, +2,2,2,2,2, +2,2,2,2,2, +2,2,0,2,2, +2,2,2,2,2, +2,2,0,2,2 + + + + + + + + + + + + diff --git a/assets/soko/levels/euler/sk_e_alkatraz.tmx b/assets/soko/levels/euler/sk_e_alkatraz.tmx new file mode 100644 index 000000000..dbfb05da8 --- /dev/null +++ b/assets/soko/levels/euler/sk_e_alkatraz.tmx @@ -0,0 +1,38 @@ + + + + + + +0,2,2,2,2,2,2,2,2,2, +0,2,1,2,2,2,2,2,1,2, +0,2,2,2,2,2,1,2,1,2, +0,2,1,1,1,1,1,2,1,2, +0,2,1,1,2,2,2,2,1,2, +0,2,1,2,2,2,2,2,2,2, +2,2,1,2,2,2,2,2,0,0, +2,2,2,2,2,2,2,2,0,0, +2,2,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0 + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/soko/levels/euler/sk_e_apollo.tmx b/assets/soko/levels/euler/sk_e_apollo.tmx new file mode 100644 index 000000000..fdbcdb133 --- /dev/null +++ b/assets/soko/levels/euler/sk_e_apollo.tmx @@ -0,0 +1,34 @@ + + + + + + +2,2,2,2,2,0, +2,2,2,0,2,0, +2,2,2,2,2,0, +2,2,0,2,2,0, +2,2,0,2,2,0, +2,2,2,2,2,0, +2,2,2,2,2,0, +0,0,0,0,0,0 + + + + + + + + + + + + + + + + + + + + diff --git a/assets/soko/levels/euler/sk_e_camera.tmx b/assets/soko/levels/euler/sk_e_camera.tmx new file mode 100644 index 000000000..53b301eb0 --- /dev/null +++ b/assets/soko/levels/euler/sk_e_camera.tmx @@ -0,0 +1,25 @@ + + + + + + +0,0,0,0,2,2, +2,2,2,2,2,2, +2,2,2,2,2,2, +2,2,2,2,2,2, +2,2,2,2,2,2 + + + + + + + + + + + + + + diff --git a/assets/soko/levels/euler/sk_e_casette.tmx b/assets/soko/levels/euler/sk_e_casette.tmx new file mode 100644 index 000000000..6bf91dd41 --- /dev/null +++ b/assets/soko/levels/euler/sk_e_casette.tmx @@ -0,0 +1,34 @@ + + + + + + +2,2,2,2,2,2, +2,2,2,2,0,2, +2,2,2,2,2,2, +2,2,2,2,2,0, +2,2,2,2,2,0, +2,2,2,2,2,0, +2,2,0,2,2,0, +2,2,2,2,2,0 + + + + + + + + + + + + + + + + + + + + diff --git a/assets/soko/levels/euler/sk_e_copymachine.tmx b/assets/soko/levels/euler/sk_e_copymachine.tmx new file mode 100644 index 000000000..3299ae6d7 --- /dev/null +++ b/assets/soko/levels/euler/sk_e_copymachine.tmx @@ -0,0 +1,50 @@ + + + + + + +0,0,0,0,0,0,2,2,0,0, +0,0,0,2,2,2,2,2,2,2, +0,0,0,2,2,2,2,1,2,2, +0,0,2,2,2,2,2,1,2,2, +0,0,2,2,2,2,2,2,2,2, +0,0,2,2,2,1,1,1,1,2, +0,0,0,0,2,2,2,2,2,2, +0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/soko/levels/euler/sk_e_curlingiron.tmx b/assets/soko/levels/euler/sk_e_curlingiron.tmx new file mode 100644 index 000000000..17285aeba --- /dev/null +++ b/assets/soko/levels/euler/sk_e_curlingiron.tmx @@ -0,0 +1,26 @@ + + + + + + +0,2,2,2,0,0,2,2,0,0,2,2,0, +2,2,2,2,2,2,2,2,2,2,2,2,2, +2,2,2,2,2,2,2,2,2,2,2,2,2, +0,2,2,2,0,0,2,2,0,0,2,2,0 + + + + + + + + + + + + + + + + diff --git a/assets/soko/levels/euler/sk_e_doubleblock.tmx b/assets/soko/levels/euler/sk_e_doubleblock.tmx new file mode 100644 index 000000000..593c29e14 --- /dev/null +++ b/assets/soko/levels/euler/sk_e_doubleblock.tmx @@ -0,0 +1,25 @@ + + + + + + +2,2,2,2,2, +2,2,2,2,2, +0,2,2,2,2, +2,2,2,2,2, +2,2,2,2,2, +0,2,2,2,2, +0,2,0,0,0 + + + + + + + + + + + + diff --git a/assets/soko/levels/euler/sk_e_feint.tmx b/assets/soko/levels/euler/sk_e_feint.tmx new file mode 100644 index 000000000..14d4295f6 --- /dev/null +++ b/assets/soko/levels/euler/sk_e_feint.tmx @@ -0,0 +1,23 @@ + + + + + + +2,2,2,2,2, +2,2,2,2,2, +0,0,2,2,2, +0,0,2,2,2, +0,0,0,2,2 + + + + + + + + + + + + diff --git a/assets/soko/levels/euler/sk_e_groundwork.tmx b/assets/soko/levels/euler/sk_e_groundwork.tmx new file mode 100644 index 000000000..84892ef9e --- /dev/null +++ b/assets/soko/levels/euler/sk_e_groundwork.tmx @@ -0,0 +1,23 @@ + + + + + + +0,2,2,2,2, +0,2,1,1,2, +0,2,1,2,2, +0,2,2,2,2, +0,2,2,2,2 + + + + + + + + + + + + diff --git a/assets/soko/levels/euler/sk_e_harmonica.tmx b/assets/soko/levels/euler/sk_e_harmonica.tmx new file mode 100644 index 000000000..90d11cb8e --- /dev/null +++ b/assets/soko/levels/euler/sk_e_harmonica.tmx @@ -0,0 +1,25 @@ + + + + + + +0,2,2,2,2,2,2,2,2,2,2,2,2,2,2, +2,2,2,2,2,1,2,2,2,1,2,2,2,2,2, +2,2,2,1,2,1,2,1,2,1,2,1,2,2,2, +2,2,2,1,2,2,2,1,2,2,2,1,2,2,2, +2,2,2,2,2,2,2,2,2,2,2,2,2,2,0 + + + + + + + + + + + + + + diff --git a/assets/soko/levels/euler/sk_e_mouse.tmx b/assets/soko/levels/euler/sk_e_mouse.tmx new file mode 100644 index 000000000..23e5e7402 --- /dev/null +++ b/assets/soko/levels/euler/sk_e_mouse.tmx @@ -0,0 +1,22 @@ + + + + + + +0,0,2,2,2,2,2, +0,0,2,0,0,0,2, +0,0,2,0,2,2,2, +2,2,2,2,2,0,0, +0,2,2,2,0,0,0, +0,2,2,2,0,0,0 + + + + + + + + + + diff --git a/assets/soko/levels/euler/sk_e_spine.tmx b/assets/soko/levels/euler/sk_e_spine.tmx new file mode 100644 index 000000000..3289a33b8 --- /dev/null +++ b/assets/soko/levels/euler/sk_e_spine.tmx @@ -0,0 +1,28 @@ + + + + + + +0,2,2,0,0,0, +2,2,2,2,0,0, +2,2,2,2,0,0, +0,2,2,2,0,0, +0,2,2,2,2,2, +0,2,2,2,2,2, +0,2,2,2,0,0, +0,0,2,2,0,0 + + + + + + + + + + + + + + diff --git a/assets/soko/levels/euler/sk_e_spiral.tmx b/assets/soko/levels/euler/sk_e_spiral.tmx new file mode 100644 index 000000000..09ef2d6da --- /dev/null +++ b/assets/soko/levels/euler/sk_e_spiral.tmx @@ -0,0 +1,23 @@ + + + + + + +2,2,2,2,2,2,2, +2,2,0,0,2,2,2, +2,2,2,2,2,2,2, +2,2,2,2,2,2,2, +2,2,2,2,2,2,2, +2,2,2,0,0,2,2, +2,2,2,2,2,2,2 + + + + + + + + + + diff --git a/assets/soko/levels/euler/sk_e_start.tmx b/assets/soko/levels/euler/sk_e_start.tmx new file mode 100644 index 000000000..fb75544ec --- /dev/null +++ b/assets/soko/levels/euler/sk_e_start.tmx @@ -0,0 +1,23 @@ + + + + + + +2,0,0,2,2,0, +2,2,2,2,2,2, +1,1,1,1,2,2, +0,2,2,2,2,2, +0,2,2,2,2,2 + + + + + + + + + + + + diff --git a/assets/soko/levels/euler/sk_e_steeringwheel.tmx b/assets/soko/levels/euler/sk_e_steeringwheel.tmx new file mode 100644 index 000000000..306a556ef --- /dev/null +++ b/assets/soko/levels/euler/sk_e_steeringwheel.tmx @@ -0,0 +1,21 @@ + + + + + + +0,2,2,2,0,2,2,2,0, +2,2,0,2,2,2,0,2,2, +2,0,0,2,2,2,2,0,2, +2,2,0,2,2,2,0,2,2, +0,2,2,2,2,2,2,2,0 + + + + + + + + + + diff --git a/assets/soko/levels/euler/sk_e_threestep.tmx b/assets/soko/levels/euler/sk_e_threestep.tmx new file mode 100644 index 000000000..1354cb252 --- /dev/null +++ b/assets/soko/levels/euler/sk_e_threestep.tmx @@ -0,0 +1,42 @@ + + + + + + +2,2,2,2,2, +2,2,2,2,2, +2,2,2,2,2, +2,2,2,2,2, +2,2,2,2,2, +2,2,2,2,2, +0,0,0,0,2, +0,0,0,0,0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/soko/levels/euler/sk_e_threestep2.tmx b/assets/soko/levels/euler/sk_e_threestep2.tmx new file mode 100644 index 000000000..5e42e2e54 --- /dev/null +++ b/assets/soko/levels/euler/sk_e_threestep2.tmx @@ -0,0 +1,38 @@ + + + + + + +2,2,2,2,2, +2,2,2,2,2, +2,2,2,2,2, +2,2,2,2,2, +2,2,2,2,2, +2,2,2,2,2, +0,0,0,0,2 + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/soko/levels/euler/sk_e_throughput.tmx b/assets/soko/levels/euler/sk_e_throughput.tmx new file mode 100644 index 000000000..525cc3836 --- /dev/null +++ b/assets/soko/levels/euler/sk_e_throughput.tmx @@ -0,0 +1,34 @@ + + + + + + +2,2,2,2,2,2,2,2,2,2, +2,2,2,2,2,0,0,0,0,0, +2,2,2,2,2,2,2,2,2,0, +2,2,2,2,2,0,0,0,0,0, +2,2,2,2,2,2,2,2,0,0, +0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0 + + + + + + + + + + + + + + + + + + + diff --git a/assets/soko/levels/euler/sk_e_tunnels.tmx b/assets/soko/levels/euler/sk_e_tunnels.tmx new file mode 100644 index 000000000..100325e86 --- /dev/null +++ b/assets/soko/levels/euler/sk_e_tunnels.tmx @@ -0,0 +1,23 @@ + + + + + + +2,2,2,2,2, +2,2,1,1,1, +2,2,2,2,2, +2,1,2,2,2, +2,2,2,2,2 + + + + + + + + + + + + diff --git a/assets/soko/levels/euler/sk_e_waterwheel.tmx b/assets/soko/levels/euler/sk_e_waterwheel.tmx new file mode 100644 index 000000000..bc5e8a79e --- /dev/null +++ b/assets/soko/levels/euler/sk_e_waterwheel.tmx @@ -0,0 +1,69 @@ + + + + + + +0,0,0,0,0,2,0,0,0, +0,0,0,0,0,2,0,0,0, +0,0,2,2,2,2,2,0,0, +2,2,2,2,2,2,2,0,0, +0,0,2,2,2,2,2,2,2, +0,0,2,2,2,2,2,0,0, +0,0,2,2,2,2,2,0,0, +0,0,0,2,0,0,0,0,0, +0,0,0,2,0,0,0,0,0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/soko/levels/sk_e_overworld.tmx b/assets/soko/levels/sk_e_overworld.tmx new file mode 100644 index 000000000..056463744 --- /dev/null +++ b/assets/soko/levels/sk_e_overworld.tmx @@ -0,0 +1,154 @@ + + + + + + + + + + + + + + + + +0,12,12,12,12,76,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +12,76,13,13,13,76,76,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +12,13,13,13,77,77,76,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +12,13,13,13,77,77,76,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +12,13,13,13,77,77,76,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +12,13,76,76,13,77,76,76,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +12,13,77,76,77,77,77,76,76,76,76,76,76,76,76,76,76,76,76,0,0,0,0, +12,13,13,76,77,77,13,13,13,13,13,13,13,13,13,13,13,77,76,76,0,0,0, +12,13,13,76,77,77,77,76,76,13,13,13,13,13,13,13,13,77,77,76,76,0,0, +12,13,13,76,76,76,77,77,76,77,77,13,13,13,13,13,13,77,77,77,76,76,0, +12,13,13,13,13,76,77,77,77,77,77,77,13,13,13,13,13,77,77,77,77,76,76, +12,13,13,13,13,76,77,77,77,77,77,76,76,76,76,76,76,76,76,76,76,76,76, +12,13,13,13,13,76,77,77,77,77,77,77,77,76,77,77,77,77,77,77,77,77,76, +12,12,13,13,13,76,13,13,77,77,77,77,77,77,77,77,77,77,77,77,77,77,76, +0,76,12,76,76,76,13,13,77,77,77,77,77,77,76,76,76,76,76,77,77,76,76, +0,0,0,0,76,13,13,13,13,13,13,77,77,77,76,77,77,77,77,77,76,76,0, +0,0,0,0,12,13,13,13,13,13,13,13,13,77,77,77,77,77,76,76,76,0,0, +0,0,0,0,12,12,12,12,12,12,12,12,12,12,76,76,76,76,76,0,0,0,0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/soko/levels/sk_overworld.tmx b/assets/soko/levels/sk_overworld.tmx new file mode 100644 index 000000000..4764283fc --- /dev/null +++ b/assets/soko/levels/sk_overworld.tmx @@ -0,0 +1,134 @@ + + + + + + + + + + + + + + + + +76,12,12,12,12,12,12,12,12,12,12,12,12,0,0,0,0,0, +12,13,13,13,13,13,13,13,13,13,13,13,12,0,0,0,0,0, +12,13,13,13,13,13,13,13,13,13,13,13,13,12,0,0,0,0, +12,13,13,13,13,13,13,13,13,13,13,13,13,13,12,0,0,0, +12,13,13,13,13,13,13,13,13,12,13,13,13,13,13,12,0,0, +12,13,13,13,13,13,13,13,13,13,12,13,13,13,13,13,12,0, +12,13,13,13,13,13,13,13,13,13,13,12,12,12,12,12,12,12, +12,13,13,13,13,12,13,13,13,13,13,13,13,13,13,13,13,12, +12,13,13,13,13,12,12,13,13,13,13,13,13,13,13,13,13,12, +12,13,13,13,13,12,12,12,13,13,13,13,13,13,13,13,13,12, +12,13,13,13,13,12,12,12,12,13,13,13,13,13,13,13,13,12, +12,13,13,13,13,13,12,12,12,12,13,13,13,13,13,13,13,12, +12,13,13,13,13,13,13,12,12,12,12,12,12,12,12,12,12,12, +12,12,13,13,13,13,13,13,12,12,12,12,76,76,0,0,0,0, +0,0,12,13,13,13,13,13,13,13,13,77,77,76,0,0,0,0, +0,0,0,12,13,13,13,13,13,13,13,13,12,76,0,0,0,0, +0,0,0,0,12,13,13,13,13,13,13,13,13,12,0,0,0,0, +0,0,0,0,12,12,12,12,12,12,12,12,12,12,0,0,0,0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/soko/sprites/pango/sk_pango_back1.png b/assets/soko/sprites/pango/sk_pango_back1.png new file mode 100644 index 0000000000000000000000000000000000000000..eeb0ec16081940cde36adccf691f1e115f5eb928 GIT binary patch literal 183 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`b)GJcAr-f_P7V}0Ai(2t_vG9= zK$ICBwQI%(yIC{O2z;B!@;vA819!F~_xu$m|6pNaWQd-#fqhSP9n+C?!`8?-$$Qdo z?{fXO?MK^oX@i#J*WVtlX5@_WI<@$f-}z**>4qEHdrDV)=6KOy$STV4-ua_e%7hSR hHi>sj{x9ItJ3OCf&Z<~TeV|JiJYD@<);T3K0RS`kNf-bC literal 0 HcmV?d00001 diff --git a/assets/soko/sprites/pango/sk_pango_back2.png b/assets/soko/sprites/pango/sk_pango_back2.png new file mode 100644 index 0000000000000000000000000000000000000000..f4e8ce13ebff142c81e94cfd5451b9e76cc9577d GIT binary patch literal 181 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`HJ&bxAr-f_P7dT_5ae;GRxd9C zqUCyJvwb*C<+W$JT(OaLO0h2yG7ae#XO29>7%RIc7@&3H|3{Q2h?V0sk>K*6& zcN6UQd06wF-F8i?!@5A%c}bu(gPWlL`(hL3kZlYZ6VJV8C~kSp?Go8y=da0sQ9zh^ c!M?A|d1gFw)^}$u0lI_1)78&qol`;+0PH$D;{X5v literal 0 HcmV?d00001 diff --git a/assets/soko/sprites/pango/sk_pango_fwd1.png b/assets/soko/sprites/pango/sk_pango_fwd1.png new file mode 100644 index 0000000000000000000000000000000000000000..674d392d0079d37cf1e4912dc6eb1b3b20e5657e GIT binary patch literal 207 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`(>z@qLn>~~oy^GBpupj}cXIhF zzdN1Q6K4r<33c~|zWd+aF*T_+}rLn?0dPG;n5P~dRgJGuOo z-SJn>uy4tzYvD8#6>o~_}=B3_Q57q*+1CVvoIW%KHP!oRrp4bwRsC3u82OL{mC zdA?CldvN8~*E6v!ZoBWh@jci+L00&-)3!^(U%n{$FVHhL2(_}_!4{DZbRvVNtDnm{ Hr-UW|$+1t^ literal 0 HcmV?d00001 diff --git a/assets/soko/sprites/pango/sk_pango_side1.png b/assets/soko/sprites/pango/sk_pango_side1.png new file mode 100644 index 0000000000000000000000000000000000000000..35bbf3d7e6b1028fba61e2fe7da0fbe5198224f7 GIT binary patch literal 211 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`vpiiKLn>~q4Zbbdpum%?Y#(z- z|9HakcSroDP8V3%T6aVF_`{hdTaH><-e54D;pz6lenS82xLqggot7CE3tzZXlEnLG zoAUSUkfht0@h)X=BDU*pO_bCnZ???KUE2IC4;A{ KpUXO@geCxZB2n}J literal 0 HcmV?d00001 diff --git a/assets/soko/sprites/pango/sk_pango_side2.png b/assets/soko/sprites/pango/sk_pango_side2.png new file mode 100644 index 0000000000000000000000000000000000000000..3869df3a880db519c24eb8474b461d156e1925cf GIT binary patch literal 284 zcmV+%0ptFOP)Px#)=5M`R5*>LQ^^g(Fbt!D?$d`dL|+{vp2?ZXnn``wQsg5AiV83i*d#^D3Gio8 zc-J5C%MC3Dzi|8H+q}A;ytwh0000((% literal 0 HcmV?d00001 diff --git a/assets/soko/sprites/pixel/sk_pixel_back.png b/assets/soko/sprites/pixel/sk_pixel_back.png new file mode 100644 index 0000000000000000000000000000000000000000..bce5ac7c8ef100517ecc23ae2161da2678e8e03c GIT binary patch literal 257 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`>pWc?Ln?0d2456Bs=!ga{9pO( z<9>HFt$(&Gau428G+lTii_SI?Bh4V|2OS%YHhWz9I$^ckf!=4_oqJCxYzla)(WG&y z`RkvXvqF>iKcB_He&lD3T~!QE?qlxfn_PJ8&-uVKri|pd`&cUu zHOqW^HG4wJ)u(nFc5gd!rsT+C4-vyfvwuBi75Z{!*A1Z+Eef0VFxlsSJ$|PA?ZsXH zLi%crw(k@8$((Ak=d!!mm&rhv^QA_3ruq6ZXaU(AKnwzxfzopr07$uN As{jB1 literal 0 HcmV?d00001 diff --git a/assets/soko/sprites/pixel/sk_pixel_front.png b/assets/soko/sprites/pixel/sk_pixel_front.png new file mode 100644 index 0000000000000000000000000000000000000000..77517ad271c00cb67b997f8e4bcdbd3bcc283955 GIT binary patch literal 297 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`*F0SuLn?0d1|1Y+GUQky7JcG= zUdo)0$Jz_n$p-E>S|us`>ll z$QQhC66YTLTl4h#(=eA$Td&M{(-w;$1 zU)pN!B;r^T#Qt*ftdrWIo7x}5=3G+U`wRy@CmrrnVu(fqq z9k^Dvq&W1(ja3)7{JgwIHgS@+?f<7c4HPx$EJ;K`R5*==lPgjKK@f(&Nu`Q2ASq1ztij|ONE97M0VcOVVWv3?h!{kATsQ;{ z5p*Lm8FsUi_p0}b20Q)H7-05cB06v$}v=8T1fEfcguXgVhNS?5O_l-;jaM3Y1gmD=rhX=9b z+=u&`tsmH~H#M)R0@8b|Xb{AI9y2BIXd!vs*4IdlW2*~X^w?mMi*AKZVZo;+Q5 QrT_o{07*qoM6N<$f*Vku(f|Me literal 0 HcmV?d00001 diff --git a/assets/soko/sprites/sk_crate.png b/assets/soko/sprites/sk_crate.png new file mode 100644 index 0000000000000000000000000000000000000000..65a07ff576aef87225defe11c3e3c6a4dcf19e7d GIT binary patch literal 1476 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|_);T0(|mmy zw18|5AO?X;!IOa`XMsm#F$061G6*wPEVVCVU|_Dx42dX-@b$4u&d=3LOvz75)vL%Y z0PC`;umUo3Q%e#RDspr3imfVamB1>jfNYSkzLEl1NlCV?QiN}Sf^&XRs)CuGfu4bq z9hZWFf=y9MnpKdC8&o@xXRDM^Qc_^0uU}qXu2*iXmtT~wZ)j<0sc&GUZ)BtkRH0j3 znOBlnp_^B%3^4>|j!SBBa#3bMNoIbY0?6FNr2NtnTO}osMQ{LdXG${Mo`TY%9I!1Z z$@-}|sky0nCB^!NdWQPg^p#|$AzYYO3=Ixo!03ZyfZ7bOYV#~8Nj3q7lxqdhJy8Dv z9hwZbx40xlA4!3}k%57Qu7Q!Rk)=M|e?aHkq$FFFWR~Qlf&&ijA8-gd=9Hj{g4Bb8 zASV+PvQ{~XdFi%F6}l;@X^EvdB}#Uod0?Yb6da36%JYk|ZS*0kQB8q}q8e_akHsA} zAm3X>2Bj9~=ahoN-_F>;zy=&9D58j%far+8ssmXRT}MDhen~zsWff&6d*+p-78Mi$ zQyJJsBdC42RiK-Nusk#`v&0T&0Gcq8E~Hqpf(Bd&I7Tpo6Q@$Bagd-zO0kga0FDM= zez4=R(TC?PJFdSueG9f=ykk|MjF7~HWiQ}unjoB~mIyvU>cd^g@k!HsE)`@-7NuOtD-|U&GF>CVa z!=HFo%`zdj33Z#B^&)}DFh%jrhwf$JuRfyl`l;DryJMwsNec)oZ2oC@oOeKJ~T~^V))(t=AB{u ze);+*qN@+EMz#vI+|tSD%$sse%H>zzgb63!EcVP1H#|1U?XQyEVhyneSI#UgegDgt zb3*F5@6r=zS|^+k4qGjfv3Sn(4R;!CeWbFMCfT{G>P)*J`$Igu zc+(B{&lT6QRd21FT6yZoPqx2Xk6k$U&1&lgF&4Yu(V>xV9;;n6b`IOMC`0T-(Tj6| ztUH_L_h%-Vn@n3S=)Iyn{>v9TrS80nm15x;<|`{gmxjfNYSkzLEl1NlCV?QiN}Sf^&XRs)CuGfu4bq z9hZWFf=y9MnpKdC8&o@xXRDM^Qc_^0uU}qXu2*iXmtT~wZ)j<0sc&GUZ)BtkRH0j3 znOBlnp_^B%3^4>|j!SBBa#3bMNoIbY0?6FNr2NtnTO}osMQ{LdXG${Mo`TY%9I!1Z z$@-}|sky0nCB^!NdWQPg^p#|$AzYYO3=Ixo!03ZyfZ7bOYV#~8Nj3q7lxqdhJy8Dv z9hwZbx40xlA4!3}k%57Qu7Q!Rk)=M|e?aHkq$FFFWR~Qlf&&ijA8-gd=9Hj{g4Bb8 zASV+PvQ{~XdFi%F6}l;@X^EvdB}#Uod0?Yb6da36%JYk|ZS*0kQB8q}q8e_akHsA} zAm3X>2Bj9~=ahoN-_F>;zy=&9D58j%far+8ssmXRT}MDhen~zsWff&6d*+p-78Mi$ zQyJJsBdC42RiK-Nusk#`v&0T&0Gcq8E~Hqpf(Bd&I7Tpo6Q@$Bagd-zO0kga0FDM= zez4=R(TC?PJFdSueGj@7e3|u zbNA4B%K-m{68eUQijSO_ts9vWe0+{%h#W5Yy((?l+=t2fX}ea%?OhihyH;Z>-?eT< zpSeXl*9LX1*_t!kE&YXRqfGv!BbN`qJ;9%_SMb8cchhc&n{VH;?a}3PZmkCv>9V>B zY6@s7GI)ibwlkHVr_}W0_Ny-^U6x-u{rbYu)vNdIde6^uSj2Dou1hneo<935*IRci zvpkKT^Fy*^!-8r`}jSMkS9BCdi zCeJGEW|_BP^4};kChZm;{ZeCw!_HA#PEQq>n|z??O@>{WEMvIX=_6%XA3y8}+Y5>5QS;RF3Lju?zSMwU@+*sGg4$x_thp_&51w V55Av2tO6<}Jzf1=);T3K0RVIv_tXFY literal 0 HcmV?d00001 diff --git a/assets/soko/sprites/sk_crate_ongoal.png b/assets/soko/sprites/sk_crate_ongoal.png new file mode 100644 index 0000000000000000000000000000000000000000..43273405a806945c57985f2b8463fcf979f74b1e GIT binary patch literal 218 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|rg*wIhFJ72 zogB#5qQK*_{D}0<^hH2;%XV^|0%PH=7gqKv7rrm>6=2Z#xtp;q{ba!Jj*tlXL%OWq z`*+H$W{Qw~V189nX!Es09PbpF)Mh@sSClk6owK>RyE&m$C3)TFnNc59yb_gm{GJqW zq3T5)_zm<jfNYSkzLEl1NlCV?QiN}Sf^&XRs)CuGfu4bq z9hZWFf=y9MnpKdC8&o@xXRDM^Qc_^0uU}qXu2*iXmtT~wZ)j<0sc&GUZ)BtkRH0j3 znOBlnp_^B%3^4>|j!SBBa#3bMNoIbY0?6FNr2NtnTO}osMQ{LdXG${Mo`TY%9I!1Z z$@-}|sky0nCB^!NdWQPg^p#|$AzYYO3=Ixo!03ZyfZ7bOYV#~8Nj3q7lxqdhJy8Dv z9hwZbx40xlA4!3}k%57Qu7Q!Rk)=M|e?aHkq$FFFWR~Qlf&&ijA8-gd=9Hj{g4Bb8 zASV+PvQ{~XdFi%F6}l;@X^EvdB}#Uod0?Yb6da36%JYk|ZS*0kQB8q}q8e_akHsA} zAm3X>2Bj9~=ahoN-_F>;zy=&9D58j%far+8ssmXRT}MDhen~zsWff&6d*+p-78Mi$ zQyJJsBdC42RiK-Nusk#`v&0T&0Gcq8E~Hqpf(Bd&I7Tpo6Q@$Bagd-zO0kga0FDM= zez4=R(TC?PJFdSueG!wu;@i6-Mt)Jr#Fd5>uxf1?2<6;Jbv1 zvP9bR>iggKRT^{K7^^&}R$DsJ@5r(}H~S-&tUrX zPob7aN|RGh@0Lb}W{0)7rqT z7kYEooq1g~S3rvOl|}UPgx4!YSk~WF{-7KwmE-JsrDDHekI>I!X8H5pUax+Xz4Nf! zp|6``Sa|Nus1ZK+?c=mVzmDw=SQql4^s`Km*VQfllE*z321cr0EO~w4dW_{<-ixh) z0t-t5-L|^rt^4hhV$2$&_2=A^e-g8oJ>;G%I#nrL<@qYD=bvW2FR=0EH+Y^u{iEj9 mn5il0GP`V+K2`jCy literal 0 HcmV?d00001 diff --git a/assets/soko/sprites/sk_e_crate.png b/assets/soko/sprites/sk_e_crate.png new file mode 100644 index 0000000000000000000000000000000000000000..5b7c207dfdc8602d0394da422a5cb032ab698c6f GIT binary patch literal 183 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|3O!vMLo9la z20QW{P~gz+uK()0Z`xd+4FVmBCvAebmM(bFwCLkJK1l{Qfyqt98hs)u%9dxe1ZFEt zo^e{m@5Hx0_Z3gL#f#+%()*%~J{%PLr~km_hH+Z-2@#D~))pTe6K+l{R_5OnT@dj_ fd|h37HMjn=cPx$Ur9tkR5*==lTA(nK@f$%8s`YgNQ_>hjfY~zPLJTym3L7#Dv6+hL_L8&gU%35 zG%kpL^IXi(&6UiJC@tZuudZo4JtoMM2Lm53y%kv`l4Ac>+7gY>TQ;W54UMeFMc zgTYhD>7v~dQ-saa4`qyzFcx9}x4knfa}rH>ODZsn`1$gJkQ;HjI1x1@QE3@rx8 z^WndN3lkWj1fi0NDh6J-FpJ9b3hehTiHkyB!dQl+KXMi_h@yxr9|k>J)bUxonu6G_ zBuK)dnaXqeIG#kmcS#IJ`fwXiUM6AoECwx|!rUh@mBawLXU9sjmj%k_BrOWIHfl7Q zyX8ADGdAn1BuUL@&axp%vWA)Ql;zad6Q3Z^_u&AMqyDRdE?9N&$;udYI$ycORaTgxE#hhr!n!(mRvl+< zjf!Sap>Vk75BZ3Lkp&w%LJA5C1YFn!@6NH(+>udnZ+gfrrF+Mp{yffy2bbzGx zHztjj(n}Cz)4NjFZf(ACZ~oQr=|^Q=1)fP1R+H!o2yA3weOUkJa-V&lZc%tb zfL=e7&M6m9LE%kDK3`avC}dQ~;rMF8j^fvK{A+)o%*}l-IwQB>oc^s_PRtf_7f!x< z`&I>ugcL*GD<9Jk_g4$QJu5o!o6}$hQ`>uu&56$rDVA5)^Sr2j_$Qg?gDg|o)X(dc z&%Kpuo!_Yamt9;lYfmtn<7r?#@TEq0ruq6ZXaU(AKnwzxf+qt>&H|6fVg?31We{ep zSZZI!z`$IW84^(v;p=0SoS&h?X&sHg;q@=(~ zU%$M(T(8_%FTW^V-_X+1Qs2Nx-^fT8s6w~6GOr}DLN~8i8Da>`9GBGM~RsBTId_ z|A5Z7NlCUU$t=l91qU45Kj08_%qc+?1*r!GK~5$pWUX=%^U`gVDs)p)(-KQ_N|fwE z^T0->C^#0Cl;;;^+vr17qnZK{MK#<;AB#I|K)$z%3`#A|&nX3kzn!sxfekoJP(%?i z0nrhGRR^*tx{iR1{E~cN$|}lC_RK3uEh;DirZTXNMo{~3t3WpkVR>j?W{Dll05oAF zT}ZKH1r4|maExFECr+hM;~+tclwu*-0UQm${9wmrqYuwpc3gjR`XqoQXRW7;V~B-d z?4&@i!vO-W@s|}uLb6yFDsZ_@{PlFdc7b%0b%Ex(Bep-5D{zPf{t9eqNDvWy<-gKW z*uR5MW%8LbbF0(uy`B>{N5{STut}|!P+5%$UwqBp_UEqWeq4RKr1SH{i4yC+&2IAi zIg^3;Rhjuek#SJ7$vad0+kZ9V!ddCjds7*7mS|Te&U|ZoC3%TLVax`@LubFnKA1UE`D=ux z!^FoPm%iORy=U^i&o)mEy|Q)Cvb=f2>Q&B(85(CfCMGqrW?UBcEdH^@NqbPq=jrO_vd$@?2>_bN Bjne=C literal 0 HcmV?d00001 diff --git a/assets/soko/sprites/sk_portal_complete.png b/assets/soko/sprites/sk_portal_complete.png new file mode 100644 index 0000000000000000000000000000000000000000..1dd3ca145f57ef997875b3af3b0a0252428180a5 GIT binary patch literal 141 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|+&x_!Lo9le zP3BpCwr6H-=oC830|Yt-$0FMzRWeP|Je*A%8i63g_>zE6*It9H11lJLcwT54^0sRw i@ffJNn=^~M literal 0 HcmV?d00001 diff --git a/assets/soko/sprites/sk_ring.png b/assets/soko/sprites/sk_ring.png new file mode 100644 index 0000000000000000000000000000000000000000..d57803713c17efe18d63ad34e2c4e7e25c6def83 GIT binary patch literal 1529 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|_);T0(|mmy zw18|5AO?X;!IOa`XMsm#F$061G6*wPEVVCVU|_Dx42dX-@b$4u&d=3LOvz75)vL%Y z0PC`;umUo3Q%e#RDspr3imfVamB1>jfNYSkzLEl1NlCV?QiN}Sf^&XRs)CuGfu4bq z9hZWFf=y9MnpKdC8&o@xXRDM^Qc_^0uU}qXu2*iXmtT~wZ)j<0sc&GUZ)BtkRH0j3 znOBlnp_^B%3^4>|j!SBBa#3bMNoIbY0?6FNr2NtnTO}osMQ{LdXG${Mo`TY%9I!1Z z$@-}|sky0nCB^!NdWQPg^p#|$AzYYO3=Ixo!03ZyfZ7bOYV#~8Nj3q7lxqdhJy8Dv z9hwZbx40xlA4!3}k%57Qu7Q!Rk)=M|e?aHkq$FFFWR~Qlf&&ijA8-gd=9Hj{g4Bb8 zASV+PvQ{~XdFi%F6}l;@X^EvdB}#Uod0?Yb6da36%JYk|ZS*0kQB8q}q8e_akHsA} zAm3X>2Bj9~=ahoN-_F>;zy=&9D58j%far+8ssmXRT}MDhen~zsWff&6d*+p-78Mi$ zQyJJsBdC42RiK-Nusk#`v&0T&0Gcq8E~Hqpf(Bd&I7Tpo6Q@$Bagd-zO0kga0FDM= zez4=R(TC?PJFdSueGF4rpEn8W!vg1wOBrY?4#l0&Y zxXxd=h4rhck`k+avFeIkhYzZIKAloG;9YX^-Lyx(nVn+)kKQ$|E54<3%#`o+yN74a zJpwPa>td%&Ry0e+5*9o1fmb z{29aThm&<`K1^HaYrXHwpU=ED42|a=9eBdZz`g8XaO&fY4QGt^btyIqrTaC;P5F3p zSSJz(O71piTcRYPnJyVh4f=PS}o^8Slr zJC}WsY>*4Qlqzi&^(uVl2k&!7DY&MA4v;Ki%(q3=2iwidIir8spb39@C>;FIJ)-xwnpHSwGDFPMg44$rjF6*2UngDdm BB#{6B literal 0 HcmV?d00001 diff --git a/assets/soko/sprites/sk_sticky_crate.png b/assets/soko/sprites/sk_sticky_crate.png new file mode 100644 index 0000000000000000000000000000000000000000..c1d4307252c18e35ee2a37c6c603555583d5de86 GIT binary patch literal 268 zcmV+n0rUQeP)Px##z{m$R5*>Llj{wGFbsshL~P{Fl+HwE!ZW2moG1jFQmK}Z5I!Gwek1^)G?pj; zD6eGsqvz{_Qff*a+mh60q>GAhw3C84 zw!0=q*uTfvS9umhpgVG*w9Q`ZSk{fauJ766Jg(eX!+#mcxCy7kEX1;ZKj#m}-c+(t SSX#0G0000Px#vPnciR5*>DQ_Bs-APh688>KTbRAy>s;vEuNnxr5-pO9i(CZ7TTgx2vw0{{rl zqV>z_{9*~X6hiBwCJ-%QV-4oZSx+J`31~3R$pAtNO{AyyxEpd(4WAr$@Ae;4< y`Lsymc-WP5DNQ$0qi`0CJlS5T#?JNsbN&JHuRZ8Mz*r&x0000 #include "mode_pinball.h" -#include "pinball_zones.h" +#include "pinball_game.h" #include "pinball_physics.h" #include "pinball_draw.h" -#include "pinball_test.h" + +//============================================================================== +// Structs +//============================================================================== + +typedef struct +{ + pbScene_t scene; + font_t ibm; +} pinball_t; //============================================================================== // Function Prototypes @@ -60,38 +69,10 @@ static void pinEnterMode(void) // Allocate all the memory pinball = calloc(sizeof(pinball_t), 1); - pinball->balls = heap_caps_calloc(MAX_NUM_BALLS, sizeof(pbCircle_t), MALLOC_CAP_SPIRAM); - pinball->bumpers = heap_caps_calloc(MAX_NUM_BUMPERS, sizeof(pbCircle_t), MALLOC_CAP_SPIRAM); - pinball->walls = heap_caps_calloc(MAX_NUM_WALLS, sizeof(pbLine_t), MALLOC_CAP_SPIRAM); - pinball->flippers = heap_caps_calloc(MAX_NUM_FLIPPERS, sizeof(pbFlipper_t), MALLOC_CAP_SPIRAM); - - pinball->ballsTouching = heap_caps_calloc(MAX_NUM_BALLS, sizeof(pbTouchRef_t*), MALLOC_CAP_SPIRAM); - for (uint32_t i = 0; i < MAX_NUM_BALLS; i++) - { - pinball->ballsTouching[i] = heap_caps_calloc(MAX_NUM_TOUCHES, sizeof(pbTouchRef_t), MALLOC_CAP_SPIRAM); - } - - // Split the table into zones - createTableZones(pinball); - - // Create random balls - createRandomBalls(pinball, 0); - pbCreateBall(pinball, 6, 114); - pbCreateBall(pinball, 274, 114); - pbCreateBall(pinball, 135, 10); + loadFont("ibm_vga8.font", &pinball->ibm, false); - // Create random bumpers - createRandomBumpers(pinball, 0); - - // Create random walls - createRandomWalls(pinball, 0); - - // Create flippers - createFlipper(pinball, TFT_WIDTH / 2 - 50, 200, true); - createFlipper(pinball, TFT_WIDTH / 2 + 50, 200, false); - - // Load font - loadFont("ibm_vga8.font", &pinball->ibm_vga8, false); + pbSceneInit(&pinball->scene); + pbStartBall(&pinball->scene); } /** @@ -100,19 +81,8 @@ static void pinEnterMode(void) */ static void pinExitMode(void) { - for (uint32_t i = 0; i < MAX_NUM_BALLS; i++) - { - free(pinball->ballsTouching[i]); - } - free(pinball->ballsTouching); - - free(pinball->balls); - free(pinball->walls); - free(pinball->bumpers); - free(pinball->flippers); - // Free font - freeFont(&pinball->ibm_vga8); - // Free the rest of the state + freeFont(&pinball->ibm); + pbSceneDestroy(&pinball->scene); free(pinball); } @@ -123,37 +93,25 @@ static void pinExitMode(void) */ static void pinMainLoop(int64_t elapsedUs) { - // Make a local copy for speed - pinball_t* p = pinball; - - // Check all queued button events - buttonEvt_t evt; + // Handle inputs + buttonEvt_t evt = {0}; while (checkButtonQueueWrapper(&evt)) { - if (PB_RIGHT == evt.button) + if (evt.down && PB_START == evt.button) { - p->flippers[1].buttonHeld = evt.down; + pbSceneDestroy(&pinball->scene); + pbSceneInit(&pinball->scene); } - else if (PB_LEFT == evt.button) + else { - p->flippers[0].buttonHeld = evt.down; + pbButtonPressed(&pinball->scene, &evt); } } - // Only check physics once per frame - p->frameTimer += elapsedUs; - while (p->frameTimer >= PIN_US_PER_FRAME) - { - p->frameTimer -= PIN_US_PER_FRAME; - updatePinballPhysicsFrame(pinball); - } - - // Always draw foreground to prevent flicker - pinballDrawForeground(pinball); - - // Log frame time for FPS - p->frameTimesIdx = (p->frameTimesIdx + 1) % NUM_FRAME_TIMES; - p->frameTimes[p->frameTimesIdx] = esp_timer_get_time(); + pbSimulate(&pinball->scene, elapsedUs); + pbGameTimers(&pinball->scene, elapsedUs); + pbAdjustCamera(&pinball->scene); + pbSceneDraw(&pinball->scene, &pinball->ibm); } /** @@ -168,5 +126,4 @@ static void pinMainLoop(int64_t elapsedUs) */ static void pinBackgroundDrawCallback(int16_t x, int16_t y, int16_t w, int16_t h, int16_t up, int16_t upNum) { - pinballDrawBackground(pinball, x, y, w, h); } diff --git a/attic/pinball/mode_pinball.h b/attic/pinball/mode_pinball.h new file mode 100644 index 000000000..e170e8c86 --- /dev/null +++ b/attic/pinball/mode_pinball.h @@ -0,0 +1,28 @@ +#pragma once + +//============================================================================== +// Includes +//============================================================================== + +#include +#include "swadge2024.h" + +//============================================================================== +// Defines +//============================================================================== + +#define PIN_US_PER_FRAME 16667 + +//============================================================================== +// Enums +//============================================================================== + +//============================================================================== +// Structs +//============================================================================== + +//============================================================================== +// Extern variables +//============================================================================== + +extern swadgeMode_t pinballMode; diff --git a/attic/pinball/pinball_circle.c b/attic/pinball/pinball_circle.c new file mode 100644 index 000000000..a606799d6 --- /dev/null +++ b/attic/pinball/pinball_circle.c @@ -0,0 +1,80 @@ +#include "pinball_circle.h" +#include "pinball_rectangle.h" + +/** + * @brief TODO doc + * + * @param tableData + * @param scene + * @return uint32_t + */ +uint32_t readCircleFromFile(uint8_t* tableData, pbScene_t* scene) +{ + pbCircle_t* circle = &scene->circles[scene->numCircles++]; + + uint32_t dIdx = 0; + circle->id = readInt16(tableData, &dIdx); + circle->groupId = readInt8(tableData, &dIdx); + circle->group = addToGroup(scene, circle, circle->groupId); + circle->pos.x = readInt16(tableData, &dIdx); + circle->pos.y = readInt16(tableData, &dIdx); + circle->radius = readInt8(tableData, &dIdx); + circle->type = readInt8(tableData, &dIdx); + circle->pushVel = readInt8(tableData, &dIdx); + + return dIdx; +} + +/** + * @brief Simulate a ball's motion + * + * @param ball + * @param dt + * @param scene + */ +void pbBallSimulate(pbBall_t* ball, int32_t elapsedUs, float dt, pbScene_t* scene) +{ + if (ball->scoopTimer <= 0) + { + ball->vel = addVecFl2d(ball->vel, mulVecFl2d(scene->gravity, dt)); + ball->pos = addVecFl2d(ball->pos, mulVecFl2d(ball->vel, dt)); + } + else + { + ball->scoopTimer -= elapsedUs; + + if (ball->scoopTimer <= 0) + { + // Respawn in the launch tube + for (int32_t pIdx = 0; pIdx < scene->numPoints; pIdx++) + { + pbPoint_t* point = &scene->points[pIdx]; + if (PB_BALL_SPAWN == point->type) + { + ball->pos = point->pos; + break; + } + } + + pbOpenLaunchTube(scene, true); + + // Give the ball initial velocity + ball->vel.x = 0; + ball->vel.y = MAX_LAUNCHER_VELOCITY; + } + } +} + +/** + * @brief TODO + * + * @param circle + * @param elapsedUs + */ +void pbCircleTimer(pbCircle_t* circle, int32_t elapsedUs) +{ + if (circle->litTimer > 0) + { + circle->litTimer -= elapsedUs; + } +} diff --git a/attic/pinball/pinball_circle.h b/attic/pinball/pinball_circle.h new file mode 100644 index 000000000..53132f731 --- /dev/null +++ b/attic/pinball/pinball_circle.h @@ -0,0 +1,10 @@ +#pragma once + +#include "pinball_typedef.h" +#include "pinball_game.h" + +#define PINBALL_RADIUS 8 + +uint32_t readCircleFromFile(uint8_t* tableData, pbScene_t* scene); +void pbBallSimulate(pbBall_t* ball, int32_t elapsedUs, float dt, pbScene_t* scene); +void pbCircleTimer(pbCircle_t* circle, int32_t elapsedUs); diff --git a/attic/pinball/pinball_draw.c b/attic/pinball/pinball_draw.c new file mode 100644 index 000000000..155741306 --- /dev/null +++ b/attic/pinball/pinball_draw.c @@ -0,0 +1,153 @@ +#include "hdw-tft.h" +#include "shapes.h" + +#include "pinball_draw.h" +#include "pinball_line.h" +#include "pinball_circle.h" +#include "pinball_rectangle.h" +#include "pinball_flipper.h" + +/** + * @brief TODO doc + * + * @param scene + */ +void pbAdjustCamera(pbScene_t* scene) +{ + // No balls? No camera adjustment! + if (0 == scene->balls.length) + { + return; + } + + // Find the ball lowest on the table + float lowestBallX = 0; + float lowestBallY = 0; + + node_t* ballNode = scene->balls.first; + while (ballNode) + { + pbBall_t* ball = ballNode->val; + if (ball->pos.y > lowestBallY) + { + lowestBallX = ball->pos.x; + lowestBallY = ball->pos.y; + } + ballNode = ballNode->next; + } + + // Adjust the lowest ball's position to screen coordinates + lowestBallY -= scene->cameraOffset.y; + +#define PIN_CAMERA_BOUND_UPPER ((TFT_HEIGHT) / 4) +#define PIN_CAMERA_BOUND_LOWER ((3 * TFT_HEIGHT) / 4) + + // If the lowest ball is lower than the boundary + if (lowestBallY > PIN_CAMERA_BOUND_LOWER) + { + // Pan the camera down + if (scene->cameraOffset.y < scene->tableDim.y - TFT_HEIGHT) + { + scene->cameraOffset.y += (lowestBallY - PIN_CAMERA_BOUND_LOWER); + } + } + // If the lowest ball is higher than the other boundary + else if (lowestBallY < PIN_CAMERA_BOUND_UPPER) + { + // Pan the camera up + if (scene->cameraOffset.y > 0) + { + scene->cameraOffset.y -= (PIN_CAMERA_BOUND_UPPER - lowestBallY); + } + } + + // Pan in the X direction to view the launch tube + int16_t xEnd = lowestBallX + PINBALL_RADIUS + 40; + if (xEnd > TFT_WIDTH) + { + scene->cameraOffset.x = xEnd - TFT_WIDTH; + } + else + { + scene->cameraOffset.x = 0; + } +} + +/** + * @brief TODO doc + * + * @param scene + */ +void pbSceneDraw(pbScene_t* scene, font_t* font) +{ + clearPxTft(); + + // Draw an indicator for the ball save + if (scene->saveTimer > 0) + { + const char text[] = "SAVE"; + int16_t tWidth = textWidth(font, text); + drawText(font, c555, text, ((280 - tWidth) / 2) - scene->cameraOffset.x, 400 - scene->cameraOffset.y); + } + + // Triangle indicators + for (int32_t i = 0; i < scene->numTriangles; i++) + { + pbTriangle_t* tri = &scene->triangles[i]; + drawTriangleOutlined(tri->p1.x - scene->cameraOffset.x, tri->p1.y - scene->cameraOffset.y, // + tri->p2.x - scene->cameraOffset.x, tri->p2.y - scene->cameraOffset.y, // + tri->p3.x - scene->cameraOffset.x, tri->p3.y - scene->cameraOffset.y, // + tri->isOn ? c550 : cTransparent, c220); + } + + // Lines + for (int32_t i = 0; i < scene->numLines; i++) + { + pinballDrawLine(&scene->lines[i], &scene->cameraOffset); + } + + // balls + node_t* bNode = scene->balls.first; + while (bNode) + { + pbBall_t* ball = bNode->val; + + // Don't draw when scooped + if (ball->scoopTimer <= 0) + { + vecFl_t* pos = &ball->pos; + drawCircleFilled(pos->x - scene->cameraOffset.x, pos->y - scene->cameraOffset.y, ball->radius, c500); + } + + bNode = bNode->next; + } + + // circles + for (int32_t i = 0; i < scene->numCircles; i++) + { + if (PB_BUMPER == scene->circles[i].type) + { + vecFl_t* pos = &scene->circles[i].pos; + drawCircleFilled(pos->x - scene->cameraOffset.x, pos->y - scene->cameraOffset.y, scene->circles[i].radius, + (scene->circles[i].litTimer > 0) ? c252 : c131); + } + } + + // flippers + for (int32_t i = 0; i < scene->numFlippers; i++) + { + pinballDrawFlipper(&scene->flippers[i], &scene->cameraOffset); + } + + // launchers + for (int32_t i = 0; i < scene->numLaunchers; i++) + { + pbLauncher_t* l = &scene->launchers[i]; + int compression = l->height * l->impulse; + vec_t offsetPos = { + .x = l->pos.x - scene->cameraOffset.x, + .y = l->pos.y + compression - scene->cameraOffset.y, + }; + drawRect(offsetPos.x, offsetPos.y, offsetPos.x + l->width, offsetPos.y + l->height - compression, c330); + } +} diff --git a/attic/pinball/pinball_draw.h b/attic/pinball/pinball_draw.h new file mode 100644 index 000000000..848c2645b --- /dev/null +++ b/attic/pinball/pinball_draw.h @@ -0,0 +1,7 @@ +#pragma once + +#include "font.h" +#include "pinball_typedef.h" + +void pbAdjustCamera(pbScene_t* scene); +void pbSceneDraw(pbScene_t* scene, font_t* font); diff --git a/attic/pinball/pinball_flipper.c b/attic/pinball/pinball_flipper.c new file mode 100644 index 000000000..04d007d9b --- /dev/null +++ b/attic/pinball/pinball_flipper.c @@ -0,0 +1,106 @@ +#include + +#include "macros.h" +#include "shapes.h" + +#include "pinball_flipper.h" + +/** + * @brief TODO doc + * + * @param tableData + * @param scene + * @return uint32_t + */ +uint32_t readFlipperFromFile(uint8_t* tableData, pbScene_t* scene) +{ + pbFlipper_t* flipper = &scene->flippers[scene->numFlippers++]; + uint32_t dIdx = 0; + + flipper->pos.x = readInt16(tableData, &dIdx); + flipper->pos.y = readInt16(tableData, &dIdx); + flipper->radius = readInt8(tableData, &dIdx); + flipper->length = readInt8(tableData, &dIdx); + flipper->facingRight = readInt8(tableData, &dIdx) != 0; + + flipper->maxRotation = 1.0f; + flipper->restAngle = 0.523599f; // 30 degrees + flipper->angularVelocity = 20.0f; + + if (!flipper->facingRight) + { + flipper->restAngle = M_PI - flipper->restAngle; + flipper->maxRotation = -flipper->maxRotation; + } + flipper->sign = (flipper->maxRotation >= 0) ? -1 : 1; + flipper->maxRotation = ABS(flipper->maxRotation); + + // changing + flipper->rotation = 0; + flipper->currentAngularVelocity = 0; + flipper->buttonHeld = false; + + return dIdx; +} + +/** + * @brief TODO doc + * + * @param flipper + * @param dt + */ +void pbFlipperSimulate(pbFlipper_t* flipper, float dt) +{ + float prevRotation = flipper->rotation; + + if (flipper->buttonHeld) + { + flipper->rotation = flipper->rotation + dt * flipper->angularVelocity; + if (flipper->rotation > flipper->maxRotation) + { + flipper->rotation = flipper->maxRotation; + } + } + else + { + flipper->rotation = flipper->rotation - dt * flipper->angularVelocity; + if (flipper->rotation < 0) + { + flipper->rotation = 0; + } + } + flipper->currentAngularVelocity = flipper->sign * (flipper->rotation - prevRotation) / dt; +} + +/** + * @brief TODO doc + * + * @param flipper + * @return vecFl_t + */ +vecFl_t pbFlipperGetTip(pbFlipper_t* flipper) +{ + float angle = flipper->restAngle + flipper->sign * flipper->rotation; + vecFl_t dir = {.x = cosf(angle), .y = sinf(angle)}; + return addVecFl2d(flipper->pos, mulVecFl2d(dir, flipper->length)); +} + +/** + * @brief TODO doc + * + * @param flipper + */ +void pinballDrawFlipper(pbFlipper_t* flipper, vec_t* cameraOffset) +{ + vecFl_t pos = { + .x = flipper->pos.x - cameraOffset->x, + .y = flipper->pos.y - cameraOffset->y, + }; + drawCircleFilled(pos.x, pos.y, flipper->radius, c115); + vecFl_t tip = pbFlipperGetTip(flipper); + tip.x -= cameraOffset->x; + tip.y -= cameraOffset->y; + drawCircleFilled(tip.x, tip.y, flipper->radius, c115); + drawLine(pos.x, pos.y + flipper->radius, tip.x, tip.y + flipper->radius, c115, 0); + drawLine(pos.x, pos.y - flipper->radius, tip.x, tip.y - flipper->radius, c115, 0); +} diff --git a/attic/pinball/pinball_flipper.h b/attic/pinball/pinball_flipper.h new file mode 100644 index 000000000..d52429deb --- /dev/null +++ b/attic/pinball/pinball_flipper.h @@ -0,0 +1,9 @@ +#pragma once + +#include "pinball_typedef.h" +#include "pinball_game.h" + +uint32_t readFlipperFromFile(uint8_t* tableData, pbScene_t* scene); +void pinballDrawFlipper(pbFlipper_t* flipper, vec_t* cameraOffset); +void pbFlipperSimulate(pbFlipper_t* flipper, float dt); +vecFl_t pbFlipperGetTip(pbFlipper_t* flipper); diff --git a/attic/pinball/pinball_game.c b/attic/pinball/pinball_game.c new file mode 100644 index 000000000..1b7bb4267 --- /dev/null +++ b/attic/pinball/pinball_game.c @@ -0,0 +1,500 @@ +#include +#include +#include +#include +#include "heatshrink_helper.h" + +#include "pinball_game.h" + +#include "pinball_line.h" +#include "pinball_circle.h" +#include "pinball_rectangle.h" +#include "pinball_flipper.h" +#include "pinball_triangle.h" +#include "pinball_point.h" + +/** + * @brief TODO doc + * + * @param data + * @param idx + * @return uint8_t + */ +uint8_t readInt8(uint8_t* data, uint32_t* idx) +{ + return data[(*idx)++]; +} + +/** + * @brief TODO doc + * + * @param data + * @param idx + * @return uint16_t + */ +uint16_t readInt16(uint8_t* data, uint32_t* idx) +{ + int16_t ret = (data[*idx] << 8) | (data[(*idx) + 1]); + (*idx) += 2; + return ret; +} + +/** + * @brief TODO + * + * @param scene + * @param obj + * @param groupId + * @return list_t* + */ +list_t* addToGroup(pbScene_t* scene, void* obj, uint8_t groupId) +{ + push(&scene->groups[groupId], obj); + return &scene->groups[groupId]; +} + +/** + * @brief TODO doc + * + * @param scene + */ +void pbSceneInit(pbScene_t* scene) +{ + scene->gravity.x = 0; + scene->gravity.y = 180; + scene->score = 0; + scene->paused = false; + + uint32_t decompressedSize = 0; + uint8_t* tableData = (uint8_t*)readHeatshrinkFile("pinball.raw", &decompressedSize, true); + uint32_t dIdx = 0; + + // Allocate groups + scene->numGroups = readInt8(tableData, &dIdx) + 1; + scene->groups = (list_t*)calloc(scene->numGroups, sizeof(list_t)); + + uint16_t linesInFile = readInt16(tableData, &dIdx); + scene->lines = calloc(linesInFile, sizeof(pbLine_t)); + scene->numLines = 0; + for (uint16_t lIdx = 0; lIdx < linesInFile; lIdx++) + { + dIdx += readLineFromFile(&tableData[dIdx], scene); + pbLine_t* newLine = &scene->lines[scene->numLines - 1]; + + // Record the table dimension + float maxX = MAX(newLine->p1.x, newLine->p2.x); + float maxY = MAX(newLine->p1.y, newLine->p2.y); + + if (maxX > scene->tableDim.x) + { + scene->tableDim.x = maxX; + } + if (maxY > scene->tableDim.y) + { + scene->tableDim.y = maxY; + } + } + + uint16_t circlesInFile = readInt16(tableData, &dIdx); + scene->circles = calloc(circlesInFile, sizeof(pbCircle_t)); + scene->numCircles = 0; + for (uint16_t cIdx = 0; cIdx < circlesInFile; cIdx++) + { + dIdx += readCircleFromFile(&tableData[dIdx], scene); + } + + uint16_t rectanglesInFile = readInt16(tableData, &dIdx); + scene->launchers = calloc(1, sizeof(pbLauncher_t)); + scene->numLaunchers = 0; + for (uint16_t rIdx = 0; rIdx < rectanglesInFile; rIdx++) + { + dIdx += readRectangleFromFile(&tableData[dIdx], scene); + } + + uint16_t flippersInFile = readInt16(tableData, &dIdx); + scene->flippers = calloc(flippersInFile, sizeof(pbFlipper_t)); + scene->numFlippers = 0; + for (uint16_t fIdx = 0; fIdx < flippersInFile; fIdx++) + { + dIdx += readFlipperFromFile(&tableData[dIdx], scene); + } + + uint16_t trianglesInFile = readInt16(tableData, &dIdx); + scene->triangles = calloc(trianglesInFile, sizeof(pbTriangle_t)); + scene->numTriangles = 0; + for (uint16_t tIdx = 0; tIdx < trianglesInFile; tIdx++) + { + dIdx += readTriangleFromFile(&tableData[dIdx], scene); + } + + uint16_t pointsInFile = readInt16(tableData, &dIdx); + scene->points = calloc(pointsInFile, sizeof(pbPoint_t)); + scene->numPoints = 0; + for (uint16_t pIdx = 0; pIdx < pointsInFile; pIdx++) + { + dIdx += readPointFromFile(&tableData[dIdx], scene); + } + + free(tableData); + + // Reset the camera + scene->cameraOffset.x = 0; + scene->cameraOffset.y = 0; + + // Start with three balls + scene->ballCount = 3; +} + +/** + * @brief TODO + * + * @param scene + */ +void pbStartBall(pbScene_t* scene) +{ + // Set the state + pbSetState(scene, PBS_WAIT_TO_LAUNCH); + + // Clear loop history + memset(scene->loopHistory, 0, sizeof(scene->loopHistory)); + + // Reset targets + for (uint16_t lIdx = 0; lIdx < scene->numLines; lIdx++) + { + pbLine_t* line = &scene->lines[lIdx]; + if (PB_DROP_TARGET == line->type) + { + line->isUp = true; + } + } + + // Open the launch tube + pbOpenLaunchTube(scene, true); + + clear(&scene->balls); + for (uint16_t pIdx = 0; pIdx < scene->numPoints; pIdx++) + { + if (PB_BALL_SPAWN == scene->points[pIdx].type) + { + pbBall_t* ball = calloc(1, sizeof(pbBall_t)); + ball->pos = scene->points[pIdx].pos; + ball->vel.x = 0; + ball->vel.y = 0; + ball->radius = PINBALL_RADIUS; + ball->mass = M_PI * 4.0f * 4.0f; + ball->restitution = 0.2f; + push(&scene->balls, ball); + return; + } + } +} + +/** + * @brief + * + * @param scene + */ +void pbStartMultiball(pbScene_t* scene) +{ + // Don't start multiball if there are already three balls + if (3 == scene->balls.length) + { + return; + } + + // Ignore the first spawn point (tube) + bool ignoreFirst = true; + + // For each point + for (uint16_t pIdx = 0; pIdx < scene->numPoints; pIdx++) + { + // If this is a spawn point + if (PB_BALL_SPAWN == scene->points[pIdx].type) + { + // Ignore the first + if (ignoreFirst) + { + ignoreFirst = false; + } + else + { + // Spawn a ball here + // TODO check if space is empty first + pbBall_t* ball = calloc(1, sizeof(pbBall_t)); + ball->pos = scene->points[pIdx].pos; + ball->vel.x = 0; + ball->vel.y = 0; + ball->radius = PINBALL_RADIUS; + ball->mass = M_PI * 4.0f * 4.0f; + ball->restitution = 0.2f; + push(&scene->balls, ball); + + // All balls spawned + if (3 == scene->balls.length) + { + return; + } + } + } + } +} + +/** + * @brief TODO + * + * @param scene + */ +void pbSceneDestroy(pbScene_t* scene) +{ + if (scene->groups) + { + // Free the rest of the state + free(scene->lines); + free(scene->circles); + free(scene->launchers); + free(scene->flippers); + free(scene->triangles); + free(scene->points); + + node_t* bNode = scene->balls.first; + while (bNode) + { + free(bNode->val); + bNode = bNode->next; + } + clear(&scene->balls); + + for (int32_t gIdx = 0; gIdx < scene->numGroups; gIdx++) + { + clear(&scene->groups[gIdx]); + } + free(scene->groups); + scene->groups = NULL; + } +} + +// ------------------------ user interaction --------------------------- + +/** + * @brief TODO doc + * + * @param scene + * @param event + */ +void pbButtonPressed(pbScene_t* scene, buttonEvt_t* event) +{ + if (event->down) + { + switch (event->button) + { + case PB_LEFT: + { + for (int32_t fIdx = 0; fIdx < scene->numFlippers; fIdx++) + { + if (scene->flippers[fIdx].facingRight) + { + scene->flippers[fIdx].buttonHeld = true; + } + } + break; + } + case PB_RIGHT: + { + for (int32_t fIdx = 0; fIdx < scene->numFlippers; fIdx++) + { + if (!scene->flippers[fIdx].facingRight) + { + scene->flippers[fIdx].buttonHeld = true; + } + } + for (int32_t rIdx = 0; rIdx < scene->numLaunchers; rIdx++) + { + scene->launchers[rIdx].buttonHeld = true; + } + break; + } + default: + { + break; + } + } + } + else + { + switch (event->button) + { + case PB_LEFT: + { + for (int32_t fIdx = 0; fIdx < scene->numFlippers; fIdx++) + { + if (scene->flippers[fIdx].facingRight) + { + scene->flippers[fIdx].buttonHeld = false; + } + } + break; + } + case PB_RIGHT: + { + for (int32_t fIdx = 0; fIdx < scene->numFlippers; fIdx++) + { + if (!scene->flippers[fIdx].facingRight) + { + scene->flippers[fIdx].buttonHeld = false; + } + } + for (int32_t rIdx = 0; rIdx < scene->numLaunchers; rIdx++) + { + scene->launchers[rIdx].buttonHeld = false; + } + break; + } + default: + { + break; + } + } + } +} + +/** + * @brief + * + * @param ball + * @param scene + */ +void pbRemoveBall(pbBall_t* ball, pbScene_t* scene) +{ + // Clear loop history + memset(scene->loopHistory, 0, sizeof(scene->loopHistory)); + + // If the save timer is running + if (scene->saveTimer > 0 && 1 == scene->balls.length) + { + // Save the ball by scooping it back + printf("Ball Saved\n"); + ball->scoopTimer = 2000000; + } + else + { + // Find the ball in the list + node_t* bNode = scene->balls.first; + while (bNode) + { + if (ball == bNode->val) + { + // Remove the ball from the list + free(bNode->val); + removeEntry(&scene->balls, bNode); + break; + } + bNode = bNode->next; + } + + // If there are no active balls left + if (0 == scene->balls.length) + { + // Decrement the overall ball count + scene->ballCount--; + + // If there are balls left + if (0 < scene->ballCount) + { + pbSetState(scene, PBS_BALL_OVER); + // TODO show bonus set up for next ball, etc. + pbStartBall(scene); + } + else + { + // No balls left + pbSetState(scene, PBS_GAME_OVER); + } + } + } +} + +/** + * @brief TODO + * + * @param scene + * @param elapsedUs + */ +void pbGameTimers(pbScene_t* scene, int32_t elapsedUs) +{ + if (scene->saveTimer > 0) + { + scene->saveTimer -= elapsedUs; + } +} + +/** + * @brief TODO + * + * @param scene + * @param open + */ +void pbOpenLaunchTube(pbScene_t* scene, bool open) +{ + if (open != scene->launchTubeClosed) + { + scene->launchTubeClosed = open; + + if (!open) + { + // Start a 15s timer to save the ball when the door closes + scene->saveTimer = 15000000; + pbSetState(scene, PBS_GAME_NO_EVENT); + } + + for (int32_t lIdx = 0; lIdx < scene->numLines; lIdx++) + { + pbLine_t* line = &scene->lines[lIdx]; + if (PB_LAUNCH_DOOR == line->type) + { + line->isUp = !open; + } + } + } +} + +/** + * @brief TODO + * + * @param scene + * @param state + */ +void pbSetState(pbScene_t* scene, pbGameState_t state) +{ + if (scene->state != state) + { + scene->state = state; + switch (state) + { + case PBS_WAIT_TO_LAUNCH: + { + printf("Ball Start\n"); + break; + } + case PBS_GAME_NO_EVENT: + { + printf("Event Finished\n"); + break; + } + case PBS_GAME_EVENT: + { + printf("Event Started\n"); + break; + } + case PBS_BALL_OVER: + { + printf("Ball Lost\n"); + break; + } + case PBS_GAME_OVER: + { + printf("Game Over\n"); + break; + } + } + } +} diff --git a/attic/pinball/pinball_game.h b/attic/pinball/pinball_game.h new file mode 100644 index 000000000..78cfc0867 --- /dev/null +++ b/attic/pinball/pinball_game.h @@ -0,0 +1,19 @@ +#pragma once + +#include "hdw-btn.h" +#include "macros.h" +#include "pinball_typedef.h" + +uint8_t readInt8(uint8_t* data, uint32_t* idx); +uint16_t readInt16(uint8_t* data, uint32_t* idx); +list_t* addToGroup(pbScene_t* scene, void* obj, uint8_t groupId); + +void pbSceneInit(pbScene_t* scene); +void pbSceneDestroy(pbScene_t* scene); +void pbButtonPressed(pbScene_t* scene, buttonEvt_t* event); +void pbRemoveBall(pbBall_t* ball, pbScene_t* scene); +void pbStartBall(pbScene_t* scene); +void pbGameTimers(pbScene_t* scene, int32_t elapsedUs); +void pbOpenLaunchTube(pbScene_t* scene, bool open); +void pbStartMultiball(pbScene_t* scene); +void pbSetState(pbScene_t* scene, pbGameState_t state); diff --git a/attic/pinball/pinball_line.c b/attic/pinball/pinball_line.c new file mode 100644 index 000000000..10e163c3f --- /dev/null +++ b/attic/pinball/pinball_line.c @@ -0,0 +1,140 @@ +#include "shapes.h" +#include "palette.h" + +#include "pinball_line.h" +#include "pinball_physics.h" + +/** + * @brief TODO doc + * + * @param tableData + * @param scene + * @return int32_t + */ +int32_t readLineFromFile(uint8_t* tableData, pbScene_t* scene) +{ + pbLine_t* line = &scene->lines[scene->numLines++]; + uint32_t dIdx = 0; + line->id = readInt16(tableData, &dIdx); + line->groupId = readInt8(tableData, &dIdx); + line->group = addToGroup(scene, line, line->groupId); + line->p1.x = readInt16(tableData, &dIdx); + line->p1.y = readInt16(tableData, &dIdx); + line->p2.x = readInt16(tableData, &dIdx); + line->p2.y = readInt16(tableData, &dIdx); + line->type = readInt8(tableData, &dIdx); + line->pushVel = readInt8(tableData, &dIdx); + line->isUp = readInt8(tableData, &dIdx); + + return dIdx; +} + +/** + * @brief TODO doc + * + * @param line + */ +void pinballDrawLine(pbLine_t* line, vec_t* cameraOffset) +{ + paletteColor_t color = c555; + switch (line->type) + { + case PB_WALL: + case PB_BALL_LOST: + case PB_LAUNCH_DOOR: + { + if (!line->isUp) + { + return; + } + color = c555; + break; + } + case PB_SLINGSHOT: + { + color = line->litTimer > 0 ? c500 : c300; + break; + } + case PB_DROP_TARGET: + { + if (line->isUp) + { + color = c050; + } + else + { + color = c010; + } + break; + } + case PB_STANDUP_TARGET: + { + color = line->litTimer > 0 ? c004 : c002; + break; + } + case PB_SPINNER: + { + color = c123; + break; + } + case PB_SCOOP: + { + color = c202; + break; + } + } + + drawLine(line->p1.x - cameraOffset->x, line->p1.y - cameraOffset->y, line->p2.x - cameraOffset->x, + line->p2.y - cameraOffset->y, color, 0); +} + +/** + * @brief TODO + * + * @param line + * @param elapsedUs + */ +void pbLineTimer(pbLine_t* line, int32_t elapsedUs, pbScene_t* scene) +{ + // Decrement the lit timer + if (line->litTimer > 0) + { + line->litTimer -= elapsedUs; + } + + // Decrement the reset timer + if (line->resetTimer > 0) + { + line->resetTimer -= elapsedUs; + + if (line->resetTimer <= 0) + { + // Make sure the line isn't intersecting a ball before popping up + bool intersecting = false; + + node_t* bNode = scene->balls.first; + while (bNode) + { + pbBall_t* ball = bNode->val; + if (ballLineIntersection(ball, line)) + { + intersecting = true; + break; + } + bNode = bNode->next; + } + + // If there are no intersections + if (!intersecting) + { + // Raise the target + line->isUp = true; + } + else + { + // Try next frame + line->resetTimer = 1; + } + } + } +} diff --git a/attic/pinball/pinball_line.h b/attic/pinball/pinball_line.h new file mode 100644 index 000000000..bb22992bf --- /dev/null +++ b/attic/pinball/pinball_line.h @@ -0,0 +1,8 @@ +#pragma once + +#include "pinball_typedef.h" +#include "pinball_game.h" + +int32_t readLineFromFile(uint8_t* tableData, pbScene_t* scene); +void pinballDrawLine(pbLine_t* line, vec_t* cameraOffset); +void pbLineTimer(pbLine_t* line, int32_t elapsedUs, pbScene_t* scene); diff --git a/attic/pinball/pinball_physics.c b/attic/pinball/pinball_physics.c new file mode 100644 index 000000000..e99939999 --- /dev/null +++ b/attic/pinball/pinball_physics.c @@ -0,0 +1,461 @@ +#include +#include +#include +#include "pinball_line.h" +#include "pinball_circle.h" +#include "pinball_rectangle.h" +#include "pinball_flipper.h" +#include "pinball_triangle.h" +#include "pinball_physics.h" + +static void handleBallBallCollision(pbBall_t* ball1, pbBall_t* ball2); +static void handleBallCircleCollision(pbScene_t* scene, pbBall_t* ball, pbCircle_t* circle); +static void handleBallFlipperCollision(pbBall_t* ball, pbFlipper_t* flipper); +static bool handleBallLineCollision(pbBall_t* ball, pbScene_t* scene); +static void handleBallLauncherCollision(pbLauncher_t* launcher, pbBall_t* ball, float dt); + +/** + * @brief TODO + * + * @param scene + * @param elapsedUs + */ +void pbSimulate(pbScene_t* scene, int32_t elapsedUs) +{ + float elapsedUsFl = elapsedUs / 1000000.0f; + + for (int32_t i = 0; i < scene->numFlippers; i++) + { + pbFlipperSimulate(&scene->flippers[i], elapsedUsFl); + } + + for (int32_t i = 0; i < scene->numLaunchers; i++) + { + pbLauncherSimulate(&scene->launchers[i], &scene->balls, elapsedUsFl); + } + + node_t* bNode = scene->balls.first; + while (bNode) + { + pbBall_t* ball = bNode->val; + + pbBallSimulate(ball, elapsedUs, elapsedUsFl, scene); + + node_t* bNode2 = bNode->next; + while (bNode2) + { + pbBall_t* ball2 = bNode2->val; + handleBallBallCollision(ball, ball2); + bNode2 = bNode2->next; + } + + for (int32_t cIdx = 0; cIdx < scene->numCircles; cIdx++) + { + handleBallCircleCollision(scene, ball, &scene->circles[cIdx]); + } + + for (int32_t fIdx = 0; fIdx < scene->numFlippers; fIdx++) + { + handleBallFlipperCollision(ball, &scene->flippers[fIdx]); + } + + for (int32_t lIdx = 0; lIdx < scene->numLaunchers; lIdx++) + { + handleBallLauncherCollision(&scene->launchers[lIdx], ball, elapsedUs); + } + + // Collide ball with lines + if (handleBallLineCollision(ball, scene)) + { + // Iterate to the next ball node + bNode = bNode->next; + + // Then remove the ball + pbRemoveBall(ball, scene); + } + else + { + // Iterate to the next ball + bNode = bNode->next; + } + } + + for (int32_t cIdx = 0; cIdx < scene->numCircles; cIdx++) + { + pbCircleTimer(&scene->circles[cIdx], elapsedUs); + } + + for (int32_t tIdx = 0; tIdx < scene->numTriangles; tIdx++) + { + pbTriangleTimer(&scene->triangles[tIdx], elapsedUs); + } + + for (int32_t lIdx = 0; lIdx < scene->numLines; lIdx++) + { + pbLineTimer(&scene->lines[lIdx], elapsedUs, scene); + } +} + +/** + * @brief Find the closest point to point p on a line segment between a and b + * + * @param p A point + * @param a One end of a line segment + * @param b The other end of a line segment + * @return A point on the line segment closest to p + */ +static vecFl_t closestPointOnSegment(vecFl_t p, vecFl_t a, vecFl_t b) +{ + vecFl_t ab = subVecFl2d(b, a); + float t = sqMagVecFl2d(ab); + + if (t == 0.0f) + { + return a; + } + + t = (dotVecFl2d(p, ab) - dotVecFl2d(a, ab)) / t; + if (t > 1) + { + t = 1; + } + else if (t < 0) + { + t = 0; + } + + return addVecFl2d(a, mulVecFl2d(ab, t)); +} + +/** + * @brief TODO doc + * + * @param ball1 + * @param ball2 + */ +static void handleBallBallCollision(pbBall_t* ball1, pbBall_t* ball2) +{ + float restitution = MIN(ball1->restitution, ball2->restitution); + vecFl_t dir = subVecFl2d(ball2->pos, ball1->pos); + float d = magVecFl2d(dir); + if (0 == d || d > (ball1->radius + ball2->radius)) + { + return; + } + + dir = divVecFl2d(dir, d); + + float corr = (ball1->radius + ball2->radius - d) / 2.0f; + ball1->pos = addVecFl2d(ball1->pos, mulVecFl2d(dir, -corr)); + ball2->pos = addVecFl2d(ball2->pos, mulVecFl2d(dir, corr)); + + float v1 = dotVecFl2d(ball1->vel, dir); + float v2 = dotVecFl2d(ball2->vel, dir); + + float m1 = ball1->mass; + float m2 = ball2->mass; + + float newV1 = (m1 * v1 + m2 * v2 - m2 * (v1 - v2) * restitution) / (m1 + m2); + float newV2 = (m1 * v1 + m2 * v2 - m1 * (v2 - v1) * restitution) / (m1 + m2); + + ball1->vel = addVecFl2d(ball1->vel, mulVecFl2d(dir, newV1 - v1)); + ball2->vel = addVecFl2d(ball2->vel, mulVecFl2d(dir, newV2 - v2)); +} + +/** + * @brief TODO doc + * + * @param scene + * @param ball + * @param circle + */ +static void handleBallCircleCollision(pbScene_t* scene, pbBall_t* ball, pbCircle_t* circle) +{ + vecFl_t dir = subVecFl2d(ball->pos, circle->pos); + float d = magVecFl2d(dir); + if (d == 0.0 || d > (ball->radius + circle->radius)) + { + if (circle->id == scene->touchedLoopId) + { + scene->touchedLoopId = PIN_INVALID_ID; + } + return; + } + + if (PB_BUMPER == circle->type) + { + // Normalize the direction + dir = divVecFl2d(dir, d); + + // Move ball backwards to not clip + float corr = ball->radius + circle->radius - d; + ball->pos = addVecFl2d(ball->pos, mulVecFl2d(dir, corr)); + + // Adjust the velocity + float v = dotVecFl2d(ball->vel, dir); + ball->vel = addVecFl2d(ball->vel, mulVecFl2d(dir, circle->pushVel - v)); + + circle->litTimer = 250000; + } + else if (PB_ROLLOVER == circle->type) + { + if (circle->id != scene->touchedLoopId) + { + scene->touchedLoopId = circle->id; + + memmove(&scene->loopHistory[1], &scene->loopHistory[0], + sizeof(scene->loopHistory) - sizeof(scene->loopHistory[0])); + scene->loopHistory[0] = circle->id; + + if (scene->loopHistory[0] + 1 == scene->loopHistory[1] + && scene->loopHistory[1] + 1 == scene->loopHistory[2]) + { + printf("Loop Counter Clockwise\n"); + } + else if (scene->loopHistory[2] + 1 == scene->loopHistory[1] + && scene->loopHistory[1] + 1 == scene->loopHistory[0]) + { + printf("Loop Clockwise\n"); + } + } + // Group two rollovers should close the launch tube + // TODO hardcoding a group ID is gross + if (5 == circle->groupId) + { + pbOpenLaunchTube(scene, false); + } + } +} + +/** + * @brief TODO doc + * + * @param ball + * @param flipper + */ +static void handleBallFlipperCollision(pbBall_t* ball, pbFlipper_t* flipper) +{ + vecFl_t closest = closestPointOnSegment(ball->pos, flipper->pos, pbFlipperGetTip(flipper)); + vecFl_t dir = subVecFl2d(ball->pos, closest); + float d = magVecFl2d(dir); + if (d == 0.0 || d > ball->radius + flipper->radius) + { + return; + } + + dir = divVecFl2d(dir, d); + + float corr = (ball->radius + flipper->radius - d); + ball->pos = addVecFl2d(ball->pos, mulVecFl2d(dir, corr)); + + // update velocity + + vecFl_t radius = closest; + radius = addVecFl2d(radius, mulVecFl2d(dir, flipper->radius)); + radius = subVecFl2d(radius, flipper->pos); + vecFl_t surfaceVel = perpendicularVecFl2d(radius); + surfaceVel = mulVecFl2d(surfaceVel, flipper->currentAngularVelocity); + + float v = dotVecFl2d(ball->vel, dir); + float vNew = dotVecFl2d(surfaceVel, dir); + + ball->vel = addVecFl2d(ball->vel, mulVecFl2d(dir, vNew - v)); +} + +/** + * @brief TODO + * + * @param ball + * @param line + * @return true + * @return false + */ +bool ballLineIntersection(pbBall_t* ball, pbLine_t* line) +{ + // Get the line segment from the list of walls + vecFl_t a = line->p1; + vecFl_t b = line->p2; + // Get the closest point on the segment to the center of the ball + vecFl_t c = closestPointOnSegment(ball->pos, a, b); + // Find the distance between the center of the ball and the closest point on the line + vecFl_t d = subVecFl2d(ball->pos, c); + float dist = magVecFl2d(d); + // If the distance is less than the radius, and the distance is less + // than the minimum distance, its the best collision + return (dist < ball->radius); +} + +/** + * @brief TODO doc + * + * @param ball + * @param lines + * @param true if the ball should be deleted, false if not + */ +static bool handleBallLineCollision(pbBall_t* ball, pbScene_t* scene) +{ + // find closest segment; + vecFl_t ballToClosest; + vecFl_t ab; + vecFl_t normal; + float minDist = FLT_MAX; + pbLine_t* cLine = NULL; + + // For each segment of the wall + for (int32_t i = 0; i < scene->numLines; i++) + { + pbLine_t* line = &scene->lines[i]; + + if (line->isUp) + { + // Get the line segment from the list of walls + vecFl_t a = line->p1; + vecFl_t b = line->p2; + // Get the closest point on the segment to the center of the ball + vecFl_t c = closestPointOnSegment(ball->pos, a, b); + // Find the distance between the center of the ball and the closest point on the line + vecFl_t d = subVecFl2d(ball->pos, c); + float dist = magVecFl2d(d); + // If the distance is less than the radius, and the distance is less + // than the minimum distance, its the best collision + if ((dist < ball->radius) && (dist < minDist)) + { + minDist = dist; + ballToClosest = d; + ab = subVecFl2d(b, a); + normal = perpendicularVecFl2d(ab); + cLine = line; + } + } + } + + // Check if there were any collisions + if (NULL == cLine) + { + return false; + } + + // push out to not clip + if (0 == minDist) + { + ballToClosest = normal; + minDist = magVecFl2d(normal); + } + ballToClosest = divVecFl2d(ballToClosest, minDist); + ball->pos = addVecFl2d(ball->pos, mulVecFl2d(ballToClosest, ball->radius - minDist)); // TODO epsilon here? + + float v = dotVecFl2d(ball->vel, ballToClosest); + if (cLine->pushVel) + { + // Adjust the velocity + ball->vel = addVecFl2d(ball->vel, mulVecFl2d(ballToClosest, cLine->pushVel - v)); + } + else + { + // update velocity + float vNew = ABS(v) * ball->restitution; // TODO care about wall's restitution? + ball->vel = addVecFl2d(ball->vel, mulVecFl2d(ballToClosest, vNew - v)); + } + + switch (cLine->type) + { + default: + case PB_WALL: + case PB_SPINNER: + case PB_LAUNCH_DOOR: + { + break; + } + case PB_STANDUP_TARGET: + { + pbSetState(scene, PBS_GAME_EVENT); + } + // Fall through + case PB_SLINGSHOT: + { + cLine->litTimer = 250000; + break; + } + case PB_DROP_TARGET: + { + cLine->isUp = false; + + // Check if all targets in the group are hit + bool someLineUp = false; + list_t* group = cLine->group; + node_t* node = group->first; + while (NULL != node) + { + pbLine_t* groupLine = node->val; + if (groupLine->isUp) + { + someLineUp = true; + break; + } + node = node->next; + } + + // If all lines are down + if (!someLineUp) + { + // Reset them + // TODO start a timer for this? Make sure a ball isn't touching the line before resetting? + node = group->first; + while (NULL != node) + { + // Start a timer to reset the target + ((pbLine_t*)node->val)->resetTimer = 3000000; + node = node->next; + } + } + break; + } + case PB_SCOOP: + { + // Count the scoop + scene->scoopCount++; + printf("Ball %" PRId32 " locked\n", scene->scoopCount); + if (3 == scene->scoopCount) + { + printf("Multiball!!!\n"); + pbStartMultiball(scene); + } + ball->scoopTimer = 2000000; + break; + } + case PB_BALL_LOST: + { + return true; + } + } + return false; +} + +/** + * @brief TODO + * + * @param launcher + * @param balls + * @param dt + */ +static void handleBallLauncherCollision(pbLauncher_t* launcher, pbBall_t* ball, float dt) +{ + if (ball->vel.y >= 0) + { + // Get the compressed Y level + float posY = launcher->pos.y + (launcher->impulse * launcher->height); + + // Check Y + if ((ball->pos.y + ball->radius > posY) && (ball->pos.y - ball->radius < posY)) + { + // Check X + if ((ball->pos.x > launcher->pos.x) && (ball->pos.x < launcher->pos.x + launcher->width)) + { + // Collision, set the position to be slightly touching + ball->pos.y = posY - ball->radius + 0.1f; + // Bounce a little + ball->vel = mulVecFl2d(ball->vel, -0.3f); + } + } + } +} diff --git a/attic/pinball/pinball_physics.h b/attic/pinball/pinball_physics.h new file mode 100644 index 000000000..8b03470c0 --- /dev/null +++ b/attic/pinball/pinball_physics.h @@ -0,0 +1,6 @@ +#pragma once + +#include "pinball_typedef.h" + +void pbSimulate(pbScene_t* scene, int32_t elapsedUs); +bool ballLineIntersection(pbBall_t* ball, pbLine_t* line); diff --git a/attic/pinball/pinball_point.c b/attic/pinball/pinball_point.c new file mode 100644 index 000000000..5ffa19c20 --- /dev/null +++ b/attic/pinball/pinball_point.c @@ -0,0 +1,25 @@ +#include "shapes.h" +#include "palette.h" + +#include "pinball_point.h" + +/** + * @brief TODO doc + * + * @param tableData + * @param scene + * @return int32_t + */ +int32_t readPointFromFile(uint8_t* tableData, pbScene_t* scene) +{ + pbPoint_t* point = &scene->points[scene->numPoints++]; + uint32_t dIdx = 0; + point->id = readInt16(tableData, &dIdx); + point->groupId = readInt8(tableData, &dIdx); + point->group = addToGroup(scene, point, point->groupId); + point->pos.x = readInt16(tableData, &dIdx); + point->pos.y = readInt16(tableData, &dIdx); + point->type = readInt8(tableData, &dIdx); + + return dIdx; +} diff --git a/attic/pinball/pinball_point.h b/attic/pinball/pinball_point.h new file mode 100644 index 000000000..d79dc6c9d --- /dev/null +++ b/attic/pinball/pinball_point.h @@ -0,0 +1,6 @@ +#pragma once + +#include "pinball_typedef.h" +#include "pinball_game.h" + +int32_t readPointFromFile(uint8_t* tableData, pbScene_t* scene); diff --git a/attic/pinball/pinball_rectangle.c b/attic/pinball/pinball_rectangle.c new file mode 100644 index 000000000..89df5560d --- /dev/null +++ b/attic/pinball/pinball_rectangle.c @@ -0,0 +1,65 @@ +#include +#include "geometryFl.h" + +#include "pinball_rectangle.h" + +/** + * @brief TODO doc + * + * @param tableData + * @param scene + * @return uint32_t + */ +uint32_t readRectangleFromFile(uint8_t* tableData, pbScene_t* scene) +{ + uint32_t dIdx = 0; + pbLauncher_t* launcher = &scene->launchers[scene->numLaunchers++]; + launcher->id = readInt16(tableData, &dIdx); + launcher->groupId = readInt8(tableData, &dIdx); + launcher->group = addToGroup(scene, launcher, launcher->groupId); + launcher->pos.x = readInt16(tableData, &dIdx); + launcher->pos.y = readInt16(tableData, &dIdx); + launcher->width = readInt16(tableData, &dIdx); + launcher->height = readInt16(tableData, &dIdx); + launcher->buttonHeld = false; + launcher->impulse = 0; + return dIdx; +} + +/** + * @brief TODO doc + * + * @param launcher + * @param balls + * @param dt + */ +void pbLauncherSimulate(pbLauncher_t* launcher, list_t* balls, float dt) +{ + if (launcher->buttonHeld) + { + launcher->impulse += (dt / 3); + if (launcher->impulse > 0.99f) + { + launcher->impulse = 0.99f; + } + } + else if (launcher->impulse) + { + rectangleFl_t r = {.pos = launcher->pos, .width = launcher->width, .height = launcher->height}; + // If touching a ball, transfer to a ball + node_t* bNode = balls->first; + while (bNode) + { + pbBall_t* ball = bNode->val; + circleFl_t b = {.pos = ball->pos, .radius = ball->radius}; + if (circleRectFlIntersection(b, r, NULL)) + { + ball->vel.y = (MAX_LAUNCHER_VELOCITY * launcher->impulse); + } + + bNode = bNode->next; + } + + launcher->impulse = 0; + } +} \ No newline at end of file diff --git a/attic/pinball/pinball_rectangle.h b/attic/pinball/pinball_rectangle.h new file mode 100644 index 000000000..dda040bfa --- /dev/null +++ b/attic/pinball/pinball_rectangle.h @@ -0,0 +1,9 @@ +#pragma once + +#include "pinball_typedef.h" +#include "pinball_game.h" + +#define MAX_LAUNCHER_VELOCITY -600 + +uint32_t readRectangleFromFile(uint8_t* tableData, pbScene_t* scene); +void pbLauncherSimulate(pbLauncher_t* launcher, list_t* balls, float dt); diff --git a/attic/pinball/pinball_triangle.c b/attic/pinball/pinball_triangle.c new file mode 100644 index 000000000..0f7011f77 --- /dev/null +++ b/attic/pinball/pinball_triangle.c @@ -0,0 +1,45 @@ +#include +#include "pinball_triangle.h" + +/** + * @brief TODO doc + * + * @param tableData + * @param scene + * @return uint32_t + */ +uint32_t readTriangleFromFile(uint8_t* tableData, pbScene_t* scene) +{ + pbTriangle_t* triangle = &scene->triangles[scene->numTriangles++]; + + uint32_t dIdx = 0; + triangle->id = readInt16(tableData, &dIdx); + triangle->groupId = readInt8(tableData, &dIdx); + triangle->group = addToGroup(scene, triangle, triangle->groupId); + triangle->p1.x = readInt16(tableData, &dIdx); + triangle->p1.y = readInt16(tableData, &dIdx); + triangle->p2.x = readInt16(tableData, &dIdx); + triangle->p2.y = readInt16(tableData, &dIdx); + triangle->p3.x = readInt16(tableData, &dIdx); + triangle->p3.y = readInt16(tableData, &dIdx); + + triangle->blinkTimer = 0; + triangle->isOn = false; + triangle->isBlinking = true; + + return dIdx; +} + +/** + * @brief + * + * @param tri + * @param elapsedUs + */ +void pbTriangleTimer(pbTriangle_t* tri, int32_t elapsedUs) +{ + if (tri->isBlinking) + { + RUN_TIMER_EVERY(tri->blinkTimer, 333333, elapsedUs, tri->isOn = !tri->isOn;); + } +} diff --git a/attic/pinball/pinball_triangle.h b/attic/pinball/pinball_triangle.h new file mode 100644 index 000000000..162a25cc1 --- /dev/null +++ b/attic/pinball/pinball_triangle.h @@ -0,0 +1,7 @@ +#pragma once + +#include "pinball_typedef.h" +#include "pinball_game.h" + +uint32_t readTriangleFromFile(uint8_t* tableData, pbScene_t* scene); +void pbTriangleTimer(pbTriangle_t* tri, int32_t elapsedUs); diff --git a/attic/pinball/pinball_typedef.h b/attic/pinball/pinball_typedef.h new file mode 100644 index 000000000..840f54168 --- /dev/null +++ b/attic/pinball/pinball_typedef.h @@ -0,0 +1,165 @@ +#pragma once + +#include +#include +#include +#include + +#include "vector2d.h" +#include "vectorFl2d.h" +#include "macros.h" +#include "linked_list.h" + +#define PIN_INVALID_ID 0xFFFF + +typedef enum +{ + PBS_WAIT_TO_LAUNCH, + PBS_GAME_NO_EVENT, + PBS_GAME_EVENT, + PBS_BALL_OVER, + PBS_GAME_OVER, +} pbGameState_t; + +typedef enum +{ + PB_WALL, + PB_SLINGSHOT, + PB_DROP_TARGET, + PB_STANDUP_TARGET, + PB_SPINNER, + PB_SCOOP, + PB_BALL_LOST, + PB_LAUNCH_DOOR, +} pbLineType_t; + +typedef enum +{ + PB_BUMPER, + PB_ROLLOVER +} pbCircleType_t; + +typedef enum +{ + PB_BALL_SPAWN, + PB_ITEM_SPAWN, +} pbPointType_t; + +typedef struct +{ + uint16_t id; + uint8_t groupId; + list_t* group; + pbPointType_t type; + vecFl_t pos; +} pbPoint_t; + +typedef struct +{ + uint16_t id; + uint8_t groupId; + list_t* group; + pbLineType_t type; + vecFl_t p1; + vecFl_t p2; + float pushVel; + bool isUp; + int32_t litTimer; + int32_t resetTimer; +} pbLine_t; + +typedef struct +{ + // fixed + float radius; + vecFl_t pos; + float length; + float restAngle; + float maxRotation; + float sign; + float angularVelocity; + // changing + float rotation; + float currentAngularVelocity; + bool buttonHeld; + bool facingRight; +} pbFlipper_t; + +// TODO merge these +typedef struct +{ + float radius; + float mass; + float restitution; + vecFl_t pos; + vecFl_t vel; + int32_t scoopTimer; +} pbBall_t; + +typedef struct +{ + uint16_t id; + uint8_t groupId; + list_t* group; + float radius; + vecFl_t pos; + pbCircleType_t type; + float pushVel; + int32_t litTimer; +} pbCircle_t; + +typedef struct +{ + uint16_t id; + uint8_t groupId; + list_t* group; + vecFl_t p1; + vecFl_t p2; + vecFl_t p3; + bool isBlinking; + bool isOn; + int32_t blinkTimer; +} pbTriangle_t; + +typedef struct +{ + uint16_t id; + uint8_t groupId; + list_t* group; + vecFl_t pos; + float width; + float height; + bool buttonHeld; + float impulse; +} pbLauncher_t; + +typedef struct +{ + vecFl_t gravity; + int32_t score; + bool paused; + int32_t numGroups; + list_t* groups; + pbLine_t* lines; + int32_t numLines; + list_t balls; + pbCircle_t* circles; + int32_t numCircles; + pbFlipper_t* flippers; + int32_t numFlippers; + pbLauncher_t* launchers; + int32_t numLaunchers; + pbTriangle_t* triangles; + int32_t numTriangles; + pbPoint_t* points; + int32_t numPoints; + vec_t cameraOffset; + vecFl_t tableDim; + bool launchTubeClosed; + uint16_t touchedLoopId; + uint16_t loopHistory[3]; + int32_t saveTimer; + int32_t scoopCount; + int32_t ballCount; + pbGameState_t state; +} pbScene_t; \ No newline at end of file diff --git a/docs/EMULATOR.md b/docs/EMULATOR.md index a55373c3f..db3c69cda 100644 --- a/docs/EMULATOR.md +++ b/docs/EMULATOR.md @@ -22,7 +22,16 @@ wireless connection. The Linux version of the emulator does not require any other software to operate. Simply extract the `.zip` file anywhere you like and run the `swadge_emulator` program, either by opening it from your -file browser or by running `./swadge_emulator` from the command-line. +file browser or by running `./swadge_emulator` from the command-line. The Linux emulator includes a +script, `install.sh`, which can be run to install the Swadge Emulator as a desktop application, which +will allow you to open the emulator directly from many desktop environments, and to asssociate the +emulator with MIDI files using the "Open with..." option in your file browser. If you do not want these +features, there is no need to run the script. You will need to run this script again if you download a +new version of the emulator. By default, the installation script will install to `~/.local`, but you +can specify an alternate installation root such as `/usr/local` by passing it as an argument to +the `install.sh` script: + + ./install.sh /usr/local ### Mac @@ -81,6 +90,7 @@ Emulates a swadge --hide-leds Don't draw simulated LEDs next to the display -k, --keymap=LAYOUT Use an alternative keymap. LAYOUT can be azerty, colemak, or dvorak -l, --lock Lock the emulator in the start mode + --midi-file=FILE Open and immediately play a MIDI file -m, --mode=MODE Start the emulator in the swadge mode MODE instead of the main menu --mode-switch[=TIME] Enable or set the timer to switch modes automatically --modes-list Print out a list of all possible values for MODE @@ -222,6 +232,8 @@ inputs to ensure that slight differences in frame timing do not cause inconsiste modes. The mode can still be changed automatically by `--mode-switch`, the console, and by a `SetMode' command when replaying recorded inputs. +`--midi-file`: Loads and plays a local MIDI or KAR file using the MIDI Player mode. + `--seed`: Sets a specific seed to the pseudorandom number generator. This is useful when trying to reproduce behavior that relies on `esp_random()`. If the seed is not set, a time-based one will be used. Note that a seed from one system will not necessarily produce the same output if it is used on a different system. @@ -252,6 +264,12 @@ time. ## MIDI Instructions +MIDI Files (`.mid`, `.midi`, and `.kar`) can be played in directly by passing the name of +the MIDI file as a command-line argument to the Swadge Emulator. On Windows, you should also be able +to drag a MIDI file on top of `SwadgeEmulator.exe` to play it. On Linux, you should be able to open +MIDI files with the emulator using your file browser's "Open with..." option after running the included +install script. + The Swadge Emulator includes MIDI support, which simulates the USB-MIDI behavior of the real Swadge using the system MIDI implementation. Note that MIDI implementation and behavior will vary between platforms. @@ -263,3 +281,5 @@ platforms. or the first one that becomes available. You may need to restart the emulator after connecting a new device. 4. Press the Pause button (O key) to open or close the menu. + +For details on the Swadge's MIDI support you can use for composing, see the [MIDI Specifications page](#MIDI). \ No newline at end of file diff --git a/docs/MIDI.md b/docs/MIDI.md new file mode 100644 index 000000000..9e3437852 --- /dev/null +++ b/docs/MIDI.md @@ -0,0 +1,102 @@ +# MIDI Specifications {#MIDI} + +This page describes the technical details of the 2025 Swadge's support for +the MIDI protocol. Note that while the Swadge only supports the instruments, +controllers, and other features described in this document, it can still +play any MIDI file, even ones containing unsupported commands. Unsupported +commands are simply ignored and the rest of the file will play normally. + +## Basics + +The Swadge's 8-bit audio synthesizer supports up to 16 MIDI channels, with channels 10 and 11 reserved for +percussion (the drum kit). By default, channel 1 through 9 are configured to use the [MAGFest instruments](#MAGPrograms) +in order. + +## Features + +* 24-voice polyphony shared between non-percussion channels +* 8 additional voices reserved for percussion only channel (channels 10/11 by default) +* Pitch bend +* AfterTouch + +## Instrument Banks (Programs) + +### General MIDI (Bank 0) +For the list of General MIDI instruments available, see [Wikipedia](https://en.wikipedia.org/wiki/General_MIDI#Program_change_events). +Note that the sound produced for these instruments is a wavetable rather than full-length samples, so they sound more like a retro +keyboard than the high-fidelity samples you might hear from a modern soft synthesizer. + +#### GM-Compatible Drum Kit +The Bank 0 Drum Kit is a set of custom drum sounds that follows the standard General MIDI note-to-drum mappings, which can be found +on [Wikipedia](https://en.wikipedia.org/wiki/General_MIDI#Percussion) and is commonly supported by MIDI devices and software. Note +that some drum sounds may be incomplete or not yet supported. This drum kit is available on MIDI Channel 10. + +### MAGFest Instruments (Bank 1) {#MAGPrograms} +These instruments are mainly basic wave shapes, with some extra goodies thrown in. + +| Program# | Name | +| -------- | ----------------| +| 0 | Square Wave | +| 1 | Sine Wave | +| 2 | Triangle Wave | +| 3 | Sawtooth Wave | +| 4 | "MAGFest Wave" | +| 5 | "MAGStock Wave" | +| 6 | Noise | +| 7 | Square Wave Hit | +| 8 | Noise Hit | + +#### Donut Swadge Drum Kit +The Bank 1 Drum Kit includes a range of noise-based drum sounds originally featured on the [King Donut Swadge](https://swadge.com/donut/). +By default, this drum kit is available on MIDI Channel 11. + +| Note | Note Number | Description | +| -------- | ----------- | ----------------- | +| B1 to B2 | 35 to 47 | Donut Drum Kit #1 | +| C2 to C4 | 48 to 60 | Donut Drum Kit #2 | +| C♯4 | 61 | WOAAAGGHHH | +| D4 | 72 | Donut "MAG" | +| D♯4 | 73 | Donut "Fest" | + + +## MIDI Continuous Controllers + +Continuous controller numbers (CC#) are shown here starting from 0. +For controllers that have two numbers in the `MIDI CC#` column, the first number is the "Coarse" (sometimes called "MSB") control, +and the second is the "Fine" (sometimes called "LSB") control. These are really two separate controllers analogous to a set of +fine and coarse adjustment knobs on a real audio device, but some editing software might display these as a single control instead. + +| MIDI CC# | Name | Value Range | Description | Notes | +| -------- | --------------- | ----------------------- | ----------- | ----- | +| 0, 32 | Bank Select | 0-1 | Selects the instrument bank used. | Only Fine adjustment (32) is used, Coarse (0) is ignored | +| 7, 39 | Channel Volume | 0-127 | Sets volume level for only this channel | Only Coarse adjustment (7) is used, Fine (39) is ignored | +| 64 | Hold Pedal | 0-63 (off), 64-127 (on) | When the hold pedal is enabled, all notes that are currently playing are sustained until the hold pedal is released, as well as any notes that begin playing while the hold pedal is already down. | | +| 66 | Sustenuto Pedal | 0-63 (off), 64-127 (on) | When the sustenuto pedal is enabled, only the notes that are currently playing are sustained until the sustenuto pedal is released. Notes that begin playing while the sustenuto pedal is already down are not affected. | | +| 72 | Release Time | 0-127 | Set the length of time (in 10ms increments) it will take for a note to fade out and stop playing after it is released. | | +| 73 | Attack Time | 0-127 | Set the length of time (in 10ms increments) it will take for a note to reach its maximum volume after it starts playing. | | +| 75 | Decay Time | 0-127 | Set the length of time (in 10ms increments) it will take for a note to fade from its maximum volume to its sustain volume. | | +| 76 | Sustain Level | 0-127 | Set the volume level the note will reach at the end of the decay time, and will be maintained as long as the note is held on. | | +| 6, 38 | Data Entry | 0-16383 | Set the value of the registered or non-registered parameter that was selected previously using the `Registered Parameter` or `Non-registered Parameter` controllers. | | +| 96 | Data Button Increment | 0 (not used) | Increments the value of the registered or non-registered parameter by one | | +| 97 | Data Button Decrement | 0 (not used) | Decrements the value of the registered or non-registered parameter by one | | +| 98, 99 | Non-registered Parameter | 0-16383 | Selects a non-registered parameter to be changed with the `Data Entry`or `Data Button` controllers. | | +| 101, 100 | Registered Parameter | 0-16383 | Selects a registered parameter value to be changed with the `Data Entry` or `Data Button` controllers. | | + + +## MIDI Non-registered Parameters + +These parameters can be changed by first using the `Non-registered Parameter` controller to select a specific parameter, then +using the `Data Entry` or `Data Button` controllers to actually change the parameter's value. + +| MIDI NRPN# | Name | Value Range | Description | +| ---------- | ---------- | --------------- | -------------------------------------------------------------------------- | +| 10 | Percussion | 0 (off), 1 (on) | Sets whether this channel plays a drum kit instead of a pitched instrument | + +## MIDI System-Exclusive (SysEx) Parameters + +The Swadge does not currently support any SysEx commands of its own, but it does support the following Universal SysEx commands. + +| Name | Data (Hex) | Description | +| ---------- | ---------------------------- | ----------- | +| GM Enable |
F0 7E 7F 09 01 F7
| Enables General MIDI Mode. All channels are fully reset, and set to use Bank 0, Program 0 ("Acoustic Grand Piano"), and Channel 10 only set to Percussion mode. | +| GM Disable |
F0 7E 7F 09 00 F7
| Disables General MIDI Mode. All channels are fully reset, and set to use Bank 1. Channels 1 through 9 are set to use Programs 0 through 8, respectively. Channel 10 is set to Percussion mode, and set to Bank 0 (General MIDI Compatible Drum kit). Channel 11 is set to Percussion mode, and set to Bank 1 (Donut Swadge Drum kit). Channels 12 through 16 **are currently** set to Bank 0, Program 0 ("Acoustic Grand Piano"), but **note** that this may change in the future and is not a guarantee. | diff --git a/docs/soko/readme.md b/docs/soko/readme.md new file mode 100644 index 000000000..0755bb3df --- /dev/null +++ b/docs/soko/readme.md @@ -0,0 +1,43 @@ +# Sokoban Game Mode + +Sokoban, unfinished for 2024, attempting to get finished for 2025! + +## Gameplay + +### Creating a Level +Levels are created with the software [Tiled](https://www.mapeditor.org/). + +Add the provided tilemap. You cannot add your own tiles. You can, but the system will ignore them. Use the provided tilemap. The image data from this map is unimportant, what is important is the 'ID' of all of the various objects and layers, and special custom properties for any of the items. ID's and custom properties are what gets converted into the .bin level data. + +There are currently 3 rulesets: CLASSIC, EULER, and LASER. + +- **CLASSIC** is traditional [sokoban](https://en.wikipedia.org/wiki/Sokoban). Can only push 1 block, must cover all goal areas with blocks. +- **EULER** is a port of Hunter's most succesful push-block-thinky game, which combines sokobon with [eulerian paths](https://en.wikipedia.org/wiki/Eulerian_path). You can only visit each square once. +- **LASER** is WIP. + +In order to indicate which ruleset your level uses... (currently the array of files has a matching sokoLevelVariants array.) +In order to indicate which theme (sprite set) your level uses.... (currently there is only one theme, and it is the default theme.) + +### Adding a Level +Create a level and save the .tsx file into the appropriate assets folder (assets/soko/levels/...). The pre-processer will convert these to a custom .bin file. This works the same way that the image pre-processor does. THe output folder is flat (all folder structure is ignored), so all levels should prefixed by "SK_" to prevent conflicts with other swadge mode files. + +There is an SK_LEVEL_LIST.txt file with one level per line, with following syntax: + +> :{id}:filename.bin: + +The id's do not need to packed. A 'levelIndices' int array is created which maps the provided id's to a clean loopable array. + +*Text is parsed by sokoExtractLevelNamesAndIndeces in soko.c. Importing is done by sokoLoadBinLevel in soko.c.* + +### Number of Levels +Level index, save data, and other level arrays are all pre-allocated to the 'SOKO_LEVEL_COUNT' in soko_consts.h. This constant to be increased to the number of levels, including overworld, as appropriate. + +### Adding to the Overworld + +Overworld is a map with all puzzles in them. 'portal' objects are used to transition into level. + In the overworld map file, the object has a custom property called 'target_id'. That gets set to the index of of the level. Everything else is handled by the engine. + +See [soko_levels.md](soko_levels) for more details on how levels work. + + + \ No newline at end of file diff --git a/docs/soko/soko_levels.md b/docs/soko/soko_levels.md new file mode 100644 index 000000000..4814a6436 --- /dev/null +++ b/docs/soko/soko_levels.md @@ -0,0 +1,96 @@ +# Soko Binary File Format + +The tools/soko folder contains a pre-processor that converts the [tiled](https://www.mapeditor.org/) tmx map files into a custom binary format (.bin). + +### How The Levels Work +The levels in the game are split into two elements. There are tiles. These are background items, such as 'Floor' or 'Wall'. There must be one at every location on the map (although "EMPTY" is an option). + +A collision matrix defined in 'soko_gamerules.c' determines what entities can walk on what tiles. + +Entities are stored in their own array, and represent anything that can move, basically. Entities can be (but mostly aren't) located at the same location as other entities. Player, Crate, WarpExternal, LaserEmitter, etc. are entities. + +In the tiled editor, there are two layers: an 'Object Layer' called entities and a 'Tiles Layer' layer called tiles. These correspond to the internal structure of the level. There should **not** be more layers than this, as the converter is fragile and poorly written. + +Each tile in the tilesheet has an id, and this is used when converting/loading the level to figure out what tile/object is where. + +#### Defining the Game Mode +The player entity should contain a 'gamemode' custom property, set to one of the following options: + +- SOKO_OVERWORLD +- SOKO_CLASSIC +- SOKO_EULER +- SOKO_LASERBOUNCE + +#### Configuring the Overworld +The overworld level is where we will add connections between levels. The structure of the game is flat: the player must return to the overworld after they complete a level. There are not multiple overworlds (zones, world 2-2, etc). + +The overworld uses an entity with the class 'warpexternal', with ID 3. This object contains a custom property 'target_id', which corresponds to the level ID value that should get loaded. + +#### Setting the Levels +First, save the level tmx file in the appropriate folder in assets. + +Then, edit the 'SK_LEVEL_LIST.txt' file. +Add a line for your level with the following syntax: + +*:id:filename.bin:* + +That's a colon, then a chosen whole-integer id value that doesn't conflict with others. They do not have to be sequenced or defined in order. Then another colon, then the filename. THis will be the same as the .tmx file,except with the .bin extension. + +Then another colon at the end of the line. + +The level is now ready to be loaded. Add it to the overworld map as described above. + +--- + +### Preprocessor +"soko_tmx_preprocessor.py" scans a directory (recursively) for these files and puts them flat inside the spiffs_image output folder. + +Because there is no folder structure on output, sokoban levels should follow a consistent naming scheme to avoid name conflicts. + +### Converstion to Binary +tmx files are xml based. Each map should contain a tiles layer (called 'tileset') which gets read as the static tileset, and an objects layer called 'entities'. + +### The Binary Format +The format is a packed sequence of bytes. + +First, 3 bytes of header information: +1. Width +2. Height +3. Gamemode + +Ignoring entities and compression, the next is a tight "grid" of tile data, left to right, top to bottom, like a book. Each byte is the id of the tile at that position, defined in the 'soko_bin_t' enum in soko.h. The values don't necesarily correspond to the enum values for the tiles or entity structs; but instead the soko_bin_t enum. Which is only used for parsing the file. + +#### Entities +As the data is parsed, the position of the last-parsed tile is kept. If the parser encounters a 'SKB_OBJSTART' byte (201), it does not 'advance' the position of the tiles, but instead creates an entity. + +Entities are 'SKB_OBJSTART', then a byte defining some number of data pieces, then a 'SKB_OBJEND' byte. Each entity is at least 3 bytes. The position of the entity is the last parsed tile position, and the type of entity is determined by it's second byte. + +If, after the modes design has been finished and all entity type sizes are known, we can remove the 'SKB_OBJEND' byte. For now, we need it. + +The rest of the bytes depend on the entity. The WARPEXTERNAL entity has one additional byte, defining the level ID to jump to. Warp internal works the same way. + +All Entity data is stored between bytes 200 and 255. + +### Compression +After the file is created, it gets compressed. + +The compression scheme is one of the compressing byte, then the 'SKB_COMPRESS' byte, followed by some number of times to repeat that previous byte. + +"Floor, Floor, Floor" could become "Floor, Compress, 2". +"Wall, Wall, Wall, Wall, Wall, Wall, Wall, Wall" would become "Wall, Compress, 7" + +Because the data is stores in horizontal rows, this only compresses contiguous horizontal sections of tiles, including when "word wrapped". Regardless, it won't hurt. *Getting around 73% ratio on 5/13/24. Mostly in overworld.* + +#### Entity Binary Encoding Schemes +*START = 'SKB_OBJSTART', END = 'SKB_OBJEND', and 'SKB_' prefix ignored.* + +- START, PLAYER, END +- START, CRATE, [StickyFlag] END + - not stick, not trail = 0 + - sticky, not trail = 1 + - not stick, trail = 2 + - sticky, trail ST = 3 +- START, WARPEXTERNAL, [Target ID], END + - TargetID should match SK_LVEL_LIST.txt data. + +*Note: Todo as I re-write the converter in python* \ No newline at end of file diff --git a/emulator/resources/SwadgeEmulator.desktop b/emulator/resources/SwadgeEmulator.desktop new file mode 100644 index 000000000..1a7481498 --- /dev/null +++ b/emulator/resources/SwadgeEmulator.desktop @@ -0,0 +1,11 @@ +[Desktop Entry] +Name=Swadge Emulator +GenericName=Swadge Emulator +Comment=Emulate the MAGFest Swadge +Exec=$ROOT/bin/swadge_emulator %U +Terminal=false +Type=Application +StartupNotify=true +MimeType=audio/midi;audio/x-midi; +Icon=$ROOT/share/icons/SwadgeEmulator/icon.png +Categories=Game;Emulator;AudioVideo;Audio;Midi;Player;Development; diff --git a/emulator/resources/install.sh b/emulator/resources/install.sh new file mode 100755 index 000000000..cdbc336ea --- /dev/null +++ b/emulator/resources/install.sh @@ -0,0 +1,57 @@ +#!/bin/bash +set -e +# Uncomment this to print all script lines +# set -x + +if [ -e swadge_emulator ]; then + BIN_SRC="swadge_emulator" +elif [ -e ../../swadge_emulator ]; then + BIN_SRC="../../swadge_emulator" +else + echo "Error: install file 'swadge_emulator' not found! Are you running this script from the correct directory?" + exit 1 +fi + +if [ -e SwadgeEmulator.desktop ]; then + DESKTOP_SRC="SwadgeEmulator.desktop" +elif [ -e emulator/resources/SwadgeEmulator.desktop ]; then + DESKTOP_SRC="emulator/resources/SwadgeEmulator.desktop" +else + echo "Error: install file 'SwadgeEmulator.desktop' not found! Are you running this script from the correct directory?" + exit 1 +fi + +if [ -e icon.png ]; then + ICON_SRC="icon.png" +elif [ -e emulator/resources/icon.png ]; then + ICON_SRC="emulator/resources/icon.png" +else + echo "Error: install file 'ico.png' not found! Are you running this script from the correct directory?" +fi + +INSTALL_DIR="${HOME}/.local" + +if [ "$#" -gt "0" ]; then + INSTALL_DIR="${1}" +fi + +echo "Installing the Swadge Emulator to: ${INSTALL_DIR}" + +mkdir -p "${INSTALL_DIR}/bin" "${INSTALL_DIR}/share/icons/SwadgeEmulator" "${INSTALL_DIR}/share/applications" + +cp "${BIN_SRC}" "${INSTALL_DIR}/bin/swadge_emulator" +cp "${ICON_SRC}" "${INSTALL_DIR}/share/icons/SwadgeEmulator/icon.png" +sed "s,\$ROOT,${INSTALL_DIR},g" "${DESKTOP_SRC}" > "${INSTALL_DIR}/share/applications/SwadgeEmulator.desktop" + +if ! [[ ":$PATH:" == *":$INSTALL_DIR/bin:"* ]]; then + echo "WARNING: ${INSTALL_DIR}/bin is not in your PATH!" + echo " If you want to be able to start the Swadge Emulator from the command-line," + echo " you can run this command to update your PATH for this session," + echo " or add it to a file like ~/.bash_profile:" + echo + echo " export PATH=\"\$PATH:${INSTALL_DIR}/bin\"" + echo +fi + +echo +echo "Installation complete!" diff --git a/emulator/src/components/hdw-nvs/hdw-nvs.c b/emulator/src/components/hdw-nvs/hdw-nvs.c index c55e7927d..5ecc8461a 100644 --- a/emulator/src/components/hdw-nvs/hdw-nvs.c +++ b/emulator/src/components/hdw-nvs/hdw-nvs.c @@ -17,6 +17,7 @@ #include "cJSON.h" #include "emu_main.h" #include "emu_utils.h" +#include "hashMap.h" //============================================================================== // Defines @@ -29,6 +30,22 @@ #define NVS_ENTRY_BYTES 32 #define NVS_OVERHEAD_ENTRIES 12 +//============================================================================== +// Structs +//============================================================================== + +typedef struct +{ + char key[36]; + uint32_t size; + bool isBlob; + union + { + void* blob; + int32_t int32; + }; +} emuNvsInjectedData_t; + //============================================================================== // Function Prototypes //============================================================================== @@ -37,6 +54,9 @@ static char* blobToStr(const void* value, size_t length); static int hexCharToInt(char c); static void strToBlob(char* str, void* outBlob, size_t blobLen); static FILE* openNvsFile(const char* mode); +static size_t emuGetInjectedBlobLength(const char* namespace, const char* key); +static void* emuGetInjectedBlob(const char* namespace, const char* key); +static bool emuGetInjected32(const char* namespace, const char* key, int32_t* out); //============================================================================== // Constants @@ -58,6 +78,8 @@ static const char* defaultNvsFiles[] = { // Variables //============================================================================== static const char** nvsFileName = defaultNvsFiles; +static bool nvsInjectedDataInit = false; +static hashMap_t nvsInjectedData; //============================================================================== // Functions @@ -72,6 +94,12 @@ static const char** nvsFileName = defaultNvsFiles; */ bool initNvs(bool firstTry) { + if (!nvsInjectedDataInit) + { + hashInit(&nvsInjectedData, 16); + nvsInjectedDataInit = true; + } + const char** curFile; for (curFile = defaultNvsFiles; curFile < (defaultNvsFiles + (sizeof(defaultNvsFiles) / sizeof(*defaultNvsFiles))); curFile++) @@ -146,6 +174,17 @@ bool initNvs(bool firstTry) */ bool deinitNvs(void) { + if (nvsInjectedDataInit) + { + hashIterator_t iter = {0}; + while (hashIterate(&nvsInjectedData, &iter)) + { + free(iter.value); + hashIterRemove(&nvsInjectedData, &iter); + } + hashDeinit(&nvsInjectedData); + nvsInjectedDataInit = false; + } return true; // Nothing to do } @@ -211,6 +250,11 @@ bool writeNvs32(const char* key, int32_t val) */ bool readNamespaceNvs32(const char* namespace, const char* key, int32_t* outVal) { + if (emuGetInjected32(namespace, key, outVal)) + { + return true; + } + // Open the file FILE* nvsFile = openNvsFile("rb"); if (NULL != nvsFile) @@ -381,6 +425,21 @@ bool writeNamespaceNvs32(const char* namespace, const char* key, int32_t val) */ bool readNamespaceNvsBlob(const char* namespace, const char* key, void* out_value, size_t* length) { + void* resultData = emuGetInjectedBlob(namespace, key); + if (resultData != NULL) + { + if (out_value != NULL) + { + memcpy(out_value, resultData, *length); + } + else + { + size_t injectedLength = emuGetInjectedBlobLength(namespace, key); + *length = injectedLength; + } + return true; + } + // Open the file FILE* nvsFile = openNvsFile("rb"); if (NULL != nvsFile) @@ -1025,3 +1084,114 @@ static FILE* openNvsFile(const char* mode) return fopen(buffer, mode); } + +void emuInjectNvsBlob(const char* namespace, const char* key, size_t length, const void* blob) +{ + if (!nvsInjectedDataInit) + { + hashInit(&nvsInjectedData, 16); + nvsInjectedDataInit = true; + } + + void* alloc = malloc(sizeof(emuNvsInjectedData_t) + length); + if (alloc != NULL) + { + emuNvsInjectedData_t* data = (emuNvsInjectedData_t*)alloc; + + snprintf(data->key, sizeof(data->key), "%s:%s", namespace, key); + data->size = (uint32_t)length; + data->isBlob = true; + data->blob = ((char*)alloc) + sizeof(emuNvsInjectedData_t); + memcpy(data->blob, blob, length); + + hashPut(&nvsInjectedData, data->key, alloc); + } +} + +void emuInjectNvs32(const char* namespace, const char* key, int32_t value) +{ + if (!nvsInjectedDataInit) + { + hashInit(&nvsInjectedData, 16); + nvsInjectedDataInit = true; + } + + void* alloc = malloc(sizeof(emuNvsInjectedData_t)); + if (alloc != NULL) + { + emuNvsInjectedData_t* data = (emuNvsInjectedData_t*)alloc; + + snprintf(data->key, sizeof(data->key), "%s:%s", namespace, key); + data->size = 4; + data->isBlob = false; + data->int32 = value; + + hashPut(&nvsInjectedData, data->key, alloc); + } +} + +static size_t emuGetInjectedBlobLength(const char* namespace, const char* key) +{ + if (!nvsInjectedDataInit) + { + return 0; + } + + char fullkey[36]; + snprintf(fullkey, sizeof(fullkey), "%s:%s", namespace, key); + void* found = hashGet(&nvsInjectedData, fullkey); + + if (found != NULL) + { + return ((emuNvsInjectedData_t*)found)->size; + } + + return 0; +} + +static void* emuGetInjectedBlob(const char* namespace, const char* key) +{ + if (!nvsInjectedDataInit) + { + return NULL; + } + + char fullkey[36]; + snprintf(fullkey, sizeof(fullkey), "%s:%s", namespace, key); + void* found = hashGet(&nvsInjectedData, fullkey); + + if (found != NULL) + { + emuNvsInjectedData_t* data = (emuNvsInjectedData_t*)found; + if (data->isBlob) + { + return data->blob; + } + } + + return NULL; +} + +static bool emuGetInjected32(const char* namespace, const char* key, int32_t* out) +{ + if (!nvsInjectedDataInit) + { + return false; + } + + char fullkey[36]; + snprintf(fullkey, sizeof(fullkey), "%s:%s", namespace, key); + void* found = hashGet(&nvsInjectedData, fullkey); + + if (found != NULL) + { + emuNvsInjectedData_t* data = (emuNvsInjectedData_t*)found; + if (!data->isBlob) + { + *out = data->int32; + return true; + } + } + + return false; +} diff --git a/emulator/src/components/hdw-nvs/hdw-nvs_emu.h b/emulator/src/components/hdw-nvs/hdw-nvs_emu.h new file mode 100644 index 000000000..12cffb35d --- /dev/null +++ b/emulator/src/components/hdw-nvs/hdw-nvs_emu.h @@ -0,0 +1,7 @@ +#pragma once + +#include +#include + +void emuInjectNvsBlob(const char* namespace, const char* key, size_t length, const void* blob); +void emuInjectNvs32(const char* namespace, const char* key, int32_t value); diff --git a/emulator/src/emu_cnfs.c b/emulator/src/emu_cnfs.c new file mode 100644 index 000000000..b2f3b723d --- /dev/null +++ b/emulator/src/emu_cnfs.c @@ -0,0 +1,161 @@ +#include "emu_cnfs.h" + +#include +#include +#include +#include +#include + +#include "esp_heap_caps.h" +#include "esp_log.h" +#include "cnfs_image.h" + +//============================================================================== +// Variables +//============================================================================== + +// Original CNFS Variables +static const uint8_t* cnfsData; +static int32_t cnfsDataSz; + +static const cnfsFileEntry* cnfsFiles; +static int32_t cnfsNumFiles; + +// Extended CNFS Variables + +static char* cnfsInjectedFilename = NULL; +static int32_t cnfsInjectedFileSize = 0; +static void* cnfsInjectedFileData = NULL; + +//============================================================================== +// Functions +//============================================================================== + +bool initCnfs(void) +{ + /* Get local references from cnfs_data.c */ + cnfsData = getCnfsImage(); + cnfsDataSz = getCnfsSize(); + cnfsFiles = getCnfsFiles(); + cnfsNumFiles = getCnfsNumFiles(); + + /* Debug print */ + ESP_LOGI("CNFS", "Size: %" PRIu32 ", Files: %" PRIu32, cnfsDataSz, cnfsNumFiles); + return (0 != cnfsDataSz) && (0 != cnfsNumFiles); +} + +bool emuCnfsInjectFile(const char* name, const char* filePath) +{ + FILE* dataFile = fopen(filePath, "rb"); + if (dataFile != NULL) + { + fseek(dataFile, 0L, SEEK_END); + size_t fileSize = ftell(dataFile); + fseek(dataFile, 0L, SEEK_SET); + + if (fileSize > 0) + { + void* fileData = malloc(fileSize); + if (fileData != NULL) + { + if (fileSize == fread(fileData, 1, fileSize, dataFile)) + { + fclose(dataFile); + emuCnfsInjectFileData(name, fileSize, fileData); + return true; + } + } + } + + fclose(dataFile); + return false; + } + else + { + printf("ERR: Could not open %s\n", filePath); + return false; + } +} + +void emuCnfsInjectFileData(const char* name, size_t length, void* data) +{ + cnfsInjectedFilename = strdup(name); + cnfsInjectedFileSize = length; + cnfsInjectedFileData = data; +} + +bool deinitCnfs(void) +{ + free(cnfsInjectedFilename); + free(cnfsInjectedFileData); + + cnfsInjectedFileSize = 0; + cnfsInjectedFilename = NULL; + cnfsInjectedFileData = NULL; + + return true; +} + +const uint8_t* cnfsGetFile(const char* fname, size_t* flen) +{ + if (cnfsInjectedFilename && !strcmp(cnfsInjectedFilename, fname)) + { + *flen = cnfsInjectedFileSize; + return (uint8_t*)cnfsInjectedFileData; + } + else + { + // Real implementation - copied from cnfs.c + int low = 0; + int high = cnfsNumFiles - 1; + int mid = (low + high) / 2; + + // Binary search the file list, since it's sorted. + while (low <= high) + { + const cnfsFileEntry* e = cnfsFiles + mid; + int sc = strcmp(e->name, fname); + if (sc < 0) + { + low = mid + 1; + } + else if (sc == 0) + { + *flen = e->len; + return &cnfsData[e->offset]; + } + else + { + high = mid - 1; + } + mid = (low + high) / 2; + } + ESP_LOGE("CNFS", "Failed to open %s", fname); + return 0; + } +} + +// Hack needed because we can't actually wrap the call that cnfsReadFile() makes to cnfsGetFile() because of compiler +// shenanigans +uint8_t* cnfsReadFile(const char* fname, size_t* outsize, bool readToSpiRam) +{ + const uint8_t* fptr = cnfsGetFile(fname, outsize); + + if (!fptr) + { + return 0; + } + + uint8_t* output; + + if (readToSpiRam) + { + output = (uint8_t*)heap_caps_calloc((*outsize + 1), sizeof(uint8_t), MALLOC_CAP_SPIRAM); + } + else + { + output = (uint8_t*)calloc((*outsize + 1), sizeof(uint8_t)); + } + memcpy(output, fptr, *outsize); + return output; +} diff --git a/emulator/src/emu_cnfs.h b/emulator/src/emu_cnfs.h new file mode 100644 index 000000000..a5fc1f0fa --- /dev/null +++ b/emulator/src/emu_cnfs.h @@ -0,0 +1,7 @@ +#pragma once + +#include +#include + +bool emuCnfsInjectFile(const char* name, const char* filePath); +void emuCnfsInjectFileData(const char* name, size_t length, void* data); diff --git a/emulator/src/extensions/emu_args.c b/emulator/src/extensions/emu_args.c index 9cfa00d7b..28ec60ea1 100644 --- a/emulator/src/extensions/emu_args.c +++ b/emulator/src/extensions/emu_args.c @@ -68,6 +68,7 @@ typedef struct //============================================================================== static bool handleArgument(const char* optName, const char* arg, int optVal); +static bool handlePositionalArgument(const char* val); static void printHelp(const char* progName); static void printUsage(const char* progName); static const optDoc_t* findOptDoc(char shortOpt, const char* longOpt); @@ -137,6 +138,7 @@ static const char argHeadless[] = "headless"; static const char argHideLeds[] = "hide-leds"; static const char argKeymap[] = "keymap"; static const char argLock[] = "lock"; +static const char argMidiFile[] = "midi-file"; static const char argMode[] = "mode"; static const char argModeSwitch[] = "mode-switch"; static const char argModeList[] = "modes-list"; @@ -167,6 +169,7 @@ static const struct option options[] = { argHideLeds, no_argument, (int*)&emulatorArgs.hideLeds, true }, { argKeymap, required_argument, NULL, 'k' }, { argLock, no_argument, (int*)&emulatorArgs.lock, true }, + { argMidiFile, required_argument, NULL, 0 }, { argMode, required_argument, NULL, 'm' }, { argPlayback, required_argument, (int*)&emulatorArgs.playback, 'p' }, { argRecord, optional_argument, (int*)&emulatorArgs.record, 'r' }, @@ -198,6 +201,7 @@ static const optDoc_t argDocs[] = { 0, argHideLeds, NULL, "Don't draw simulated LEDs next to the display" }, {'k', argKeymap, "LAYOUT", "Use an alternative keymap. LAYOUT can be azerty, colemak, or dvorak"}, {'l', argLock, NULL, "Lock the emulator in the start mode" }, + { 0, argMidiFile, "FILE", "Open and immediately play a MIDI file" }, {'m', argMode, "MODE", "Start the emulator in the swadge mode MODE instead of the main menu"}, { 0, argModeSwitch, "TIME", "Enable or set the timer to switch modes automatically" }, { 0, argModeList, NULL, "Print out a list of all possible values for MODE" }, @@ -313,6 +317,10 @@ static bool handleArgument(const char* optName, const char* arg, int optVal) } return true; } + else if (argMidiFile == optName) + { + emulatorArgs.midiFile = arg; + } else if (argMode == optName) { emulatorArgs.startMode = arg; @@ -409,6 +417,21 @@ static bool handleArgument(const char* optName, const char* arg, int optVal) return true; } +static bool handlePositionalArgument(const char* val) +{ + if (val) + { + if ((strlen(val) > 4 && (!strcmp(&val[strlen(val) - 4], ".mid") || !strcmp(&val[strlen(val) - 4], ".kar"))) + || (strlen(val) > 5 && !strcmp(&val[strlen(val) - 5], ".midi"))) + { + emulatorArgs.midiFile = val; + return true; + } + } + + return false; +} + /** * @brief Print out the help text for this program, describing all options. * @@ -834,8 +857,21 @@ bool emuParseArgs(int argc, char** argv) if (optVal < 0) { - // No more options - break; + // Not an option + if (optind < argc) + { + // ...but there are still arguments, so this is a positional arg + if (!handlePositionalArgument(argv[optind])) + { + printf("Unknown argument '%s'\n", argv[optind]); + return false; + } + } + else + { + // No more options + break; + } } else if (optVal == '?') { diff --git a/emulator/src/extensions/emu_args.h b/emulator/src/extensions/emu_args.h index 78e03a629..85bd62f66 100644 --- a/emulator/src/extensions/emu_args.h +++ b/emulator/src/extensions/emu_args.h @@ -74,6 +74,9 @@ typedef struct /// @brief Whether VSync is enabled bool vsync; + + // MIDI + const char* midiFile; } emuArgs_t; //============================================================================== diff --git a/emulator/src/extensions/emu_ext.c b/emulator/src/extensions/emu_ext.c index 09c395175..9ce0cddba 100644 --- a/emulator/src/extensions/emu_ext.c +++ b/emulator/src/extensions/emu_ext.c @@ -17,6 +17,7 @@ #include "ext_leds.h" #include "ext_fuzzer.h" #include "ext_keymap.h" +#include "ext_midi.h" #include "ext_modes.h" #include "ext_replay.h" #include "ext_tools.h" @@ -31,7 +32,7 @@ static const emuExtension_t* registeredExtensions[] = { &touchEmuCallback, &ledEmuExtension, &fuzzerEmuExtension, &toolsEmuExtension, - &keymapEmuCallback, &modesEmuExtension, &replayEmuExtension, + &keymapEmuCallback, &modesEmuExtension, &replayEmuExtension, &midiEmuExtension, }; //============================================================================== diff --git a/emulator/src/extensions/midi/ext_midi.c b/emulator/src/extensions/midi/ext_midi.c new file mode 100644 index 000000000..ca39ac292 --- /dev/null +++ b/emulator/src/extensions/midi/ext_midi.c @@ -0,0 +1,59 @@ +#include "ext_midi.h" +#include "emu_ext.h" +#include "emu_main.h" + +#include "hdw-nvs_emu.h" +#include "emu_cnfs.h" +#include "ext_modes.h" +#include "mode_synth.h" + +#include + +//============================================================================== +// Function Prototypes +//============================================================================== + +static bool midiInitCb(emuArgs_t* emuArgs); + +//============================================================================== +// Variables +//============================================================================== + +emuExtension_t midiEmuExtension = { + .name = "midi", + .fnInitCb = midiInitCb, + .fnPreFrameCb = NULL, + .fnPostFrameCb = NULL, + .fnKeyCb = NULL, + .fnMouseMoveCb = NULL, + .fnMouseButtonCb = NULL, + .fnRenderCb = NULL, +}; + +//============================================================================== +// Functions +//============================================================================== + +static bool midiInitCb(emuArgs_t* emuArgs) +{ + if (emuArgs->midiFile) + { + printf("Opening MIDI file: %s\n", emuArgs->midiFile); + if (emuCnfsInjectFile(emuArgs->midiFile, emuArgs->midiFile)) + { + emuInjectNvs32("storage", "synth_playmode", 1); + emuInjectNvsBlob("storage", "synth_lastsong", strlen(emuArgs->midiFile), emuArgs->midiFile); + emulatorSetSwadgeModeByName(synthMode.modeName); + } + else + { + printf("Could not read MIDI file!\n"); + emulatorQuit(); + return false; + } + + return true; + } + + return false; +} \ No newline at end of file diff --git a/emulator/src/extensions/midi/ext_midi.h b/emulator/src/extensions/midi/ext_midi.h new file mode 100644 index 000000000..50e1811f9 --- /dev/null +++ b/emulator/src/extensions/midi/ext_midi.h @@ -0,0 +1,10 @@ +/*! \file ext_midi.h + * + * \section ext_midi Extended Emulator MIDI Support + */ + +#pragma once + +#include "emu_ext.h" + +extern emuExtension_t midiEmuExtension; diff --git a/emulator/src/extensions/modes/ext_modes.c b/emulator/src/extensions/modes/ext_modes.c index 2263fc93e..e35c18e88 100644 --- a/emulator/src/extensions/modes/ext_modes.c +++ b/emulator/src/extensions/modes/ext_modes.c @@ -25,11 +25,13 @@ #include "jukebox.h" #include "keebTest.h" #include "mainMenu.h" -#include "modeTimer.h" +#include "mode_2048.h" #include "mode_bigbug.h" #include "mode_credits.h" -#include "mode_pinball.h" #include "mode_synth.h" +#include "modeTimer.h" +#include "pango.h" +#include "soko.h" #include "touchTest.h" #include "tunernome.h" #include "ultimateTTT.h" @@ -70,8 +72,10 @@ static swadgeMode_t* allSwadgeModes[] = { &keebTestMode, &mainMenuMode, &modeCredits, - &pinballMode, + &pangoMode, + &sokoMode, &synthMode, + &t48Mode, &timerMode, &touchTestMode, &tttMode, diff --git a/emulator/src/idf/midi_device.c b/emulator/src/idf/midi_device.c index 798d45a27..257b7cd1d 100644 --- a/emulator/src/idf/midi_device.c +++ b/emulator/src/idf/midi_device.c @@ -25,6 +25,9 @@ #include #include +// Un-comment to enable printing all received MIDI packets +// #define DEBUG_MIDI_PACKETS + static uint8_t runningStatus = 0; static struct platform_midi_driver* midiDriver = NULL; @@ -66,12 +69,14 @@ bool tud_midi_n_packet_read(uint8_t itf, uint8_t packet[4]) int read = midiDriver ? platform_midi_read(midiDriver, real_packet, sizeof(real_packet)) : 0; if (read > 0) { +#ifdef DEBUG_MIDI_PACKETS printf("Packet: "); for (int i = 0; i < read; i++) { printf("%hhx, ", real_packet[i]); } printf("\n"); +#endif // Normally start reading after the status byte int dataOffset = 1; diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index b399a88c3..d891f43f5 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -1,10 +1,10 @@ idf_component_register(SRCS "asset_loaders/common/heatshrink_encoder.c" - "asset_loaders/heatshrink_decoder.c" - "asset_loaders/heatshrink_helper.c" "asset_loaders/fs_font.c" "asset_loaders/fs_json.c" "asset_loaders/fs_txt.c" "asset_loaders/fs_wsg.c" + "asset_loaders/heatshrink_decoder.c" + "asset_loaders/heatshrink_helper.c" "colorchord/DFT32.c" "colorchord/embeddedNf.c" "colorchord/embeddedOut.c" @@ -12,6 +12,7 @@ idf_component_register(SRCS "asset_loaders/common/heatshrink_encoder.c" "display/font.c" "display/shapes.c" "display/wsg.c" + "display/wsgPalette.c" "menu/menu.c" "menu/menuManiaRenderer.c" "menu/menu_utils.c" @@ -22,24 +23,35 @@ idf_component_register(SRCS "asset_loaders/common/heatshrink_encoder.c" "midi/midiUsb.c" "midi/midiUtil.c" "midi/waveTables.c" + "modes/games/2048/2048_game.c" + "modes/games/2048/2048_menus.c" + "modes/games/2048/mode_2048.c" "modes/games/bigbug/aabb_utils_bigbug.c" "modes/games/bigbug/entity_bigbug.c" "modes/games/bigbug/entityManager_bigbug.c" "modes/games/bigbug/gameData_bigbug.c" "modes/games/bigbug/mode_bigbug.c" + "modes/games/bigbug/pathfinding_bigbug.c" "modes/games/bigbug/soundManager_bigbug.c" "modes/games/bigbug/tilemap_bigbug.c" - "modes/games/bigbug/pathfinding_bigbug.c" - "modes/games/pinball/mode_pinball.c" - "modes/games/pinball/pinball_draw.c" - "modes/games/pinball/pinball_physics.c" - "modes/games/pinball/pinball_test.c" - "modes/games/pinball/pinball_zones.c" + "modes/games/pango/paEntity.c" + "modes/games/pango/paEntityManager.c" + "modes/games/pango/paGameData.c" + "modes/games/pango/pango.c" + "modes/games/pango/paSoundManager.c" + "modes/games/pango/paTilemap.c" + "modes/games/pango/paWsgManager.c" + "modes/games/soko/soko.c" + "modes/games/soko/soko_game.c" + "modes/games/soko/soko_gamerules.c" + "modes/games/soko/soko_input.c" + "modes/games/soko/soko_save.c" + "modes/games/soko/soko_undo.c" "modes/games/ultimateTTT/ultimateTTT.c" "modes/games/ultimateTTT/ultimateTTTgame.c" "modes/games/ultimateTTT/ultimateTTThowTo.c" - "modes/games/ultimateTTT/ultimateTTTp2p.c" "modes/games/ultimateTTT/ultimateTTTmarkerSelect.c" + "modes/games/ultimateTTT/ultimateTTTp2p.c" "modes/games/ultimateTTT/ultimateTTTresult.c" "modes/music/colorchord/colorchord.c" "modes/music/jukebox/jukebox.c" @@ -54,16 +66,16 @@ idf_component_register(SRCS "asset_loaders/common/heatshrink_encoder.c" "modes/system/quickSettings/quickSettings.c" "modes/test/accelTest/accelTest.c" "modes/test/factoryTest/factoryTest.c" - "modes/test/touchTest/touchTest.c" "modes/test/keebTest/keebTest.c" + "modes/test/touchTest/touchTest.c" "modes/utilities/dance/dance.c" "modes/utilities/dance/portableDance.c" "modes/utilities/gamepad/gamepad.c" "modes/utilities/timer/modeTimer.c" "swadge2024.c" - "utils/color_utils.c" "utils/cnfs.c" "utils/cnfs_image.c" + "utils/color_utils.c" "utils/dialogBox.c" "utils/fl_math/geometryFl.c" "utils/fl_math/vectorFl2d.c" @@ -94,7 +106,7 @@ idf_component_register(SRCS "asset_loaders/common/heatshrink_encoder.c" crashwrap REQUIRES esp_timer spi_flash - INCLUDE_DIRS "." + INCLUDE_DIRS "./" "./asset_loaders" "./asset_loaders/common" "./colorchord" @@ -103,14 +115,16 @@ idf_component_register(SRCS "asset_loaders/common/heatshrink_encoder.c" "./midi" "./modes" "./modes/games" - "./modes/games/pinball" + "./modes/games/2048" "./modes/games/bigbug" + "./modes/games/pango" + "./modes/games/soko" "./modes/games/ultimateTTT" "./modes/music" "./modes/music/colorchord" "./modes/music/jukebox" - "./modes/music/usbsynth" "./modes/music/tunernome" + "./modes/music/usbsynth" "./modes/system" "./modes/system/credits" "./modes/system/intro" @@ -119,8 +133,8 @@ idf_component_register(SRCS "asset_loaders/common/heatshrink_encoder.c" "./modes/test" "./modes/test/accelTest" "./modes/test/factoryTest" - "./modes/test/touchTest" "./modes/test/keebTest" + "./modes/test/touchTest" "./modes/utilities" "./modes/utilities/dance" "./modes/utilities/gamepad" diff --git a/main/display/wsg.c b/main/display/wsg.c index ba4e12a76..9617cf415 100644 --- a/main/display/wsg.c +++ b/main/display/wsg.c @@ -10,12 +10,6 @@ #include "fill.h" #include "wsg.h" -//============================================================================== -// Function Prototypes -//============================================================================== - -static void rotatePixel(int16_t* x, int16_t* y, int16_t rotateDeg, int16_t width, int16_t height); - //============================================================================== // Functions //============================================================================== @@ -25,12 +19,12 @@ static void rotatePixel(int16_t* x, int16_t* y, int16_t rotateDeg, int16_t width * then reflection over Y axis, then reflection over X axis, then translation * * @param x The x coordinate of the pixel location to transform - * @param y The y coordinate of the pixel location to trasform + * @param y The y coordinate of the pixel location to transform * @param rotateDeg The number of degrees to rotate clockwise, must be 0-359 * @param width The width of the image * @param height The height of the image */ -static void rotatePixel(int16_t* x, int16_t* y, int16_t rotateDeg, int16_t width, int16_t height) +void rotatePixel(int16_t* x, int16_t* y, int16_t rotateDeg, int16_t width, int16_t height) { // This function has been micro optimized by cnlohr on 2022-09-07, using gcc version 8.4.0 (crosstool-NG // esp-2021r2-patch3) @@ -253,7 +247,7 @@ void drawWsg(const wsg_t* wsg, int16_t xOff, int16_t yOff, bool flipLR, bool fli uint32_t dstY = srcY + yOff; // It is too complicated to detect both directions and backoff correctly, so we just do this here. - // It does slow things down a "tiny" bit. People in the future could optimze out this check. + // It does slow things down a "tiny" bit. People in the future could optimize out this check. if (dstY >= TFT_HEIGHT) { continue; @@ -494,4 +488,4 @@ void drawWsgTile(const wsg_t* wsg, int32_t xOff, int32_t yOff) pxDisp += dWidth; pxWsg += wWidth; } -} +} \ No newline at end of file diff --git a/main/display/wsg.h b/main/display/wsg.h index 5e8856693..a1fceabb2 100644 --- a/main/display/wsg.h +++ b/main/display/wsg.h @@ -17,13 +17,16 @@ * * \section wsg_usage Usage * - * There are three ways to draw a WSG to the display each with varying complexity and speed + * There are five ways to draw a WSG to the display each with varying complexity and speed * - drawWsg(): Draw a WSG to the display with transparency, rotation, and flipping over horizontal or vertical axes. * This is the slowest option. * - drawWsgSimple(): Draw a WSG to the display with transparency. This is the medium speed option and should be used if * the WSG is not rotated or flipped. * - drawWsgTile(): Draw a WSG to the display without transparency. Any transparent pixels will be an indeterminate * color. This is the fastest option, and best for background tiles or images. + * - drawWsgSimpleScaled(): Draw a WSG to the display with transparency at a specified scale. Scales are integer + * values, so 2x, 3x, 4x... are the valid options. + * - drawWsgSimpleHalf(): Draw a WSG to the display with transparency at half the original resolution. * * \section wsg_example Example * @@ -45,6 +48,7 @@ #include #include +#include /** * @brief A sprite using paletteColor_t colors that can be drawn to the display @@ -56,6 +60,7 @@ typedef struct uint16_t h; ///< The height of the image } wsg_t; +void rotatePixel(int16_t* x, int16_t* y, int16_t rotateDeg, int16_t width, int16_t height); void drawWsg(const wsg_t* wsg, int16_t xOff, int16_t yOff, bool flipLR, bool flipUD, int16_t rotateDeg); void drawWsgSimple(const wsg_t* wsg, int16_t xOff, int16_t yOff); void drawWsgSimpleScaled(const wsg_t* wsg, int16_t xOff, int16_t yOff, int16_t xScale, int16_t yScale); diff --git a/main/display/wsgPalette.c b/main/display/wsgPalette.c new file mode 100644 index 000000000..9e96ba102 --- /dev/null +++ b/main/display/wsgPalette.c @@ -0,0 +1,404 @@ +/** + * @file wsgPalette.c + * @author Jeremy Stintzcum (jeremy.stintzcum@gmail.com) + * @brief Provides palette swap functionality for Swadge + * @version 1.0.0 + * @date 2024-09-20 + * + * @copyright Copyright (c) 2024 + * + */ + +//============================================================================== +// Includes +//============================================================================== + +#include "wsgPalette.h" +#include "hdw-tft.h" +#include "trigonometry.h" +#include "macros.h" +#include "fill.h" + +//============================================================================== +// Functions +//============================================================================== + +/** + * @brief Draw a WSG to the display utilizing a palette + * + * @param wsg The WSG to draw to the display + * @param xOff The x offset to draw the WSG at + * @param yOff The y offset to draw the WSG at + * @param palette The new palette used to translate the colors + * @param flipLR true to flip the image across the Y axis + * @param flipUD true to flip the image across the X axis + * @param rotateDeg The number of degrees to rotate clockwise, must be 0-359 + */ +void drawWsgPalette(const wsg_t* wsg, int16_t xOff, int16_t yOff, wsgPalette_t* palette, bool flipLR, bool flipUD, + int16_t rotateDeg) +{ + // This function has been micro optimized by cnlohr on 2022-09-08, using gcc version 8.4.0 (crosstool-NG + // esp-2021r2-patch3) + + if (NULL == wsg->px) + { + return; + } + + if (rotateDeg) + { + SETUP_FOR_TURBO(); + uint32_t wsgw = wsg->w; + uint32_t wsgh = wsg->h; + for (int32_t srcY = 0; srcY < wsgh; srcY++) + { + int32_t usey = srcY; + + // Reflect over X axis? + if (flipUD) + { + usey = wsg->h - 1 - usey; + } + + const paletteColor_t* linein = &wsg->px[usey * wsgw]; + + // Reflect over Y axis? + uint32_t readX = 0; + uint32_t advanceX = 1; + if (flipLR) + { + readX = wsgw - 1; + advanceX = -1; + } + + int32_t localX = 0; + for (int32_t srcX = 0; srcX != wsgw; srcX++) + { + // Draw if not transparent + uint8_t color = palette->newColors[linein[srcX]]; + if (cTransparent != color) + { + uint16_t tx = localX; + uint16_t ty = srcY; + + rotatePixel((int16_t*)&tx, (int16_t*)&ty, rotateDeg, wsgw, wsgh); + tx += xOff; + ty += yOff; + TURBO_SET_PIXEL_BOUNDS(tx, ty, color); + } + localX++; + readX += advanceX; + } + } + } + else + { + // Draw the image's pixels (no rotation or transformation) + uint32_t w = TFT_WIDTH; + paletteColor_t* px = getPxTftFramebuffer(); + + uint16_t wsgw = wsg->w; + uint16_t wsgh = wsg->h; + + int32_t xstart = 0; + int16_t xend = wsgw; + int32_t xinc = 1; + + // Reflect over Y axis? + if (flipLR) + { + xstart = wsgw - 1; + xend = -1; + xinc = -1; + } + + if (xOff < 0) + { + if (xinc > 0) + { + xstart -= xOff; + if (xstart >= xend) + { + return; + } + } + else + { + xstart += xOff; + if (xend >= xstart) + { + return; + } + } + xOff = 0; + } + + if (xOff + wsgw > w) + { + int32_t peelBack = (xOff + wsgw) - w; + if (xinc > 0) + { + xend -= peelBack; + if (xstart >= xend) + { + return; + } + } + else + { + xend += peelBack; + if (xend >= xstart) + { + return; + } + } + } + + for (int16_t srcY = 0; srcY < wsgh; srcY++) + { + int32_t usey = srcY; + + // Reflect over X axis? + if (flipUD) + { + usey = wsgh - 1 - usey; + } + + const paletteColor_t* linein = &wsg->px[usey * wsgw]; + + // Transform this pixel's draw location as necessary + uint32_t dstY = srcY + yOff; + + // It is too complicated to detect both directions and backoff correctly, so we just do this here. + // It does slow things down a "tiny" bit. People in the future could optimize out this check. + if (dstY >= TFT_HEIGHT) + { + continue; + } + + int32_t lineOffset = dstY * w; + int32_t dstx = xOff + lineOffset; + + for (int32_t srcX = xstart; srcX != xend; srcX += xinc) + { + // Get colors from remap + uint8_t color = palette->newColors[linein[srcX]]; + + // Draw if not transparent + if (cTransparent != color) + { + px[dstx] = color; + } + dstx++; + } + } + } +} + +/** + * @brief Draw a WSG to the display without flipping or rotation + * + * @param wsg The WSG to draw to the display + * @param xOff The x offset to draw the WSG at + * @param yOff The y offset to draw the WSG at + * @param palette Color Map to use + */ +void drawWsgPaletteSimple(const wsg_t* wsg, int16_t xOff, int16_t yOff, wsgPalette_t* palette) +{ + // This function has been micro optimized by cnlohr on 2022-09-07, using gcc version 8.4.0 (crosstool-NG + // esp-2021r2-patch3) + + if (NULL == wsg->px) + { + return; + } + + // Only draw in bounds + int dWidth = TFT_WIDTH; + int wWidth = wsg->w; + int xMin = CLAMP(xOff, 0, dWidth); + int xMax = CLAMP(xOff + wWidth, 0, dWidth); + int yMin = CLAMP(yOff, 0, TFT_HEIGHT); + int yMax = CLAMP(yOff + wsg->h, 0, TFT_HEIGHT); + paletteColor_t* px = getPxTftFramebuffer(); + int numX = xMax - xMin; + int wsgY = (yMin - yOff); + int wsgX = (xMin - xOff); + paletteColor_t* lineout = &px[(yMin * dWidth) + xMin]; + const paletteColor_t* linein = &wsg->px[wsgY * wWidth + wsgX]; + + // Draw each pixel + for (int y = yMin; y < yMax; y++) + { + for (int x = 0; x < numX; x++) + { + uint8_t color = palette->newColors[linein[x]]; + if (color != cTransparent) + { + lineout[x] = color; + } + } + lineout += dWidth; + linein += wWidth; + wsgY++; + } +} + +/** + * @brief Draw a WSG to the display without flipping or rotation + * + * @param wsg The WSG to draw to the display + * @param xOff The x offset to draw the WSG at + * @param yOff The y offset to draw the WSG at + * @param palette Color Map to use + * @param xScale The amount to scale the image horizontally + * @param yScale The amount to scale the image vertically + */ +void drawWsgPaletteSimpleScaled(const wsg_t* wsg, int16_t xOff, int16_t yOff, wsgPalette_t* palette, int16_t xScale, + int16_t yScale) +{ + // This function has been micro optimized by cnlohr on 2022-09-07, using gcc version 8.4.0 (crosstool-NG + // esp-2021r2-patch3) + + if (NULL == wsg->px) + { + return; + } + + // Only draw in bounds + int dWidth = TFT_WIDTH; + int dHeight = TFT_HEIGHT; + int wWidth = wsg->w; + int xMax = CLAMP(xOff + wWidth * xScale, 0, dWidth); + int yMax = CLAMP(yOff + wsg->h * yScale, 0, dHeight); + const paletteColor_t* linein = wsg->px; + + int x1; + int y1; + + // Draw each pixel, scaled + for (int y = yOff, iy = 0; y < yMax && iy < wsg->h; y += yScale, iy++) + { + if (y >= TFT_HEIGHT) + { + return; + } + + y1 = y + yScale; + + // Entire "pixel" is off-screen + if (y1 <= 0) + { + linein += wWidth; + continue; + } + + for (int x = xOff, ix = 0; x < xMax && ix < wsg->w; x += xScale, ix++) + { + if (x >= TFT_WIDTH) + { + // next line + break; + } + + x1 = x + xScale; + + if (x1 <= 0) + { + // next pixel + continue; + } + + uint8_t color = palette->newColors[linein[ix]]; + if (color != cTransparent) + { + fillDisplayArea(MAX(x, 0), MAX(y, 0), MIN(x1, dWidth), MIN(y1, dHeight), color); + } + } + linein += wWidth; + } +} + +/** + * @brief Draw a WSG to the display without flipping or rotation at half size + * + * @param wsg The WSG to draw to the display + * @param xOff The x offset to draw the WSG at + * @param yOff The y offset to draw the WSG at + * @param palette Color Map to use + */ +void drawWsgPaletteSimpleHalf(const wsg_t* wsg, int16_t xOff, int16_t yOff, wsgPalette_t* palette) +{ + // This function has been micro optimized by cnlohr on 2022-09-07, using gcc version 8.4.0 (crosstool-NG + // esp-2021r2-patch3) + + if (NULL == wsg->px) + { + return; + } + + // Only draw in bounds + int dWidth = TFT_WIDTH; + int wWidth = wsg->w; + int xMin = CLAMP(xOff, 0, dWidth); + int xMax = CLAMP(xOff + (wWidth / 2), 0, dWidth); + int yMin = CLAMP(yOff, 0, TFT_HEIGHT); + int yMax = CLAMP(yOff + (wsg->h / 2), 0, TFT_HEIGHT); + paletteColor_t* px = getPxTftFramebuffer(); + int numX = xMax - xMin; + int wsgY = (yMin - yOff); + int wsgX = (xMin - xOff); + paletteColor_t* lineout = &px[(yMin * dWidth) + xMin]; + const paletteColor_t* linein = &wsg->px[wsgY * wWidth + wsgX]; + + // Draw each pixel + for (int y = yMin; y < yMax; y++) + { + for (int x = 0; x < numX; x++) + { + uint8_t color = palette->newColors[linein[x * 2]]; + if (color != cTransparent) + { + lineout[x] = color; + } + } + lineout += dWidth; + linein += (2 * wWidth); + wsgY++; + } +} + +/** + * @brief Resets the palette to initial state + * + * @param palette Color map to modify + */ +void wsgPaletteReset(wsgPalette_t* palette) +{ + // Reset the palette + for (int32_t i = 0; i < 217; i++) + { + palette->newColors[i] = i; + } +} + +/** + * @brief Sets a single color to the provided palette + * + * @param palette Color map to modify + * @param replacedColor Color to be replaced + * @param newColor Color that will replace the previous + */ +void wsgPaletteSet(wsgPalette_t* palette, paletteColor_t replacedColor, paletteColor_t newColor) +{ + palette->newColors[replacedColor] = newColor; +} + +void wsgPaletteSetGroup(wsgPalette_t* palette, paletteColor_t* replacedColors, paletteColor_t* newColors, + uint8_t arrSize) +{ + for (int32_t i = 0; i < arrSize; i++) + { + wsgPaletteSet(palette, replacedColors[i], newColors[i]); + } +} \ No newline at end of file diff --git a/main/display/wsgPalette.h b/main/display/wsgPalette.h new file mode 100644 index 000000000..190ca062d --- /dev/null +++ b/main/display/wsgPalette.h @@ -0,0 +1,91 @@ +/** + * @file wsgPalette.h + * @author Jeremy Stintzcum (jeremy.stintzcum@gmail.com) + * @brief Provides palette swap functionality for Swadge + * @version 1.0.0 + * @date 2024-09-20 + * + * @copyright Copyright (c) 2024 + * + */ + +/*! \file wsgPalette.h + * + * \section wsgPalette_design Design Philosophy + * + * Provides functionality to WSGs to use a palette a la the NES. See wsg.h for how these are implemented. + * + * Clones all the current options for drawing WSGs, but all of them require a new parameter, 'palette' which is a array + of paletteColor_t objects. The functions largely just intercepts the color given by the WSG and converts it based on + the newColor map. + * + * \section wsgPalette_usage Usage + * + * There are three setup functions: + * - wsgPaletteReset(): Resets the provided palette to draw the default colors + * - wsgPaletteSet(): Provided the palette, a color to overwrite and a new color to use, sets the color + * - wsgPaletteSetGroup(): Does the same as above, but using lists to make generation easier + * + * If wsgPaletteReset() isn't called for the palette being used, all colors not specifically assigned will be black. + * + * There are four drawing functions provided with the palette + * - drawWsgPalette(): Draws the WSG with the appropriate palette + * - drawWsgPaletteSimple(): Draws the WSG with palette, but can't be rotated or flipped. + * - drawWsgPaletteSimpleScaled(): Draws the WSG with palette at a larger size set by the provided scale (integer + values, 2x, 3x, 4x...). + * - drawWsgPaletteSimpleHalf(): Draws the WSG at half scale with the included palette. + * + * \section wsgPalette_example Example + * + * \code{.c} + * // In modeData_t + * { + * wsgPalette_t pal; + * } + + * // In modeEnter + * { + * // Palette setup + * wsgPaletteReset(&pal); + * wsgPaletteSet(&pal, c000, c555); + * } + * + * // Where the WSG is drawn + * { + * drawWsgPalette(&wsg, x, y, &pal, vertFlip, HorFlip, rotation); + * } + * \endcode + */ +#pragma once + +//============================================================================== +// Includes +//============================================================================== + +#include +#include +#include "wsg.h" + +//============================================================================== +// Struct +//============================================================================== + +typedef struct +{ + paletteColor_t newColors[217]; ///< Color map +} wsgPalette_t; + +//============================================================================== +// Functions +//============================================================================== + +void drawWsgPalette(const wsg_t* wsg, int16_t xOff, int16_t yOff, wsgPalette_t* palette, bool flipLR, bool flipUD, + int16_t rotateDeg); +void drawWsgPaletteSimple(const wsg_t* wsg, int16_t xOff, int16_t yOff, wsgPalette_t* palette); +void drawWsgPaletteSimpleScaled(const wsg_t* wsg, int16_t xOff, int16_t yOff, wsgPalette_t* palette, int16_t xScale, + int16_t yScale); +void drawWsgPaletteSimpleHalf(const wsg_t* wsg, int16_t xOff, int16_t yOff, wsgPalette_t* palette); +void wsgPaletteReset(wsgPalette_t* palette); +void wsgPaletteSet(wsgPalette_t* palette, paletteColor_t replaced, paletteColor_t newColor); +void wsgPaletteSetGroup(wsgPalette_t* palette, paletteColor_t* replacedColors, paletteColor_t* newColors, + uint8_t arrSize); \ No newline at end of file diff --git a/main/menu/menuManiaRenderer.c b/main/menu/menuManiaRenderer.c index 0f1c6931a..2486a9826 100644 --- a/main/menu/menuManiaRenderer.c +++ b/main/menu/menuManiaRenderer.c @@ -113,8 +113,8 @@ menuManiaRenderer_t* initMenuManiaRenderer(font_t* titleFont, font_t* titleFontO } else { - renderer->titleFont = titleFont; - renderer->titleFontAllocated = false; + renderer->titleFontOutline = titleFont; + renderer->titleFontOutlineAllocated = false; } // Save or allocate menu font diff --git a/main/midi/midiFileParser.c b/main/midi/midiFileParser.c index 7d308e681..eebe407a2 100644 --- a/main/midi/midiFileParser.c +++ b/main/midi/midiFileParser.c @@ -516,26 +516,6 @@ static bool trackParseNext(midiFileReader_t* reader, midiTrackState_t* track) // A sysex event in a MIDI file is different from one in streaming mode (i.e. USB), because there is no // length byte transmitted in streaming mode, so we need to check the manufacturer length there - // TODO: Move this to the streaming parser - /* - // We'll need to read at least one more byte - //if (!TRK_REMAIN()) { ERR(); } - uint16_t manufacturer = *(track->cur++); - if (!manufacturer) - { - if (TRK_REMAIN() < 2) { ERR(); } - // A manufacturer ID of 0 means the ID is actually in the next 2 bytes - manufacturer = *(track->cur++); - manufacturer <<= 7; - manufacturer |= *(track->cur++); - } - else - { - // Technically 0x00 0x00 0x41 is considered a different manufacturer from the single-byte value 0x41 - // So in that case just put a 1 in the 15th bit that's otherwise unused - manufacturer |= (1 << 15); - }*/ - uint32_t sysexLength; read = readVariableLength(track->cur, TRK_REMAIN(), &sysexLength); if (!read) @@ -554,10 +534,38 @@ static bool trackParseNext(midiFileReader_t* reader, midiTrackState_t* track) track->nextEvent.sysex.data = track->cur; // If the status is 0xF0, the 0xF0 should be prefixed to the data. track->nextEvent.sysex.prefix = (status == 0xF0) ? 0xF0 : 0x00; - // TODO - track->nextEvent.sysex.manufacturerId = 0; - track->cur += sysexLength; + // Store the pointer to the end of data so we don't need to do math later + const uint8_t* sysexEnd = (track->cur + sysexLength); + + // TODO: Move this to the streaming parser + // We'll need to read at least one more byte + if (!TRK_REMAIN()) + { + ERR(); + } + uint16_t manufacturer = *(track->cur++); + if (!manufacturer) + { + if (TRK_REMAIN() < 2) + { + ERR(); + } + // A manufacturer ID of 0 means the ID is actually in the next 2 bytes + manufacturer = *(track->cur++); + manufacturer <<= 7; + manufacturer |= *(track->cur++); + } + else + { + // Technically 0x00 0x00 0x41 is considered a different manufacturer from the single-byte value 0x41 + // So in that case just put a 1 in the 15th bit that's otherwise unused + manufacturer |= (1 << 15); + } + + track->nextEvent.sysex.manufacturerId = manufacturer; + + track->cur = sysexEnd; } else { diff --git a/main/midi/midiPlayer.c b/main/midi/midiPlayer.c index 8cb9ff3ae..848c1d39b 100644 --- a/main/midi/midiPlayer.c +++ b/main/midi/midiPlayer.c @@ -14,6 +14,9 @@ #include "macros.h" #include "cnfs.h" +// Uncomment to enable logging SysEx commands in detail +// #define DEBUG_SYSEX 1 + #define OSC_DITHER //============================================================================== @@ -48,6 +51,26 @@ static const uint8_t oscDither[] = { * ((int)(voice)->transitionTicksTotal - (int)(voice)->transitionTicks) \ / (int)(voice)->transitionTicksTotal)) +/// @brief Set only the MSB of a 14-bit value +#define SET_MSB(target, val) \ + do \ + { \ + uint16_t new14bitVal = ((val) & 0x7F); \ + new14bitVal <<= 7; \ + new14bitVal |= ((target) & 0x7F); \ + (target) = new14bitVal; \ + } while (0) + +/// @brief Set only the LSB of a 14-bit value +#define SET_LSB(target, val) \ + do \ + { \ + uint16_t new14bitVal = ((target) >> 7) & 0x7F; \ + new14bitVal <<= 7; \ + new14bitVal |= ((val) & 0x7F); \ + (target) = new14bitVal; \ + } while (0) + // Values for the percussion special states bitmap #define SHIFT_HI_HAT (0) #define SHIFT_WHISTLE (6) @@ -807,6 +830,251 @@ static void handleSysexEvent(midiPlayer_t* player, const midiSysexEvent_t* sysex // Actually we can assign a non-registered control to R, G, and B // I think there's enough for every LED too assuming there's still like, 7 or so // AND: if possible have a sysex command (hmm) that sets all the LEDs to individual values at once + + // GM Enable (01) + // GM Disable (02) + // GM2 Enable (03) + // GM Disable (00) (incorrect but I bet people are doing it because teragonaudio says to) + uint8_t mfrLen = 1; + +#ifdef DEBUG_SYSEX + uint8_t mfrHex[3] = {0, 0, 0}; +#endif + + const uint8_t* end = sysex->data + sysex->length; + + // Determine the length of the manufacturer ID so we know how many bytes to skip for the real data + if (sysex->manufacturerId & (1 << 15)) + { + // This is a 1-byte manufacturer ID + mfrLen = 1; +#ifdef DEBUG_SYSEX + mfrHex[0] = sysex->manufacturerId & ~(1 << 15); +#endif + } + else + { + // This is a 3-byte manufacturer ID + mfrLen = 3; +#ifdef DEBUG_SYSEX + mfrHex[1] = (sysex->manufacturerId >> 7) & 0x7F; + mfrHex[2] = (sysex->manufacturerId & 0x7F); +#endif + } + + const uint8_t* dataPtr = sysex->data + mfrLen; + +#ifdef DEBUG_SYSEX + printf("Got SysEx event length=%" PRIu32 ":\n", sysex->length); + printf("Manufacturer: "); + + for (int i = 0; i < mfrLen; i++) + { + printf("%02" PRIx8 " ", mfrHex[i]); + } + printf("\n"); + + for (uint32_t i = 0; i < sysex->length; i++) + { + if (i % 8 == 0) + { + printf("\n%04" PRIx32 " ", i); + } + + printf("%02" PRIx8 " ", sysex->data[i]); + } + + printf("\n"); +#endif + + switch (sysex->manufacturerId) + { + case MMFR_EDUCATIONAL_USE: + break; + + case MMFR_UNIVERSAL_NON_REAL_TIME: + case MMFR_UNIVERSAL_REAL_TIME: + { + bool realTime = (sysex->manufacturerId == MMFR_UNIVERSAL_REAL_TIME); + + // Universal SysEx messages have 127 "channel" values, with 0x7F meaning "Disregard Channel" + // uint8_t sysexChannel = *dataPtr++; + + if (dataPtr >= end) + { + // Err + return; + } + + uint8_t subId = *dataPtr++; + + if (dataPtr >= end) + { + // Err + return; + } + + uint8_t subId2 = *dataPtr++; + + if (realTime) + { + // Real Time Universal SysEx + switch (subId) + { + // 0: UNUSED + case 0x0: + // MIDI Time Code + case 0x1: + // MIDI Show Control + case 0x2: + // Notation Information + case 0x3: + break; + + // Device Control + case 0x4: + { + switch (subId2) + { + // Master Volume + case 1: + { + break; + } + + // Master Balance + case 2: + break; + + // Master Fine Tuning + case 3: + { + break; + } + + // Master Coarse Tuning + case 4: + { + break; + } + + case 5: + default: + break; + } + + break; + } + + // Real Time MTC Cueing + case 0x5: + // MIDI Machine Control Commands + case 0x6: + // MIDI Machine Control Responses + case 0x7: + // MIDI Tuning Standard (Real Time) + case 0x8: + // Controller Destination Setting + case 0x9: + // Key-based Instrument Control + case 0xA: + // Scalable Polyphony MIDI MIP Message + case 0xB: + // Mobile Phone Control Message + case 0xC: + default: + break; + } + } + else + { + // Non-Real Time Universal SysEx + switch (subId) + { + // 0: UNUSED + case 0x0: + // Sample Dump + case 0x1: + // Sample Data Packet + case 0x2: + // Sample Dump Request + case 0x3: + // MIDI Time Code + case 0x4: + // Sample Dump Extensions + case 0x5: + // General Information + case 0x6: + // File Dump + case 0x7: + // MIDI Tuning Standard + case 0x8: + break; + + // General MIDI + case 0x9: + { + switch (subId2) + { + case 0: + { + // NOTE: This value is NOT defined by the MIDI spec + // However, some resources incorrectly claim that a value of `0` for sub-ID 2 is the + // value for a "GM Off" event (with `1` being "GM On"), even though a value of `2` is + // specified for `GM Off`, so it is possible that a `0` value will be sent with the + // intent to disable GM. + midiGmOff(player); + break; + } + + // General MIDI 1 On + case 1: + { + midiGmOn(player); + break; + } + + // General MIDI Off + case 2: + { + midiGmOff(player); + break; + } + + // General MIDI 2 On (Unsupported) + case 3: + default: + break; + } + break; + } + + // Downloadable Sounds + case 0xA: + // File Reference Message + case 0xB: + // MIDI Visual Control + case 0xC: + // MIDI Capability Inquiry + case 0xD: + // End of File + case 0x7B: + // Wait + case 0x7C: + // Cancel + case 0x7D: + // NAK + case 0x7E: + // ACK + case 0x7F: + default: + break; + } + } + + break; + } + } } /** @@ -894,7 +1162,7 @@ void midiPlayerInit(midiPlayer_t* player) void midiPlayerReset(midiPlayer_t* player) { midiAllSoundOff(player); - midiGmOn(player); + midiGmOff(player); // We need the tempo to not be zero, so set it to the default of 120BPM until we get a tempo event // 120 BPM == 500,000 microseconds per quarter note @@ -1121,11 +1389,14 @@ void midiResetChannelControllers(midiPlayer_t* player, uint8_t channel) { midiChannel_t* chan = &player->channels[channel]; midiSustain(player, channel, MIDI_FALSE); - chan->volume = UINT14_MAX; - chan->pitchBend = PITCH_BEND_CENTER; - chan->program = 0; - chan->held = 0; - chan->sustenuto = 0; + chan->volume = UINT14_MAX; + chan->pitchBend = PITCH_BEND_CENTER; + chan->registeredParameter = true; + // Set selected RPN to "RPN Reset" which means none currently selected + chan->selectedParameter = UINT14_MAX; + chan->program = 0; + chan->held = 0; + chan->sustenuto = 0; initTimbre(&chan->timbre, getTimbreForProgram(chan->percussion, chan->bank, chan->program)); } @@ -1153,8 +1424,44 @@ void midiGmOff(midiPlayer_t* player) midiResetChannelControllers(player, chanIdx); // Channel 10 (index 9) is reserved for percussion. - chan->percussion = (9 == chanIdx); - chan->bank = 1; + // Also enable percussion on channel 11 (index 10) with an alternate drum kit + chan->percussion = (9 == chanIdx || 10 == chanIdx); + // Set bank 1 (MAGFest sounds) for everything except the first drumkit on 10 + chan->bank = (9 == chanIdx) ? 0 : 1; + + switch (chanIdx) + { + case 0: + case 1: + case 2: + case 3: + case 4: + case 5: + case 6: + case 7: + case 8: + { + chan->program = chanIdx; + break; + } + + case 9: + case 10: + { + chan->program = 0; + break; + } + + case 11: + case 12: + case 13: + case 14: + case 15: + { + chan->program = 0; + break; + } + } initTimbre(&chan->timbre, getTimbreForProgram(chan->percussion, chan->bank, chan->program)); } @@ -1597,19 +1904,35 @@ void midiControlChange(midiPlayer_t* player, uint8_t channel, midiControl_t cont { case MCC_BANK_MSB: { - uint16_t newBank = (val & 0x7F); - newBank <<= 7; - newBank |= (player->channels[channel].bank & 0x7F); - player->channels[channel].bank = newBank; + SET_MSB(player->channels[channel].bank, val); break; } case MCC_BANK_LSB: { - uint16_t newBank = (player->channels[channel].bank >> 7) & 0x7F; - newBank <<= 7; - newBank |= (val & 0x7F); - player->channels[channel].bank = newBank; + SET_LSB(player->channels[channel].bank, val); + break; + } + + // Data Entry MSB (6) + case MCC_DATA_ENTRY_MSB: + { + uint16_t curVal = midiGetParameterValue(player, channel, player->channels[channel].registeredParameter, + player->channels[channel].selectedParameter); + SET_MSB(curVal, val); + midiSetParameter(player, channel, player->channels[channel].registeredParameter, + player->channels[channel].selectedParameter, curVal); + break; + } + + // Data Entry LSB (38) + case MCC_DATA_ENTRY_LSB: + { + uint16_t curVal = midiGetParameterValue(player, channel, player->channels[channel].registeredParameter, + player->channels[channel].selectedParameter); + SET_LSB(curVal, val); + midiSetParameter(player, channel, player->channels[channel].registeredParameter, + player->channels[channel].selectedParameter, curVal); break; } @@ -1672,6 +1995,56 @@ void midiControlChange(midiPlayer_t* player, uint8_t channel, midiControl_t cont break; } + // Data Button Increment (96) + case MCC_DATA_BUTTON_INC: + // Data Button Decrement (97) + case MCC_DATA_BUTTON_DEC: + { + bool inc = (control == MCC_DATA_BUTTON_INC); + uint16_t param = player->channels[channel].selectedParameter; + uint16_t curVal + = midiGetParameterValue(player, channel, player->channels[channel].registeredParameter, param); + + // Prevent rollover + if ((inc && curVal < UINT14_MAX) || (!inc && curVal > 0)) + { + midiSetParameter(player, channel, player->channels[channel].registeredParameter, param, curVal); + } + break; + } + + // Non-registered Parameter Number LSB (98) + case MCC_NON_REGISTERED_PARAM_LSB: + { + player->channels[channel].registeredParameter = false; + SET_LSB(player->channels[channel].selectedParameter, val); + break; + } + + // Non-registered Parameter Number MSB (99) + case MCC_NON_REGISTERED_PARAM_MSB: + { + player->channels[channel].registeredParameter = false; + SET_MSB(player->channels[channel].selectedParameter, val); + break; + } + + // Registered Parameter Number LSB (100) + case MCC_REGISTERED_PARAM_LSB: + { + player->channels[channel].registeredParameter = true; + SET_LSB(player->channels[channel].selectedParameter, val); + break; + } + + // Registered Parameter Number MSB (101) + case MCC_REGISTERED_PARAM_MSB: + { + player->channels[channel].registeredParameter = true; + SET_MSB(player->channels[channel].selectedParameter, val); + break; + } + // All sounds off (120) case MCC_ALL_SOUND_OFF: { @@ -1763,6 +2136,94 @@ uint16_t midiGetControlValue14bit(midiPlayer_t* player, uint8_t channel, midiCon return result; } +void midiSetParameter(midiPlayer_t* player, uint8_t channel, bool registeredParam, uint16_t param, uint16_t value) +{ + if (registeredParam) + { + switch (param) + { + // Pitch Bend Range + case 0x0000: + { + // Not supported (yet?) + break; + } + + // Master Fine Tuning + case 0x0001: + { + // Not supported + break; + } + + // Master Coarse Tuning + case 0x0002: + { + // Not supported + break; + } + + // "Not Set" + case 0x3FFF: + default: + { + // No action necessary + break; + } + } + } + else + { + switch (param) + { + case 10: + { + // Set Percussion + if ((value != 0) != player->channels[channel].percussion) + { + player->channels[channel].percussion = (value != 0); + // Necessary to actually configure the channel for its new percussion state + midiSetProgram(player, channel, player->channels[channel].program); + } + break; + } + + default: + { + // Ignore all others + break; + } + } + } +} + +uint16_t midiGetParameterValue(midiPlayer_t* player, uint8_t channel, bool registered, uint16_t param) +{ + if (registered) + { + switch (param) + { + default: + return 0; + } + } + else + { + switch (param) + { + case 10: + { + return player->channels[channel].percussion ? 1 : 0; + } + + default: + { + return 0; + } + } + } +} + void midiPitchWheel(midiPlayer_t* player, uint8_t channel, uint16_t value) { // Save the pitch bend value diff --git a/main/midi/midiPlayer.h b/main/midi/midiPlayer.h index 8751d9072..71c8aad36 100644 --- a/main/midi/midiPlayer.h +++ b/main/midi/midiPlayer.h @@ -334,6 +334,16 @@ typedef enum MCC_POLY_OPERATION = 127, } midiControl_t; +/** + * @brief Values that can be directly compared against midiSysexEvent_t::manufacturerId + */ +typedef enum +{ + MMFR_EDUCATIONAL_USE = 0x807D, + MMFR_UNIVERSAL_NON_REAL_TIME = 0x807E, + MMFR_UNIVERSAL_REAL_TIME = 0x807F, +} midiManufacturerId_t; + //============================================================================== // Structs //============================================================================== @@ -584,6 +594,12 @@ typedef struct /// @brief The ID of the program (timbre) set for this channel uint8_t program; + /// @brief Whether selectedParameter represents a registered or non-registered parameter + bool registeredParameter; + + /// @brief The ID of the currently selected registered or non-registered parameter + uint16_t selectedParameter; + /// @brief The actual current timbre definition which the program ID corresponds to midiTimbre_t timbre; @@ -870,6 +886,29 @@ uint8_t midiGetControlValue(midiPlayer_t* player, uint8_t channel, midiControl_t */ uint16_t midiGetControlValue14bit(midiPlayer_t* player, uint8_t channel, midiControl_t control); +/** + * @brief Set a registered or non-registered parameter value + * + * @param player The MIDI player + * @param channel The channel to set the parameter on + * @param registered true if param refers to a registered parameter number and false if it refers to a non-registered + * @param param The registered or non-registered MIDI parameter to set the value of + * @param value The 14-bit value to set the parameter to + */ +void midiSetParameter(midiPlayer_t* player, uint8_t channel, bool registered, uint16_t param, uint16_t value); + +/** + * @brief Get the value of a registered or non-registered parameter + * + * @param player The MIDI player + * @param channel The channel to retrieve the parameter from + * @param registered true if param refers to a registered parameter number and false if it refers to a non-registered + * @param param The registered or non-registered MIDI parameter number to retrieve the value for + * @return The current 14-bit value of the given registered or non-registered parameter, or 0 if the parameter is + * unsupported + */ +uint16_t midiGetParameterValue(midiPlayer_t* player, uint8_t channel, bool registered, uint16_t param); + /** * @brief Set the pitch wheel value on a given MIDI channel * diff --git a/main/modes/games/2048/2048_game.c b/main/modes/games/2048/2048_game.c new file mode 100644 index 000000000..a94e54025 --- /dev/null +++ b/main/modes/games/2048/2048_game.c @@ -0,0 +1,1017 @@ +/** + * @file 2048_game.c + * @author Jeremy Stintzcum (jeremy.stintzcum@gmail.com) + * @author Adam Feinstein (gelakinetic@gmail.com) + * @brief Core of 2048 mode + * @version 1.0.0 + * @date 2024-09-17 + * + * @copyright Copyright (c) 2024 + * + */ + +//============================================================================== +// Includes +//============================================================================== + +#include "2048_game.h" + +//============================================================================== +// Defines +//============================================================================== + +// Pixel counts +#define T48_CELL_SIZE 50 +#define T48_LINE_WEIGHT 4 +#define T48_SIDE_MARGIN 30 +#define T48_TOP_MARGIN 20 + +//============================================================================== +// Function Prototypes +//============================================================================== + +// Game +static bool t48_setRandomCell(t48_t* t48, int32_t value); +static bool t48_slideTiles(t48_t* t48, buttonBit_t direction); +static bool t48_checkWin(t48_t* t48); +static bool t48_checkOver(t48_t* t48); + +// Drawing +static bool t48_drawCellTiles(t48_t* t48, int32_t x, int32_t y, uint32_t elapsedUs); +static void t48_initSparkles(t48_t* t48, int32_t x, int32_t y, wsg_t* spr); +static bool t48_drawSparkles(t48cell_t* cell, uint32_t elapsedUs); +static void t48_drawNewTile(t48_t* t48, uint32_t elapsedUs); + +// Helpers +static int32_t t48_horz_offset(int32_t col); +static int32_t t48_vert_offset(int32_t row); +static void FisherYatesShuffle(int32_t* array, int32_t size); + +// LEDs +static void t48_lightLEDs(t48_t* t48, bool tileMoved, buttonBit_t direction); +static led_t t48_getLEDColor(t48_t* t48); + +//============================================================================== +// Const Variables +//============================================================================== + +static const char paused[] = "Paused!"; +static const char pausedA[] = "Press A to continue playing"; +static const char pausedB[] = "Press B to abandon game"; + +static const int32_t tileIndices[] = { + 0, 8, 2, 12, 5, 9, 14, 10, 7, 15, 1, 3, 6, 13, 4, 11, 0, 8, 0, 0, 0, 0, 0, 0, 0, +}; + +static const int32_t sparkleIndices[] = { + 3, 3, 4, 1, 6, 0, 7, 0, 3, 4, 4, 7, 5, 6, 6, 2, 2, 1, 3, 3, 3, 3, 3, 3, 3, +}; + +//============================================================================== +// Functions +//============================================================================== + +/** + * @brief Initialize the game state + * + * @param t48 The game data to initialize + */ +void t48_gameInit(t48_t* t48) +{ + // Clear the board + memset(t48->board, 0, sizeof(t48->board)); + + // Reset the score + t48->score = 0; + + // Set two cells randomly + for (int32_t i = 0; i < 2; i++) + { + t48_setRandomCell(t48, 2); + } + + // Accept input + t48->acceptGameInput = true; +} + +/** + * @brief Run the main game loop for 2048. This animates and draws the board and handles game logic + * + * @param t48 The game data to loop + * @param elapsedUs The time since this was last called, for animation + */ +void t48_gameLoop(t48_t* t48, int32_t elapsedUs) +{ + if (t48->paused) + { + // Draw pause screen + fillDisplayArea(64, 75, TFT_WIDTH - 64, 100, c100); + drawText(&t48->titleFont, c555, paused, (TFT_WIDTH - textWidth(&t48->titleFont, paused)) / 2, 80); + fillDisplayArea(32, 110, TFT_WIDTH - 32, 130, c100); + drawText(&t48->font, c555, pausedA, (TFT_WIDTH - textWidth(&t48->font, pausedA)) / 2, 115); + fillDisplayArea(32, 135, TFT_WIDTH - 32, 155, c100); + drawText(&t48->font, c555, pausedB, (TFT_WIDTH - textWidth(&t48->font, pausedB)) / 2, 140); + return; // Bail instead of drawing the rest of the game + } + + // Blank the display + fillDisplayArea(0, 0, TFT_WIDTH, TFT_HEIGHT, c000); + + // Draw grid lines first + for (uint8_t i = 0; i < T48_GRID_SIZE + 1; i++) + { + int16_t left = i * (T48_CELL_SIZE + T48_LINE_WEIGHT); + fillDisplayArea(T48_SIDE_MARGIN + left, // + T48_TOP_MARGIN, // + T48_SIDE_MARGIN + left + T48_LINE_WEIGHT, // + TFT_HEIGHT, // + c111); + + int16_t top = i * (T48_CELL_SIZE + T48_LINE_WEIGHT); + fillDisplayArea(T48_SIDE_MARGIN, // + top + T48_TOP_MARGIN, // + TFT_WIDTH - T48_SIDE_MARGIN, // + top + T48_TOP_MARGIN + T48_LINE_WEIGHT, // + c111); + } + + // Draw Score second + static char textBuffer[32]; + snprintf(textBuffer, sizeof(textBuffer) - 1, "Score: %" PRIu32, t48->score); + drawText(&t48->font, c555, textBuffer, T48_SIDE_MARGIN, 4); + + // Check if anything is animating + bool animationInProgress = false; + + // Draw new tile third + if (t48->nTile.active) + { + t48_drawNewTile(t48, elapsedUs); + } + + // Draw Tiles fourth + for (int32_t x = 0; x < T48_GRID_SIZE; x++) + { + for (int32_t y = 0; y < T48_GRID_SIZE; y++) + { + // Get a reference to the cell + t48cell_t* cell = &t48->board[x][y]; + + // If tile just spawned, don't draw until done animating + if (t48->nTile.active && (t48->nTile.pos.x == x && t48->nTile.pos.y == y)) + { + continue; + } + + // Draw the tile(s) for this cell + if (t48_drawCellTiles(t48, x, y, elapsedUs)) + { + animationInProgress = true; + } + else if (cell->drawnTiles[1].value) + { + // Movement is done + if (cell->drawnTiles[0].value) + { + // If this is a real loop, not after a warp + if (0 != elapsedUs) + { + // There are two values that need to get merged. Init some sparkles + t48_initSparkles(t48, x, y, + &t48->sparkleSprites[sparkleIndices[31 - __builtin_clz(cell->value)]]); + } + // Tally score + t48->score += cell->value; + } + + // Set the drawn tiles to only draw one, with no offset + memset(cell->drawnTiles, 0, sizeof(cell->drawnTiles)); + cell->drawnTiles[0].value = cell->value; + } + } + } + + // Draw sparkles fifth + for (int32_t x = 0; x < T48_GRID_SIZE; x++) + { + for (int32_t y = 0; y < T48_GRID_SIZE; y++) + { + // Draw sparkles for this cell + if (t48_drawSparkles(&t48->board[x][y], elapsedUs)) + { + animationInProgress = true; + } + } + } + + // When the animation is done + if (!t48->acceptGameInput && !animationInProgress) + { + // Check if game has been won + if (!t48->alreadyWon && t48_checkWin(t48)) + { + t48->state = T48_WIN_SCREEN; + t48->alreadyWon = true; + } + + // Check for loss condition (no valid moves) + if (t48_checkOver(t48)) + { + if (t48->score > t48->highScore[4]) + { + t48->state = T48_HS_SCREEN; + } + else + { + t48->state = T48_END_SCREEN; + } + } + + // Accept input again + t48->acceptGameInput = true; + } +} + +/** + * @brief Process game input + * + * @param t48 The game state + * @param button The button which was pressed + */ +void t48_gameInput(t48_t* t48, buttonBit_t button) +{ + switch (button) + { + case PB_UP: + case PB_DOWN: + case PB_LEFT: + case PB_RIGHT: + { + // If the game is paused + if (t48->paused) + { + break; + } + + // If input isn't being accepted + if (!t48->acceptGameInput) + { + // Warp everything to final destination + for (int32_t x = 0; x < T48_GRID_SIZE; x++) + { + for (int32_t y = 0; y < T48_GRID_SIZE; y++) + { + // Warp all drawn tiles + for (int32_t t = 0; t < T48_TILES_PER_CELL; t++) + { + t48->board[x][y].drawnTiles[t].xOffset = 0; + t48->board[x][y].drawnTiles[t].yOffset = 0; + } + + // Kill all sparkles + for (int32_t t = 0; t < T48_SPARKLES_PER_CELL; t++) + { + t48->board[x][y].sparkles[t].active = false; + } + } + } + // Run the game loop once to update state based on the warped destination + t48_gameLoop(t48, 0); + } + + // Start sliding tiles + if (t48_slideTiles(t48, button)) + { + // Spawn a random tile, 10% chance of 4, 90% chance of 2 + // See https://play2048.co/index.js, addRandomTile() + t48_setRandomCell(t48, (esp_random() % 10 == 0) ? 4 : 2); + // Don't accept input until the slide is done + t48->acceptGameInput = false; + // Play a click + soundPlaySfx(&t48->click, MIDI_SFX); + } + break; + } + case PB_A: + { + t48->paused = false; + break; + } + case PB_B: + { + if (t48->paused) + { + t48->state = T48_START_SCREEN; + t48->paused = false; + } + break; + } + case PB_START: + { + t48->paused = !t48->paused; + } + case PB_SELECT: + default: + { + break; + } + } +} + +/** + * @brief Set a random cell to the given value + * + * @param t48 The game data to set a cell in + * @param value The value to set the cell to + * @return true if the value was set, false if there are no empty cells + */ +static bool t48_setRandomCell(t48_t* t48, int32_t value) +{ + int32_t emptyCells[T48_GRID_SIZE * T48_GRID_SIZE]; + int32_t numEmptyCells = 0; + + // Get a list of empty cells + for (int32_t id = 0; id < T48_GRID_SIZE * T48_GRID_SIZE; id++) + { + t48cell_t* cell = &t48->board[id / T48_GRID_SIZE][id % T48_GRID_SIZE]; + if (0 == cell->value) + { + emptyCells[numEmptyCells++] = id; + } + } + + // Cant set a random cell if there are no empty spots + if (0 == numEmptyCells) + { + return false; + } + + // Fill in one of the random cells + int32_t id = emptyCells[esp_random() % numEmptyCells]; + t48cell_t* cell = &t48->board[id / T48_GRID_SIZE][id % T48_GRID_SIZE]; + cell->value = value; + cell->drawnTiles[0].value = value; + cell->drawnTiles[1].value = 0; + + // Set newTile for animation + vec_t newPos = {.x = id / T48_GRID_SIZE, .y = id % T48_GRID_SIZE}; + t48->nTile.pos = newPos; + t48->nTile.active = true; + t48->nTile.spawnTime = 0; + t48->nTile.sequence = 0; + return true; +} + +/** + * @brief Slide the tiles in the game, setting up animations and such + * + * @param t48 The game state to slide + * @param direction The direction to slide + * @return true if something moved, false if nothing moved + */ +static bool t48_slideTiles(t48_t* t48, buttonBit_t direction) +{ + // Check if any tile moved + bool tileMoved = false; + + // For each row or column + for (int32_t outer = 0; outer < T48_GRID_SIZE; outer++) + { + // Make a slice depending on the slide direction. This is either a row or a column, forwards or backwards + t48cell_t* slice[T48_GRID_SIZE]; + for (int32_t inner = 0; inner < T48_GRID_SIZE; inner++) + { + switch (direction) + { + case PB_LEFT: + { + slice[inner] = &t48->board[inner][outer]; + break; + } + case PB_RIGHT: + { + slice[inner] = &t48->board[T48_GRID_SIZE - inner - 1][outer]; + break; + } + case PB_UP: + { + slice[inner] = &t48->board[outer][inner]; + break; + } + case PB_DOWN: + { + slice[inner] = &t48->board[outer][T48_GRID_SIZE - inner - 1]; + break; + } + default: + { + return false; + } + } + } + + // Clear out merged flags in this slice + for (int32_t idx = 0; idx < T48_GRID_SIZE; idx++) + { + slice[idx]->merged = false; + } + + // Now slide the slice + // Check sources to slide from front to back + for (int32_t src = 1; src < T48_GRID_SIZE; src++) + { + // No tile to move, continue + if (0 == slice[src]->value) + { + continue; + } + + // Keep track of this tile's potential destination + int32_t validDest = src; + + // Check destinations to slide to from back to front + for (int32_t dest = src - 1; dest >= 0; dest--) + { + if (0 == slice[dest]->value) + { + // Free to slide here, then keep checking + validDest = dest; + } + else if (!slice[dest]->merged && (slice[src]->value == slice[dest]->value)) + { + // Slide and merge here + slice[dest]->merged = true; + validDest = dest; + // Merge here, so break + break; + } + else + { + // Can't move further, break + break; + } + } + + // If the destination doesn't match the source + if (validDest != src) + { + // At least one tile moved + tileMoved = true; + + // Set up pixel offset for this tile to animate the slide + int32_t xOffset = 0; + int32_t yOffset = 0; + switch (direction) + { + case PB_LEFT: + { + xOffset = t48_horz_offset(src) - t48_horz_offset(validDest); + break; + } + case PB_RIGHT: + { + xOffset = t48_horz_offset(validDest) - t48_horz_offset(src); + break; + } + case PB_UP: + { + yOffset = t48_vert_offset(src) - t48_vert_offset(validDest); + break; + } + case PB_DOWN: + { + yOffset = t48_vert_offset(validDest) - t48_vert_offset(src); + break; + } + default: + { + return false; + } + } + + // Draw the pre-merge values before the slide animation finishes + // Up to two tiles can be sliding into a single cell! + // Find an empty slot in this cell to store the tile animation state + for (int t = 0; t < T48_TILES_PER_CELL; t++) + { + t48drawnTile_t* tile = &slice[validDest]->drawnTiles[t]; + if (0 == tile->value) + { + // Store tile animation state in the destination + tile->value = slice[src]->value; + tile->xOffset = xOffset; + tile->yOffset = yOffset; + break; + } + } + // Clear tile animation state in the source + memset(slice[src]->drawnTiles, 0, sizeof(slice[src]->drawnTiles)); + + // Move the underlying value (actual game state) + slice[validDest]->value += slice[src]->value; + slice[src]->value = 0; + } + } + } + + // Light LEDs + t48_lightLEDs(t48, tileMoved, direction); + + return tileMoved; +} + +/** + * @brief Checks if any of the cells have reached 2048 + * + * @param t48 Game data + * @return true if a cell is 2048 + * @return false otherwise + */ +static bool t48_checkWin(t48_t* t48) +{ + bool won = false; + for (int32_t id = 0; id < T48_GRID_SIZE * T48_GRID_SIZE; id++) + { + if (t48->board[id / T48_GRID_SIZE][id % T48_GRID_SIZE].value == 2048) + { + won = true; + } + } + return won; +} + +/** + * @brief Checks if any valid moves are left + * + * @param t48 game data + * @return true if the game no longer has viable moves + * @return false otherwise + */ +static bool t48_checkOver(t48_t* t48) +{ + // Check if any cells are open + for (int32_t id = 0; id < T48_GRID_SIZE * T48_GRID_SIZE; id++) + { + if (t48->board[id / T48_GRID_SIZE][id % T48_GRID_SIZE].value == 0) + { + return false; + } + } + // Check if any two consecutive block match vertically + for (uint8_t row = 0; row < T48_GRID_SIZE; row++) + { + for (uint8_t col = 0; col < T48_GRID_SIZE - 1; col++) + { // -1 to account for comparison + if (t48->board[row][col].value == t48->board[row][col + 1].value) + { + return false; + } + } + } + // Check if any two consecutive block match horizontally + for (uint8_t row = 0; row < T48_GRID_SIZE - 1; row++) + { // -1 to account for comparison + for (uint8_t col = 0; col < T48_GRID_SIZE - 1; col++) + { + if (t48->board[row][col].value == t48->board[row + 1][col].value) + { + return false; + } + } + } + // Game is over + return true; +} + +/** + * @brief Draw all of a cell's tiles + * + * @param t48 The game state to draw tiles from + * @param x The X index of the cell on the board + * @param y The Y index of the cell on the board + * @param elapsedUs The time since this was last called, for animation + * @return true if animation is in progress, false if it isn't + */ +static bool t48_drawCellTiles(t48_t* t48, int32_t x, int32_t y, uint32_t elapsedUs) +{ + // Get a reference to the cell + t48cell_t* cell = &t48->board[x][y]; + + // Check if animation is in progress + bool animationInProgress = false; + + // For each tile to draw on this cell (may be multiple in motion) + for (int t = 0; t < T48_TILES_PER_CELL; t++) + { + // Get a reference to the tile + t48drawnTile_t* tile = &cell->drawnTiles[t]; + + // If there is something to draw + if (tile->value) + { + // TODO make this animation microsecond based + + int32_t pxPerFrame = 8; + + // Move tile towards target X + if (tile->xOffset <= -pxPerFrame) + { + tile->xOffset += pxPerFrame; + animationInProgress = true; + } + else if (tile->xOffset >= pxPerFrame) + { + tile->xOffset -= pxPerFrame; + animationInProgress = true; + } + else if (tile->xOffset) + { + tile->xOffset = 0; + animationInProgress = true; + } + + // Move tile towards target Y + if (tile->yOffset <= -pxPerFrame) + { + tile->yOffset += pxPerFrame; + animationInProgress = true; + } + else if (tile->yOffset >= pxPerFrame) + { + tile->yOffset -= pxPerFrame; + animationInProgress = true; + } + else if (tile->yOffset) + { + tile->yOffset = 0; + animationInProgress = true; + } + + // Draw the sprite first + uint16_t x_offset = t48_horz_offset(x) + tile->xOffset; + uint16_t y_offset = t48_vert_offset(y) + tile->yOffset; + wsg_t* tileWsg = &t48->tiles[tileIndices[31 - __builtin_clz(tile->value)]]; + drawWsgSimple(tileWsg, x_offset, y_offset); + + // Draw the text on top + static char buffer[16]; + snprintf(buffer, sizeof(buffer) - 1, "%" PRIu32, tile->value); + uint16_t tWidth = textWidth(&t48->font, buffer); + drawText(&t48->font, c555, buffer, // + x_offset + (T48_CELL_SIZE - tWidth) / 2, // + y_offset + (T48_CELL_SIZE - t48->font.height) / 2); + } + } + return animationInProgress; +} + +/** + * @brief Draws a flash of light to indicate the new tile's spawn location + * + * @param t48 Game Data + * @param elapsedUs Time since last frame + */ +static void t48_drawNewTile(t48_t* t48, uint32_t elapsedUs) +{ + // Add US + t48->nTile.spawnTime += elapsedUs; + + if (t48->nTile.spawnTime >= T48_NEW_SPARKLE_SEQ) + { + t48->nTile.sequence++; + t48->nTile.spawnTime -= T48_NEW_SPARKLE_SEQ; + } + + switch (t48->nTile.sequence) + { + case 0: + case 10: + { + drawWsgSimple(&t48->newSparkles[0], // + t48_horz_offset(t48->nTile.pos.x), // + t48_vert_offset(t48->nTile.pos.y)); + break; + } + case 1: + case 9: + { + drawWsgSimple(&t48->newSparkles[1], // + t48_horz_offset(t48->nTile.pos.x), // + t48_vert_offset(t48->nTile.pos.y)); + break; + } + case 2: + case 3: + case 4: + case 5: + case 6: + case 7: + case 8: + { + drawWsgSimple(&t48->newSparkles[2], // + t48_horz_offset(t48->nTile.pos.x), // + t48_vert_offset(t48->nTile.pos.y)); + break; + } + default: + { + // If no longer inside animation sequence, deactivate + t48->nTile.active = false; + break; + } + } +} + +/** + * @brief Initialize sparkles for a cell + * + * @param t48 The game data to initialize sparkles in + * @param x The X index of the cell on the board + * @param y The Y index of the cell on the board + * @param spr The sprite to use as a sparkle + */ +static void t48_initSparkles(t48_t* t48, int32_t x, int32_t y, wsg_t* spr) +{ + // If there are any active sparkles already, return + for (int32_t sIdx = 0; sIdx < T48_SPARKLES_PER_CELL; sIdx++) + { + t48Sparkle_t* sparkle = &t48->board[x][y].sparkles[sIdx]; + if (sparkle->active) + { + return; + } + } + + // Create an array of possible directions. This ensures no random duplicates. + int32_t directions[T48_SPARKLES_PER_CELL * 2]; + for (int i = 0; i < ARRAY_SIZE(directions); i++) + { + directions[i] = i - (ARRAY_SIZE(directions) / 2); + } + // Shuffle the directions + FisherYatesShuffle(directions, ARRAY_SIZE(directions)); + + // For each sparkle + for (int32_t sIdx = 0; sIdx < T48_SPARKLES_PER_CELL; sIdx++) + { + // Get a reference + t48Sparkle_t* sparkle = &t48->board[x][y].sparkles[sIdx]; + + // Set speed + sparkle->xSpd = directions[sIdx]; + sparkle->ySpd = -8 - (esp_random() % 8); + + // Convert cell coords to pixel space + sparkle->x = t48_horz_offset(x) + T48_CELL_SIZE / 2; + sparkle->y = t48_vert_offset(y) + T48_CELL_SIZE / 2; + + // Set image + sparkle->img = spr; + + // set active + sparkle->active = true; + } +} + +/** + * @brief Draw a cell's sparkles + * + * @param cell The cell to draw sparkles for + * @param elapsedUs The time since this was last called, for animation + * @return true if animation is in progress, false if it isn't + */ +static bool t48_drawSparkles(t48cell_t* cell, uint32_t elapsedUs) +{ + bool animating = false; + + // Draw all this cell's sparkles + for (int32_t sIdx = 0; sIdx < T48_SPARKLES_PER_CELL; sIdx++) + { + // Get a reference to the sparkle + t48Sparkle_t* sparkle = &cell->sparkles[sIdx]; + + // If the sparkle is active + if (sparkle->active) + { + // Deactivate it if it's offscreen + if ((sparkle->y >= TFT_HEIGHT) || (sparkle->x + sparkle->img->w < 0) || (sparkle->x >= TFT_WIDTH)) + { + sparkle->active = false; + } + else + { + // Otherwise move and draw it + // TODO make this animation microsecond based + sparkle->ySpd += 1; + sparkle->y += sparkle->ySpd; + sparkle->x += sparkle->xSpd; + drawWsgSimple(sparkle->img, sparkle->x, sparkle->y); + animating = true; + } + } + } + return animating; +} + +/** + * @brief Get the horizontal pixel offset for a column on the board + * + * @param col The column index + * @return The pixel offset for this column + */ +static int32_t t48_horz_offset(int32_t col) +{ + return col * (T48_CELL_SIZE + T48_LINE_WEIGHT) + T48_SIDE_MARGIN + T48_LINE_WEIGHT; +} + +/** + * @brief Get the horizontal pixel offset for a row on the board + * + * @param row The row index + * @return The pixel offset for this row + */ +static int32_t t48_vert_offset(int32_t row) +{ + return row * (T48_CELL_SIZE + T48_LINE_WEIGHT) + T48_TOP_MARGIN + T48_LINE_WEIGHT; +} + +/** + * @brief Shuffle the items in an array + * + * See https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle + * + * @param array The array to shuffle + * @param size The number of elements in the array to shuffle + */ +static void FisherYatesShuffle(int32_t* array, int32_t size) +{ + // Iterate through the array in reverse order + for (int32_t n = size - 1; n > 0; n--) + { + // Generate a random index 'k' between 0 and n (inclusive) + int32_t k = esp_random() % (n + 1); + + // Swap the elements at indices 'n' and 'k' + int32_t temp = array[n]; + array[n] = array[k]; + array[k] = temp; + } +} + +/** + * @brief Illuminates the LEDs in the directions that the player pressed + * + * @param t48 Game data + * @param tileMoved If the tile moved or bounced + * @param direction Direction tiles moved in + */ +static void t48_lightLEDs(t48_t* t48, bool tileMoved, buttonBit_t direction) +{ + if (tileMoved) + { + // Illuminate LEDS based on direction and value + led_t led = t48_getLEDColor(t48); + switch (direction) + { + case PB_LEFT: + { + // LED 3 and 4 + t48->leds[3] = led; + t48->leds[4] = led; + break; + } + case PB_RIGHT: + { + // LED 0 and 1 + t48->leds[0] = led; + t48->leds[1] = led; + break; + } + case PB_UP: + { + // LEDs 1, 2, 3 + t48->leds[1] = led; + t48->leds[2] = led; + t48->leds[3] = led; + break; + } + case PB_DOWN: + { + // LEDs 4 and 0 + t48->leds[0] = led; + t48->leds[4] = led; + break; + } + default: + { + break; + } + } + } + else + { + for (int32_t i = 0; i < CONFIG_NUM_LEDS; i++) + { + led_t led = {.r = 128, .b = 128, .g = 128}; + t48->leds[i] = led; + } + } +} + +/** + * @brief Gets the correct LED color + * + * @param t48 Game data + * @return led_t The led object containing the colors to load into the LEDs + */ +static led_t t48_getLEDColor(t48_t* t48) +{ + // Get the largest value available + int32_t value = 0; + for (int32_t i = 0; i < T48_GRID_SIZE * T48_GRID_SIZE; i++) + { + if (t48->board[i / T48_GRID_SIZE][i % T48_GRID_SIZE].value > value) + { + value = t48->board[i / T48_GRID_SIZE][i % T48_GRID_SIZE].value; + } + } + led_t col = {0}; + switch (value) + { + case 4: + { + // Cyan + col.g = 255; + col.b = 255; + return col; + } + case 8: + case 8192: + { + // Red + col.r = 255; + return col; + } + case 16: + case 2048: + case 16384: + { + // Green + col.g = 128; + return col; + } + case 2: + case 32: + case 128: + case 131072: + { + // Pink + col.r = 200; + col.g = 150; + col.b = 150; + return col; + } + case 64: + case 512: + { + // Yellow + col.r = 128; + col.g = 128; + return col; + } + case 256: + { + // Orange + col.r = 255; + col.g = 165; + return col; + } + case 1024: + case 65536: + { + // Blue + col.b = 255; + return col; + } + case 4096: + { + // Dark Pink + col.r = 255; + col.g = 64; + col.b = 64; + return col; + } + case 32768: + { + // Purple + col.r = 200; + col.b = 200; + return col; + } + default: + col.r = 255; + col.g = 128; + col.b = 128; + return col; + } +} \ No newline at end of file diff --git a/main/modes/games/2048/2048_game.h b/main/modes/games/2048/2048_game.h new file mode 100644 index 000000000..654a50719 --- /dev/null +++ b/main/modes/games/2048/2048_game.h @@ -0,0 +1,18 @@ +/** + * @file 2048_game.h + * @author Jeremy Stintzcum (jeremy.stintzcum@gmail.com) + * @brief Core of 2048 mode + * @version 1.0.0 + * @date 2024-09-17 + * + * @copyright Copyright (c) 2024 + * + */ + +#pragma once + +#include "mode_2048.h" + +void t48_gameInit(t48_t* t48); +void t48_gameLoop(t48_t* t48, int32_t elapsedUs); +void t48_gameInput(t48_t* t48, buttonBit_t button); diff --git a/main/modes/games/2048/2048_menus.c b/main/modes/games/2048/2048_menus.c new file mode 100644 index 000000000..163db13de --- /dev/null +++ b/main/modes/games/2048/2048_menus.c @@ -0,0 +1,159 @@ +/** + * @file 2048_menus.c + * @author Jeremy Stintzcum (jeremy.stintzcum@gmail.com) + * @brief Handles the initialization and display of non-game screens + * @version 1.0.0 + * @date 2024-09-17 + * + * @copyright Copyright (c) 2024 + * + */ + +//============================================================================== +// Includes +//============================================================================== + +#include "2048_menus.h" + +//============================================================================== +// Const Variables +//============================================================================== + +const char mode[] = "2048"; +static const char pressKey[] = "Press any key to play"; +static const char highScore[] = "You got a high score!"; +static const char pressAB[] = "Press A or B to reset the game"; + +//============================================================================== +// Functions +//============================================================================== + +/** + * @brief Initializes all the blocks for the start screen + * + * @param t48 Game data + */ +void t48_initStartScreen(t48_t* t48) +{ + for (uint8_t i = 0; i < T48_START_SCREEN_BLOCKS; i++) + { + t48->ssBlocks[i].pos.x = esp_random() % (TFT_WIDTH - 50); // -50 because tile width + t48->ssBlocks[i].pos.y = esp_random() % (TFT_HEIGHT - 50); // +25 to re-center + + t48->ssBlocks[i].speed.x = (16667 / 4) + (esp_random() % 32000); + t48->ssBlocks[i].dir.x = (esp_random() % 2) ? -1 : 1; + + t48->ssBlocks[i].speed.y = (16667 / 4) + (esp_random() % 32000); + t48->ssBlocks[i].dir.y = (esp_random() % 2) ? -1 : 1; + } +} + +/** + * @brief Draws the Start screen + * + * @param t48 Game data + * @param color Color of the title text + * @param elapsedUs Time elapsed since last call + */ +void t48_drawStartScreen(t48_t* t48, paletteColor_t color, int32_t elapsedUs) +{ + // Blank + fillDisplayArea(0, 0, TFT_WIDTH, TFT_HEIGHT, c000); + + // Draw random blocks + for (uint8_t i = 0; i < T48_START_SCREEN_BLOCKS; i++) + { + t48StartScreenBlocks_t* block = &t48->ssBlocks[i]; + + // Move + block->timer.x += elapsedUs; + while (block->timer.x >= block->speed.x) + { + block->timer.x -= block->speed.x; + block->pos.x += (block->dir.x); + } + + block->timer.y += elapsedUs; + while (block->timer.y >= block->speed.y) + { + block->timer.y -= block->speed.y; + block->pos.y += (block->dir.y); + } + + // Update coordinates if out of bounds + if (block->pos.x < -50) + { + block->pos.x = TFT_WIDTH; + } + else if (block->pos.x > TFT_WIDTH) + { + block->pos.x = -50; + } + if (block->pos.y < -50) + { + block->pos.y = TFT_HEIGHT; + } + else if (block->pos.y > TFT_HEIGHT) + { + block->pos.y = -50; + } + + // Draw + drawWsgSimple(&t48->tiles[i], block->pos.x, block->pos.y); + } + + // Title + drawText(&t48->titleFont, color, mode, (TFT_WIDTH - textWidth(&t48->titleFont, mode)) / 2, TFT_HEIGHT / 2 - 12); + drawText(&t48->titleFontOutline, c555, mode, (TFT_WIDTH - textWidth(&t48->titleFont, mode)) / 2, + TFT_HEIGHT / 2 - 12); + + // Draw current High Score + static char textBuffer[20]; + snprintf(textBuffer, sizeof(textBuffer) - 1, "High score: %" PRIu32, t48->highScore[0]); + drawText(&t48->font, c444, textBuffer, (TFT_WIDTH - textWidth(&t48->font, textBuffer)) / 2, TFT_HEIGHT - 32); + + // Press any key... + drawText(&t48->font, c555, pressKey, (TFT_WIDTH - textWidth(&t48->font, pressKey)) / 2, TFT_HEIGHT - 64); +} + +/** + * @brief Draws the screen showing all the high scores, player score, and prompts them to continue + * + * @param t48 Game Data + * @param score Player score + * @param pc Color of test to display + */ +void t48_drawGameOverScreen(t48_t* t48, int64_t score, paletteColor_t pc) +{ + // Clear display + fillDisplayArea(0, 0, TFT_WIDTH, TFT_HEIGHT, c000); + + // Draw player score + static char textBuffer[32]; + snprintf(textBuffer, sizeof(textBuffer) - 1, "Final score: %" PRIu64, score); + drawText(&t48->titleFont, c550, textBuffer, 16, 48); + + // Draw high scores + for (int8_t i = 0; i < T48_HS_COUNT; i++) + { + static char initBuff[20]; + static paletteColor_t color; + if (score == t48->highScore[i]) + { + int16_t x = 16; + int16_t y = 80; + drawTextWordWrap(&t48->titleFont, pc, highScore, &x, &y, TFT_WIDTH - 16, y + 120); + color = c500; + } + else + { + color = c444; + } + snprintf(initBuff, sizeof(initBuff) - 1, "%d: %d - ", i + 1, (int)t48->highScore[i]); + strcat(initBuff, t48->hsInitials[i]); + drawText(&t48->font, color, initBuff, 16, TFT_HEIGHT - (98 - (16 * i))); + } + + // Prompt player to continue + drawText(&t48->font, c444, pressAB, 18, TFT_HEIGHT - 16); +} \ No newline at end of file diff --git a/main/modes/games/2048/2048_menus.h b/main/modes/games/2048/2048_menus.h new file mode 100644 index 000000000..32d5706d4 --- /dev/null +++ b/main/modes/games/2048/2048_menus.h @@ -0,0 +1,18 @@ +/** + * @file 2048_menus.h + * @author Jeremy Stintzcum (jeremy.stintzcum@gmail.com) + * @brief Handles the initialization and display of non-game screens + * @version 1.0.0 + * @date 2024-09-17 + * + * @copyright Copyright (c) 2024 + * + */ + +#pragma once + +#include "mode_2048.h" + +void t48_initStartScreen(t48_t* t48); +void t48_drawStartScreen(t48_t* t48, paletteColor_t color, int32_t elapsedUs); +void t48_drawGameOverScreen(t48_t* t48, int64_t score, paletteColor_t pc); \ No newline at end of file diff --git a/main/modes/games/2048/mode_2048.c b/main/modes/games/2048/mode_2048.c new file mode 100644 index 000000000..22d55309f --- /dev/null +++ b/main/modes/games/2048/mode_2048.c @@ -0,0 +1,498 @@ +/** + * @file mode_2048.c + * @author Jeremy Stintzcum (jeremy.stintzcum@gmail.com) + * @brief A game of 2048 for 2024-2025 Swadge hardware + * @version 1.5.0 + * @date 2024-06-28 + * + * @copyright Copyright (c) 2024 + * + */ + +//============================================================================== +// Includes +//============================================================================== + +#include "mode_2048.h" +#include "2048_game.h" +#include "2048_menus.h" +#include "textEntry.h" + +//============================================================================== +// Function Prototypes +//============================================================================== + +static void t48EnterMode(void); +static void t48ExitMode(void); +static void t48MainLoop(int64_t elapsedUs); +static void t48BgmCb(void); +static void t48InitHighScores(void); +static void t48SortHighScores(void); +static paletteColor_t t48_generateRainbow(void); +static void t48_fadeLEDs(int32_t elapsedUs); +static void t48_chaseLEDs(int32_t elapseUs); +static led_t t48_randColor(void); + +//============================================================================== +// Const Variables +//============================================================================== + +const char modeName[] = "2048"; +static const char youWin[] = "You got 2048!"; +static const char continueAB[] = "Press A or B to continue"; + +static const char* tileSpriteNames[] = { + "Tile-Blue-Diamond.wsg", "Tile-Blue-Square.wsg", "Tile-Cyan-Legs.wsg", "Tile-Green-Diamond.wsg", + "Tile-Green-Octo.wsg", "Tile-Green-Square.wsg", "Tile-Mauve-Legs.wsg", "Tile-Orange-Legs.wsg", + "Tile-Pink-Diamond.wsg", "Tile-Pink-Octo.wsg", "Tile-Pink-Square.wsg", "Tile-Purple-Legs.wsg", + "Tile-Red-Octo.wsg", "Tile-Red-Square.wsg", "Tile-Yellow-Diamond.wsg", "Tile-Yellow-Octo.wsg", +}; + +static const char* sparkleSpriteNames[] = { + "Sparkle_Blue.wsg", "Sparkle_Cyan.wsg", "Sparkle_Green.wsg", "Sparkle_Orange.wsg", + "Sparkle_Pink.wsg", "Sparkle_Purple.wsg", "Sparkle_Red.wsg", "Sparkle_Yellow.wsg", +}; + +static const char* newSparkleSprNames[] = { + "New_Dot.wsg", + "New_Small_Star.wsg", + "New_Med_Star.wsg", +}; + +const char highScoreKey[T48_HS_COUNT][T48_HS_KEYLEN] = { + "t48HighScore0", "t48HighScore1", "t48HighScore2", "t48HighScore3", "t48HighScore4", +}; + +const char highScoreInitialsKey[T48_HS_COUNT][T48_HS_KEYLEN] = { + "t48HSInitial0", "t48HSInitial1", "t48HSInitial2", "t48HSInitial3", "t48HSInitial4", +}; + +//============================================================================== +// Variables +//============================================================================== + +swadgeMode_t t48Mode = { + .modeName = modeName, + .wifiMode = NO_WIFI, + .overrideUsb = false, + .usesAccelerometer = false, + .usesThermometer = false, + .overrideSelectBtn = false, + .fnEnterMode = t48EnterMode, + .fnExitMode = t48ExitMode, + .fnMainLoop = t48MainLoop, + .fnAudioCallback = NULL, + .fnBackgroundDrawCallback = NULL, + .fnEspNowRecvCb = NULL, + .fnEspNowSendCb = NULL, + .fnAdvancedUSB = NULL, +}; + +t48_t* t48; + +//============================================================================== +// Functions +//============================================================================== + +/** + * @brief Enter 2048 mode and set everything up + */ +static void t48EnterMode(void) +{ + setFrameRateUs(16667); // 60 FPS + + // Init Mode & resources + t48 = calloc(sizeof(t48_t), 1); + + // Load fonts + loadFont("ibm_vga8.font", &t48->font, false); + loadFont("sonic.font", &t48->titleFont, false); + makeOutlineFont(&t48->titleFont, &t48->titleFontOutline, false); + + // Load images + t48->tiles = calloc(ARRAY_SIZE(tileSpriteNames), sizeof(wsg_t)); + for (int32_t tIdx = 0; tIdx < ARRAY_SIZE(tileSpriteNames); tIdx++) + { + loadWsg(tileSpriteNames[tIdx], &t48->tiles[tIdx], true); + } + + t48->sparkleSprites = calloc(ARRAY_SIZE(sparkleSpriteNames), sizeof(wsg_t)); + for (int32_t sIdx = 0; sIdx < ARRAY_SIZE(sparkleSpriteNames); sIdx++) + { + loadWsg(sparkleSpriteNames[sIdx], &t48->sparkleSprites[sIdx], true); + } + + t48->newSparkles = calloc(ARRAY_SIZE(newSparkleSprNames), sizeof(wsg_t)); + for (int32_t sIdx = 0; sIdx < ARRAY_SIZE(newSparkleSprNames); sIdx++) + { + loadWsg(newSparkleSprNames[sIdx], &t48->newSparkles[sIdx], true); + } + + // Load sounds + loadMidiFile("Follinesque.mid", &t48->bgm, true); + loadMidiFile("sndBounce.mid", &t48->click, true); + + // Init Text Entry + textEntryInit(&t48->font, 4, t48->playerInitials); + textEntrySetBGColor(c001); + textEntrySetEmphasisColor(c500); + textEntrySetNewCapsStyle(true); + textEntrySetNewEnterStyle(true); + textEntrySetCapMode(); + + // Initialize the scores + t48InitHighScores(); + + // Initialize the game + t48->state = T48_START_SCREEN; + t48_initStartScreen(t48); +} + +/** + * @brief Exit 2048 mode and free all resources + */ +static void t48ExitMode(void) +{ + freeFont(&t48->titleFontOutline); + freeFont(&t48->titleFont); + freeFont(&t48->font); + + for (uint8_t i = 0; i < ARRAY_SIZE(sparkleSpriteNames); i++) + { + freeWsg(&t48->sparkleSprites[i]); + } + free(t48->sparkleSprites); + + for (uint8_t i = 0; i < ARRAY_SIZE(tileSpriteNames); i++) + { + freeWsg(&t48->tiles[i]); + } + free(t48->tiles); + + for (int32_t sIdx = 0; sIdx < ARRAY_SIZE(newSparkleSprNames); sIdx++) + { + freeWsg(&t48->newSparkles[sIdx]); + } + free(t48->newSparkles); + + soundStop(true); + unloadMidiFile(&t48->bgm); + unloadMidiFile(&t48->click); + + free(t48); +} + +/** + * @brief The main game loop, responsible for input handling, game logic, and animation + * + * @param elapsedUs The time since this was last called, for animation + */ +static void t48MainLoop(int64_t elapsedUs) +{ + // Dim LEDs + t48_fadeLEDs(elapsedUs); + + // Play BGM if it's not playing + if (!t48->bgmIsPlaying) + { + soundPlayBgmCb(&t48->bgm, MIDI_BGM, t48BgmCb); + t48->bgmIsPlaying = true; + } + + // Handle inputs + buttonEvt_t evt; + switch (t48->state) + { + case T48_IN_GAME: + { + // Get inputs + while (checkButtonQueueWrapper(&evt)) + { + if (evt.down) + { + // Process inputs + t48_gameInput(t48, evt.button); + } + } + + // Loop the game + t48_gameLoop(t48, elapsedUs); + break; + } + case T48_START_SCREEN: + { + // Check any button is pressed + while (checkButtonQueueWrapper(&evt)) + { + if (evt.down) + { + soundPlaySfx(&t48->click, MIDI_SFX); + t48_gameInit(t48); + t48->state = T48_IN_GAME; + } + } + // Draw + t48_drawStartScreen(t48, t48_generateRainbow(), elapsedUs); + t48_chaseLEDs(elapsedUs); + break; + } + case T48_WIN_SCREEN: + { + // Get inputs + while (checkButtonQueueWrapper(&evt)) + { + if (evt.down && (evt.button & PB_A || evt.button & PB_B)) + { + t48->state = T48_IN_GAME; + } + } + fillDisplayArea(0, 0, TFT_WIDTH, TFT_HEIGHT, c000); + drawText(&t48->titleFont, c055, youWin, (TFT_WIDTH - textWidth(&t48->titleFont, youWin)) / 2, 48); + drawText(&t48->font, c555, continueAB, (TFT_WIDTH - textWidth(&t48->font, continueAB)) / 2, + TFT_HEIGHT - 64); + break; + } + case T48_HS_SCREEN: + { + // Handle input in text entry object + while (checkButtonQueueWrapper(&evt)) + { + t48->textEntryDone = !textEntryInput(evt.down, evt.button); + } + + // If the text entry is done, sort the scores, reset the text entry object, + // and display the final screen + if (t48->textEntryDone) + { + t48SortHighScores(); + textEntrySoftReset(); + t48->state = T48_END_SCREEN; + } + + // Draw text entry + textEntryDraw(elapsedUs); + break; + } + case T48_END_SCREEN: + { + // Check any button is pressed + while (checkButtonQueueWrapper(&evt)) + { + if (evt.down && (evt.button & PB_A || evt.button & PB_B)) + { + soundPlaySfx(&t48->click, MIDI_SFX); + t48->state = T48_START_SCREEN; + } + } + + // Draw the final score screen + t48_drawGameOverScreen(t48, t48->score, t48_generateRainbow()); + t48_chaseLEDs(elapsedUs); + break; + } + default: + { + break; + } + } +} + +/** + * @brief Restarts the BGM once the track ends + * + */ +static void t48BgmCb() +{ + t48->bgmIsPlaying = false; +} + +/** + * @brief Initializes the high scores based either from NVS or predetermined scores to beat + * + */ +static void t48InitHighScores() +{ + // Init High scores + for (int8_t i = 0; i < T48_HS_COUNT; i++) + { + if (!readNvs32(highScoreKey[i], &t48->highScore[i])) + { + switch (i) + { + case 0: + t48->highScore[i] = 96880; + break; + case 1: + t48->highScore[i] = 69224; + break; + case 2: + t48->highScore[i] = 24244; + break; + case 3: + t48->highScore[i] = 11020; + break; + case 4: + t48->highScore[i] = 5176; + break; + } + writeNvs32(highScoreKey[i], t48->highScore[i]); + } + size_t len = 4; + if (!readNvsBlob(highScoreInitialsKey[i], &t48->hsInitials[i], &len)) + { + static char buff[5]; + switch (i) + { + case 0: + strcpy(buff, "JW"); + break; + case 1: + strcpy(buff, "Pan"); + break; + case 2: + strcpy(buff, "Pix"); + break; + case 3: + strcpy(buff, "Poe"); + break; + case 4: + strcpy(buff, "DrG"); + break; + } + strcpy(t48->hsInitials[i], buff); + writeNvsBlob(highScoreInitialsKey[i], &t48->hsInitials[i], len); + } + } +} + +/** + * @brief Sorts the high scores and saves them to the NVM at the end of a game + * + */ +static void t48SortHighScores() +{ + // 5th place needs to compare to the score + if (t48->highScore[T48_HS_COUNT - 1] < t48->score) + { + t48->highScore[T48_HS_COUNT - 1] = t48->score; + strcpy(t48->hsInitials[T48_HS_COUNT - 1], t48->playerInitials); + } + else + { + // Scores *should* be sorted already. Save cycles. + return; + } + for (int8_t i = T48_HS_COUNT - 2; i >= 0; i--) + { + if (t48->highScore[i] < t48->highScore[i + 1]) + { + // Swap + int32_t swap = t48->highScore[i]; + t48->highScore[i] = t48->highScore[i + 1]; + t48->highScore[i + 1] = swap; + char swapI[4]; + strcpy(swapI, t48->hsInitials[i]); + strcpy(t48->hsInitials[i], t48->hsInitials[i + 1]); + strcpy(t48->hsInitials[i + 1], swapI); + } + } + // Save out the new scores + for (int8_t i = 0; i < T48_HS_COUNT; i++) + { + writeNvs32(highScoreKey[i], t48->highScore[i]); + writeNvsBlob(highScoreInitialsKey[i], &t48->hsInitials[i], 4); + } +} + +/** + * @brief Generates a rainbow color in sequence + * + * @return paletteColor_t + */ +static paletteColor_t t48_generateRainbow() +{ + uint8_t hue = t48->hue++; + uint8_t sat = 255; + uint8_t val = 255; + return paletteHsvToHex(hue, sat, val); +} + +/** + * @brief Dims the LEDs uniformly over time + * + * @param t48 Game data + * @param elapsedUs Time since last frame + */ +static void t48_fadeLEDs(int32_t elapsedUs) +{ + t48->fadeTimer += elapsedUs; + while (t48->fadeTimer >= T48_FADE_SPEED) + { + t48->fadeTimer -= T48_FADE_SPEED; + for (int32_t i = 0; i < CONFIG_NUM_LEDS; i++) + { + // Red + if (t48->leds[i].r < 6) + { + t48->leds[i].r = 0; + } + else + { + t48->leds[i].r -= 6; + } + // Green + if (t48->leds[i].g < 6) + { + t48->leds[i].g = 0; + } + else + { + t48->leds[i].g -= 6; + } + // Blue + if (t48->leds[i].b < 6) + { + t48->leds[i].b = 0; + } + else + { + t48->leds[i].b -= 6; + } + } + } + setLeds(t48->leds, CONFIG_NUM_LEDS); +} + +/** + * @brief Chase LEDs around the swadge + * + * @param elapseUs Time since last update + */ +static void t48_chaseLEDs(int32_t elapseUs) +{ + t48->nextLedTimer += elapseUs; + if (t48->nextLedTimer >= T48_NEXT_LED) + { + t48->nextLedTimer = 0; + t48->currLED++; + if (t48->currLED > CONFIG_NUM_LEDS - 1) + { + t48->currLED = 0; + } + t48->leds[t48->currLED] = t48_randColor(); + } +} + +/** + * @brief Generates a random LED color + * + * @return led_t Led object to set array to + */ +led_t t48_randColor() +{ + led_t col = {0}; + col.r = 128 + (esp_random() % 127); + col.g = 128 + (esp_random() % 127); + col.b = 128 + (esp_random() % 127); + return col; +} \ No newline at end of file diff --git a/main/modes/games/2048/mode_2048.h b/main/modes/games/2048/mode_2048.h new file mode 100644 index 000000000..8064c0623 --- /dev/null +++ b/main/modes/games/2048/mode_2048.h @@ -0,0 +1,147 @@ +/** + * @file mode_2048.h + * @author Jeremy Stintzcum (jeremy.stintzcum@gmail.com) + * @brief A game of 2048 for 2024-2025 Swadge hardware + * @version 1.5.0 + * @date 2024-06-28 + * + * @copyright Copyright (c) 2024 + * + */ +#pragma once + +//============================================================================== +// Includes +//============================================================================== + +#include + +#include "swadge2024.h" + +//============================================================================== +// Defines +//============================================================================== + +// Animations +#define T48_GRID_SIZE 4 +#define T48_TILES_PER_CELL 2 +#define T48_SPARKLES_PER_CELL 32 + +// Animation +#define T48_NEW_SPARKLE_SEQ 33333 +#define T48_START_SCREEN_BLOCKS 16 + +// High Scores +#define T48_HS_COUNT 5 +#define T48_HS_KEYLEN 14 + +// LEDs +#define T48_FADE_SPEED 66666 +#define T48_NEXT_LED 100000 + +//============================================================================== +// Enums +//============================================================================== + +typedef enum +{ + T48_IN_GAME, + T48_START_SCREEN, + T48_WIN_SCREEN, + T48_HS_SCREEN, + T48_END_SCREEN, +} t48ModeStateEnum_t; + +//============================================================================== +// Structs +//============================================================================== + +typedef struct +{ + uint32_t value; ///< The current value for the tile being drawn + int32_t xOffset; ///< The X offset from the cell for this tile + int32_t yOffset; ///< The Y offset from the cell for this tile +} t48drawnTile_t; + +typedef struct +{ + wsg_t* img; ///< A pointer to an image of a sparkle + int16_t x; ///< The sparkle's X coordinate (screen space) + int16_t y; ///< The sparkle's Y coordinate (screen space) + int16_t xSpd; ///< The number of X pixels to move per-frame + int16_t ySpd; ///< The number of Y pixels to move per-frame + bool active; ///< True if the spark is being animated and drawn +} t48Sparkle_t; + +typedef struct +{ + int32_t value; ///< This cell's current value + t48drawnTile_t drawnTiles[T48_TILES_PER_CELL]; ///< All the tiles being drawn for this cell + t48Sparkle_t sparkles[T48_SPARKLES_PER_CELL]; ///< All the sparkles being drawn for this cell + bool merged; ///< If tile was merged +} t48cell_t; + +typedef struct +{ + vec_t pos; ///< x and y coordinates of the new tile + int32_t spawnTime; ///< Time since the object was created + bool active; ///< If the new tile should be drawing anything + int8_t sequence; ///< Current position in the sequence +} t48newTile_t; + +typedef struct +{ + vec_t pos; + vec_t timer; + vec_t speed; + vec_t dir; +} t48StartScreenBlocks_t; + +typedef struct +{ + // Assets + font_t font; ///< Font used for tile values + font_t titleFont; ///< Font used for the title + font_t titleFontOutline; ///< Font used for the title outline + wsg_t* tiles; ///< A list of tile sprites + wsg_t* sparkleSprites; ///< A list of sparkle sprites + wsg_t* newSparkles; ///< New sparkles for a new tile + midiFile_t bgm; ///< The background music + midiFile_t click; ///< The click sound + + // Game state + t48cell_t board[T48_GRID_SIZE][T48_GRID_SIZE]; ///< The board with cells, tiles, and sparkles + int32_t score; ///< The current score + bool acceptGameInput; ///< true if the game accepts input, false if it is animating + bool paused; ///< If the game is paused + bool alreadyWon; ///< If the win screen has already displayed + t48ModeStateEnum_t state; ///< Where in the game sequence we are + + // Audio + bool bgmIsPlaying; ///< Allows the BGM to restart + + // High Score + char playerInitials[4]; ///< Contains the play initials + char hsInitials[T48_HS_COUNT][4]; ///< Contains all the high score initials + bool textEntryDone; ///< Tested when checking if text entry is finished + int32_t highScore[T48_HS_COUNT]; ///< Array of high scores + + // Start screen + uint8_t hue; ///< Color of the title text + + // Animations + t48newTile_t nTile; ///< New tile animation data + t48StartScreenBlocks_t ssBlocks[T48_START_SCREEN_BLOCKS]; ///< Start screen falling blocks + + // LEDs + led_t leds[CONFIG_NUM_LEDS]; ///< LEDs to set + int32_t fadeTimer; ///< Timer between fades + int32_t nextLedTimer; ///< Timer before the next LED illuminates + uint8_t currLED; ///< Index of the led for the chase mode +} t48_t; + +//============================================================================== +// Extern variables +//============================================================================== + +extern swadgeMode_t t48Mode; diff --git a/main/modes/games/pango/paEntity.c b/main/modes/games/pango/paEntity.c new file mode 100644 index 000000000..a33ca8a9c --- /dev/null +++ b/main/modes/games/pango/paEntity.c @@ -0,0 +1,1547 @@ +//============================================================================== +// Includes +//============================================================================== + +#include +#include "paEntity.h" +#include "paEntityManager.h" +#include "paTilemap.h" +#include "paGameData.h" +#include "soundFuncs.h" +#include "hdw-btn.h" +#include "esp_random.h" +// #include "aabb_utils.h" +#include "trigonometry.h" +#include +#include "soundFuncs.h" + +//============================================================================== +// Constants +//============================================================================== +#define SUBPIXEL_RESOLUTION 4 +#define PA_TILE_SIZE_IN_POWERS_OF_2 4 +#define PA_TILE_SIZE 16 +#define PA_HALF_TILESIZE 8 +#define DESPAWN_THRESHOLD 64 + +#define SIGNOF(x) ((x > 0) - (x < 0)) +#define PA_TO_TILECOORDS(x) ((x) >> PA_TILE_SIZE_IN_POWERS_OF_2) +#define PA_GET_TAXICAB_DISTANCE(x1, y1, x2, y2) (abs(x1 - x2) + abs(y1 - y2)) +// #define TO_PIXEL_COORDS(x) ((x) >> SUBPIXEL_RESOLUTION) +// #define TO_SUBPIXEL_COORDS(x) ((x) << SUBPIXEL_RESOLUTION) + +//============================================================================== +// Functions +//============================================================================== +void pa_initializeEntity(paEntity_t* self, paEntityManager_t* entityManager, paTilemap_t* tilemap, + paGameData_t* gameData, paSoundManager_t* soundManager) +{ + self->active = false; + self->tilemap = tilemap; + self->gameData = gameData; + self->soundManager = soundManager; + self->homeTileX = 0; + self->homeTileY = 0; + self->gravity = false; + self->falling = false; + self->entityManager = entityManager; + self->fallOffTileHandler = &defaultFallOffTileHandler; + self->spriteFlipHorizontal = false; + self->spriteFlipVertical = false; + self->facingDirection = PA_DIRECTION_SOUTH; + self->stateTimer = -1; + self->tempStateTimer = -1; + self->baseSpeed = 0; + self->stateFlag = false; + + // Fields not explicitly initialized + // self->type = 0; + // self->updateFunction = NULL; + // self->x = 0; + // self->y = 0; + // self->xspeed = 0; + // self->yspeed = 0; + // self->xMaxSpeed = 0; + // self->yMaxSpeed = 0; + // self->xDamping = 0; + // self->yDamping = 0; + // self->gravityEnabled = false; + // self->spriteIndex = 0; + // self->animationTimer = 0; + // self->jumpPower = 0; + // self->visible = false; + // self->hp = 0; + // self->invincibilityFrames = 0; + // self->scoreValue = 0; + // self->collisionHandler = NULL; + // self->tileCollisionHandler = NULL; + // self->overlapTileHandler = NULL; +} + +void pa_updatePlayer(paEntity_t* self) +{ + switch (self->state) + { + case PA_PL_ST_NORMAL: + default: + { + if (self->gameData->btnState & PB_LEFT) + { + self->xspeed -= 4; + + if (self->xspeed < -16) + { + self->xspeed = -16; + } + } + else if (self->gameData->btnState & PB_RIGHT) + { + self->xspeed += 4; + + if (self->xspeed > 16) + { + self->xspeed = 16; + } + } + + if (self->gameData->btnState & PB_UP) + { + self->yspeed -= 4; + + if (self->yspeed < -16) + { + self->yspeed = -16; + } + } + else if (self->gameData->btnState & PB_DOWN) + { + self->yspeed += 4; + + if (self->yspeed > 16) + { + self->yspeed = 16; + } + } + + if (self->animationTimer > 0) + { + self->animationTimer--; + } + + if (((self->gameData->btnState & PB_START) && !(self->gameData->prevBtnState & PB_START))) + { + self->gameData->changeState = PA_ST_PAUSE; + } + + /* + if(self->xspeed){ + self->targetTileX = PA_TO_TILECOORDS(self->x >> SUBPIXEL_RESOLUTION) + SIGNOF(self->xspeed); + + if(!self->yspeed){ + self->targetTileY = PA_TO_TILECOORDS(self->y >> SUBPIXEL_RESOLUTION); + } + } + + if(self->yspeed){ + self->targetTileY = PA_TO_TILECOORDS(self->y >> SUBPIXEL_RESOLUTION) + SIGNOF(self->yspeed); + + if(!self->xspeed){ + self->targetTileX = PA_TO_TILECOORDS(self->x >> SUBPIXEL_RESOLUTION); + } + } + */ + + switch (self->facingDirection) + { + case PA_DIRECTION_WEST: + self->targetTileX = PA_TO_TILECOORDS(self->x >> SUBPIXEL_RESOLUTION) - 1; + self->targetTileY = PA_TO_TILECOORDS(self->y >> SUBPIXEL_RESOLUTION); + break; + case PA_DIRECTION_EAST: + self->targetTileX = PA_TO_TILECOORDS(self->x >> SUBPIXEL_RESOLUTION) + 1; + self->targetTileY = PA_TO_TILECOORDS(self->y >> SUBPIXEL_RESOLUTION); + break; + case PA_DIRECTION_NORTH: + self->targetTileX = PA_TO_TILECOORDS(self->x >> SUBPIXEL_RESOLUTION); + self->targetTileY = PA_TO_TILECOORDS(self->y >> SUBPIXEL_RESOLUTION) - 1; + break; + case PA_DIRECTION_SOUTH: + default: + self->targetTileX = PA_TO_TILECOORDS(self->x >> SUBPIXEL_RESOLUTION); + self->targetTileY = PA_TO_TILECOORDS(self->y >> SUBPIXEL_RESOLUTION) + 1; + break; + } + + if (self->gameData->btnState & PB_A && !(self->gameData->prevBtnState & PB_A)) + { + uint8_t t = pa_getTile(self->tilemap, self->targetTileX, self->targetTileY); + if (t == PA_TILE_BLOCK || t == PA_TILE_SPAWN_BLOCK_0 || t == PA_TILE_BONUS_BLOCK_0) + { + paEntity_t* newHitBlock = createHitBlock( + self->entityManager, (self->targetTileX << SUBPIXEL_RESOLUTION) + PA_HALF_TILESIZE, + (self->targetTileY << SUBPIXEL_RESOLUTION) + PA_HALF_TILESIZE); + + if (newHitBlock != NULL) + { + pa_setTile(self->tilemap, self->targetTileX, self->targetTileY, PA_TILE_EMPTY); + newHitBlock->jumpPower = t; + switch (self->facingDirection) + { + case PA_DIRECTION_WEST: + newHitBlock->xspeed = -64; + break; + case PA_DIRECTION_EAST: + newHitBlock->xspeed = 64; + break; + case PA_DIRECTION_NORTH: + newHitBlock->yspeed = -64; + break; + case PA_DIRECTION_SOUTH: + default: + newHitBlock->yspeed = 64; + break; + } + soundPlaySfx(&(self->soundManager->sndSquish), BZR_LEFT); + } + } + + self->state = PA_PL_ST_PUSHING; + self->stateTimer = 8; + + switch (self->facingDirection) + { + case PA_DIRECTION_WEST: + self->spriteIndex = PA_SP_PLAYER_PUSH_SIDE_1; + break; + case PA_DIRECTION_EAST: + self->spriteIndex = PA_SP_PLAYER_PUSH_SIDE_1; + break; + case PA_DIRECTION_NORTH: + self->spriteIndex = PA_SP_PLAYER_PUSH_NORTH_1; + break; + case PA_DIRECTION_SOUTH: + default: + self->spriteIndex = PA_SP_PLAYER_PUSH_SOUTH_1; + break; + } + + break; + } + animatePlayer(self); + break; + } + case PA_PL_ST_PUSHING: + { + self->stateTimer--; + + if (self->stateTimer < 0) + { + self->state = PA_PL_ST_NORMAL; + break; + } + + if (self->stateTimer == 2) + { + self->spriteIndex++; + } + + break; + } + } + + pa_moveEntityWithTileCollisions(self); + applyDamping(self); + pa_detectEntityCollisions(self); +} + +void updateCrabdozer(paEntity_t* self) +{ + switch (self->state) + { + case PA_EN_ST_STUN: + self->stateTimer--; + if (self->stateTimer < 0) + { + self->facingDirection = PA_DIRECTION_NONE; + + /*if(self->stateFlag){ + self->state = PA_EN_ST_AGGRESSIVE; + self->stateTimer = 32767; //effectively always aggressive + self->entityManager->aggroEnemies++; + } else*/ + { + self->state = PA_EN_ST_NORMAL; + self->stateTimer = (300 + esp_random() % 600); // Min 5 seconds, max 15 seconds + } + } + else + { + if (self->gameData->frameCount % ((self->stateTimer >> 1) + 1) == 0) + { + self->spriteIndex = PA_SP_ENEMY_STUN; + self->spriteFlipHorizontal = !self->spriteFlipHorizontal; + } + } + + pa_detectEntityCollisions(self); + break; + case PA_EN_ST_NORMAL: + case PA_EN_ST_AGGRESSIVE: + case PA_EN_ST_RUNAWAY: + { + self->stateTimer--; + if (self->stateTimer < 0 || self->entityManager->aggroEnemies < self->gameData->minAggroEnemies) + { + if (self->state == PA_EN_ST_RUNAWAY) + { + killEnemy(self); + break; + } + else if (self->state == PA_EN_ST_NORMAL + && (self->entityManager->aggroEnemies < self->gameData->maxAggroEnemies)) + { + self->state = PA_EN_ST_AGGRESSIVE; + self->entityManager->aggroEnemies++; + self->baseSpeed += 2; + self->stateTimer = (300 + esp_random() % 300); // Min 5 seconds, max 10 seconds + } + else if (self->state == PA_EN_ST_AGGRESSIVE) + { + self->state = PA_EN_ST_NORMAL; + self->entityManager->aggroEnemies--; + self->baseSpeed -= 2; + self->stateTimer = (300 + esp_random() % 300); // Min 5 seconds, max 10 seconds + } + } + + if (self->state != PA_EN_ST_RUNAWAY && self->entityManager->activeEnemies == 1 + && self->gameData->remainingEnemies == 0) + { + self->state = PA_EN_ST_RUNAWAY; + self->entityManager->aggroEnemies = 1; + self->baseSpeed = 20; + self->stateTimer = 480; // 8 seconds + + self->targetTileX = (esp_random() % 2) ? 1 : 15; + self->targetTileY = (esp_random() % 2) ? 1 : 13; + } + + uint8_t tx = PA_TO_TILECOORDS(self->x >> SUBPIXEL_RESOLUTION); + uint8_t ty = PA_TO_TILECOORDS(self->y >> SUBPIXEL_RESOLUTION); + + uint8_t t1, t2, t3 = 0; + uint8_t distT1, distT2, distT3; + + if (self->state != PA_EN_ST_RUNAWAY) + { + self->targetTileX = PA_TO_TILECOORDS(self->entityManager->playerEntity->x >> SUBPIXEL_RESOLUTION); + self->targetTileY = PA_TO_TILECOORDS(self->entityManager->playerEntity->y >> SUBPIXEL_RESOLUTION); + } + + int16_t hcof = (((self->x >> SUBPIXEL_RESOLUTION) % PA_TILE_SIZE) - PA_HALF_TILESIZE); + int16_t vcof = (((self->y >> SUBPIXEL_RESOLUTION) % PA_TILE_SIZE) - PA_HALF_TILESIZE); + + bool doAgression = (self->state == PA_EN_ST_AGGRESSIVE) /*? esp_random() % 2 : false*/; + + switch (self->facingDirection) + { + case PA_DIRECTION_WEST: + if (hcof) + { + break; + } + + t1 = pa_getTile(self->tilemap, tx - 1, ty); + t2 = pa_getTile(self->tilemap, tx, ty - 1); + t3 = pa_getTile(self->tilemap, tx, ty + 1); + + distT1 = PA_GET_TAXICAB_DISTANCE(tx - 1, ty, self->targetTileX, self->targetTileY); + distT2 = PA_GET_TAXICAB_DISTANCE(tx, ty - 1, self->targetTileX, self->targetTileY); + distT3 = PA_GET_TAXICAB_DISTANCE(tx, ty + 1, self->targetTileX, self->targetTileY); + + if ((!t2 || doAgression) && distT2 < distT1 && (t3 || distT2 < distT3)) + { + if (doAgression && t2 == PA_TILE_BLOCK) + { + pa_enemyBreakBlock(self, PA_DIRECTION_NORTH, self->baseSpeed >> 1, tx, ty); + } + else + { + pa_enemyChangeDirection(self, PA_DIRECTION_NORTH, self->baseSpeed); + } + break; + } + + if ((!t3 || doAgression) && distT3 < distT1) + { + if (doAgression && t3 == PA_TILE_BLOCK) + { + pa_enemyBreakBlock(self, PA_DIRECTION_SOUTH, self->baseSpeed >> 1, tx, ty); + } + else + { + pa_enemyChangeDirection(self, PA_DIRECTION_SOUTH, self->baseSpeed); + } + break; + } + + if (t1) + { + if (doAgression && t1 == PA_TILE_BLOCK) + { + pa_enemyBreakBlock(self, PA_DIRECTION_WEST, self->baseSpeed >> 1, tx, ty); + break; + } + + if ((!t2 || doAgression) && (t3 || distT2 < distT3)) + { + if (doAgression && t2 == PA_TILE_BLOCK) + { + pa_enemyBreakBlock(self, PA_DIRECTION_NORTH, self->baseSpeed >> 1, tx, ty); + } + else + { + pa_enemyChangeDirection(self, PA_DIRECTION_NORTH, self->baseSpeed); + } + break; + } + + if (!t3 || doAgression) + { + if (doAgression && t3 == PA_TILE_BLOCK) + { + pa_enemyBreakBlock(self, PA_DIRECTION_SOUTH, self->baseSpeed >> 1, tx, ty); + } + else + { + pa_enemyChangeDirection(self, PA_DIRECTION_SOUTH, self->baseSpeed); + } + break; + } + + pa_enemyChangeDirection(self, PA_DIRECTION_EAST, self->baseSpeed); + break; + } + + break; + case PA_DIRECTION_EAST: + if (hcof) + { + break; + } + + t1 = pa_getTile(self->tilemap, tx + 1, ty); + t2 = pa_getTile(self->tilemap, tx, ty - 1); + t3 = pa_getTile(self->tilemap, tx, ty + 1); + + distT1 = PA_GET_TAXICAB_DISTANCE(tx + 1, ty, self->targetTileX, self->targetTileY); + distT2 = PA_GET_TAXICAB_DISTANCE(tx, ty - 1, self->targetTileX, self->targetTileY); + distT3 = PA_GET_TAXICAB_DISTANCE(tx, ty + 1, self->targetTileX, self->targetTileY); + + if ((!t2 || doAgression) && distT2 < distT1 && (t3 || distT2 < distT3)) + { + if (doAgression && t2 == PA_TILE_BLOCK) + { + pa_enemyBreakBlock(self, PA_DIRECTION_NORTH, self->baseSpeed >> 1, tx, ty); + } + else + { + pa_enemyChangeDirection(self, PA_DIRECTION_NORTH, self->baseSpeed); + } + break; + } + if ((!t3 || doAgression) && distT3 < distT1) + { + if (doAgression && t3 == PA_TILE_BLOCK) + { + pa_enemyBreakBlock(self, PA_DIRECTION_SOUTH, self->baseSpeed >> 1, tx, ty); + } + else + { + pa_enemyChangeDirection(self, PA_DIRECTION_SOUTH, self->baseSpeed); + } + break; + } + if (t1) + { + if (doAgression && t1 == PA_TILE_BLOCK) + { + pa_enemyBreakBlock(self, PA_DIRECTION_EAST, self->baseSpeed >> 1, tx, ty); + break; + } + + if ((!t2 || doAgression) && (t3 || distT2 < distT3)) + { + if (doAgression && t2 == PA_TILE_BLOCK) + { + pa_enemyBreakBlock(self, PA_DIRECTION_NORTH, self->baseSpeed >> 1, tx, ty); + } + else + { + pa_enemyChangeDirection(self, PA_DIRECTION_NORTH, self->baseSpeed); + } + break; + } + + if (!t3 || doAgression) + { + if (doAgression && t2 == PA_TILE_BLOCK) + { + pa_enemyBreakBlock(self, PA_DIRECTION_SOUTH, self->baseSpeed >> 1, tx, ty); + } + else + { + pa_enemyChangeDirection(self, PA_DIRECTION_SOUTH, self->baseSpeed); + } + break; + } + + pa_enemyChangeDirection(self, PA_DIRECTION_WEST, self->baseSpeed); + break; + } + + break; + case PA_DIRECTION_NORTH: + if (vcof) + { + break; + } + + t1 = pa_getTile(self->tilemap, tx, ty - 1); + t2 = pa_getTile(self->tilemap, tx - 1, ty); + t3 = pa_getTile(self->tilemap, tx + 1, ty); + + distT1 = PA_GET_TAXICAB_DISTANCE(tx, ty - 1, self->targetTileX, self->targetTileY); + distT2 = PA_GET_TAXICAB_DISTANCE(tx - 1, ty, self->targetTileX, self->targetTileY); + distT3 = PA_GET_TAXICAB_DISTANCE(tx + 1, ty, self->targetTileX, self->targetTileY); + + if ((!t2 || doAgression) && distT2 < distT1 && (t3 || distT2 < distT3)) + { + if (doAgression && t2 == PA_TILE_BLOCK) + { + pa_enemyBreakBlock(self, PA_DIRECTION_WEST, self->baseSpeed >> 1, tx, ty); + } + else + { + pa_enemyChangeDirection(self, PA_DIRECTION_WEST, self->baseSpeed); + } + break; + } + + if ((!t3 || doAgression) && distT3 < distT1) + { + if (doAgression && t3 == PA_TILE_BLOCK) + { + pa_enemyBreakBlock(self, PA_DIRECTION_EAST, self->baseSpeed >> 1, tx, ty); + } + else + { + pa_enemyChangeDirection(self, PA_DIRECTION_EAST, self->baseSpeed); + } + break; + } + + if (t1) + { + if (doAgression && t1 == PA_TILE_BLOCK) + { + pa_enemyBreakBlock(self, PA_DIRECTION_NORTH, self->baseSpeed >> 1, tx, ty); + break; + } + + if ((!t2 || doAgression) && (t3 || distT2 < distT3)) + { + if (doAgression && t2 == PA_TILE_BLOCK) + { + pa_enemyBreakBlock(self, PA_DIRECTION_WEST, self->baseSpeed >> 1, tx, ty); + } + else + { + pa_enemyChangeDirection(self, PA_DIRECTION_WEST, self->baseSpeed); + } + break; + } + + if (!t3 || doAgression) + { + if (doAgression && t3 == PA_TILE_BLOCK) + { + pa_enemyBreakBlock(self, PA_DIRECTION_EAST, self->baseSpeed >> 1, tx, ty); + } + else + { + pa_enemyChangeDirection(self, PA_DIRECTION_EAST, self->baseSpeed); + } + + break; + } + + pa_enemyChangeDirection(self, PA_DIRECTION_SOUTH, self->baseSpeed); + break; + } + + break; + case PA_DIRECTION_NONE: + default: + pa_enemyChangeDirection(self, 1 >> (esp_random() % 3), self->baseSpeed); + break; + case PA_DIRECTION_SOUTH: + if (vcof) + { + break; + } + + t1 = pa_getTile(self->tilemap, tx, ty + 1); + t2 = pa_getTile(self->tilemap, tx - 1, ty); + t3 = pa_getTile(self->tilemap, tx + 1, ty); + + distT1 = PA_GET_TAXICAB_DISTANCE(tx, ty + 1, self->targetTileX, self->targetTileY); + distT2 = PA_GET_TAXICAB_DISTANCE(tx - 1, ty, self->targetTileX, self->targetTileY); + distT3 = PA_GET_TAXICAB_DISTANCE(tx + 1, ty, self->targetTileX, self->targetTileY); + + if ((!t2 || doAgression) && distT2 < distT1 && (t3 || distT2 < distT3)) + { + if (doAgression && t2 == PA_TILE_BLOCK) + { + pa_enemyBreakBlock(self, PA_DIRECTION_WEST, self->baseSpeed >> 1, tx, ty); + } + else + { + pa_enemyChangeDirection(self, PA_DIRECTION_WEST, self->baseSpeed); + } + break; + } + + if ((!t3 || doAgression) && distT3 < distT1) + { + if (doAgression && t3 == PA_TILE_BLOCK) + { + pa_enemyBreakBlock(self, PA_DIRECTION_EAST, self->baseSpeed >> 1, tx, ty); + } + else + { + pa_enemyChangeDirection(self, PA_DIRECTION_EAST, self->baseSpeed); + } + break; + } + + if (t1) + { + if (doAgression && t1 == PA_TILE_BLOCK) + { + pa_enemyBreakBlock(self, PA_DIRECTION_SOUTH, self->baseSpeed >> 1, tx, ty); + break; + } + + if ((!t2 || doAgression) && (t3 || distT2 < distT3)) + { + if (doAgression && t2 == PA_TILE_BLOCK) + { + pa_enemyBreakBlock(self, PA_DIRECTION_WEST, self->baseSpeed >> 1, tx, ty); + } + else + { + pa_enemyChangeDirection(self, PA_DIRECTION_WEST, self->baseSpeed); + } + break; + } + + if (!t3 || doAgression) + { + if (doAgression && t3 == PA_TILE_BLOCK) + { + pa_enemyBreakBlock(self, PA_DIRECTION_EAST, self->baseSpeed >> 1, tx, ty); + } + else + { + pa_enemyChangeDirection(self, PA_DIRECTION_EAST, self->baseSpeed); + } + break; + } + + pa_enemyChangeDirection(self, PA_DIRECTION_NORTH, self->baseSpeed); + break; + } + + break; + } + + pa_animateEnemy(self); + despawnWhenOffscreen(self); + if (self->state != PA_EN_ST_BREAK_BLOCK) + { + // Need to skip this if enemy has just changed to breaking block state + // or else enemy will be stopped + pa_moveEntityWithTileCollisions(self); + } + pa_detectEntityCollisions(self); + + break; + } + case PA_EN_ST_BREAK_BLOCK: + { + /*//Need to force a speed value because + //tile collision will stop the enemy before we get here + switch(self->facingDirection){ + case PA_DIRECTION_WEST: + self->xspeed = -8; + break; + case PA_DIRECTION_EAST: + self->xspeed = 8; + break; + case PA_DIRECTION_NORTH: + self->yspeed = -8; + break; + case PA_DIRECTION_SOUTH: + self->yspeed = 8; + break; + default: + break; + }*/ + + self->x += self->xspeed; + self->y += self->yspeed; + + self->stateTimer--; + if (self->stateTimer < 0) + { + self->state = PA_EN_ST_AGGRESSIVE; + self->xspeed *= 2; + self->yspeed *= 2; + self->stateTimer = self->tempStateTimer; + } + + pa_animateEnemy(self); + break; + } + default: + { + break; + } + } +} + +void pa_enemyChangeDirection(paEntity_t* self, uint16_t newDirection, int16_t speed) +{ + switch (newDirection) + { + case PA_DIRECTION_WEST: + self->yspeed = 0; + self->xspeed = -speed; + break; + case PA_DIRECTION_EAST: + self->yspeed = 0; + self->xspeed = speed; + break; + case PA_DIRECTION_NORTH: + self->xspeed = 0; + self->yspeed = -speed; + break; + case PA_DIRECTION_NONE: + default: + self->xspeed = 0; + self->yspeed = 0; + break; + case PA_DIRECTION_SOUTH: + self->xspeed = 0; + self->yspeed = speed; + break; + } + + self->facingDirection = newDirection; +} + +void pa_enemyBreakBlock(paEntity_t* self, uint16_t newDirection, int16_t speed, uint8_t tx, uint8_t ty) +{ + switch (newDirection) + { + case PA_DIRECTION_WEST: + pa_createBreakBlock(self->entityManager, ((tx - 1) << SUBPIXEL_RESOLUTION) + PA_HALF_TILE_SIZE, + (ty << SUBPIXEL_RESOLUTION) + PA_HALF_TILE_SIZE); + break; + case PA_DIRECTION_EAST: + pa_createBreakBlock(self->entityManager, ((tx + 1) << SUBPIXEL_RESOLUTION) + PA_HALF_TILE_SIZE, + (ty << SUBPIXEL_RESOLUTION) + PA_HALF_TILE_SIZE); + break; + case PA_DIRECTION_NORTH: + pa_createBreakBlock(self->entityManager, (tx << SUBPIXEL_RESOLUTION) + PA_HALF_TILE_SIZE, + ((ty - 1) << SUBPIXEL_RESOLUTION) + PA_HALF_TILE_SIZE); + break; + case PA_DIRECTION_NONE: + default: + break; + case PA_DIRECTION_SOUTH: + pa_createBreakBlock(self->entityManager, (tx << SUBPIXEL_RESOLUTION) + PA_HALF_TILE_SIZE, + ((ty + 1) << SUBPIXEL_RESOLUTION) + PA_HALF_TILE_SIZE); + break; + } + + self->state = PA_EN_ST_BREAK_BLOCK; + self->tempStateTimer = self->stateTimer; + self->stateTimer = 16; + pa_enemyChangeDirection(self, newDirection, speed); +} + +void pa_animateEnemy(paEntity_t* self) +{ + if (self->xspeed != 0) + { + if ((self->xspeed < 0) || (self->xspeed > 0)) + { + // Running + self->spriteFlipHorizontal = (self->xspeed > 0) ? 0 : 1; + + if (self->gameData->frameCount % 5 == 0) + { + self->spriteIndex + = PA_SP_ENEMY_SIDE_1 + ((self->spriteIndex + 1) % 2) + ((self->state != PA_EN_ST_NORMAL) ? 4 : 0); + self->facingDirection = self->spriteFlipHorizontal ? PA_DIRECTION_WEST : PA_DIRECTION_EAST; + } + } + else + { + // self->spriteIndex = SP_PLAYER_SLIDE; + } + } + else if (self->yspeed > 0) + { + if (self->yspeed > 0) + { + if (self->gameData->frameCount % 5 == 0) + { + self->spriteIndex = PA_SP_ENEMY_SOUTH + ((self->state != PA_EN_ST_NORMAL) ? 4 : 0); + self->spriteFlipHorizontal = (self->gameData->frameCount >> 1) % 2; + self->facingDirection = PA_DIRECTION_SOUTH; + } + } + } + else if (self->yspeed < 0) + { + if (self->yspeed < 0) + { + if (self->gameData->frameCount % 5 == 0) + { + self->spriteIndex = PA_SP_ENEMY_NORTH + ((self->state != PA_EN_ST_NORMAL) ? 4 : 0); + self->spriteFlipHorizontal = (self->gameData->frameCount >> 1) % 2; + self->facingDirection = PA_DIRECTION_NORTH; + } + } + } + else + { + self->facingDirection = PA_DIRECTION_NONE; + } +} + +void updateHitBlock(paEntity_t* self) +{ + self->animationTimer++; + + if (self->homeTileY > self->tilemap->mapHeight) + { + pa_destroyEntity(self, false); + return; + } + + pa_moveEntityWithTileCollisions(self); +} + +void pa_moveEntityWithTileCollisions(paEntity_t* self) +{ + uint16_t newX = self->x; + uint16_t newY = self->y; + uint8_t tx = PA_TO_TILECOORDS(self->x >> SUBPIXEL_RESOLUTION); + uint8_t ty = PA_TO_TILECOORDS(self->y >> SUBPIXEL_RESOLUTION); + // bool collision = false; + + // Are we inside a block? Push self out of block + uint8_t t = pa_getTile(self->tilemap, tx, ty); + self->overlapTileHandler(self, t, tx, ty); + + if (pa_isSolid(t)) + { + if (self->xspeed == 0 && self->yspeed == 0) + { + newX += (self->spriteFlipHorizontal) ? 16 : -16; + } + else + { + if (self->yspeed != 0) + { + self->yspeed = -self->yspeed; + } + else + { + self->xspeed = -self->xspeed; + } + } + } + else + { + if (self->yspeed != 0) + { + int16_t hcof = (((self->x >> SUBPIXEL_RESOLUTION) % PA_TILE_SIZE) - PA_HALF_TILESIZE); + + // Handle halfway though tile + uint8_t at = pa_getTile(self->tilemap, tx + SIGNOF(hcof), ty); + + if (pa_isSolid(at)) + { + // collision = true; + newX = ((tx + 1) * PA_TILE_SIZE - PA_HALF_TILESIZE) << SUBPIXEL_RESOLUTION; + } + + uint8_t newTy = PA_TO_TILECOORDS(((self->y + self->yspeed) >> SUBPIXEL_RESOLUTION) + + SIGNOF(self->yspeed) * PA_HALF_TILESIZE); + + if (newTy != ty) + { + uint8_t newVerticalTile = pa_getTile(self->tilemap, tx, newTy); + + // if (newVerticalTile > PA_TILE_UNUSED_29 && newVerticalTile < PA_TILE_BG_GOAL_ZONE) + { + if (self->tileCollisionHandler(self, newVerticalTile, tx, newTy, 2 << (self->yspeed > 0))) + { + newY = ((newTy + ((ty < newTy) ? -1 : 1)) * PA_TILE_SIZE + PA_HALF_TILESIZE) + << SUBPIXEL_RESOLUTION; + } + } + } + } + + if (self->xspeed != 0) + { + int16_t vcof = (((self->y >> SUBPIXEL_RESOLUTION) % PA_TILE_SIZE) - PA_HALF_TILESIZE); + + // Handle halfway though tile + uint8_t att = pa_getTile(self->tilemap, tx, ty + SIGNOF(vcof)); + + if (pa_isSolid(att)) + { + // collision = true; + newY = ((ty + 1) * PA_TILE_SIZE - PA_HALF_TILESIZE) << SUBPIXEL_RESOLUTION; + } + + // Handle outside of tile + uint8_t newTx = PA_TO_TILECOORDS(((self->x + self->xspeed) >> SUBPIXEL_RESOLUTION) + + SIGNOF(self->xspeed) * PA_HALF_TILESIZE); + + if (newTx != tx) + { + uint8_t newHorizontalTile = pa_getTile(self->tilemap, newTx, ty); + + // if (newHorizontalTile > PA_TILE_UNUSED_29 && newHorizontalTile < PA_TILE_BG_GOAL_ZONE) + { + if (self->tileCollisionHandler(self, newHorizontalTile, newTx, ty, (self->xspeed > 0))) + { + newX = ((newTx + ((tx < newTx) ? -1 : 1)) * PA_TILE_SIZE + PA_HALF_TILESIZE) + << SUBPIXEL_RESOLUTION; + } + } + + if (!self->falling) + { + uint8_t newBelowTile = pa_getTile(self->tilemap, tx, ty + 1); + + if ((self->gravityEnabled + && !pa_isSolid( + newBelowTile)) /*(|| (!self->gravityEnabled && newBelowTile != PA_TILE_LADDER)*/) + { + self->fallOffTileHandler(self); + } + } + } + } + } + + self->x = newX + self->xspeed; + self->y = newY + self->yspeed; +} + +void defaultFallOffTileHandler(paEntity_t* self) +{ + self->falling = true; +} + +void applyDamping(paEntity_t* self) +{ + if (self->xspeed > 0) + { + self->xspeed -= self->xDamping; + + if (self->xspeed < 0) + { + self->xspeed = 0; + } + } + else if (self->xspeed < 0) + { + self->xspeed += self->xDamping; + + if (self->xspeed > 0) + { + self->xspeed = 0; + } + } + + if (self->yspeed > 0) + { + self->yspeed -= self->yDamping; + + if (self->yspeed < 0) + { + self->yspeed = 0; + } + } + else if (self->yspeed < 0) + { + self->yspeed += self->yDamping; + + if (self->yspeed > 0) + { + self->yspeed = 0; + } + } +} + +void applyGravity(paEntity_t* self) +{ + if (!self->gravityEnabled || !self->falling) + { + return; + } + + self->yspeed += self->gravity; + + if (self->yspeed > self->yMaxSpeed) + { + self->yspeed = self->yMaxSpeed; + } +} + +void despawnWhenOffscreen(paEntity_t* self) +{ + if ((self->x >> SUBPIXEL_RESOLUTION) < (self->tilemap->mapOffsetX - DESPAWN_THRESHOLD) + || (self->x >> SUBPIXEL_RESOLUTION) + > (self->tilemap->mapOffsetX + PA_TILE_MAP_DISPLAY_WIDTH_PIXELS + DESPAWN_THRESHOLD)) + { + pa_destroyEntity(self, true); + } + + if (self->y > 63616) + { + return; + } + + if ((self->y >> SUBPIXEL_RESOLUTION) < (self->tilemap->mapOffsetY - (DESPAWN_THRESHOLD << 2)) + || (self->y >> SUBPIXEL_RESOLUTION) + > (self->tilemap->mapOffsetY + PA_TILE_MAP_DISPLAY_HEIGHT_PIXELS + DESPAWN_THRESHOLD)) + { + pa_destroyEntity(self, true); + } +} + +void pa_destroyEntity(paEntity_t* self, bool respawn) +{ + /*if (respawn && !(self->homeTileX == 0 && self->homeTileY == 0)) + { + self->tilemap->map[self->homeTileY * self->tilemap->mapWidth + self->homeTileX] = self->type + 128; + }*/ + + // self->entityManager->activeEntities--; + self->active = false; +} + +void animatePlayer(paEntity_t* self) +{ + if (abs(self->xspeed) > abs(self->yspeed)) + { + if (((self->gameData->btnState & PB_LEFT) && self->xspeed < 0) + || ((self->gameData->btnState & PB_RIGHT) && self->xspeed > 0)) + { + // Running + self->spriteFlipHorizontal = (self->xspeed > 0) ? 0 : 1; + self->facingDirection = self->spriteFlipHorizontal ? PA_DIRECTION_WEST : PA_DIRECTION_EAST; + + if (self->gameData->frameCount % 7 == 0) + { + self->spriteIndex = PA_SP_PLAYER_SIDE + ((self->spriteIndex + 1) % 3); + } + } + else + { + // self->spriteIndex = SP_PLAYER_SLIDE; + } + } + else if (self->yspeed > 0) + { + if ((self->gameData->btnState & PB_DOWN) && self->yspeed > 0) + { + self->facingDirection = PA_DIRECTION_SOUTH; + + if (self->gameData->frameCount % 7 == 0) + { + self->spriteIndex = PA_SP_PLAYER_SOUTH + ((self->spriteIndex + 1) % 2); + self->spriteFlipHorizontal = (self->gameData->frameCount >> 1) % 2; + } + } + } + else if (self->yspeed < 0) + { + if ((self->gameData->btnState & PB_UP) && self->yspeed < 0) + { + self->facingDirection = PA_DIRECTION_NORTH; + + if (self->gameData->frameCount % 7 == 0) + { + self->spriteIndex = PA_SP_PLAYER_NORTH + ((self->spriteIndex + 1) % 2); + self->spriteFlipHorizontal = (self->gameData->frameCount >> 1) % 2; + } + } + } + else + { + // Standing + // self->spriteIndex = PA_SP_PLAYER_SOUTH; + } +} + +void pa_detectEntityCollisions(paEntity_t* self) +{ + for (uint8_t i = 0; i < MAX_ENTITIES; i++) + { + paEntity_t* checkEntity = &(self->entityManager->entities[i]); + if (checkEntity->active && checkEntity != self) + { + uint32_t dist = abs(self->x - checkEntity->x) + abs(self->y - checkEntity->y); + + if (dist < 200) + { + self->collisionHandler(self, checkEntity); + } + } + } +} + +void pa_playerCollisionHandler(paEntity_t* self, paEntity_t* other) +{ + switch (other->type) + { + case PA_ENTITY_CRABDOZER: + { + other->xspeed = -other->xspeed; + + /*if (self->y < other->y || self->yspeed > 0) + { + pa_scorePoints(self->gameData, other->scoreValue); + + killEnemy(other); + soundPlaySfx(&(self->soundManager->sndSquish), BZR_LEFT); + + self->yspeed = -180; + self->jumpPower = 64 + ((abs(self->xspeed) + 16) >> 3); + self->falling = true; + } + else*/ + if (self->invincibilityFrames <= 0 && other->state != PA_EN_ST_STUN) + { + self->hp--; + pa_updateLedsHpMeter(self->entityManager, self->gameData); + self->gameData->comboTimer = 0; + + if (!self->gameData->debugMode && self->hp == 0) + { + self->updateFunction = &updateEntityDead; + self->type = ENTITY_DEAD; + self->xspeed = 0; + self->yspeed = -60; + self->spriteIndex = PA_SP_PLAYER_HURT; + self->gameData->changeState = PA_ST_DEAD; + self->gravityEnabled = true; + self->falling = true; + } + else + { + self->xspeed = 0; + self->yspeed = 0; + self->jumpPower = 0; + self->invincibilityFrames = 120; + soundPlaySfx(&(self->soundManager->sndHurt), BZR_LEFT); + } + } + + break; + } + case ENTITY_HIT_BLOCK: + { + if (self->x < other->x) + { + self->x = other->x - (PA_TILE_SIZE << SUBPIXEL_RESOLUTION); + self->xspeed = 0; + } + else if (self->x > other->x) + { + self->x = other->x + (PA_TILE_SIZE << SUBPIXEL_RESOLUTION); + self->xspeed = 0; + } + else if (self->y < other->y) + { + self->y = other->y - (PA_TILE_SIZE << SUBPIXEL_RESOLUTION); + self->yspeed = 0; + } + else if (self->y > other->y) + { + self->y = other->y + (PA_TILE_SIZE << SUBPIXEL_RESOLUTION); + self->yspeed = 0; + } + break; + } + default: + { + break; + } + } +} + +void pa_enemyCollisionHandler(paEntity_t* self, paEntity_t* other) +{ + switch (other->type) + { + case PA_ENTITY_CRABDOZER: + if ((self->xspeed > 0 && self->x < other->x) || (self->xspeed < 0 && self->x > other->x)) + { + self->xspeed = -self->xspeed; + // self->spriteFlipHorizontal = -self->spriteFlipHorizontal; + } + + if ((self->yspeed > 0 && self->y < other->y) || (self->yspeed < 0 && self->y > other->y)) + { + self->yspeed = -self->yspeed; + // self->spriteFlipHorizontal = -self->spriteFlipHorizontal; + } + break; + case ENTITY_HIT_BLOCK: + self->xspeed = other->xspeed * 2; + self->yspeed = other->yspeed * 2; + pa_scorePoints(self->gameData, self->scoreValue); + soundPlaySfx(&(self->soundManager->sndHurt), 2); + killEnemy(self); + break; + default: + { + break; + } + } +} + +void pa_dummyCollisionHandler(paEntity_t* self, paEntity_t* other) +{ + return; +} + +bool pa_playerTileCollisionHandler(paEntity_t* self, uint8_t tileId, uint8_t tx, uint8_t ty, uint8_t direction) +{ + /*switch (tileId) + { + case PA_TILE_COIN_1 ... PA_TILE_COIN_3: + { + pa_setTile(self->tilemap, tx, ty, PA_TILE_EMPTY); + addCoins(self->gameData, 1); + pa_scorePoints(self->gameData, 50); + break; + } + case PA_TILE_LADDER: + { + self->gravityEnabled = false; + self->falling = false; + break; + } + default: + { + break; + } + }*/ + + if (pa_isSolid(tileId)) + { + switch (direction) + { + case 0: // LEFT + self->xspeed = 0; + break; + case 1: // RIGHT + self->xspeed = 0; + break; + case 2: // UP + self->yspeed = 0; + break; + case 4: // DOWN + // Landed on platform + self->falling = false; + self->yspeed = 0; + break; + default: // Should never hit + return false; + } + // trigger tile collision resolution + return true; + } + + return false; +} + +bool pa_enemyTileCollisionHandler(paEntity_t* self, uint8_t tileId, uint8_t tx, uint8_t ty, uint8_t direction) +{ + /*switch (tileId) + { + case PA_TILE_BOUNCE_BLOCK: + { + switch (direction) + { + case 0: + // hitBlock->xspeed = -64; + if (tileId == PA_TILE_BOUNCE_BLOCK) + { + self->xspeed = 48; + } + break; + case 1: + // hitBlock->xspeed = 64; + if (tileId == PA_TILE_BOUNCE_BLOCK) + { + self->xspeed = -48; + } + break; + case 2: + // hitBlock->yspeed = -128; + if (tileId == PA_TILE_BOUNCE_BLOCK) + { + self->yspeed = 48; + } + break; + case 4: + // hitBlock->yspeed = (tileId == PA_TILE_BRICK_BLOCK) ? 32 : 64; + if (tileId == PA_TILE_BOUNCE_BLOCK) + { + self->yspeed = -48; + } + break; + default: + break; + } + break; + } + default: + { + break; + } + }*/ + + if (pa_isSolid(tileId)) + { + switch (direction) + { + case 0: // LEFT + self->xspeed = 0; + break; + case 1: // RIGHT + self->xspeed = 0; + break; + case 2: // UP + self->yspeed = 0; + break; + case 4: // DOWN + // Landed on platform + self->falling = false; + self->yspeed = 0; + break; + default: // Should never hit + return false; + } + // trigger tile collision resolution + return true; + } + + return false; +} + +bool pa_dummyTileCollisionHandler(paEntity_t* self, uint8_t tileId, uint8_t tx, uint8_t ty, uint8_t direction) +{ + return false; +} + +bool pa_hitBlockTileCollisionHandler(paEntity_t* self, uint8_t tileId, uint8_t tx, uint8_t ty, uint8_t direction) +{ + if (pa_isSolid(tileId)) + { + soundPlaySfx(&(self->soundManager->sndHit), 1); + pa_destroyEntity(self, false); + + if (PA_TO_TILECOORDS(self->x >> SUBPIXEL_RESOLUTION) == self->homeTileX + && PA_TO_TILECOORDS(self->y >> SUBPIXEL_RESOLUTION) == self->homeTileY) + { + pa_createBreakBlock(self->entityManager, self->x >> SUBPIXEL_RESOLUTION, self->y >> SUBPIXEL_RESOLUTION); + if (self->jumpPower == PA_TILE_SPAWN_BLOCK_0) + { + self->entityManager->gameData->remainingEnemies--; + } + } + else + { + self->tilemap->map[PA_TO_TILECOORDS(self->y >> SUBPIXEL_RESOLUTION) * self->tilemap->mapWidth + + PA_TO_TILECOORDS(self->x >> SUBPIXEL_RESOLUTION)] + = self->jumpPower; + } + + return true; + } + return false; +} + +void dieWhenFallingOffScreen(paEntity_t* self) +{ + uint16_t deathBoundary = (self->tilemap->mapOffsetY + PA_TILE_MAP_DISPLAY_HEIGHT_PIXELS + DESPAWN_THRESHOLD); + if (((self->y >> SUBPIXEL_RESOLUTION) > deathBoundary) + && ((self->y >> SUBPIXEL_RESOLUTION) < deathBoundary + DESPAWN_THRESHOLD)) + { + self->hp = 0; + pa_updateLedsHpMeter(self->entityManager, self->gameData); + self->gameData->changeState = PA_ST_DEAD; + pa_destroyEntity(self, true); + } +} + +void pa_updateDummy(paEntity_t* self) +{ + // Do nothing, because that's what dummies do! +} + +void pa_updateBreakBlock(paEntity_t* self) +{ + if (self->gameData->frameCount % 4 == 0) + { + self->spriteIndex++; + + if (self->spriteIndex > PA_SP_BREAK_BLOCK_3) + { + pa_createBlockFragment(self->entityManager, self->x >> SUBPIXEL_RESOLUTION, self->y >> SUBPIXEL_RESOLUTION); + pa_createBlockFragment(self->entityManager, self->x >> SUBPIXEL_RESOLUTION, self->y >> SUBPIXEL_RESOLUTION); + pa_createBlockFragment(self->entityManager, self->x >> SUBPIXEL_RESOLUTION, self->y >> SUBPIXEL_RESOLUTION); + pa_createBlockFragment(self->entityManager, self->x >> SUBPIXEL_RESOLUTION, self->y >> SUBPIXEL_RESOLUTION); + pa_destroyEntity(self, false); + } + } +} + +void updateEntityDead(paEntity_t* self) +{ + applyGravity(self); + self->x += self->xspeed; + self->y += self->yspeed; + + despawnWhenOffscreen(self); +} + +void pa_updateBlockFragment(paEntity_t* self) +{ + self->animationTimer++; + if (self->animationTimer > 8) + { + pa_destroyEntity(self, false); + return; + } + + self->x += self->xspeed; + self->y += self->yspeed; + + applyGravity(self); + despawnWhenOffscreen(self); +} + +void killEnemy(paEntity_t* target) +{ + target->homeTileX = 0; + target->homeTileY = 0; + target->gravityEnabled = true; + target->falling = true; + target->type = ENTITY_DEAD; + target->spriteFlipVertical = true; + target->updateFunction = &updateEntityDead; + + target->entityManager->activeEnemies--; + if (target->state == PA_EN_ST_AGGRESSIVE) + { + target->entityManager->aggroEnemies--; + } + + if (target->entityManager->activeEnemies == 0 && target->entityManager->gameData->remainingEnemies == 0) + { + target->gameData->changeState = PA_ST_LEVEL_CLEAR; + target->entityManager->playerEntity->spriteIndex = PA_SP_PLAYER_WIN; + target->entityManager->playerEntity->updateFunction = &pa_updateDummy; + } +} + +void pa_playerOverlapTileHandler(paEntity_t* self, uint8_t tileId, uint8_t tx, uint8_t ty) +{ + /*switch (tileId) + { + case PA_TILE_COIN_1 ... PA_TILE_COIN_3: + { + pa_setTile(self->tilemap, tx, ty, PA_TILE_EMPTY); + addCoins(self->gameData, 1); + pa_scorePoints(self->gameData, 50); + break; + } + case PA_TILE_LADDER: + { + if (self->gravityEnabled) + { + self->gravityEnabled = false; + self->xspeed = 0; + } + break; + } + default: + { + break; + } + } + + if (!self->gravityEnabled && tileId != PA_TILE_LADDER) + { + self->gravityEnabled = true; + self->falling = true; + if (self->yspeed < 0) + { + self->yspeed = -32; + } + }*/ +} + +void pa_defaultOverlapTileHandler(paEntity_t* self, uint8_t tileId, uint8_t tx, uint8_t ty) +{ + // Nothing to do. +} + +void killPlayer(paEntity_t* self) +{ + self->hp = 0; + pa_updateLedsHpMeter(self->entityManager, self->gameData); + + self->updateFunction = &updateEntityDead; + self->type = ENTITY_DEAD; + self->xspeed = 0; + self->yspeed = -60; + self->spriteIndex = PA_SP_PLAYER_HURT; + self->gameData->changeState = PA_ST_DEAD; + self->falling = true; +} + +void drawEntityTargetTile(paEntity_t* self) +{ + drawRect((self->targetTileX << PA_TILE_SIZE_IN_POWERS_OF_2) - self->tilemap->mapOffsetX, + self->targetTileY << PA_TILE_SIZE_IN_POWERS_OF_2, + (self->targetTileX << PA_TILE_SIZE_IN_POWERS_OF_2) + PA_TILE_SIZE - self->tilemap->mapOffsetX, + (self->targetTileY << PA_TILE_SIZE_IN_POWERS_OF_2) + PA_TILE_SIZE, esp_random() % 216); +} diff --git a/main/modes/games/pango/paEntity.h b/main/modes/games/pango/paEntity.h new file mode 100644 index 000000000..dc89f4587 --- /dev/null +++ b/main/modes/games/pango/paEntity.h @@ -0,0 +1,191 @@ +#ifndef _PA_ENTITY_H_ +#define _PA_ENTITY_H_ + +//============================================================================== +// Includes +//============================================================================== + +#include +#include +#include "pango_typedef.h" +#include "paTilemap.h" +#include "paGameData.h" +#include "paSoundManager.h" +#include "shapes.h" + +//============================================================================== +// Enums +//============================================================================== + +typedef enum +{ + ENTITY_PLAYER, + PA_ENTITY_CRABDOZER, + PA_ENTITY_BREAK_BLOCK, + PA_ENTITY_BLOCK_FRAGMENT, + ENTITY_HIT_BLOCK, + ENTITY_DEAD +} paEntityIndex_t; + +typedef enum +{ + PA_DIRECTION_NONE, + PA_DIRECTION_NORTH, + PA_DIRECTION_SOUTH, + PA_DIRECTION_NULL_3, + PA_DIRECTION_WEST, + PA_DIRECTION_NORTHWEST, + PA_DIRECTION_SOUTHWEST, + PA_DIRECTION_NULL_7, + PA_DIRECTION_EAST, + PA_DIRECTION_NORTHEAST, + PA_DIRECTION_SOUTHEAST +} paCompassDirection_t; + +typedef enum +{ + PA_EN_ST_SPAWNING, + PA_EN_ST_STUN, + PA_EN_ST_NORMAL, + PA_EN_ST_AGGRESSIVE, + PA_EN_ST_RUNAWAY, + PA_EN_ST_BREAK_BLOCK, +} paEnemyState_t; + +typedef enum +{ + PA_PL_ST_NORMAL, + PA_PL_ST_PUSHING +} paPlayerState_t; + +//============================================================================== +// Structs +//============================================================================== + +typedef void (*pa_updateFunction_t)(struct paEntity_t* self); +typedef void (*pa_collisionHandler_t)(struct paEntity_t* self, struct paEntity_t* other); +typedef bool (*PA_TILE_CollisionHandler_t)(struct paEntity_t* self, uint8_t tileId, uint8_t tx, uint8_t ty, + uint8_t direction); +typedef void (*pa_fallOffTileHandler_t)(struct paEntity_t* self); +typedef void (*pa_overlapTileHandler_t)(struct paEntity_t* self, uint8_t tileId, uint8_t tx, uint8_t ty); + +struct paEntity_t +{ + bool active; + // bool important; + + uint8_t type; + pa_updateFunction_t updateFunction; + + uint16_t x; + uint16_t y; + + int16_t xspeed; + int16_t yspeed; + + int16_t xMaxSpeed; + int16_t yMaxSpeed; + + int16_t xDamping; + int16_t yDamping; + + bool gravityEnabled; + int16_t gravity; + bool falling; + + uint16_t facingDirection; + + uint8_t spriteIndex; + bool spriteFlipHorizontal; + bool spriteFlipVertical; + uint8_t animationTimer; + + paTilemap_t* tilemap; + paGameData_t* gameData; + paSoundManager_t* soundManager; + + uint8_t homeTileX; + uint8_t homeTileY; + + int16_t jumpPower; + + bool visible; + uint8_t hp; + int8_t invincibilityFrames; + uint16_t scoreValue; + + uint8_t targetTileX; + uint8_t targetTileY; + uint16_t state; + bool stateFlag; + int16_t stateTimer; + int16_t tempStateTimer; + int16_t baseSpeed; + + // paEntity_t *entities; + paEntityManager_t* entityManager; + + pa_collisionHandler_t collisionHandler; + PA_TILE_CollisionHandler_t tileCollisionHandler; + pa_fallOffTileHandler_t fallOffTileHandler; + pa_overlapTileHandler_t overlapTileHandler; +}; + +//============================================================================== +// Prototypes +//============================================================================== +void pa_initializeEntity(paEntity_t* self, paEntityManager_t* entityManager, paTilemap_t* tilemap, + paGameData_t* gameData, paSoundManager_t* soundManager); + +void pa_updatePlayer(paEntity_t* self); +void updateCrabdozer(paEntity_t* self); +void pa_enemyChangeDirection(paEntity_t* self, uint16_t newDirection, int16_t speed); +void pa_enemyBreakBlock(paEntity_t* self, uint16_t newDirection, int16_t speed, uint8_t tx, uint8_t ty); +void pa_animateEnemy(paEntity_t* self); +void updateHitBlock(paEntity_t* self); + +void pa_moveEntityWithTileCollisions(paEntity_t* self); +void defaultFallOffTileHandler(paEntity_t* self); + +void despawnWhenOffscreen(paEntity_t* self); + +void pa_destroyEntity(paEntity_t* self, bool respawn); + +void applyDamping(paEntity_t* self); + +void applyGravity(paEntity_t* self); + +void animatePlayer(paEntity_t* self); + +void pa_detectEntityCollisions(paEntity_t* self); + +void pa_playerCollisionHandler(paEntity_t* self, paEntity_t* other); +void pa_enemyCollisionHandler(paEntity_t* self, paEntity_t* other); +void pa_dummyCollisionHandler(paEntity_t* self, paEntity_t* other); + +bool pa_playerTileCollisionHandler(paEntity_t* self, uint8_t tileId, uint8_t tx, uint8_t ty, uint8_t direction); +bool pa_enemyTileCollisionHandler(paEntity_t* self, uint8_t tileId, uint8_t tx, uint8_t ty, uint8_t direction); +bool pa_dummyTileCollisionHandler(paEntity_t* self, uint8_t tileId, uint8_t tx, uint8_t ty, uint8_t direction); + +void dieWhenFallingOffScreen(paEntity_t* self); + +void pa_updateDummy(paEntity_t* self); + +void updateEntityDead(paEntity_t* self); + +void killEnemy(paEntity_t* target); + +void turnAroundAtEdgeOfTileHandler(paEntity_t* self); + +void pa_playerOverlapTileHandler(paEntity_t* self, uint8_t tileId, uint8_t tx, uint8_t ty); +void pa_defaultOverlapTileHandler(paEntity_t* self, uint8_t tileId, uint8_t tx, uint8_t ty); + +void killPlayer(paEntity_t* self); + +void drawEntityTargetTile(paEntity_t* self); + +bool pa_hitBlockTileCollisionHandler(paEntity_t* self, uint8_t tileId, uint8_t tx, uint8_t ty, uint8_t direction); +void pa_updateBreakBlock(paEntity_t* self); +void pa_updateBlockFragment(paEntity_t* self); + +#endif diff --git a/main/modes/games/pango/paEntityManager.c b/main/modes/games/pango/paEntityManager.c new file mode 100644 index 000000000..de3c819bf --- /dev/null +++ b/main/modes/games/pango/paEntityManager.c @@ -0,0 +1,439 @@ +//============================================================================== +// Includes +//============================================================================== + +#include +#include +#include + +#include "paEntityManager.h" +#include "esp_random.h" +#include "palette.h" +#include "soundFuncs.h" + +#include "cnfs.h" +#include "fs_wsg.h" + +//============================================================================== +// Constants +//============================================================================== +#define SUBPIXEL_RESOLUTION 4 +#define PA_TO_TILECOORDS(x) ((x) >> PA_TILE_SIZE_IN_POWERS_OF_2) + +//============================================================================== +// Functions +//============================================================================== +void pa_initializeEntityManager(paEntityManager_t* entityManager, paWsgManager_t* wsgManager, paTilemap_t* tilemap, + paGameData_t* gameData, paSoundManager_t* soundManager) +{ + entityManager->wsgManager = wsgManager; + entityManager->entities = calloc(MAX_ENTITIES, sizeof(paEntity_t)); + + for (uint8_t i = 0; i < MAX_ENTITIES; i++) + { + pa_initializeEntity(&(entityManager->entities[i]), entityManager, tilemap, gameData, soundManager); + } + + entityManager->activeEntities = 0; + entityManager->tilemap = tilemap; + entityManager->gameData = gameData; + entityManager->soundManager = soundManager; + + // entityManager->viewEntity = pa_createPlayer(entityManager, entityManager->tilemap->warps[0].x * 16, + // entityManager->tilemap->warps[0].y * 16); + entityManager->playerEntity = entityManager->viewEntity; + + // entityManager->activeEnemies = 0; + // entityManager->maxEnemies = 3; +} + +void pa_updateEntities(paEntityManager_t* entityManager) +{ + for (uint8_t i = 0; i < MAX_ENTITIES; i++) + { + if (entityManager->entities[i].active) + { + entityManager->entities[i].updateFunction(&(entityManager->entities[i])); + + /*if (&(entityManager->entities[i]) == entityManager->viewEntity) + { + pa_viewFollowEntity(entityManager->tilemap, &(entityManager->entities[i])); + }*/ + } + } +} + +void pa_deactivateAllEntities(paEntityManager_t* entityManager, bool excludePlayer) +{ + for (uint8_t i = 0; i < MAX_ENTITIES; i++) + { + paEntity_t* currentEntity = &(entityManager->entities[i]); + currentEntity->active = false; + + // clear out invisible block tiles that are placed for every Break Block object + // if(currentEntity->type == PA_ENTITY_BREAK_BLOCK){ + // pa_setTile(currentEntity->tilemap, PA_TO_TILECOORDS(currentEntity->x >> SUBPIXEL_RESOLUTION), + // PA_TO_TILECOORDS(currentEntity->y >> SUBPIXEL_RESOLUTION), PA_TILE_EMPTY); + // } + + if (currentEntity->type == ENTITY_HIT_BLOCK && currentEntity->jumpPower == PA_TILE_SPAWN_BLOCK_0) + { + entityManager->gameData->remainingEnemies--; + } + + if (excludePlayer && currentEntity == entityManager->playerEntity) + { + currentEntity->active = true; + } + } + + entityManager->activeEntities = 0; + entityManager->aggroEnemies = 0; +} + +void pa_drawEntities(paEntityManager_t* entityManager) +{ + for (uint8_t i = 0; i < MAX_ENTITIES; i++) + { + paEntity_t currentEntity = entityManager->entities[i]; + + if (currentEntity.active && currentEntity.visible) + { + drawWsg(entityManager->wsgManager->sprites[currentEntity.spriteIndex].wsg, + (currentEntity.x >> SUBPIXEL_RESOLUTION) + - entityManager->wsgManager->sprites[currentEntity.spriteIndex].originX + - entityManager->tilemap->mapOffsetX, + (currentEntity.y >> SUBPIXEL_RESOLUTION) - entityManager->tilemap->mapOffsetY + - entityManager->wsgManager->sprites[currentEntity.spriteIndex].originY, + currentEntity.spriteFlipHorizontal, currentEntity.spriteFlipVertical, 0); + } + } +} + +paEntity_t* pa_findInactiveEntity(paEntityManager_t* entityManager) +{ + if (entityManager->activeEntities == MAX_ENTITIES) + { + return NULL; + }; + + uint8_t entityIndex = 0; + + while (entityManager->entities[entityIndex].active) + { + entityIndex++; + + // Extra safeguard to make sure we don't get stuck here + if (entityIndex > MAX_ENTITIES) + { + return NULL; + } + } + + return &(entityManager->entities[entityIndex]); +} + +void pa_viewFollowEntity(paTilemap_t* tilemap, paEntity_t* entity) +{ + int16_t moveViewByX = (entity->x) >> SUBPIXEL_RESOLUTION; + int16_t moveViewByY = (entity->y > 63616) ? 0 : (entity->y) >> SUBPIXEL_RESOLUTION; + + int16_t centerOfViewX = tilemap->mapOffsetX + 140; + int16_t centerOfViewY = tilemap->mapOffsetY + 120; + + // if(centerOfViewX != moveViewByX) { + moveViewByX -= centerOfViewX; + //} + + // if(centerOfViewY != moveViewByY) { + moveViewByY -= centerOfViewY; + //} + + // if(moveViewByX && moveViewByY){ + pa_scrollTileMap(tilemap, moveViewByX, moveViewByY); + //} +} + +paEntity_t* pa_createEntity(paEntityManager_t* entityManager, uint8_t objectIndex, uint16_t x, uint16_t y) +{ + // if(entityManager->activeEntities == MAX_ENTITIES){ + // return NULL; + // } + + paEntity_t* createdEntity; + + switch (objectIndex) + { + case ENTITY_PLAYER: + createdEntity = pa_createPlayer(entityManager, x, y); + break; + case PA_ENTITY_CRABDOZER: + createdEntity = createCrabdozer(entityManager, x, y); + break; + case ENTITY_HIT_BLOCK: + createdEntity = createHitBlock(entityManager, x, y); + break; + default: + createdEntity = NULL; + } + + // if(createdEntity != NULL) { + // entityManager->activeEntities++; + // } + + return createdEntity; +} + +paEntity_t* pa_createPlayer(paEntityManager_t* entityManager, uint16_t x, uint16_t y) +{ + paEntity_t* entity = pa_findInactiveEntity(entityManager); + + if (entity == NULL) + { + return NULL; + } + + entity->active = true; + entity->visible = true; + entity->x = x << SUBPIXEL_RESOLUTION; + entity->y = y << SUBPIXEL_RESOLUTION; + + entity->xspeed = 0; + entity->yspeed = 0; + entity->xMaxSpeed = 40; // 72; Walking + entity->yMaxSpeed = 64; // 72; + entity->xDamping = 2; + entity->yDamping = 2; + entity->gravityEnabled = false; + entity->gravity = 4; + entity->falling = false; + entity->jumpPower = 0; + entity->spriteFlipVertical = false; + entity->hp = 1; + entity->animationTimer = 0; // Used as a cooldown for shooting square wave balls + entity->state = PA_PL_ST_NORMAL; + entity->stateTimer = -1; + + entity->type = ENTITY_PLAYER; + entity->spriteIndex = PA_SP_PLAYER_SOUTH; + entity->updateFunction = &pa_updatePlayer; + entity->collisionHandler = &pa_playerCollisionHandler; + entity->tileCollisionHandler = &pa_playerTileCollisionHandler; + entity->fallOffTileHandler = &defaultFallOffTileHandler; + entity->overlapTileHandler = &pa_playerOverlapTileHandler; + return entity; +} + +paEntity_t* createCrabdozer(paEntityManager_t* entityManager, uint16_t x, uint16_t y) +{ + paEntity_t* entity = pa_findInactiveEntity(entityManager); + + if (entity == NULL) + { + return NULL; + } + + entity->active = true; + entity->visible = true; + entity->x = x << SUBPIXEL_RESOLUTION; + entity->y = y << SUBPIXEL_RESOLUTION; + + entity->xspeed = 0; + entity->yspeed = 0; + entity->xMaxSpeed = 132; + entity->yMaxSpeed = 132; + entity->gravityEnabled = false; + entity->gravity = 0; + entity->spriteFlipHorizontal = false; + entity->spriteFlipVertical = false; + entity->scoreValue = 100; + entity->stateTimer = -1; + entity->tempStateTimer = -1; + entity->stateFlag = false; + entity->baseSpeed = entityManager->gameData->enemyInitialSpeed; + + entity->type = PA_ENTITY_CRABDOZER; + entity->spriteIndex = PA_SP_ENEMY_SOUTH; + entity->facingDirection = PA_DIRECTION_NONE; + entity->state = PA_EN_ST_NORMAL; + entity->stateTimer = 300 + (esp_random() % 600); // Min 5 seconds, max 15 seconds + entity->updateFunction = &updateCrabdozer; + entity->collisionHandler = &pa_enemyCollisionHandler; + entity->tileCollisionHandler = &pa_enemyTileCollisionHandler; + entity->fallOffTileHandler = &defaultFallOffTileHandler; + entity->overlapTileHandler = &pa_defaultOverlapTileHandler; + + return entity; +} + +paEntity_t* pa_createBreakBlock(paEntityManager_t* entityManager, uint16_t x, uint16_t y) +{ + paEntity_t* entity = pa_findInactiveEntity(entityManager); + + if (entity == NULL) + { + return NULL; + } + + entity->active = true; + entity->visible = true; + entity->x = x << SUBPIXEL_RESOLUTION; + entity->y = y << SUBPIXEL_RESOLUTION; + + entity->xspeed = 0; + entity->yspeed = 0; + entity->xMaxSpeed = 132; + entity->yMaxSpeed = 132; + entity->gravityEnabled = false; + entity->gravity = 0; + entity->spriteFlipHorizontal = false; + entity->spriteFlipVertical = false; + entity->scoreValue = 100; + entity->animationTimer = 0; + entity->type = PA_ENTITY_BREAK_BLOCK; + entity->spriteIndex = PA_SP_BREAK_BLOCK; + entity->facingDirection = PA_DIRECTION_NONE; + entity->updateFunction = &pa_updateBreakBlock; + entity->collisionHandler = &pa_dummyCollisionHandler; + entity->tileCollisionHandler = &pa_dummyTileCollisionHandler; + entity->fallOffTileHandler = &defaultFallOffTileHandler; + entity->overlapTileHandler = &pa_defaultOverlapTileHandler; + + pa_setTile(entityManager->tilemap, PA_TO_TILECOORDS(x), PA_TO_TILECOORDS(y), PA_TILE_EMPTY); + + return entity; +} + +paEntity_t* pa_createBlockFragment(paEntityManager_t* entityManager, uint16_t x, uint16_t y) +{ + paEntity_t* entity = pa_findInactiveEntity(entityManager); + + if (entity == NULL) + { + return NULL; + } + + entity->active = true; + entity->visible = true; + entity->x = x << SUBPIXEL_RESOLUTION; + entity->y = y << SUBPIXEL_RESOLUTION; + + entity->xspeed = -64 + (esp_random() % 128); + entity->yspeed = -64 + (esp_random() % 128); + entity->xMaxSpeed = 132; + entity->yMaxSpeed = 132; + entity->gravityEnabled = true; + entity->gravity = 0; + entity->spriteFlipHorizontal = false; + entity->spriteFlipVertical = false; + entity->scoreValue = 100; + entity->animationTimer = 0; + entity->type = PA_ENTITY_BLOCK_FRAGMENT; + entity->spriteIndex = PA_SP_BLOCK_FRAGMENT; + entity->facingDirection = PA_DIRECTION_NONE; + entity->updateFunction = &pa_updateBlockFragment; + entity->collisionHandler = &pa_dummyCollisionHandler; + entity->tileCollisionHandler = &pa_dummyTileCollisionHandler; + entity->fallOffTileHandler = &defaultFallOffTileHandler; + entity->overlapTileHandler = &pa_defaultOverlapTileHandler; + + return entity; +} + +paEntity_t* createHitBlock(paEntityManager_t* entityManager, uint16_t x, uint16_t y) +{ + paEntity_t* entity = pa_findInactiveEntity(entityManager); + + if (entity == NULL) + { + return NULL; + } + + entity->active = true; + entity->visible = true; + entity->x = x << SUBPIXEL_RESOLUTION; + entity->y = y << SUBPIXEL_RESOLUTION; + entity->homeTileX = PA_TO_TILECOORDS(x); + entity->homeTileY = PA_TO_TILECOORDS(y); + + entity->xspeed = 0; + entity->yspeed = 0; + entity->yDamping = 0; + entity->xMaxSpeed = 132; + entity->yMaxSpeed = 132; + entity->gravityEnabled = true; + entity->gravity = 4; + + entity->spriteFlipHorizontal = false; + entity->spriteFlipVertical = false; + + entity->type = ENTITY_HIT_BLOCK; + entity->spriteIndex = PA_SP_BLOCK; + entity->animationTimer = 0; + entity->updateFunction = &updateHitBlock; + entity->collisionHandler = &pa_dummyCollisionHandler; + entity->tileCollisionHandler = &pa_hitBlockTileCollisionHandler; + entity->overlapTileHandler = &pa_defaultOverlapTileHandler; + + return entity; +} + +void pa_freeEntityManager(paEntityManager_t* self) +{ + free(self->entities); +} + +paEntity_t* pa_spawnEnemyFromSpawnBlock(paEntityManager_t* entityManager) +{ + paEntity_t* newEnemy = NULL; + + if (entityManager->gameData->remainingEnemies > 0 + && entityManager->activeEnemies < entityManager->gameData->maxActiveEnemies) + { + uint16_t iterations = 0; + while (newEnemy == NULL && iterations < 2) + { + for (uint16_t ty = 1; ty < 14; ty++) + { + for (uint16_t tx = 1; tx < 16; tx++) + { + uint8_t t = pa_getTile(entityManager->tilemap, tx, ty); + + if (t == PA_TILE_SPAWN_BLOCK_0 + && (iterations > 0 || !(esp_random() % entityManager->gameData->remainingEnemies))) + { + newEnemy + = createCrabdozer(entityManager, (tx << PA_TILE_SIZE_IN_POWERS_OF_2) + PA_HALF_TILE_SIZE, + (ty << PA_TILE_SIZE_IN_POWERS_OF_2) + PA_HALF_TILE_SIZE); + + if (newEnemy != NULL) + { + // pa_setTile(entityManager->tilemap, tx, ty, PA_TILE_EMPTY); + newEnemy->state = PA_EN_ST_STUN; + newEnemy->stateTimer = 120; + /*if(entityManager->activeEnemies == 0 || entityManager->gameData->remainingEnemies == 1){ + //The first and last enemies are permanently angry + newEnemy->stateFlag = true; + }*/ + + paEntity_t* newBreakBlock = pa_createBreakBlock( + entityManager, (tx << PA_TILE_SIZE_IN_POWERS_OF_2) + PA_HALF_TILE_SIZE, + (ty << PA_TILE_SIZE_IN_POWERS_OF_2) + PA_HALF_TILE_SIZE); + if (newBreakBlock != NULL) + { + soundPlaySfx(&(entityManager->soundManager->sndSpawn), 3); + } + + entityManager->activeEnemies++; + entityManager->gameData->remainingEnemies--; + return newEnemy; + } + } + } + } + iterations++; + } + } + + return newEnemy; +} \ No newline at end of file diff --git a/main/modes/games/pango/paEntityManager.h b/main/modes/games/pango/paEntityManager.h new file mode 100644 index 000000000..48412900f --- /dev/null +++ b/main/modes/games/pango/paEntityManager.h @@ -0,0 +1,68 @@ +#ifndef _PA_ENTITYMANAGER_H_ +#define _PA_ENTITYMANAGER_H_ + +//============================================================================== +// Includes +//============================================================================== + +#include +#include +#include "pango_typedef.h" +#include "paEntity.h" +#include "paTilemap.h" +#include "paGameData.h" +#include "hdw-tft.h" +#include "paSprite.h" +#include "paSoundManager.h" +#include "paWsgManager.h" + +//============================================================================== +// Constants +//============================================================================== +#define MAX_ENTITIES 32 +#define SPRITESET_SIZE 32 + +//============================================================================== +// Structs +//============================================================================== + +struct paEntityManager_t +{ + paEntity_t* entities; + uint8_t activeEntities; + + int16_t activeEnemies; + int16_t aggroEnemies; + + paGameData_t* gameData; + + paEntity_t* viewEntity; + paEntity_t* playerEntity; + + paWsgManager_t* wsgManager; + paTilemap_t* tilemap; + + paSoundManager_t* soundManager; +}; + +//============================================================================== +// Prototypes +//============================================================================== +void pa_initializeEntityManager(paEntityManager_t* entityManager, paWsgManager_t* wsgManager, paTilemap_t* tilemap, + paGameData_t* gameData, paSoundManager_t* soundManager); +void pa_updateEntities(paEntityManager_t* entityManager); +void pa_deactivateAllEntities(paEntityManager_t* entityManager, bool excludePlayer); +void pa_drawEntities(paEntityManager_t* entityManager); +paEntity_t* pa_findInactiveEntity(paEntityManager_t* entityManager); + +void pa_viewFollowEntity(paTilemap_t* tilemap, paEntity_t* entity); +paEntity_t* pa_createEntity(paEntityManager_t* entityManager, uint8_t objectIndex, uint16_t x, uint16_t y); +paEntity_t* pa_createPlayer(paEntityManager_t* entityManager, uint16_t x, uint16_t y); +paEntity_t* createCrabdozer(paEntityManager_t* entityManager, uint16_t x, uint16_t y); +paEntity_t* createHitBlock(paEntityManager_t* entityManager, uint16_t x, uint16_t y); +void pa_freeEntityManager(paEntityManager_t* entityManager); +paEntity_t* pa_spawnEnemyFromSpawnBlock(paEntityManager_t* entityManager); +paEntity_t* pa_createBreakBlock(paEntityManager_t* entityManager, uint16_t x, uint16_t y); +paEntity_t* pa_createBlockFragment(paEntityManager_t* entityManager, uint16_t x, uint16_t y); + +#endif diff --git a/main/modes/games/pango/paGameData.c b/main/modes/games/pango/paGameData.c new file mode 100644 index 000000000..cf394f013 --- /dev/null +++ b/main/modes/games/pango/paGameData.c @@ -0,0 +1,282 @@ +//============================================================================== +// Includes +//============================================================================== + +#include +#include "paGameData.h" +#include "paTables.h" +#include "paEntityManager.h" +#include "esp_random.h" +#include "hdw-btn.h" +#include "soundFuncs.h" + +//============================================================================== +// Functions +//============================================================================== +void pa_initializeGameData(paGameData_t* gameData, paSoundManager_t* soundManager) +{ + gameData->gameState = 0; + gameData->btnState = 0; + gameData->score = 0; + gameData->lives = 3; + gameData->countdown = 000; + gameData->world = 1; + gameData->level = 1; + gameData->frameCount = 0; + gameData->coins = 0; + gameData->combo = 0; + gameData->comboTimer = 0; + gameData->bgColor = c000; + gameData->initials[0] = 'A'; + gameData->initials[1] = 'A'; + gameData->initials[2] = 'A'; + gameData->rank = 5; + gameData->extraLifeCollected = false; + gameData->checkpoint = 0; + gameData->levelDeaths = 0; + gameData->initialHp = 1; + gameData->debugMode = false; + gameData->continuesUsed = false; + gameData->inGameTimer = 0; + gameData->soundManager = soundManager; +} + +void pa_initializeGameDataFromTitleScreen(paGameData_t* gameData, uint16_t levelIndex) +{ + gameData->gameState = 0; + gameData->btnState = 0; + gameData->score = 0; + gameData->lives = 3; + gameData->countdown = 000; + gameData->frameCount = 0; + gameData->coins = 0; + gameData->combo = 0; + gameData->comboTimer = 0; + gameData->bgColor = c000; + gameData->extraLifeCollected = false; + gameData->checkpoint = 0; + gameData->levelDeaths = 0; + gameData->currentBgm = 0; + gameData->changeBgm = 0; + gameData->initialHp = 1; + gameData->continuesUsed = (gameData->world == 1 && gameData->level == 1) ? false : true; + gameData->inGameTimer = 0; + + pa_setDifficultyLevel(gameData, levelIndex); + + pa_resetGameDataLeds(gameData); +} + +void pa_updateLedsHpMeter(paEntityManager_t* entityManager, paGameData_t* gameData) +{ + if (entityManager->playerEntity == NULL) + { + return; + } + + uint8_t hp = entityManager->playerEntity->hp; + if (hp > 3) + { + hp = 3; + } + + // HP meter led pairs: + // 3 4 + // 2 5 + // 1 6 + for (int32_t i = 1; i < 7; i++) + { + gameData->leds[i].r = 0x80; + gameData->leds[i].g = 0x00; + gameData->leds[i].b = 0x00; + } + + for (int32_t i = 1; i < 1 + hp; i++) + { + gameData->leds[i].r = 0x00; + gameData->leds[i].g = 0x80; + + gameData->leds[7 - i].r = 0x00; + gameData->leds[7 - i].g = 0x80; + } + + setLeds(gameData->leds, CONFIG_NUM_LEDS); +} + +void pa_scorePoints(paGameData_t* gameData, uint16_t points) +{ + gameData->combo++; + + uint32_t comboPoints = points * gameData->combo; + + gameData->score += comboPoints; + gameData->comboScore = comboPoints; + + gameData->comboTimer = (gameData->levelDeaths < 3) ? 240 : 1; +} + +void addCoins(paGameData_t* gameData, uint8_t coins) +{ + gameData->coins += coins; + if (gameData->coins > 99) + { + gameData->lives++; + soundPlaySfx(&(gameData->soundManager->snd1up), BZR_LEFT); + gameData->coins = 0; + } + else + { + soundPlaySfx(&(gameData->soundManager->sndCoin), BZR_LEFT); + } +} + +void updateComboTimer(paGameData_t* gameData) +{ + gameData->comboTimer--; + + if (gameData->comboTimer < 0) + { + gameData->comboTimer = 0; + gameData->combo = 0; + } +} + +void pa_resetGameDataLeds(paGameData_t* gameData) +{ + for (uint8_t i = 0; i < CONFIG_NUM_LEDS; i++) + { + gameData->leds[i].r = 0; + gameData->leds[i].g = 0; + gameData->leds[i].b = 0; + } + + setLeds(gameData->leds, CONFIG_NUM_LEDS); +} + +void pa_updateLedsShowHighScores(paGameData_t* gameData) +{ + if (((gameData->frameCount) % 10) == 0) + { + for (int32_t i = 0; i < 8; i++) + { + if (((gameData->frameCount >> 4) % CONFIG_NUM_LEDS) == i) + { + gameData->leds[i].r = 0xF0; + gameData->leds[i].g = 0xF0; + gameData->leds[i].b = 0x00; + } + + if (gameData->leds[i].r > 0) + { + gameData->leds[i].r -= 0x05; + } + + if (gameData->leds[i].g > 0) + { + gameData->leds[i].g -= 0x10; + } + + if (gameData->leds[i].b > 0) + { + gameData->leds[i].b = 0x00; + } + } + } + setLeds(gameData->leds, CONFIG_NUM_LEDS); +} + +void pa_updateLedsGameOver(paGameData_t* gameData) +{ + if (((gameData->frameCount) % 10) == 0) + { + for (int32_t i = 0; i < CONFIG_NUM_LEDS; i++) + { + if (((gameData->frameCount >> 4) % CONFIG_NUM_LEDS) == i) + { + gameData->leds[i].r = 0xF0; + gameData->leds[i].g = 0x00; + gameData->leds[i].b = 0x00; + } + + gameData->leds[i].r -= 0x10; + gameData->leds[i].g = 0x00; + gameData->leds[i].b = 0x00; + } + } + setLeds(gameData->leds, CONFIG_NUM_LEDS); +} + +void pa_updateLedsLevelClear(paGameData_t* gameData) +{ + if (((gameData->frameCount) % 10) == 0) + { + for (int32_t i = 0; i < CONFIG_NUM_LEDS; i++) + { + if (((gameData->frameCount >> 4) % CONFIG_NUM_LEDS) == i) + { + gameData->leds[i].g = (esp_random() % 24) * (10); + gameData->leds[i].b = (esp_random() % 24) * (10); + } + + if (gameData->leds[i].r > 0) + { + gameData->leds[i].r -= 0x10; + } + + if (gameData->leds[i].g > 0) + { + gameData->leds[i].g -= 0x10; + } + + if (gameData->leds[i].b > 0) + { + gameData->leds[i].b -= 0x10; + } + } + } + setLeds(gameData->leds, CONFIG_NUM_LEDS); +} + +void pa_updateLedsGameClear(paGameData_t* gameData) +{ + if (((gameData->frameCount) % 10) == 0) + { + for (int32_t i = 0; i < CONFIG_NUM_LEDS; i++) + { + if (((gameData->frameCount >> 4) % CONFIG_NUM_LEDS) == i) + { + gameData->leds[i].r = (esp_random() % 24) * (10); + gameData->leds[i].g = (esp_random() % 24) * (10); + gameData->leds[i].b = (esp_random() % 24) * (10); + } + + if (gameData->leds[i].r > 0) + { + gameData->leds[i].r -= 0x10; + } + + if (gameData->leds[i].g > 0) + { + gameData->leds[i].g -= 0x10; + } + + if (gameData->leds[i].b > 0) + { + gameData->leds[i].b -= 0x10; + } + } + } + setLeds(gameData->leds, CONFIG_NUM_LEDS); +} + +void pa_setDifficultyLevel(paGameData_t* gameData, uint16_t levelIndex) +{ + gameData->remainingEnemies + = masterDifficulty[(levelIndex * MASTER_DIFFICULTY_TABLE_ROW_LENGTH) + TOTAL_ENEMIES_LOOKUP_OFFSET]; + gameData->maxActiveEnemies + = masterDifficulty[(levelIndex * MASTER_DIFFICULTY_TABLE_ROW_LENGTH) + MAX_ACTIVE_ENEMIES_LOOKUP_OFFSET]; + gameData->enemyInitialSpeed + = masterDifficulty[(levelIndex * MASTER_DIFFICULTY_TABLE_ROW_LENGTH) + ENEMY_INITIAL_SPEED_LOOKUP_OFFSET]; + gameData->minAggroEnemies = 1; + gameData->maxAggroEnemies = 1; +} \ No newline at end of file diff --git a/main/modes/games/pango/paGameData.h b/main/modes/games/pango/paGameData.h new file mode 100644 index 000000000..2a93f4547 --- /dev/null +++ b/main/modes/games/pango/paGameData.h @@ -0,0 +1,86 @@ +#ifndef _PA_GAMEDATA_H_ +#define _PA_GAMEDATA_H_ + +//============================================================================== +// Includes +//============================================================================== + +#include +#include "hdw-led.h" +#include "pango_typedef.h" +#include "palette.h" +#include "paSoundManager.h" + +//============================================================================== +// Constants +//============================================================================== + +//============================================================================== +// Structs +//============================================================================== + +typedef struct +{ + int16_t btnState; + int16_t prevBtnState; + uint8_t gameState; + uint8_t changeState; + + uint32_t score; + uint8_t lives; + uint8_t coins; + int16_t countdown; + uint16_t frameCount; + + uint8_t world; + uint8_t level; + + uint16_t combo; + int16_t comboTimer; + uint32_t comboScore; + + bool extraLifeCollected; + uint8_t checkpoint; + uint8_t levelDeaths; + uint8_t initialHp; + + led_t leds[CONFIG_NUM_LEDS]; + + paletteColor_t bgColor; + + char initials[3]; + uint8_t rank; + bool debugMode; + + uint8_t changeBgm; + uint8_t currentBgm; + + bool continuesUsed; + uint32_t inGameTimer; + + int16_t maxActiveEnemies; + int16_t remainingEnemies; + int16_t enemyInitialSpeed; + int16_t minAggroEnemies; + int16_t maxAggroEnemies; + + paSoundManager_t* soundManager; +} paGameData_t; + +//============================================================================== +// Functions +//============================================================================== +void pa_initializeGameData(paGameData_t* gameData, paSoundManager_t* soundManager); +void pa_initializeGameDataFromTitleScreen(paGameData_t* gameData, uint16_t levelIndex); +void pa_updateLedsHpMeter(paEntityManager_t* entityManager, paGameData_t* gameData); +void pa_scorePoints(paGameData_t* gameData, uint16_t points); +void addCoins(paGameData_t* gameData, uint8_t coins); +void updateComboTimer(paGameData_t* gameData); +void pa_resetGameDataLeds(paGameData_t* gameData); +void pa_updateLedsShowHighScores(paGameData_t* gameData); +void pa_updateLedsLevelClear(paGameData_t* gameData); +void pa_updateLedsGameClear(paGameData_t* gameData); +void pa_updateLedsGameOver(paGameData_t* gameData); +void pa_setDifficultyLevel(paGameData_t* gameData, uint16_t levelIndex); + +#endif \ No newline at end of file diff --git a/main/modes/games/pango/paLeveldef.h b/main/modes/games/pango/paLeveldef.h new file mode 100644 index 000000000..b75733b85 --- /dev/null +++ b/main/modes/games/pango/paLeveldef.h @@ -0,0 +1,19 @@ +#ifndef _LEVELDEF_H_ +#define _LEVELDEF_H_ + +//============================================================================== +// Includes +//============================================================================== +#include + +//============================================================================== +// Structs +//============================================================================== +typedef struct +{ + char filename[16]; + uint16_t timeLimit; + uint16_t checkpointTimeLimit; +} paLeveldef_t; + +#endif diff --git a/main/modes/games/pango/paSoundManager.c b/main/modes/games/pango/paSoundManager.c new file mode 100644 index 000000000..9344b388c --- /dev/null +++ b/main/modes/games/pango/paSoundManager.c @@ -0,0 +1,97 @@ +//============================================================================== +// Includes +//============================================================================== + +#include +#include "paSoundManager.h" + +//============================================================================== +// Functions +//============================================================================== +void pa_initializeSoundManager(paSoundManager_t* self) +{ + loadMidiFile("bgmCastle.mid", &self->bgmCastle, false); + // self->bgmCastle.shouldLoop = true; + + loadMidiFile("bgmDeMAGio.mid", &self->bgmDemagio, false); + // self->bgmDemagio.shouldLoop = true; + + loadMidiFile("bgmGameStart.mid", &self->bgmGameStart, false); + loadMidiFile("bgmIntro.mid", &self->bgmIntro, false); + loadMidiFile("bgmNameEntry.mid", &self->bgmNameEntry, false); + // self->bgmNameEntry.shouldLoop = true; + + loadMidiFile("bgmSmooth.mid", &self->bgmSmooth, false); + // self->bgmSmooth.shouldLoop = true; + + loadMidiFile("bgmUnderground.mid", &self->bgmUnderground, false); + // self->bgmUnderground.shouldLoop = true; + + loadMidiFile("snd1up.mid", &self->snd1up, false); + // loadMidiFile("sndBreak.mid", &self->sndBreak, false); + loadMidiFile("sndCheckpoint.mid", &self->sndCheckpoint, false); + loadMidiFile("sndCoin.mid", &self->sndCoin, false); + loadMidiFile("sndDie.mid", &self->sndDie, false); + loadMidiFile("bgmGameOver.mid", &self->bgmGameOver, false); + loadMidiFile("sndBlockStop.mid", &self->sndHit, false); + loadMidiFile("sndSquish.mid", &self->sndHurt, false); + loadMidiFile("sndJump1.mid", &self->sndJump1, false); + loadMidiFile("sndJump2.mid", &self->sndJump2, false); + loadMidiFile("sndJump3.mid", &self->sndJump3, false); + loadMidiFile("sndLevelClearA.mid", &self->sndLevelClearA, false); + loadMidiFile("sndLevelClearB.mid", &self->sndLevelClearB, false); + loadMidiFile("sndLevelClearC.mid", &self->sndLevelClearC, false); + loadMidiFile("sndLevelClearD.mid", &self->sndLevelClearD, false); + loadMidiFile("sndLevelClearS.mid", &self->sndLevelClearS, false); + loadMidiFile("sndMenuConfirm.mid", &self->sndMenuConfirm, false); + loadMidiFile("sndMenuDeny.mid", &self->sndMenuDeny, false); + loadMidiFile("sndMenuSelect.mid", &self->sndMenuSelect, false); + loadMidiFile("sndOutOfTime.mid", &self->sndOuttaTime, false); + loadMidiFile("sndPause.mid", &self->sndPause, false); + loadMidiFile("sndPowerUp.mid", &self->sndPowerUp, false); + loadMidiFile("sndSlide.mid", &self->sndSquish, false); + // loadMidiFile("sndTally.mid", &self->sndTally, false); + loadMidiFile("sndWarp.mid", &self->sndWarp, false); + loadMidiFile("sndWaveBall.mid", &self->sndWaveBall, false); + + loadMidiFile("sndSpawn.mid", &self->sndSpawn, false); +} + +void pa_freeSoundManager(paSoundManager_t* self) +{ + unloadMidiFile(&self->bgmCastle); + unloadMidiFile(&self->bgmDemagio); + unloadMidiFile(&self->bgmGameStart); + unloadMidiFile(&self->bgmIntro); + unloadMidiFile(&self->bgmNameEntry); + unloadMidiFile(&self->bgmSmooth); + unloadMidiFile(&self->bgmUnderground); + unloadMidiFile(&self->snd1up); + unloadMidiFile(&self->sndBreak); + unloadMidiFile(&self->sndCheckpoint); + unloadMidiFile(&self->sndCoin); + unloadMidiFile(&self->sndDie); + unloadMidiFile(&self->bgmGameOver); + unloadMidiFile(&self->sndHit); + unloadMidiFile(&self->sndHurt); + unloadMidiFile(&self->sndJump1); + unloadMidiFile(&self->sndJump2); + unloadMidiFile(&self->sndJump3); + unloadMidiFile(&self->sndLevelClearA); + unloadMidiFile(&self->sndLevelClearB); + unloadMidiFile(&self->sndLevelClearC); + unloadMidiFile(&self->sndLevelClearD); + unloadMidiFile(&self->sndLevelClearS); + unloadMidiFile(&self->sndMenuConfirm); + unloadMidiFile(&self->sndMenuDeny); + unloadMidiFile(&self->sndMenuSelect); + unloadMidiFile(&self->sndOuttaTime); + unloadMidiFile(&self->sndPause); + unloadMidiFile(&self->sndPowerUp); + unloadMidiFile(&self->sndSquish); + unloadMidiFile(&self->sndTally); + unloadMidiFile(&self->sndWarp); + unloadMidiFile(&self->sndWaveBall); + + unloadMidiFile(&self->sndSpawn); +} \ No newline at end of file diff --git a/main/modes/games/pango/paSoundManager.h b/main/modes/games/pango/paSoundManager.h new file mode 100644 index 000000000..48c625ac5 --- /dev/null +++ b/main/modes/games/pango/paSoundManager.h @@ -0,0 +1,65 @@ +#ifndef _PA_SOUNDMANAGER_H_ +#define _PA_SOUNDMANAGER_H_ + +//============================================================================== +// Includes +//============================================================================== + +#include +#include +#include + +//============================================================================== +// Constants +//============================================================================== + +//============================================================================== +// Structs +//============================================================================== + +typedef struct +{ + midiFile_t bgmDemagio; + midiFile_t bgmIntro; + midiFile_t bgmSmooth; + midiFile_t bgmUnderground; + midiFile_t bgmCastle; + midiFile_t bgmGameStart; + midiFile_t sndDie; + midiFile_t sndMenuSelect; + midiFile_t sndMenuConfirm; + midiFile_t sndMenuDeny; + midiFile_t sndPause; + midiFile_t sndHit; + midiFile_t sndSquish; + midiFile_t sndBreak; + midiFile_t sndCoin; + midiFile_t sndPowerUp; + midiFile_t sndJump1; + midiFile_t sndJump2; + midiFile_t sndJump3; + midiFile_t sndWarp; + midiFile_t sndHurt; + midiFile_t sndWaveBall; + midiFile_t snd1up; + midiFile_t sndCheckpoint; + midiFile_t sndLevelClearD; + midiFile_t sndLevelClearC; + midiFile_t sndLevelClearB; + midiFile_t sndLevelClearA; + midiFile_t sndLevelClearS; + midiFile_t sndTally; + midiFile_t bgmNameEntry; + midiFile_t bgmGameOver; + midiFile_t sndOuttaTime; + + midiFile_t sndSpawn; +} paSoundManager_t; + +//============================================================================== +// Functions +//============================================================================== +void pa_initializeSoundManager(paSoundManager_t* self); +void pa_freeSoundManager(paSoundManager_t* self); + +#endif \ No newline at end of file diff --git a/main/modes/games/pango/paSprite.h b/main/modes/games/pango/paSprite.h new file mode 100644 index 000000000..5898a8ee5 --- /dev/null +++ b/main/modes/games/pango/paSprite.h @@ -0,0 +1,20 @@ +#ifndef _PA_SPRITE_H_ +#define _PA_SPRITE_H_ + +//============================================================================== +// Includes +//============================================================================== +#include +#include "wsg.h" + +//============================================================================== +// Structs +//============================================================================== +typedef struct +{ + wsg_t* wsg; + int16_t originX; + int16_t originY; +} paSprite_t; + +#endif diff --git a/main/modes/games/pango/paTables.h b/main/modes/games/pango/paTables.h new file mode 100644 index 000000000..ebdd6c82c --- /dev/null +++ b/main/modes/games/pango/paTables.h @@ -0,0 +1,69 @@ +#ifndef _PA_TABLES_H_ +#define _PA_TABLES_H_ + +//============================================================================== +// Includes +//============================================================================== +#include + +//============================================================================== +// Look Up Tables +//============================================================================== + +#define DEFAULT_ENEMY_SPAWN_LOCATION_TABLE_LENGTH 20 + +#define DEFAULT_ENEMY_SPAWN_LOCATION_TX_LOOKUP_OFFSET 0 +#define DEFAULT_ENEMY_SPAWN_LOCATION_TY_LOOKUP_OFFSET 1 +#define DEFAULT_ENEMY_SPAWN_LOCATION_ROW_LENGTH 2 + +static const uint8_t + defaultEnemySpawnLocations[DEFAULT_ENEMY_SPAWN_LOCATION_TABLE_LENGTH * DEFAULT_ENEMY_SPAWN_LOCATION_ROW_LENGTH] + = { + + // tx,ty + 1, 1, 15, 1, 1, 13, 15, 13, 8, 1, 15, 7, 8, 13, 1, 7, 4, 1, 12, 1, + 15, 4, 15, 10, 12, 13, 4, 13, 1, 10, 1, 4, 6, 1, 10, 1, 6, 13, 10, 13}; + +/*#define MASTER_DIFFICULTY2_TABLE_LENGTH 6 + +#define TOTAL_ENEMIES_LOOKUP_OFFSET 0 +#define INITIAL_ACTIVE_ENEMIES_LOOKUP_OFFSET 1 +#define MAX_ACTIVE_ENEMIES_LOOKUP_OFFSET 2 +#define INITIAL_AGGRESSIVE_ENEMIES_LOOKUP_OFFET 3 +#define MAX_AGGRESSIVE_ENEMIES_LOOKUP_OFFSET 4 +#define INITIAL_ENEMY_SPEED 5 +#define MAX_ENEMY_SPEED 6 +#define MASTER_DIFFICULTY2_TABLE_ROW_LENGTH 7 + +static const int16_t masterDifficulty2[MASTER_DIFFICULTY2_TABLE_LENGTH * MASTER_DIFFICULTY2_TABLE_ROW_LENGTH] = { + +//In-level difficulty curve: +//Starting at 15 seconds, the leftmost parameter will increment every 10 seconds until it reaches the max, +//Then the same will happen with the next parameter in the table until all reach the end-state for the current level. + +// Total initial max initial max initial max +// enemies, active, active, aggressive, aggressive, speed, speed + 5, 1, 2, 0, 1, 12, 12, + 5, 2, 3, 0, 1, 12, 12, + 6, 3, 3, 0, 2, 12, 14, + 6, 3, 3, 0, 3, 12, 14, + 7, 3, 3, 0, 3, 13, 15, + 7, 4, 4, 0, 1, 8, 10, +};*/ + +#define MASTER_DIFFICULTY_TABLE_LENGTH 16 + +#define TOTAL_ENEMIES_LOOKUP_OFFSET 0 +#define MAX_ACTIVE_ENEMIES_LOOKUP_OFFSET 1 +#define ENEMY_INITIAL_SPEED_LOOKUP_OFFSET 2 +#define MASTER_DIFFICULTY_TABLE_ROW_LENGTH 3 + +static const int16_t masterDifficulty[MASTER_DIFFICULTY_TABLE_LENGTH * MASTER_DIFFICULTY_TABLE_ROW_LENGTH] = { + + // Total max min max + // enemies, active, speed aggro, aggro, + 5, 2, 12, 5, 3, 12, 6, 3, 13, 7, 4, 10, 8, 3, 13, 8, 3, 14, 8, 3, 15, 7, 2, 16, + 8, 3, 15, 8, 3, 16, 9, 3, 16, 8, 4, 12, 9, 3, 16, 8, 3, 17, 10, 4, 14, 12, 1, 18, +}; + +#endif \ No newline at end of file diff --git a/main/modes/games/pango/paTilemap.c b/main/modes/games/pango/paTilemap.c new file mode 100644 index 000000000..92808b766 --- /dev/null +++ b/main/modes/games/pango/paTilemap.c @@ -0,0 +1,429 @@ +//============================================================================== +// Includes +//============================================================================== + +#include +#include +#include +#include + +#include "fs_wsg.h" +#include "paTilemap.h" +#include "paLeveldef.h" +#include "esp_random.h" + +#include "cnfs.h" + +//============================================================================== +// Function Prototypes +//============================================================================== + +// bool isInteractive(uint8_t tileId); + +//============================================================================== +// Functions +//============================================================================== + +void pa_initializeTileMap(paTilemap_t* tilemap, paWsgManager_t* wsgManager) +{ + tilemap->mapOffsetX = 0; + tilemap->mapOffsetY = 0; + + tilemap->tileSpawnEnabled = false; + tilemap->executeTileSpawnColumn = -1; + tilemap->executeTileSpawnRow = -1; + + tilemap->animationFrame = 0; + tilemap->animationTimer = 23; + + tilemap->wsgManager = wsgManager; +} + +void pa_drawTileMap(paTilemap_t* tilemap) +{ + tilemap->animationTimer--; + if (tilemap->animationTimer < 0) + { + tilemap->animationFrame = ((tilemap->animationFrame + 1) % 3); + tilemap->animationTimer = 23; + } + + for (int32_t y = (tilemap->mapOffsetY >> PA_TILE_SIZE_IN_POWERS_OF_2); + y < (tilemap->mapOffsetY >> PA_TILE_SIZE_IN_POWERS_OF_2) + PA_TILE_MAP_DISPLAY_HEIGHT_TILES; y++) + { + if (y >= tilemap->mapHeight) + { + break; + } + + for (int32_t x = (tilemap->mapOffsetX >> PA_TILE_SIZE_IN_POWERS_OF_2); + x < (tilemap->mapOffsetX >> PA_TILE_SIZE_IN_POWERS_OF_2) + PA_TILE_MAP_DISPLAY_WIDTH_TILES; x++) + { + if (x >= tilemap->mapWidth) + { + break; + } + else if (x < 0) + { + continue; + } + + uint8_t tile = tilemap->map[(y * tilemap->mapWidth) + x]; + + if (tile < PA_TILE_WALL_0 || tile == PA_TILE_INVISIBLE_BLOCK) + { + continue; + } + + // Test animated tiles + if (tile == PA_TILE_SPAWN_BLOCK_0 || tile == PA_TILE_BONUS_BLOCK_0) + { + tile += tilemap->animationFrame; + } + + // Draw only non-garbage tiles + if (tile > 0 && tile < 13) + { + if (pa_needsTransparency(tile)) + { + // drawWsgSimpleFast(&tilemap->tiles[tile - 32], x * PA_TILE_SIZE - tilemap->mapOffsetX, y * + // PA_TILE_SIZE - tilemap->mapOffsetY); + drawWsgSimple(tilemap->wsgManager->tiles[tile - 1], x * PA_TILE_SIZE - tilemap->mapOffsetX, + y * PA_TILE_SIZE - tilemap->mapOffsetY); + } + else + { + drawWsgTile(tilemap->wsgManager->tiles[tile - 1], x * PA_TILE_SIZE - tilemap->mapOffsetX, + y * PA_TILE_SIZE - tilemap->mapOffsetY); + } + } + else if (tile > 127 && tilemap->tileSpawnEnabled + && (tilemap->executeTileSpawnColumn == x || tilemap->executeTileSpawnRow == y + || tilemap->executeTileSpawnAll)) + { + pa_tileSpawnEntity(tilemap, tile - 128, x, y); + } + } + } + + tilemap->executeTileSpawnAll = 0; +} + +void pa_scrollTileMap(paTilemap_t* tilemap, int16_t x, int16_t y) +{ + if (x != 0) + { + uint8_t oldTx = tilemap->mapOffsetX >> PA_TILE_SIZE_IN_POWERS_OF_2; + tilemap->mapOffsetX = CLAMP(tilemap->mapOffsetX + x, tilemap->minMapOffsetX, tilemap->maxMapOffsetX); + uint8_t newTx = tilemap->mapOffsetX >> PA_TILE_SIZE_IN_POWERS_OF_2; + + if (newTx > oldTx) + { + tilemap->executeTileSpawnColumn = oldTx + PA_TILE_MAP_DISPLAY_WIDTH_TILES; + } + else if (newTx < oldTx) + { + tilemap->executeTileSpawnColumn = newTx; + } + else + { + tilemap->executeTileSpawnColumn = -1; + } + } + + if (y != 0) + { + uint8_t oldTy = tilemap->mapOffsetY >> PA_TILE_SIZE_IN_POWERS_OF_2; + tilemap->mapOffsetY = CLAMP(tilemap->mapOffsetY + y, tilemap->minMapOffsetY, tilemap->maxMapOffsetY); + uint8_t newTy = tilemap->mapOffsetY >> PA_TILE_SIZE_IN_POWERS_OF_2; + + if (newTy > oldTy) + { + tilemap->executeTileSpawnRow = oldTy + PA_TILE_MAP_DISPLAY_HEIGHT_TILES; + } + else if (newTy < oldTy) + { + tilemap->executeTileSpawnRow = newTy; + } + else + { + tilemap->executeTileSpawnRow = -1; + } + } +} + +bool pa_loadMapFromFile(paTilemap_t* tilemap, const char* name) +{ + if (tilemap->map != NULL) + { + free(tilemap->map); + } + + size_t sz; + uint8_t* buf = cnfsReadFile(name, &sz, false); + + if (NULL == buf) + { + ESP_LOGE("MAP", "Failed to read %s", name); + return false; + } + + uint8_t width = buf[0]; + uint8_t height = buf[1]; + + tilemap->map = (uint8_t*)heap_caps_calloc(width * height, sizeof(uint8_t), MALLOC_CAP_SPIRAM); + memcpy(tilemap->map, &buf[2], width * height); + + tilemap->mapWidth = width; + tilemap->mapHeight = height; + + tilemap->minMapOffsetX = 0; + tilemap->maxMapOffsetX = width * PA_TILE_SIZE - PA_TILE_MAP_DISPLAY_WIDTH_PIXELS; + + tilemap->minMapOffsetY = 0; + tilemap->maxMapOffsetY = height * PA_TILE_SIZE - PA_TILE_MAP_DISPLAY_HEIGHT_PIXELS; + + /*for (uint16_t i = 0; i < 16; i++) + { + tilemap->warps[i].x = buf[2 + width * height + i * 2]; + tilemap->warps[i].y = buf[2 + width * height + i * 2 + 1]; + }*/ + + free(buf); + + return true; +} + +void pa_tileSpawnEntity(paTilemap_t* tilemap, uint8_t objectIndex, uint8_t tx, uint8_t ty) +{ + paEntity_t* entityCreated + = pa_createEntity(tilemap->entityManager, objectIndex, (tx << PA_TILE_SIZE_IN_POWERS_OF_2) + 8, + (ty << PA_TILE_SIZE_IN_POWERS_OF_2) + 8); + + if (entityCreated != NULL) + { + entityCreated->homeTileX = tx; + entityCreated->homeTileY = ty; + tilemap->map[ty * tilemap->mapWidth + tx] = 0; + } +} + +uint8_t pa_getTile(paTilemap_t* tilemap, uint8_t tx, uint8_t ty) +{ + // ty = CLAMP(ty, 0, tilemap->mapHeight - 1); + + if (/*ty < 0 ||*/ ty >= tilemap->mapHeight) + { + // ty = 0; + return 0; + } + + if (/*tx < 0 ||*/ tx >= tilemap->mapWidth) + { + return 0; + } + + return tilemap->map[ty * tilemap->mapWidth + tx]; +} + +void pa_setTile(paTilemap_t* tilemap, uint8_t tx, uint8_t ty, uint8_t newTileId) +{ + // ty = CLAMP(ty, 0, tilemap->mapHeight - 1); + + if (ty >= tilemap->mapHeight || tx >= tilemap->mapWidth) + { + return; + } + + tilemap->map[ty * tilemap->mapWidth + tx] = newTileId; +} + +bool pa_isSolid(uint8_t tileId) +{ + switch (tileId) + { + case PA_TILE_EMPTY: + return false; + break; + default: + return true; + } +} + +// bool isInteractive(uint8_t tileId) +// { +// return tileId > PA_TILE_INVISIBLE_BLOCK && tileId < PA_TILE_BG_GOAL_ZONE; +// } + +void pa_unlockScrolling(paTilemap_t* tilemap) +{ + tilemap->minMapOffsetX = 0; + tilemap->maxMapOffsetX = tilemap->mapWidth * PA_TILE_SIZE - PA_TILE_MAP_DISPLAY_WIDTH_PIXELS; + + tilemap->minMapOffsetY = 0; + tilemap->maxMapOffsetY = tilemap->mapHeight * PA_TILE_SIZE - PA_TILE_MAP_DISPLAY_HEIGHT_PIXELS; +} + +bool pa_needsTransparency(uint8_t tileId) +{ + switch (tileId) + { + /*case PA_TILE_BOUNCE_BLOCK: + case PA_TILE_GIRDER: + case PA_TILE_CONTAINER_1 ... PA_TILE_CONTAINER_3: + case PA_TILE_COIN_1 ... PA_TILE_COIN_3: + case PA_TILE_LADDER: + case PA_TILE_BG_GOAL_ZONE ... PA_TILE_BG_CLOUD_D: + return true; + case PA_TILE_BG_CLOUD: + return false; + case PA_TILE_BG_TALL_GRASS ... PA_TILE_BG_MOUNTAIN_R: + return true; + case PA_TILE_BG_MOUNTAIN ... PA_TILE_BG_METAL: + return false; + case PA_TILE_BG_CHAINS: + return true; + case PA_TILE_BG_WALL: + return false;*/ + default: + return true; + } +} + +void pa_freeTilemap(paTilemap_t* tilemap) +{ + free(tilemap->map); +} + +void pa_generateMaze(paTilemap_t* tilemap) +{ + int32_t tx = 1; + int32_t ty = 13; + pa_setTile(tilemap, tx, ty, PA_TILE_EMPTY); + + while (ty > 1) + { + tx = 1; + while (tx < 15) + { + if (!pa_getTile(tilemap, tx, ty) && !pa_genPathContinue(tilemap, tx, ty)) + { + pa_genMakePath(tilemap, tx, ty); + } + tx += 2; + } + ty -= 2; + } +} + +bool pa_genPathContinue(paTilemap_t* tilemap, uint32_t x, uint32_t y) +{ + if (pa_getTile(tilemap, x, y - 2)) + { + return false; + } + if (pa_getTile(tilemap, x, y + 2)) + { + return false; + } + if (pa_getTile(tilemap, x + 2, y)) + { + return false; + } + if (pa_getTile(tilemap, x - 2, y)) + { + return false; + } + + return true; +} + +void pa_genMakePath(paTilemap_t* tilemap, uint32_t x, uint32_t y) +{ + bool done = 0; + uint32_t nx = x; + uint32_t ny = y; + + while (!done) + { + uint32_t r = esp_random() % 4; + + switch (r) + { + case 0: + if (pa_getTile(tilemap, nx, ny - 2)) + { + pa_setTile(tilemap, nx, ny - 1, 0); + pa_setTile(tilemap, nx, ny - 2, 0); + ny -= 2; + } + break; + case 1: + if (pa_getTile(tilemap, nx, ny + 2)) + { + pa_setTile(tilemap, nx, ny + 1, 0); + pa_setTile(tilemap, nx, ny + 2, 0); + ny += 2; + } + break; + case 2: + if (pa_getTile(tilemap, nx - 2, ny)) + { + pa_setTile(tilemap, nx - 1, ny, 0); + pa_setTile(tilemap, nx - 2, ny, 0); + nx -= 2; + } + break; + case 3: + if (pa_getTile(tilemap, nx + 2, ny)) + { + pa_setTile(tilemap, nx + 1, ny, 0); + pa_setTile(tilemap, nx + 2, ny, 0); + nx += 2; + } + break; + } + + done = pa_genPathContinue(tilemap, nx, ny); + } +} + +void pa_placeEnemySpawns(paTilemap_t* tilemap) +{ + int16_t enemySpawnsToPlace = tilemap->entityManager->gameData->remainingEnemies; + int16_t enemiesPlaced = 0; + bool previouslyPlaced = false; + int16_t iterations = 0; + + // Place enemy spawn blocks + while (enemySpawnsToPlace > 0 && iterations < 16) + { + for (uint16_t ty = 1; ty < 13; ty++) + { + for (uint16_t tx = 1; tx < 15; tx++) + { + if (enemySpawnsToPlace <= 0) + { + break; + } + + uint8_t t = pa_getTile(tilemap, tx, ty); + + if (t == PA_TILE_BLOCK && !previouslyPlaced && !(esp_random() % 15)) + { + pa_setTile(tilemap, tx, ty, PA_TILE_SPAWN_BLOCK_0); + enemySpawnsToPlace--; + enemiesPlaced++; + previouslyPlaced = true; + } + else + { + previouslyPlaced = false; + } + } + } + iterations++; + } + + tilemap->entityManager->gameData->remainingEnemies = enemiesPlaced; +} \ No newline at end of file diff --git a/main/modes/games/pango/paTilemap.h b/main/modes/games/pango/paTilemap.h new file mode 100644 index 000000000..66ea92770 --- /dev/null +++ b/main/modes/games/pango/paTilemap.h @@ -0,0 +1,111 @@ +#ifndef _PA_TILE_MAP_H_ +#define _PA_TILE_MAP_H_ + +//============================================================================== +// Includes +//============================================================================== + +#include +#include +#include "wsg.h" +#include "paWsgManager.h" +#include "macros.h" +#include "pango_typedef.h" +#include "paEntityManager.h" + +//============================================================================== +// Constants +//============================================================================== +#define PA_TILE_MAP_DISPLAY_WIDTH_PIXELS 280 // The screen size +#define PA_TILE_MAP_DISPLAY_HEIGHT_PIXELS 240 // The screen size +#define PA_TILE_MAP_DISPLAY_WIDTH_TILES 19 // The screen size in tiles + 1 +#define PA_TILE_MAP_DISPLAY_HEIGHT_TILES 16 // The screen size in tiles + 1 + +#define PA_TILE_SIZE 16 +#define PA_HALF_TILE_SIZE 8 +#define PA_TILE_SIZE_IN_POWERS_OF_2 4 + +#define PA_TILE_SET_SIZE 15 + +//============================================================================== +// Enums +//============================================================================== +typedef enum +{ + PA_TILE_EMPTY, + PA_TILE_WALL_0, + PA_TILE_WALL_1, + PA_TILE_WALL_2, + PA_TILE_WALL_3, + PA_TILE_WALL_4, + PA_TILE_WALL_5, + PA_TILE_WALL_6, + PA_TILE_WALL_7, + PA_TILE_BLOCK, + PA_TILE_SPAWN_BLOCK_0, + PA_TILE_SPAWN_BLOCK_1, + PA_TILE_SPAWN_BLOCK_2, + PA_TILE_BONUS_BLOCK_0, + PA_TILE_BONUS_BLOCK_1, + PA_TILE_BONUS_BLOCK_2, + PA_TILE_INVISIBLE_BLOCK +} PA_TILE_Index_t; + +//============================================================================== +// Structs +//============================================================================== +typedef struct +{ + uint8_t x; + uint8_t y; +} pa_warp_t; +struct paTilemap_t +{ + paWsgManager_t* wsgManager; + + uint8_t* map; + uint8_t mapWidth; + uint8_t mapHeight; + + pa_warp_t warps[16]; + + int16_t mapOffsetX; + int16_t mapOffsetY; + + int16_t minMapOffsetX; + int16_t maxMapOffsetX; + int16_t minMapOffsetY; + int16_t maxMapOffsetY; + + bool tileSpawnEnabled; + int16_t executeTileSpawnColumn; + int16_t executeTileSpawnRow; + bool executeTileSpawnAll; + + paEntityManager_t* entityManager; + + uint8_t animationFrame; + int16_t animationTimer; +}; + +//============================================================================== +// Prototypes +//============================================================================== +void pa_initializeTileMap(paTilemap_t* tilemap, paWsgManager_t* wsgManager); +void pa_drawTileMap(paTilemap_t* tilemap); +void pa_scrollTileMap(paTilemap_t* tilemap, int16_t x, int16_t y); +void pa_drawTile(paTilemap_t* tilemap, uint8_t tileId, int16_t x, int16_t y); +bool pa_loadMapFromFile(paTilemap_t* tilemap, const char* name); +void pa_tileSpawnEntity(paTilemap_t* tilemap, uint8_t objectIndex, uint8_t tx, uint8_t ty); +uint8_t pa_getTile(paTilemap_t* tilemap, uint8_t tx, uint8_t ty); +void pa_setTile(paTilemap_t* tilemap, uint8_t tx, uint8_t ty, uint8_t newTileId); +bool pa_isSolid(uint8_t tileId); +void pa_unlockScrolling(paTilemap_t* tilemap); +bool pa_needsTransparency(uint8_t tileId); +void pa_freeTilemap(paTilemap_t* tilemap); +void pa_generateMaze(paTilemap_t* tilemap); +bool pa_genPathContinue(paTilemap_t* tilemap, uint32_t x, uint32_t y); +void pa_genMakePath(paTilemap_t* tilemap, uint32_t x, uint32_t y); +void pa_placeEnemySpawns(paTilemap_t* tilemap); + +#endif diff --git a/main/modes/games/pango/paWsgManager.c b/main/modes/games/pango/paWsgManager.c new file mode 100644 index 000000000..6ba690e25 --- /dev/null +++ b/main/modes/games/pango/paWsgManager.c @@ -0,0 +1,295 @@ +//============================================================================== +// Includes +//============================================================================== + +#include +#include +#include "fs_wsg.h" +#include "paWsgManager.h" + +//============================================================================== +// Functions +//============================================================================== + +void pa_initializeWsgManager(paWsgManager_t* self) +{ + pa_loadWsgs(self); + pa_initializeSprites(self); + pa_initializeTiles(self); +} + +void pa_freeWsgManager(paWsgManager_t* self) +{ + for (uint16_t i = 0; i < PA_WSGS_SIZE; i++) + { + freeWsg(&self->wsgs[i]); + } +} + +void pa_loadWsgs(paWsgManager_t* self) +{ + loadWsg("pa-100.wsg", &self->wsgs[PA_WSG_PANGO_SOUTH], false); + loadWsg("pa-101.wsg", &self->wsgs[PA_WSG_PANGO_WALK_SOUTH], false); + loadWsg("pa-102.wsg", &self->wsgs[PA_WSG_PANGO_NORTH], false); + loadWsg("pa-103.wsg", &self->wsgs[PA_WSG_PANGO_WALK_NORTH], false); + loadWsg("pa-104.wsg", &self->wsgs[PA_WSG_PANGO_SIDE], false); + loadWsg("pa-106.wsg", &self->wsgs[PA_WSG_PANGO_WALK_SIDE_1], false); + loadWsg("pa-105.wsg", &self->wsgs[PA_WSG_PANGO_WALK_SIDE_2], false); + loadWsg("pa-107.wsg", &self->wsgs[PA_WSG_PANGO_PUSH_SOUTH_1], false); + loadWsg("pa-108.wsg", &self->wsgs[PA_WSG_PANGO_PUSH_SOUTH_2], false); + loadWsg("pa-109.wsg", &self->wsgs[PA_WSG_PANGO_PUSH_NORTH_1], false); + loadWsg("pa-110.wsg", &self->wsgs[PA_WSG_PANGO_PUSH_NORTH_2], false); + loadWsg("pa-111.wsg", &self->wsgs[PA_WSG_PANGO_PUSH_SIDE_1], false); + loadWsg("pa-112.wsg", &self->wsgs[PA_WSG_PANGO_PUSH_SIDE_2], false); + loadWsg("pa-113.wsg", &self->wsgs[PA_WSG_PANGO_HURT], false); + loadWsg("pa-114.wsg", &self->wsgs[PA_WSG_PANGO_WIN], false); + loadWsg("pa-015.wsg", &self->wsgs[PA_WSG_PANGO_ICON], false); + loadWsg("po-000.wsg", &self->wsgs[PA_WSG_PO_SOUTH], false); + loadWsg("po-001.wsg", &self->wsgs[PA_WSG_PO_WALK_SOUTH], false); + loadWsg("po-002.wsg", &self->wsgs[PA_WSG_PO_NORTH], false); + loadWsg("po-003.wsg", &self->wsgs[PA_WSG_PO_WALK_NORTH], false); + loadWsg("po-004.wsg", &self->wsgs[PA_WSG_PO_SIDE], false); + loadWsg("po-006.wsg", &self->wsgs[PA_WSG_PO_WALK_SIDE_1], false); + loadWsg("po-005.wsg", &self->wsgs[PA_WSG_PO_WALK_SIDE_2], false); + loadWsg("po-007.wsg", &self->wsgs[PA_WSG_PO_PUSH_SOUTH_1], false); + loadWsg("po-008.wsg", &self->wsgs[PA_WSG_PO_PUSH_SOUTH_2], false); + loadWsg("po-009.wsg", &self->wsgs[PA_WSG_PO_PUSH_NORTH_1], false); + loadWsg("po-010.wsg", &self->wsgs[PA_WSG_PO_PUSH_NORTH_2], false); + loadWsg("po-011.wsg", &self->wsgs[PA_WSG_PO_PUSH_SIDE_1], false); + loadWsg("po-012.wsg", &self->wsgs[PA_WSG_PO_PUSH_SIDE_2], false); + loadWsg("po-013.wsg", &self->wsgs[PA_WSG_PO_HURT], false); + loadWsg("po-014.wsg", &self->wsgs[PA_WSG_PO_WIN], false); + loadWsg("pa-015.wsg", &self->wsgs[PA_WSG_PO_ICON], false); + loadWsg("px-000.wsg", &self->wsgs[PA_WSG_PIXEL_SOUTH], false); + loadWsg("px-001.wsg", &self->wsgs[PA_WSG_PIXEL_WALK_SOUTH], false); + loadWsg("px-002.wsg", &self->wsgs[PA_WSG_PIXEL_NORTH], false); + loadWsg("px-003.wsg", &self->wsgs[PA_WSG_PIXEL_WALK_NORTH], false); + loadWsg("px-004.wsg", &self->wsgs[PA_WSG_PIXEL_SIDE], false); + loadWsg("px-006.wsg", &self->wsgs[PA_WSG_PIXEL_WALK_SIDE_1], false); + loadWsg("px-005.wsg", &self->wsgs[PA_WSG_PIXEL_WALK_SIDE_2], false); + loadWsg("px-007.wsg", &self->wsgs[PA_WSG_PIXEL_PUSH_SOUTH_1], false); + loadWsg("px-008.wsg", &self->wsgs[PA_WSG_PIXEL_PUSH_SOUTH_2], false); + loadWsg("px-009.wsg", &self->wsgs[PA_WSG_PIXEL_PUSH_NORTH_1], false); + loadWsg("px-010.wsg", &self->wsgs[PA_WSG_PIXEL_PUSH_NORTH_2], false); + loadWsg("px-011.wsg", &self->wsgs[PA_WSG_PIXEL_PUSH_SIDE_1], false); + loadWsg("px-012.wsg", &self->wsgs[PA_WSG_PIXEL_PUSH_SIDE_2], false); + loadWsg("px-013.wsg", &self->wsgs[PA_WSG_PIXEL_HURT], false); + loadWsg("px-014.wsg", &self->wsgs[PA_WSG_PIXEL_WIN], false); + loadWsg("px-015.wsg", &self->wsgs[PA_WSG_PIXEL_ICON], false); + loadWsg("kr-000.wsg", &self->wsgs[PA_WSG_GIRL_SOUTH], false); + loadWsg("kr-001.wsg", &self->wsgs[PA_WSG_GIRL_WALK_SOUTH], false); + loadWsg("kr-002.wsg", &self->wsgs[PA_WSG_GIRL_NORTH], false); + loadWsg("kr-003.wsg", &self->wsgs[PA_WSG_GIRL_WALK_NORTH], false); + loadWsg("kr-004.wsg", &self->wsgs[PA_WSG_GIRL_SIDE], false); + loadWsg("kr-006.wsg", &self->wsgs[PA_WSG_GIRL_WALK_SIDE_1], false); + loadWsg("kr-005.wsg", &self->wsgs[PA_WSG_GIRL_WALK_SIDE_2], false); + loadWsg("kr-007.wsg", &self->wsgs[PA_WSG_GIRL_PUSH_SOUTH_1], false); + loadWsg("kr-008.wsg", &self->wsgs[PA_WSG_GIRL_PUSH_SOUTH_2], false); + loadWsg("kr-009.wsg", &self->wsgs[PA_WSG_GIRL_PUSH_NORTH_1], false); + loadWsg("kr-010.wsg", &self->wsgs[PA_WSG_GIRL_PUSH_NORTH_2], false); + loadWsg("kr-011.wsg", &self->wsgs[PA_WSG_GIRL_PUSH_SIDE_1], false); + loadWsg("kr-012.wsg", &self->wsgs[PA_WSG_GIRL_PUSH_SIDE_2], false); + loadWsg("kr-013.wsg", &self->wsgs[PA_WSG_GIRL_HURT], false); + loadWsg("kr-014.wsg", &self->wsgs[PA_WSG_GIRL_WIN], false); + loadWsg("kr-015.wsg", &self->wsgs[PA_WSG_GIRL_ICON], false); + // loadWsg("pa-tile-009.wsg", &self->wsgs[PA_WSG_BLOCK], false); + // loadWsg("pa-tile-013.wsg", &self->wsgs[PA_WSG_BONUS_BLOCK], false); + loadWsg("pa-en-004.wsg", &self->wsgs[PA_WSG_ENEMY_SOUTH], false); + loadWsg("pa-en-006.wsg", &self->wsgs[PA_WSG_ENEMY_NORTH], false); + loadWsg("pa-en-000.wsg", &self->wsgs[PA_WSG_ENEMY_SIDE_1], false); + loadWsg("pa-en-001.wsg", &self->wsgs[PA_WSG_ENEMY_SIDE_2], false); + loadWsg("pa-en-005.wsg", &self->wsgs[PA_WSG_ENEMY_DRILL_SOUTH], false); + loadWsg("pa-en-007.wsg", &self->wsgs[PA_WSG_ENEMY_DRILL_NORTH], false); + loadWsg("pa-en-008.wsg", &self->wsgs[PA_WSG_ENEMY_STUN], false); + loadWsg("pa-en-002.wsg", &self->wsgs[PA_WSG_ENEMY_DRILL_SIDE_1], false); + loadWsg("pa-en-003.wsg", &self->wsgs[PA_WSG_ENEMY_DRILL_SIDE_2], false); + loadWsg("break-000.wsg", &self->wsgs[PA_WSG_BREAK_BLOCK], false); + loadWsg("break-001.wsg", &self->wsgs[PA_WSG_BREAK_BLOCK_1], false); + loadWsg("break-002.wsg", &self->wsgs[PA_WSG_BREAK_BLOCK_2], false); + loadWsg("break-003.wsg", &self->wsgs[PA_WSG_BREAK_BLOCK_3], false); + loadWsg("blockfragment.wsg", &self->wsgs[PA_WSG_BLOCK_FRAGMENT], false); + + loadWsg("pa-tile-001.wsg", &self->wsgs[PA_WSG_WALL_0], false); + loadWsg("pa-tile-002.wsg", &self->wsgs[PA_WSG_WALL_1], false); + loadWsg("pa-tile-003.wsg", &self->wsgs[PA_WSG_WALL_2], false); + loadWsg("pa-tile-004.wsg", &self->wsgs[PA_WSG_WALL_3], false); + loadWsg("pa-tile-005.wsg", &self->wsgs[PA_WSG_WALL_4], false); + loadWsg("pa-tile-006.wsg", &self->wsgs[PA_WSG_WALL_5], false); + loadWsg("pa-tile-007.wsg", &self->wsgs[PA_WSG_WALL_6], false); + loadWsg("pa-tile-008.wsg", &self->wsgs[PA_WSG_WALL_7], false); + loadWsg("pa-tile-009.wsg", &self->wsgs[PA_WSG_BLOCK], false); + loadWsg("pa-tile-010.wsg", &self->wsgs[PA_WSG_SPAWN_BLOCK_0], false); + loadWsg("pa-tile-011.wsg", &self->wsgs[PA_WSG_SPAWN_BLOCK_1], false); + loadWsg("pa-tile-012.wsg", &self->wsgs[PA_WSG_SPAWN_BLOCK_2], false); + loadWsg("pa-tile-013.wsg", &self->wsgs[PA_WSG_BONUS_BLOCK_0], false); + loadWsg("pa-tile-014.wsg", &self->wsgs[PA_WSG_BONUS_BLOCK_1], false); + loadWsg("pa-tile-015.wsg", &self->wsgs[PA_WSG_BONUS_BLOCK_2], false); +} + +void pa_initializeSprites(paWsgManager_t* self) +{ + self->sprites[PA_SP_PLAYER_SOUTH].wsg = &self->wsgs[PA_WSG_PANGO_SOUTH]; + self->sprites[PA_SP_PLAYER_SOUTH].originX = 8; + self->sprites[PA_SP_PLAYER_SOUTH].originY = 16; + + self->sprites[PA_SP_PLAYER_WALK_SOUTH].wsg = &self->wsgs[PA_WSG_PANGO_WALK_SOUTH]; + self->sprites[PA_SP_PLAYER_WALK_SOUTH].originX = 8; + self->sprites[PA_SP_PLAYER_WALK_SOUTH].originY = 16; + + self->sprites[PA_SP_PLAYER_NORTH].wsg = &self->wsgs[PA_WSG_PANGO_NORTH]; + self->sprites[PA_SP_PLAYER_NORTH].originX = 8; + self->sprites[PA_SP_PLAYER_NORTH].originY = 16; + + self->sprites[PA_SP_PLAYER_WALK_NORTH].wsg = &self->wsgs[PA_WSG_PANGO_WALK_NORTH]; + self->sprites[PA_SP_PLAYER_WALK_NORTH].originX = 8; + self->sprites[PA_SP_PLAYER_WALK_NORTH].originY = 16; + + self->sprites[PA_SP_PLAYER_SIDE].wsg = &self->wsgs[PA_WSG_PANGO_SIDE]; + self->sprites[PA_SP_PLAYER_SIDE].originX = 8; + self->sprites[PA_SP_PLAYER_SIDE].originY = 16; + + self->sprites[PA_SP_PLAYER_WALK_SIDE_1].wsg = &self->wsgs[PA_WSG_PANGO_WALK_SIDE_1]; + self->sprites[PA_SP_PLAYER_WALK_SIDE_1].originX = 8; + self->sprites[PA_SP_PLAYER_WALK_SIDE_1].originY = 16; + + self->sprites[PA_SP_PLAYER_WALK_SIDE_2].wsg = &self->wsgs[PA_WSG_PANGO_WALK_SIDE_2]; + self->sprites[PA_SP_PLAYER_WALK_SIDE_2].originX = 8; + self->sprites[PA_SP_PLAYER_WALK_SIDE_2].originY = 16; + + self->sprites[PA_SP_PLAYER_PUSH_SOUTH_1].wsg = &self->wsgs[PA_WSG_PANGO_PUSH_SOUTH_1]; + self->sprites[PA_SP_PLAYER_PUSH_SOUTH_1].originX = 8; + self->sprites[PA_SP_PLAYER_PUSH_SOUTH_1].originY = 16; + + self->sprites[PA_SP_PLAYER_PUSH_SOUTH_2].wsg = &self->wsgs[PA_WSG_PANGO_PUSH_SOUTH_2]; + self->sprites[PA_SP_PLAYER_PUSH_SOUTH_2].originX = 8; + self->sprites[PA_SP_PLAYER_PUSH_SOUTH_2].originY = 16; + + self->sprites[PA_SP_PLAYER_PUSH_NORTH_1].wsg = &self->wsgs[PA_WSG_PANGO_PUSH_NORTH_1]; + self->sprites[PA_SP_PLAYER_PUSH_NORTH_1].originX = 8; + self->sprites[PA_SP_PLAYER_PUSH_NORTH_1].originY = 16; + + self->sprites[PA_SP_PLAYER_PUSH_NORTH_2].wsg = &self->wsgs[PA_WSG_PANGO_PUSH_NORTH_2]; + self->sprites[PA_SP_PLAYER_PUSH_NORTH_2].originX = 8; + self->sprites[PA_SP_PLAYER_PUSH_NORTH_2].originY = 16; + + self->sprites[PA_SP_PLAYER_PUSH_SIDE_1].wsg = &self->wsgs[PA_WSG_PANGO_PUSH_SIDE_1]; + self->sprites[PA_SP_PLAYER_PUSH_SIDE_1].originX = 8; + self->sprites[PA_SP_PLAYER_PUSH_SIDE_1].originY = 16; + + self->sprites[PA_SP_PLAYER_PUSH_SIDE_2].wsg = &self->wsgs[PA_WSG_PANGO_PUSH_SIDE_2]; + self->sprites[PA_SP_PLAYER_PUSH_SIDE_2].originX = 8; + self->sprites[PA_SP_PLAYER_PUSH_SIDE_2].originY = 16; + + self->sprites[PA_SP_PLAYER_HURT].wsg = &self->wsgs[PA_WSG_PANGO_HURT]; + self->sprites[PA_SP_PLAYER_HURT].originX = 8; + self->sprites[PA_SP_PLAYER_HURT].originY = 16; + + self->sprites[PA_SP_PLAYER_WIN].wsg = &self->wsgs[PA_SP_PLAYER_WIN]; + self->sprites[PA_SP_PLAYER_WIN].originX = 8; + self->sprites[PA_SP_PLAYER_WIN].originY = 16; + + self->sprites[PA_SP_PLAYER_ICON].wsg = &self->wsgs[PA_SP_PLAYER_ICON]; + self->sprites[PA_SP_PLAYER_ICON].originX = 8; + self->sprites[PA_SP_PLAYER_ICON].originY = 16; + + self->sprites[PA_SP_BLOCK].wsg = &self->wsgs[PA_WSG_BLOCK]; + self->sprites[PA_SP_BLOCK].originX = 8; + self->sprites[PA_SP_BLOCK].originY = 8; + + self->sprites[PA_SP_BONUS_BLOCK].wsg = &self->wsgs[PA_WSG_BONUS_BLOCK_0]; + self->sprites[PA_SP_BONUS_BLOCK].originX = 8; + self->sprites[PA_SP_BONUS_BLOCK].originY = 8; + + self->sprites[PA_SP_ENEMY_SOUTH].wsg = &self->wsgs[PA_WSG_ENEMY_SOUTH]; + self->sprites[PA_SP_ENEMY_SOUTH].originX = 8; + self->sprites[PA_SP_ENEMY_SOUTH].originY = 16; + + self->sprites[PA_SP_ENEMY_NORTH].wsg = &self->wsgs[PA_WSG_ENEMY_NORTH]; + self->sprites[PA_SP_ENEMY_NORTH].originX = 8; + self->sprites[PA_SP_ENEMY_NORTH].originY = 16; + + self->sprites[PA_SP_ENEMY_SIDE_1].wsg = &self->wsgs[PA_WSG_ENEMY_SIDE_1]; + self->sprites[PA_SP_ENEMY_SIDE_1].originX = 8; + self->sprites[PA_SP_ENEMY_SIDE_1].originY = 16; + + self->sprites[PA_SP_ENEMY_SIDE_2].wsg = &self->wsgs[PA_WSG_ENEMY_SIDE_2]; + self->sprites[PA_SP_ENEMY_SIDE_2].originX = 8; + self->sprites[PA_SP_ENEMY_SIDE_2].originY = 16; + + self->sprites[PA_SP_ENEMY_DRILL_SOUTH].wsg = &self->wsgs[PA_WSG_ENEMY_DRILL_SOUTH]; + self->sprites[PA_SP_ENEMY_DRILL_SOUTH].originX = 8; + self->sprites[PA_SP_ENEMY_DRILL_SOUTH].originY = 16; + + self->sprites[PA_SP_ENEMY_DRILL_NORTH].wsg = &self->wsgs[PA_WSG_ENEMY_DRILL_NORTH]; + self->sprites[PA_SP_ENEMY_DRILL_NORTH].originX = 8; + self->sprites[PA_SP_ENEMY_DRILL_NORTH].originY = 16; + + self->sprites[PA_SP_ENEMY_STUN].wsg = &self->wsgs[PA_WSG_ENEMY_STUN]; + self->sprites[PA_SP_ENEMY_STUN].originX = 8; + self->sprites[PA_SP_ENEMY_STUN].originY = 16; + + self->sprites[PA_SP_ENEMY_DRILL_SIDE_1].wsg = &self->wsgs[PA_WSG_ENEMY_DRILL_SIDE_1]; + self->sprites[PA_SP_ENEMY_DRILL_SIDE_1].originX = 8; + self->sprites[PA_SP_ENEMY_DRILL_SIDE_1].originY = 16; + + self->sprites[PA_SP_ENEMY_DRILL_SIDE_2].wsg = &self->wsgs[PA_WSG_ENEMY_DRILL_SIDE_2]; + self->sprites[PA_SP_ENEMY_DRILL_SIDE_2].originX = 8; + self->sprites[PA_SP_ENEMY_DRILL_SIDE_2].originY = 16; + + self->sprites[PA_SP_BREAK_BLOCK].wsg = &self->wsgs[PA_WSG_BREAK_BLOCK]; + self->sprites[PA_SP_BREAK_BLOCK].originX = 8; + self->sprites[PA_SP_BREAK_BLOCK].originY = 8; + + self->sprites[PA_SP_BREAK_BLOCK_1].wsg = &self->wsgs[PA_WSG_BREAK_BLOCK_1]; + self->sprites[PA_SP_BREAK_BLOCK_1].originX = 8; + self->sprites[PA_SP_BREAK_BLOCK_1].originY = 8; + + self->sprites[PA_SP_BREAK_BLOCK_2].wsg = &self->wsgs[PA_WSG_BREAK_BLOCK_2]; + self->sprites[PA_SP_BREAK_BLOCK_2].originX = 8; + self->sprites[PA_SP_BREAK_BLOCK_2].originY = 8; + + self->sprites[PA_SP_BREAK_BLOCK_3].wsg = &self->wsgs[PA_WSG_BREAK_BLOCK_3]; + self->sprites[PA_SP_BREAK_BLOCK_3].originX = 8; + self->sprites[PA_SP_BREAK_BLOCK_3].originY = 8; + + self->sprites[PA_SP_BLOCK_FRAGMENT].wsg = &self->wsgs[PA_WSG_BLOCK_FRAGMENT]; + self->sprites[PA_SP_BLOCK_FRAGMENT].originX = 3; + self->sprites[PA_SP_BLOCK_FRAGMENT].originY = 3; +} + +void pa_initializeTiles(paWsgManager_t* self) +{ + self->tiles[0] = &self->wsgs[PA_WSG_WALL_0]; + self->tiles[1] = &self->wsgs[PA_WSG_WALL_1]; + self->tiles[2] = &self->wsgs[PA_WSG_WALL_2]; + self->tiles[3] = &self->wsgs[PA_WSG_WALL_3]; + self->tiles[4] = &self->wsgs[PA_WSG_WALL_4]; + self->tiles[5] = &self->wsgs[PA_WSG_WALL_5]; + self->tiles[6] = &self->wsgs[PA_WSG_WALL_6]; + self->tiles[7] = &self->wsgs[PA_WSG_WALL_7]; + self->tiles[8] = &self->wsgs[PA_WSG_BLOCK]; + self->tiles[9] = &self->wsgs[PA_WSG_SPAWN_BLOCK_0]; + self->tiles[10] = &self->wsgs[PA_WSG_SPAWN_BLOCK_1]; + self->tiles[11] = &self->wsgs[PA_WSG_SPAWN_BLOCK_2]; + self->tiles[12] = &self->wsgs[PA_WSG_BONUS_BLOCK_0]; + self->tiles[13] = &self->wsgs[PA_WSG_BONUS_BLOCK_1]; + self->tiles[14] = &self->wsgs[PA_WSG_BONUS_BLOCK_2]; +} + +void pa_remapWsgToSprite(paWsgManager_t* self, uint16_t spriteIndex, uint16_t wsgIndex) +{ + self->sprites[spriteIndex].wsg = &self->wsgs[wsgIndex]; +} + +void pa_remapWsgToTile(paWsgManager_t* self, uint16_t tileIndex, uint16_t wsgIndex) +{ + self->tiles[tileIndex] = &self->wsgs[wsgIndex]; +} + +void pa_remapPlayerCharacter(paWsgManager_t* self, uint16_t newBaseIndex) +{ + for (uint16_t i = 0; i < (PA_SP_PLAYER_ICON + 1); i++) + { + pa_remapWsgToSprite(self, i, newBaseIndex + i); + } +} diff --git a/main/modes/games/pango/paWsgManager.h b/main/modes/games/pango/paWsgManager.h new file mode 100644 index 000000000..a6bbe9f4a --- /dev/null +++ b/main/modes/games/pango/paWsgManager.h @@ -0,0 +1,147 @@ +#ifndef _PA_WSGMANAGER_H_ +#define _PA_WSGMANAGER_H_ + +//============================================================================== +// Includes +//============================================================================== + +#include +#include "wsg.h" +#include "paSprite.h" +#include "pango_typedef.h" + +//============================================================================== +// Constants +//============================================================================== +#define PA_WSGS_SIZE 93 +#define PA_SPRITESET_SIZE 32 +#define PA_TILE_SET_SIZE 15 + +//============================================================================== +// Enums +//============================================================================== +typedef enum +{ + PA_WSG_PANGO_SOUTH, + PA_WSG_PANGO_WALK_SOUTH, + PA_WSG_PANGO_NORTH, + PA_WSG_PANGO_WALK_NORTH, + PA_WSG_PANGO_SIDE, + PA_WSG_PANGO_WALK_SIDE_1, + PA_WSG_PANGO_WALK_SIDE_2, + PA_WSG_PANGO_PUSH_SOUTH_1, + PA_WSG_PANGO_PUSH_SOUTH_2, + PA_WSG_PANGO_PUSH_NORTH_1, + PA_WSG_PANGO_PUSH_NORTH_2, + PA_WSG_PANGO_PUSH_SIDE_1, + PA_WSG_PANGO_PUSH_SIDE_2, + PA_WSG_PANGO_HURT, + PA_WSG_PANGO_WIN, + PA_WSG_PANGO_ICON, + PA_WSG_PO_SOUTH, + PA_WSG_PO_WALK_SOUTH, + PA_WSG_PO_NORTH, + PA_WSG_PO_WALK_NORTH, + PA_WSG_PO_SIDE, + PA_WSG_PO_WALK_SIDE_1, + PA_WSG_PO_WALK_SIDE_2, + PA_WSG_PO_PUSH_SOUTH_1, + PA_WSG_PO_PUSH_SOUTH_2, + PA_WSG_PO_PUSH_NORTH_1, + PA_WSG_PO_PUSH_NORTH_2, + PA_WSG_PO_PUSH_SIDE_1, + PA_WSG_PO_PUSH_SIDE_2, + PA_WSG_PO_HURT, + PA_WSG_PO_WIN, + PA_WSG_PO_ICON, + PA_WSG_PIXEL_SOUTH, + PA_WSG_PIXEL_WALK_SOUTH, + PA_WSG_PIXEL_NORTH, + PA_WSG_PIXEL_WALK_NORTH, + PA_WSG_PIXEL_SIDE, + PA_WSG_PIXEL_WALK_SIDE_1, + PA_WSG_PIXEL_WALK_SIDE_2, + PA_WSG_PIXEL_PUSH_SOUTH_1, + PA_WSG_PIXEL_PUSH_SOUTH_2, + PA_WSG_PIXEL_PUSH_NORTH_1, + PA_WSG_PIXEL_PUSH_NORTH_2, + PA_WSG_PIXEL_PUSH_SIDE_1, + PA_WSG_PIXEL_PUSH_SIDE_2, + PA_WSG_PIXEL_HURT, + PA_WSG_PIXEL_WIN, + PA_WSG_PIXEL_ICON, + PA_WSG_GIRL_SOUTH, + PA_WSG_GIRL_WALK_SOUTH, + PA_WSG_GIRL_NORTH, + PA_WSG_GIRL_WALK_NORTH, + PA_WSG_GIRL_SIDE, + PA_WSG_GIRL_WALK_SIDE_1, + PA_WSG_GIRL_WALK_SIDE_2, + PA_WSG_GIRL_PUSH_SOUTH_1, + PA_WSG_GIRL_PUSH_SOUTH_2, + PA_WSG_GIRL_PUSH_NORTH_1, + PA_WSG_GIRL_PUSH_NORTH_2, + PA_WSG_GIRL_PUSH_SIDE_1, + PA_WSG_GIRL_PUSH_SIDE_2, + PA_WSG_GIRL_HURT, + PA_WSG_GIRL_WIN, + PA_WSG_GIRL_ICON, + // PA_WSG_BLOCK, + // PA_WSG_BONUS_BLOCK, + PA_WSG_ENEMY_SOUTH, + PA_WSG_ENEMY_NORTH, + PA_WSG_ENEMY_SIDE_1, + PA_WSG_ENEMY_SIDE_2, + PA_WSG_ENEMY_DRILL_SOUTH, + PA_WSG_ENEMY_DRILL_NORTH, + PA_WSG_ENEMY_DRILL_SIDE_1, + PA_WSG_ENEMY_DRILL_SIDE_2, + PA_WSG_ENEMY_STUN, + PA_WSG_BREAK_BLOCK, + PA_WSG_BREAK_BLOCK_1, + PA_WSG_BREAK_BLOCK_2, + PA_WSG_BREAK_BLOCK_3, + PA_WSG_BLOCK_FRAGMENT, + PA_WSG_WALL_0, + PA_WSG_WALL_1, + PA_WSG_WALL_2, + PA_WSG_WALL_3, + PA_WSG_WALL_4, + PA_WSG_WALL_5, + PA_WSG_WALL_6, + PA_WSG_WALL_7, + PA_WSG_BLOCK, + PA_WSG_SPAWN_BLOCK_0, + PA_WSG_SPAWN_BLOCK_1, + PA_WSG_SPAWN_BLOCK_2, + PA_WSG_BONUS_BLOCK_0, + PA_WSG_BONUS_BLOCK_1, + PA_WSG_BONUS_BLOCK_2 +} paWsgIndex_t; + +//============================================================================== +// Structs +//============================================================================== +typedef struct +{ + wsg_t wsgs[PA_WSGS_SIZE]; + paSprite_t sprites[PA_SPRITESET_SIZE]; + wsg_t* tiles[PA_TILE_SET_SIZE]; +} paWsgManager_t; + +//============================================================================== +// Function Definitions +//============================================================================== +void pa_initializeWsgManager(paWsgManager_t* self); +void pa_freeWsgManager(paWsgManager_t* self); + +void pa_loadWsgs(paWsgManager_t* self); +void pa_initializeSprites(paWsgManager_t* self); +void pa_initializeTiles(paWsgManager_t* tiles); + +void pa_remapWsgToSprite(paWsgManager_t* self, uint16_t spriteIndex, uint16_t wsgIndex); +void pa_remapWsgToTile(paWsgManager_t* self, uint16_t tileIndex, uint16_t wsgIndex); + +void pa_remapPlayerCharacter(paWsgManager_t* self, uint16_t newBaseIndex); + +#endif \ No newline at end of file diff --git a/main/modes/games/pango/pango.c b/main/modes/games/pango/pango.c new file mode 100644 index 000000000..b50a2cbd9 --- /dev/null +++ b/main/modes/games/pango/pango.c @@ -0,0 +1,1757 @@ +/** + * @file pango.c + * @author J.Vega (JVeg199X) + * @brief Pango + * @date 2024-05-04 + * + */ + +//============================================================================== +// Includes +//============================================================================== + +#include +#include + +#include "esp_log.h" +#include "esp_timer.h" + +#include "pango.h" +#include "esp_random.h" + +#include "pango_typedef.h" +#include "paWsgManager.h" +#include "paTilemap.h" +#include "paGameData.h" +#include "paEntityManager.h" +#include "paLeveldef.h" + +#include "hdw-led.h" +#include "palette.h" +#include "hdw-nvs.h" +#include "paSoundManager.h" +#include +#include "mainMenu.h" +#include "fill.h" +#include "paTables.h" + +//============================================================================== +// Constants +//============================================================================== +#define BIG_SCORE 4000000UL +#define BIGGER_SCORE 10000000UL +#define FAST_TIME 1500 // 25 minutes + +const char pangoName[] = "Pango"; + +static const paletteColor_t highScoreNewEntryColors[4] = {c050, c055, c005, c055}; + +static const paletteColor_t redColors[4] = {c501, c540, c550, c540}; +static const paletteColor_t yellowColors[4] = {c550, c331, c550, c555}; +static const paletteColor_t greenColors[4] = {c555, c051, c030, c051}; +static const paletteColor_t cyanColors[4] = {c055, c455, c055, c033}; +static const paletteColor_t purpleColors[4] = {c213, c535, c555, c535}; +static const paletteColor_t rgbColors[4] = {c500, c050, c005, c050}; + +static const int16_t cheatCode[11] + = {PB_UP, PB_UP, PB_DOWN, PB_DOWN, PB_LEFT, PB_RIGHT, PB_LEFT, PB_RIGHT, PB_B, PB_A, PB_START}; + +//============================================================================== +// Functions Prototypes +//============================================================================== + +void pangoEnterMode(void); +void pangoExitMode(void); +void pangoMainLoop(int64_t elapsedUs); + +//============================================================================== +// Structs +//============================================================================== + +typedef void (*pa_gameUpdateFuncton_t)(pango_t* self, int64_t elapsedUs); +struct pango_t +{ + font_t radiostars; + + paWsgManager_t wsgManager; + paTilemap_t tilemap; + paEntityManager_t entityManager; + paGameData_t gameData; + + paSoundManager_t soundManager; + + uint8_t menuState; + uint8_t menuSelection; + uint8_t cheatCodeIdx; + + int16_t btnState; + int16_t prevBtnState; + + int32_t frameTimer; + + pangoHighScores_t highScores; + pangoUnlockables_t unlockables; + bool easterEgg; + + pa_gameUpdateFuncton_t update; + + menuManiaRenderer_t* menuRenderer; + menu_t* menu; + menuItem_t* levelSelectMenuItem; +}; + +//============================================================================== +// Function Prototypes +//============================================================================== +void drawPangoHud(font_t* font, paGameData_t* gameData); +void drawPangoTitleScreen(font_t* font, paGameData_t* gameData); +void pangoBuildMainMenu(pango_t* self); +static void pangoUpdateMainMenu(pango_t* self, int64_t elapsedUs); +void changeStateReadyScreen(pango_t* self); +void updateReadyScreen(pango_t* self, int64_t elapsedUs); +void drawReadyScreen(font_t* font, paGameData_t* gameData); +void changeStateGame(pango_t* self); +void detectGameStateChange(pango_t* self); +void detectBgmChange(pango_t* self); +void changeStateDead(pango_t* self); +void updateDead(pango_t* self, int64_t elapsedUs); +void changeStateGameOver(pango_t* self); +void updateGameOver(pango_t* self, int64_t elapsedUs); +void drawGameOver(font_t* font, paGameData_t* gameData); +void changeStateTitleScreen(pango_t* self); +void changeStateLevelClear(pango_t* self); +void updateLevelClear(pango_t* self, int64_t elapsedUs); +void drawLevelClear(font_t* font, paGameData_t* gameData); +void changeStateGameClear(pango_t* self); +void updateGameClear(pango_t* self, int64_t elapsedUs); +void drawGameClear(font_t* font, paGameData_t* gameData); +void pangoInitializeHighScores(pango_t* self); +void loadPangoHighScores(pango_t* self); +void pangoSaveHighScores(pango_t* self); +void pangoInitializeUnlockables(pango_t* self); +void loadPangoUnlockables(pango_t* self); +void pangoSaveUnlockables(pango_t* self); +void drawPangoHighScores(font_t* font, pangoHighScores_t* highScores, paGameData_t* gameData); +uint8_t getHighScoreRank(pangoHighScores_t* highScores, uint32_t newScore); +void insertScoreIntoHighScores(pangoHighScores_t* highScores, uint32_t newScore, char newInitials[], uint8_t rank); +void changeStateNameEntry(pango_t* self); +void updateNameEntry(pango_t* self, int64_t elapsedUs); +void drawNameEntry(font_t* font, paGameData_t* gameData, uint8_t currentInitial); +void pangoChangeStateShowHighScores(pango_t* self); +void updateShowHighScores(pango_t* self, int64_t elapsedUs); +void drawShowHighScores(font_t* font, uint8_t menuState); +void changeStatePause(pango_t* self); +void updatePause(pango_t* self, int64_t elapsedUs); +void drawPause(font_t* font); +uint16_t getLevelIndex(uint8_t world, uint8_t level); +void pangoChangeStateMainMenu(pango_t* self); + +//============================================================================== +// Variables +//============================================================================== + +pango_t* pango = NULL; + +swadgeMode_t pangoMode = {.modeName = pangoName, + .wifiMode = NO_WIFI, + .overrideUsb = false, + .usesAccelerometer = false, + .usesThermometer = false, + .fnEnterMode = pangoEnterMode, + .fnExitMode = pangoExitMode, + .fnMainLoop = pangoMainLoop, + .fnAudioCallback = NULL, + .fnBackgroundDrawCallback = NULL, + .fnEspNowRecvCb = NULL, + .fnEspNowSendCb = NULL}; + +#define NUM_LEVELS 16 + +static const paLeveldef_t leveldef[17] = {{.filename = "level1-1.bin", .timeLimit = 180, .checkpointTimeLimit = 90}, + {.filename = "dac01.bin", .timeLimit = 180, .checkpointTimeLimit = 90}, + {.filename = "level1-3.bin", .timeLimit = 180, .checkpointTimeLimit = 90}, + {.filename = "level1-4.bin", .timeLimit = 180, .checkpointTimeLimit = 90}, + {.filename = "level2-1.bin", .timeLimit = 180, .checkpointTimeLimit = 90}, + {.filename = "dac03.bin", .timeLimit = 220, .checkpointTimeLimit = 90}, + {.filename = "level2-3.bin", .timeLimit = 200, .checkpointTimeLimit = 90}, + {.filename = "level2-4.bin", .timeLimit = 180, .checkpointTimeLimit = 90}, + {.filename = "level3-1.bin", .timeLimit = 180, .checkpointTimeLimit = 90}, + {.filename = "dac02.bin", .timeLimit = 180, .checkpointTimeLimit = 90}, + {.filename = "level3-3.bin", .timeLimit = 180, .checkpointTimeLimit = 90}, + {.filename = "level3-4.bin", .timeLimit = 220, .checkpointTimeLimit = 110}, + {.filename = "level4-1.bin", .timeLimit = 270, .checkpointTimeLimit = 90}, + {.filename = "level4-2.bin", .timeLimit = 240, .checkpointTimeLimit = 90}, + {.filename = "level4-3.bin", .timeLimit = 240, .checkpointTimeLimit = 90}, + {.filename = "level4-4.bin", .timeLimit = 240, .checkpointTimeLimit = 90}, + {.filename = "debug.bin", .timeLimit = 180, .checkpointTimeLimit = 90}}; + +led_t platLeds[CONFIG_NUM_LEDS]; + +static const char str_ready[] = "Ready?"; +static const char str_set[] = "Set..."; +static const char str_pango[] = "PANGO!"; +static const char str_time_up[] = "-Time Up!-"; +static const char str_game_over[] = "Game Over"; +static const char str_well_done[] = "Nice Clear!"; +static const char str_congrats[] = "Congratulations!"; +static const char str_initials[] = "Enter your initials!"; +static const char str_hbd[] = "Happy Birthday, Evelyn!"; +static const char str_registrated[] = "Your name registrated."; +static const char str_do_your_best[] = "Do your best!"; +static const char str_pause[] = "-Pause-"; + +static const char pangoMenuNewGame[] = "New Game"; +static const char pangoMenuContinue[] = "Continue - Lv"; +static const char pangoMenuCharacter[] = "Character"; +static const char pangoMenuHighScores[] = "High Scores"; +static const char pangoMenuResetScores[] = "Reset Scores"; +static const char pangoMenuResetProgress[] = "Reset Progress"; +static const char pangoMenuExit[] = "Exit"; +static const char pangoMenuSaveAndExit[] = "Save & Exit"; + +static const char KEY_SCORES[] = "pf_scores"; +static const char KEY_UNLOCKS[] = "pf_unlocks"; + +//============================================================================== +// Functions +//============================================================================== + +/** + * @brief TODO + * + */ +void pangoEnterMode(void) +{ + // Allocate memory for this mode + pango = (pango_t*)calloc(1, sizeof(pango_t)); + memset(pango, 0, sizeof(pango_t)); + + pango->menuState = 0; + pango->menuSelection = 0; + pango->btnState = 0; + pango->prevBtnState = 0; + + loadPangoHighScores(pango); + loadPangoUnlockables(pango); + if (pango->highScores.initials[0][0] == 'E' && pango->highScores.initials[0][1] == 'F' + && pango->highScores.initials[0][2] == 'V') + { + pango->easterEgg = true; + } + + loadFont("pango-fw.font", &pango->radiostars, false); + pango->menuRenderer = initMenuManiaRenderer(&pango->radiostars, &pango->radiostars, &pango->radiostars); + + pa_initializeWsgManager(&(pango->wsgManager)); + + pa_initializeTileMap(&(pango->tilemap), &(pango->wsgManager)); + pa_loadMapFromFile(&(pango->tilemap), "preset.bin"); + pa_generateMaze(&(pango->tilemap)); + pango->tilemap.mapOffsetX = -4; + + pa_initializeSoundManager(&(pango->soundManager)); + + pa_initializeGameData(&(pango->gameData), &(pango->soundManager)); + pa_initializeEntityManager(&(pango->entityManager), &(pango->wsgManager), &(pango->tilemap), &(pango->gameData), + &(pango->soundManager)); + + pango->tilemap.entityManager = &(pango->entityManager); + pango->tilemap.tileSpawnEnabled = true; + + setFrameRateUs(16666); + + pango->menu = NULL; + changeStateTitleScreen(pango); +} + +/** + * @brief TODO + * + */ +void pangoExitMode(void) +{ + // deinitMenu can't set menu pointer to NULL, + // so this is the only way to know that the menu has not been previously freed. + if (pango->update == &pangoUpdateMainMenu) + { + // Deinitialize the menu. + // This will also free the "level select" menu item. + deinitMenu(pango->menu); + } + deinitMenuManiaRenderer(pango->menuRenderer); + + freeFont(&pango->radiostars); + pa_freeWsgManager(&(pango->wsgManager)); + pa_freeTilemap(&(pango->tilemap)); + pa_freeSoundManager(&(pango->soundManager)); + pa_freeEntityManager(&(pango->entityManager)); + free(pango); +} + +/** + * @brief This callback function is called when an item is selected from the menu + * + * @param label The item that was selected from the menu + * @param selected True if the item was selected with the A button, false if this is a multi-item which scrolled to + * @param settingVal The value of the setting, if the menu item is a settings item + */ +static void pangoMenuCb(const char* label, bool selected, uint32_t settingVal) +{ + if (selected) + { + if (label == pangoMenuNewGame) + { + pango->gameData.world = 1; + pango->gameData.level = 1; + pango->entityManager.activeEnemies = 0; + pa_initializeGameDataFromTitleScreen(&(pango->gameData), 0); + pa_loadMapFromFile(&(pango->tilemap), "preset.bin"); + pa_generateMaze(&(pango->tilemap)); + pa_placeEnemySpawns(&(pango->tilemap)); + + changeStateReadyScreen(pango); + deinitMenu(pango->menu); + } + else if (label == pangoMenuContinue) + { + pango->gameData.world = 1; + pango->gameData.level = settingVal; + pa_initializeGameDataFromTitleScreen(&(pango->gameData), settingVal); + pango->entityManager.activeEnemies = 0; + pa_loadMapFromFile(&(pango->tilemap), "preset.bin"); + pa_generateMaze(&(pango->tilemap)); + pa_placeEnemySpawns(&(pango->tilemap)); + + changeStateReadyScreen(pango); + deinitMenu(pango->menu); + } + else if (label == pangoMenuCharacter) + { + pa_remapPlayerCharacter(&(pango->wsgManager), 16 * settingVal); + soundPlaySfx(&(pango->soundManager.sndMenuConfirm), BZR_STEREO); + } + else if (label == pangoMenuHighScores) + { + pangoChangeStateShowHighScores(pango); + pango->gameData.btnState = 0; + deinitMenu(pango->menu); + } + else if (label == pangoMenuResetScores) + { + pangoInitializeHighScores(pango); + // soundPlaySfx(&(pango->soundManager.detonate), BZR_STEREO); + } + else if (label == pangoMenuResetProgress) + { + pangoInitializeUnlockables(pango); + // soundPlaySfx(&(pango->soundManager.die), BZR_STEREO); + } + else if (label == pangoMenuSaveAndExit) + { + pangoSaveHighScores(pango); + pangoSaveUnlockables(pango); + switchToSwadgeMode(&mainMenuMode); + } + else if (label == pangoMenuExit) + { + switchToSwadgeMode(&mainMenuMode); + } + } + else + { + // soundPlaySfx(&(pango->soundManager.hit3), BZR_STEREO); + } +} + +/** + * @brief TODO + * + * @param elapsedUs + */ +void pangoMainLoop(int64_t elapsedUs) +{ + // Check inputs + buttonEvt_t evt = {0}; + while (checkButtonQueueWrapper(&evt)) + { + // Save the button state + pango->btnState = evt.state; + pango->gameData.btnState = evt.state; + + if (pango->update == &pangoUpdateMainMenu) + { + // Pass button events to the menu + pango->menu = menuButton(pango->menu, evt); + } + } + + pango->update(pango, elapsedUs); + + pango->prevBtnState = pango->btnState; + pango->gameData.prevBtnState = pango->prevBtnState; +} + +void pangoChangeStateMainMenu(pango_t* self) +{ + self->gameData.frameCount = 0; + self->update = &pangoUpdateMainMenu; + pangoBuildMainMenu(pango); +} + +void pangoBuildMainMenu(pango_t* self) +{ + // Initialize the menu + pango->menu = initMenu(pangoName, pangoMenuCb); + addSingleItemToMenu(pango->menu, pangoMenuNewGame); + + /* + Manually allocate and build "level select" menu item + because the max setting will have to change as levels are unlocked + */ + if (pango->unlockables.maxLevelIndexUnlocked > 1 || pango->gameData.debugMode) + { + pango->levelSelectMenuItem = calloc(1, sizeof(menuItem_t)); + pango->levelSelectMenuItem->label = pangoMenuContinue; + pango->levelSelectMenuItem->minSetting = 1; + pango->levelSelectMenuItem->maxSetting + = (pango->gameData.debugMode) ? NUM_LEVELS - 1 : pango->unlockables.maxLevelIndexUnlocked; + pango->levelSelectMenuItem->currentSetting + = (pango->gameData.level == 1) ? pango->levelSelectMenuItem->maxSetting : pango->gameData.level; + pango->levelSelectMenuItem->options = NULL; + pango->levelSelectMenuItem->subMenu = NULL; + + push(pango->menu->items, pango->levelSelectMenuItem); + } + + settingParam_t characterSettingBounds = { + .def = 0, + .min = 0, + .max = 3, + .key = NULL, + }; + addSettingsItemToMenu(pango->menu, pangoMenuCharacter, &characterSettingBounds, 0); + + addSingleItemToMenu(pango->menu, pangoMenuHighScores); + + if (pango->gameData.debugMode) + { + addSingleItemToMenu(pango->menu, pangoMenuResetProgress); + addSingleItemToMenu(pango->menu, pangoMenuResetScores); + addSingleItemToMenu(pango->menu, pangoMenuSaveAndExit); + } + else + { + addSingleItemToMenu(pango->menu, pangoMenuExit); + } +} + +static void pangoUpdateMainMenu(pango_t* self, int64_t elapsedUs) +{ + // Draw the menu + drawMenuMania(pango->menu, pango->menuRenderer, elapsedUs); +} + +void updateGame(pango_t* self, int64_t elapsedUs) +{ + // Clear the display + fillDisplayArea(0, 0, TFT_WIDTH, TFT_HEIGHT, self->gameData.bgColor); + + pa_updateEntities(&(self->entityManager)); + + pa_drawTileMap(&(self->tilemap)); + pa_drawEntities(&(self->entityManager)); + + // drawEntityTargetTile(self->entityManager.playerEntity); + + detectGameStateChange(self); + detectBgmChange(self); + + // self->gameData.coins = self->gameData.remainingEnemies; + self->gameData.coins = self->entityManager.aggroEnemies; + drawPangoHud(&(self->radiostars), &(self->gameData)); + + self->gameData.frameCount++; + if (self->gameData.frameCount > 59) + { + self->gameData.frameCount = 0; + self->gameData.countdown--; + self->gameData.inGameTimer++; + + if (self->gameData.countdown < 10) + { + soundPlayBgm(&(self->soundManager.sndOuttaTime), BZR_STEREO); + } + + if (self->gameData.countdown < 0) + { + killPlayer(self->entityManager.playerEntity); + } + + pa_spawnEnemyFromSpawnBlock(&(self->entityManager)); + } + + updateComboTimer(&(self->gameData)); +} + +void drawPangoHud(font_t* font, paGameData_t* gameData) +{ + char coinStr[8]; + snprintf(coinStr, sizeof(coinStr) - 1, "C:%02d", gameData->coins); + + char scoreStr[32]; + snprintf(scoreStr, sizeof(scoreStr) - 1, "%06" PRIu32, gameData->score); + + char levelStr[15]; + snprintf(levelStr, sizeof(levelStr) - 1, "Level %d-%d", gameData->world, gameData->level); + + char livesStr[8]; + snprintf(livesStr, sizeof(livesStr) - 1, "x%d", gameData->lives); + + char timeStr[10]; + snprintf(timeStr, sizeof(timeStr) - 1, "T:%03d", gameData->countdown); + + if (gameData->frameCount > 29) + { + drawText(font, c500, "1UP", 24, 2); + } + + drawText(font, c553, livesStr, 56, 2); + // drawText(font, c553, coinStr, 160, 224); + drawText(font, c553, scoreStr, 112, 2); + drawText(font, c553, levelStr, 32, 226); + drawText(font, (gameData->countdown > 30) ? c553 : redColors[(gameData->frameCount >> 3) % 4], timeStr, 200, 226); + + if (gameData->comboTimer == 0) + { + return; + } + + snprintf(scoreStr, sizeof(scoreStr) - 1, "+%" PRIu32 /*" (x%d)"*/, gameData->comboScore /*, gameData->combo*/); + drawText(font, (gameData->comboTimer < 60) ? c030 : greenColors[(pango->gameData.frameCount >> 3) % 4], scoreStr, + 190, 2); +} + +void updateTitleScreen(pango_t* self, int64_t elapsedUs) +{ + // Clear the display + fillDisplayArea(0, 0, TFT_WIDTH, TFT_HEIGHT, self->gameData.bgColor); + + self->gameData.frameCount++; + + if (self->gameData.frameCount > 600) + { + // resetGameDataLeds(&(self->gameData)); + pango->menuSelection = 0; + pangoChangeStateShowHighScores(self); + + return; + } + + if ((self->gameData.btnState & cheatCode[pango->menuSelection]) + && !(self->gameData.prevBtnState & cheatCode[pango->menuSelection])) + { + pango->menuSelection++; + + if (pango->menuSelection > 8) + { + pango->menuSelection = 0; + pango->menuState = 1; + pango->gameData.debugMode = true; + // soundPlaySfx(&(pango->soundManager.levelClear), BZR_STEREO); + } + else + { + // soundPlaySfx(&(pango->soundManager.hit3), BZR_STEREO); + } + } + + if ((((self->gameData.btnState & PB_A) && !(self->gameData.prevBtnState & PB_A)) + || ((self->gameData.btnState & PB_START) && !(self->gameData.prevBtnState & PB_START)))) + { + self->gameData.btnState = 0; + pango->menuSelection = 0; + + if (!pango->gameData.debugMode) + { + // soundPlaySfx(&(pango->soundManager.launch), BZR_STEREO); + } + + pangoChangeStateMainMenu(self); + return; + } + + // Handle inputs + /*switch (pango->menuState) + { + case 0: + { + if (self->gameData.frameCount > 600) + { + pa_resetGameDataLeds(&(self->gameData)); + changeStateShowHighScores(self); + } + + if ((self->gameData.btnState & cheatCode[pango->cheatCodeIdx]) + && !(self->gameData.prevBtnState & cheatCode[pango->cheatCodeIdx])) + { + pango->cheatCodeIdx++; + + if (pango->cheatCodeIdx > 10) + { + pango->cheatCodeIdx = 0; + pango->menuState = 1; + pango->gameData.debugMode = true; + soundPlaySfx(&(pango->soundManager.sndLevelClearS), BZR_STEREO); + break; + } + else + { + soundPlaySfx(&(pango->soundManager.sndMenuSelect), BZR_STEREO); + break; + } + } + else + { + if (!(self->gameData.frameCount % 150)) + { + pango->cheatCodeIdx = 0; + } + } + + if (((self->gameData.btnState & PB_START) && !(self->gameData.prevBtnState & PB_START)) + || ((self->gameData.btnState & PB_A) && !(self->gameData.prevBtnState & PB_A))) + { + soundPlaySfx(&(self->soundManager.sndMenuConfirm), BZR_STEREO); + pango->menuState = 1; + pango->menuSelection = 0; + } + + break; + } + case 1: + { + if (((self->gameData.btnState & PB_START) && !(self->gameData.prevBtnState & PB_START)) + || ((self->gameData.btnState & PB_A) && !(self->gameData.prevBtnState & PB_A))) + { + switch (self->menuSelection) + { + case 0 ... 1: + { + uint16_t levelIndex = getLevelIndex(self->gameData.world, self->gameData.level); + if ((levelIndex >= NUM_LEVELS) + || (!self->gameData.debugMode && levelIndex > self->unlockables.maxLevelIndexUnlocked)) + { + soundPlaySfx(&(self->soundManager.sndMenuDeny), BZR_STEREO); + break; + } + + //if(self->menuSelection == 0){ + // self->gameData.world = 1; + // self->gameData.level = 1; + // } + + pa_initializeGameDataFromTitleScreen(&(self->gameData), levelIndex); + self->entityManager.activeEnemies = 0; + pa_loadMapFromFile(&(pango->tilemap), "preset.bin"); + pa_generateMaze(&(pango->tilemap)); + pa_placeEnemySpawns(&(pango->tilemap)); + + changeStateReadyScreen(self); + break; + } + case 2: + { + if (self->gameData.debugMode) + { + // Reset Progress + pangoInitializeUnlockables(self); + soundPlaySfx(&(self->soundManager.sndBreak), BZR_STEREO); + } + else + { + // Show High Scores + self->menuSelection = 0; + self->menuState = 0; + + changeStateShowHighScores(self); + soundPlaySfx(&(self->soundManager.sndMenuConfirm), BZR_STEREO); + } + break; + } + case 3: + { + if (self->gameData.debugMode) + { + // Reset High Scores + pangoInitializeHighScores(self); + soundPlaySfx(&(self->soundManager.sndBreak), BZR_STEREO); + } + else + { + // Show Achievements + self->menuSelection = 0; + self->menuState = 2; + soundPlaySfx(&(self->soundManager.sndMenuConfirm), BZR_STEREO); + } + break; + } + case 4: + { + if (self->gameData.debugMode) + { + // Save & Quit + pangoSaveHighScores(self); + pangoSaveUnlockables(self); + soundPlaySfx(&(self->soundManager.sndMenuConfirm), BZR_STEREO); + switchToSwadgeMode(&mainMenuMode); + } + else + { + soundPlaySfx(&(self->soundManager.sndMenuConfirm), BZR_STEREO); + switchToSwadgeMode(&mainMenuMode); + } + break; + } + default: + { + soundPlaySfx(&(self->soundManager.sndMenuDeny), BZR_STEREO); + self->menuSelection = 0; + } + } + } + else if ((self->gameData.btnState & PB_UP && !(self->gameData.prevBtnState & PB_UP))) + { + if (pango->menuSelection > 0) + { + pango->menuSelection--; + + if (!self->gameData.debugMode && pango->menuSelection == 1 + && self->unlockables.maxLevelIndexUnlocked == 0) + { + pango->menuSelection--; + } + + soundPlaySfx(&(self->soundManager.sndMenuSelect), BZR_STEREO); + } + } + else if ((self->gameData.btnState & PB_DOWN && !(self->gameData.prevBtnState & PB_DOWN))) + { + if (pango->menuSelection < 4) + { + pango->menuSelection++; + + if (!self->gameData.debugMode && pango->menuSelection == 1 + && self->unlockables.maxLevelIndexUnlocked == 0) + { + pango->menuSelection++; + } + + soundPlaySfx(&(self->soundManager.sndMenuSelect), BZR_STEREO); + } + else + { + soundPlaySfx(&(self->soundManager.sndMenuDeny), BZR_STEREO); + } + } + else if ((self->gameData.btnState & PB_LEFT && !(self->gameData.prevBtnState & PB_LEFT))) + { + if (pango->menuSelection == 1) + { + if (pango->gameData.level == 1 && pango->gameData.world == 1) + { + soundPlaySfx(&(self->soundManager.sndMenuDeny), BZR_STEREO); + } + else + { + pango->gameData.level--; + if (pango->gameData.level < 1) + { + pango->gameData.level = 4; + if (pango->gameData.world > 1) + { + pango->gameData.world--; + } + } + soundPlaySfx(&(self->soundManager.sndMenuSelect), BZR_STEREO); + } + } + } + else if ((self->gameData.btnState & PB_RIGHT && !(self->gameData.prevBtnState & PB_RIGHT))) + { + if (pango->menuSelection == 1) + { + if ((pango->gameData.level == 4 && pango->gameData.world == 4) + || (!pango->gameData.debugMode + && getLevelIndex(pango->gameData.world, pango->gameData.level + 1) + > pango->unlockables.maxLevelIndexUnlocked)) + { + soundPlaySfx(&(self->soundManager.sndMenuDeny), BZR_STEREO); + } + else + { + pango->gameData.level++; + if (pango->gameData.level > 4) + { + pango->gameData.level = 1; + if (pango->gameData.world < 8) + { + pango->gameData.world++; + } + } + soundPlaySfx(&(self->soundManager.sndMenuSelect), BZR_STEREO); + } + } + } + else if ((self->gameData.btnState & PB_B && !(self->gameData.prevBtnState & PB_B))) + { + self->gameData.frameCount = 0; + pango->menuState = 0; + soundPlaySfx(&(self->soundManager.sndMenuConfirm), BZR_STEREO); + } + break; + } + case 2: + { + if ((self->gameData.btnState & PB_B && !(self->gameData.prevBtnState & PB_B))) + { + self->gameData.frameCount = 0; + pango->menuState = 1; + soundPlaySfx(&(self->soundManager.sndMenuConfirm), BZR_STEREO); + } + break; + } + default: + pango->menuState = 0; + soundPlaySfx(&(pango->soundManager.sndMenuDeny), BZR_STEREO); + break; + }*/ + + // pa_scrollTileMap(&(pango->tilemap), 1, 0); + // if (self->tilemap.mapOffsetX >= self->tilemap.maxMapOffsetX && self->gameData.frameCount > 58) + //{ + // self->tilemap.mapOffsetX = 0; + // } + + drawPangoTitleScreen(&(self->radiostars), &(self->gameData)); + + if (((self->gameData.frameCount) % 10) == 0) + { + for (int32_t i = 0; i < CONFIG_NUM_LEDS; i++) + { + // self->gameData.leds[i].r = (( (self->gameData.frameCount >> 4) % NUM_LEDS) == i) ? 0xFF : 0x00; + + platLeds[i].r += (esp_random() % 1); + platLeds[i].g += (esp_random() % 8); + platLeds[i].b += (esp_random() % 8); + } + } + setLeds(platLeds, CONFIG_NUM_LEDS); +} + +void drawPangoTitleScreen(font_t* font, paGameData_t* gameData) +{ + // pa_drawTileMap(&(pango->tilemap)); + + drawText(font, c555, "P A N G O", 96, 32); + + if (pango->gameData.debugMode) + { + drawText(font, c555, "Debug Mode", 80, 48); + } + + switch (pango->menuState) + { + case 0: + { + if ((gameData->frameCount % 60) < 30) + { + drawText(font, c555, "- Press START button -", 20, 128); + } + break; + } + + case 1: + { + drawText(font, c555, "Start Game", 48, 128); + + if (pango->gameData.debugMode || pango->unlockables.maxLevelIndexUnlocked > 0) + { + char levelStr[24]; + snprintf(levelStr, sizeof(levelStr) - 1, "Level Select: %d-%d", gameData->world, gameData->level); + drawText(font, c555, levelStr, 48, 144); + } + + if (pango->gameData.debugMode) + { + drawText(font, c555, "Reset Progress", 48, 160); + drawText(font, c555, "Reset High Scores", 48, 176); + drawText(font, c555, "Save & Exit to Menu", 48, 192); + } + else + { + drawText(font, c555, "High Scores", 48, 160); + drawText(font, c555, "Achievements", 48, 176); + drawText(font, c555, "Exit to Menu", 48, 192); + } + + drawText(font, c555, "->", 24, 128 + pango->menuSelection * 16); + + break; + } + + case 2: + { + if (pango->unlockables.gameCleared) + { + drawText(font, redColors[(gameData->frameCount >> 3) % 4], "Beat the game!", 48, 80); + } + + if (pango->unlockables.oneCreditCleared) + { + drawText(font, yellowColors[(gameData->frameCount >> 3) % 4], "1 Credit Clear!", 48, 96); + } + + if (pango->unlockables.bigScore) + { + drawText(font, greenColors[(gameData->frameCount >> 3) % 4], "Got 4 million points!", 48, 112); + } + + if (pango->unlockables.biggerScore) + { + drawText(font, cyanColors[(gameData->frameCount >> 3) % 4], "Got 10 million points!", 48, 128); + } + + if (pango->unlockables.fastTime) + { + drawText(font, purpleColors[(gameData->frameCount >> 3) % 4], "Beat within 25 min!", 48, 144); + } + + if (pango->unlockables.gameCleared && pango->unlockables.oneCreditCleared && pango->unlockables.bigScore + && pango->unlockables.biggerScore && pango->unlockables.fastTime) + { + drawText(font, rgbColors[(gameData->frameCount >> 3) % 4], "100% 100% 100%", 48, 160); + } + + drawText(font, c555, "Press B to Return", 48, 192); + break; + } + + default: + break; + } +} + +void changeStateReadyScreen(pango_t* self) +{ + self->gameData.frameCount = 0; + + soundPlayBgm(&(self->soundManager.bgmIntro), BZR_STEREO); + + pa_resetGameDataLeds(&(self->gameData)); + + self->update = &updateReadyScreen; +} + +void updateReadyScreen(pango_t* self, int64_t elapsedUs) +{ + // Clear the display + fillDisplayArea(0, 0, TFT_WIDTH, TFT_HEIGHT, c000); + + self->gameData.frameCount++; + if (self->gameData.frameCount > 179) + { + soundStop(true); + changeStateGame(self); + } + + drawReadyScreen(&(self->radiostars), &(self->gameData)); +} + +void drawReadyScreen(font_t* font, paGameData_t* gameData) +{ + drawPangoHud(font, gameData); + + drawText(font, c555, str_ready, 80, 96); + + if (gameData->frameCount > 60) + { + drawText(font, c555, str_set, 112, 128); + } + + if (gameData->frameCount > 120) + { + drawText(font, c555, str_pango, 144, 160); + } + + /*if (getLevelIndex(gameData->world, gameData->level) == 0) + { + drawText(font, c555, "A: Jump", xOff, 128 + (font->height + 3) * 3); + drawText(font, c555, "B: Run / Fire", xOff, 128 + (font->height + 3) * 4); + }*/ +} + +void changeStateGame(pango_t* self) +{ + self->gameData.frameCount = 0; + self->gameData.currentBgm = 0; + pa_resetGameDataLeds(&(self->gameData)); + + pa_deactivateAllEntities(&(self->entityManager), false); + + uint16_t levelIndex = getLevelIndex(self->gameData.world, self->gameData.level); + // pa_loadMapFromFile(&(pango->tilemap), "preset.bin"); + // pa_generateMaze(&(pango->tilemap)); + // pa_placeEnemySpawns(&(pango->tilemap)); + + self->gameData.countdown = leveldef[levelIndex].timeLimit; + + paEntityManager_t* entityManager = &(self->entityManager); + entityManager->viewEntity = pa_createPlayer(entityManager, (9 << PA_TILE_SIZE_IN_POWERS_OF_2) + PA_HALF_TILE_SIZE, + (7 << PA_TILE_SIZE_IN_POWERS_OF_2) + PA_HALF_TILE_SIZE); + entityManager->playerEntity = entityManager->viewEntity; + entityManager->playerEntity->hp = self->gameData.initialHp; + + if (entityManager->activeEnemies == 0) + { + for (uint16_t i = 0; i < self->gameData.maxActiveEnemies; i++) + { + pa_spawnEnemyFromSpawnBlock(&(self->entityManager)); + } + } + else + { + // uint16_t randomAggroEnemy = esp_random() % self->gameData.maxActiveEnemies; + + int16_t skippedEnemyRespawnCount = 0; + + for (uint16_t i = 0; i < entityManager->activeEnemies; i++) + { + if (i >= DEFAULT_ENEMY_SPAWN_LOCATION_TABLE_LENGTH) + { + skippedEnemyRespawnCount++; + continue; + } + + uint8_t spawnTx = defaultEnemySpawnLocations[i * DEFAULT_ENEMY_SPAWN_LOCATION_ROW_LENGTH + + DEFAULT_ENEMY_SPAWN_LOCATION_TX_LOOKUP_OFFSET]; + uint8_t spawnTy = defaultEnemySpawnLocations[i * DEFAULT_ENEMY_SPAWN_LOCATION_ROW_LENGTH + + DEFAULT_ENEMY_SPAWN_LOCATION_TY_LOOKUP_OFFSET]; + uint8_t tileAtSpawn = pa_getTile(&(self->tilemap), spawnTx, spawnTy); + + switch (tileAtSpawn) + { + default: + break; + case PA_TILE_BLOCK: + pa_setTile(&(self->tilemap), spawnTx, spawnTy, PA_TILE_EMPTY); + break; + case PA_TILE_SPAWN_BLOCK_0: + skippedEnemyRespawnCount++; + continue; + break; + } + + /*paEntity_t* newEnemy = */ createCrabdozer(&(self->entityManager), + (spawnTx << PA_TILE_SIZE_IN_POWERS_OF_2) + 8, + (spawnTy << PA_TILE_SIZE_IN_POWERS_OF_2) + 8); + + /*if(newEnemy != NULL && i == randomAggroEnemy){ + newEnemy->stateFlag = true; + newEnemy->state = PA_EN_ST_STUN; + newEnemy->stateTimer = 1; + }*/ + } + + entityManager->activeEnemies -= skippedEnemyRespawnCount; + } + + // pa_viewFollowEntity(&(self->tilemap), entityManager->playerEntity); + + pa_updateLedsHpMeter(&(self->entityManager), &(self->gameData)); + + self->tilemap.executeTileSpawnAll = true; + + self->update = &updateGame; +} + +void detectGameStateChange(pango_t* self) +{ + if (!self->gameData.changeState) + { + return; + } + + switch (self->gameData.changeState) + { + case PA_ST_DEAD: + changeStateDead(self); + break; + + case PA_ST_READY_SCREEN: + changeStateReadyScreen(self); + break; + + case PA_ST_LEVEL_CLEAR: + changeStateLevelClear(self); + break; + + case PA_ST_PAUSE: + changeStatePause(self); + break; + + default: + break; + } + + self->gameData.changeState = 0; +} + +void detectBgmChange(pango_t* self) +{ + if (!self->gameData.changeBgm) + { + return; + } + + switch (self->gameData.changeBgm) + { + case PA_BGM_NULL: + if (self->gameData.currentBgm != PA_BGM_NULL) + { + soundStop(true); + } + break; + + case PA_BGM_MAIN: + if (self->gameData.currentBgm != PA_BGM_MAIN) + { + soundPlayBgm(&(self->soundManager.bgmDemagio), BZR_STEREO); + } + break; + + case PA_BGM_ATHLETIC: + if (self->gameData.currentBgm != PA_BGM_ATHLETIC) + { + soundPlayBgm(&(self->soundManager.bgmSmooth), BZR_STEREO); + } + break; + + case PA_BGM_UNDERGROUND: + if (self->gameData.currentBgm != PA_BGM_UNDERGROUND) + { + soundPlayBgm(&(self->soundManager.bgmUnderground), BZR_STEREO); + } + break; + + case PA_BGM_FORTRESS: + if (self->gameData.currentBgm != PA_BGM_FORTRESS) + { + soundPlayBgm(&(self->soundManager.bgmCastle), BZR_STEREO); + } + break; + + default: + break; + } + + self->gameData.currentBgm = self->gameData.changeBgm; + self->gameData.changeBgm = 0; +} + +void changeStateDead(pango_t* self) +{ + self->gameData.frameCount = 0; + self->gameData.lives--; + self->gameData.levelDeaths++; + self->gameData.combo = 0; + self->gameData.comboTimer = 0; + self->gameData.initialHp = 1; + + soundStop(true); + soundPlayBgm(&(self->soundManager.sndDie), BZR_STEREO); + + self->update = &updateDead; +} + +void updateDead(pango_t* self, int64_t elapsedUs) +{ + // Clear the display + fillDisplayArea(0, 0, TFT_WIDTH, TFT_HEIGHT, self->gameData.bgColor); + + self->gameData.frameCount++; + if (self->gameData.frameCount > 179) + { + if (self->gameData.lives > 0) + { + changeStateReadyScreen(self); + } + else + { + changeStateGameOver(self); + } + } + + pa_updateEntities(&(self->entityManager)); + pa_drawTileMap(&(self->tilemap)); + pa_drawEntities(&(self->entityManager)); + drawPangoHud(&(self->radiostars), &(self->gameData)); + + if (self->gameData.countdown < 0) + { + drawText(&(self->radiostars), c555, str_time_up, (TFT_WIDTH - textWidth(&(self->radiostars), str_time_up)) / 2, + 128); + } +} + +void updateGameOver(pango_t* self, int64_t elapsedUs) +{ + // Clear the display + fillDisplayArea(0, 0, TFT_WIDTH, TFT_HEIGHT, c000); + + self->gameData.frameCount++; + if (self->gameData.frameCount > 179) + { + // Handle unlockables + + if (self->gameData.score >= BIG_SCORE) + { + self->unlockables.bigScore = true; + } + + if (self->gameData.score >= BIGGER_SCORE) + { + self->unlockables.biggerScore = true; + } + + if (!self->gameData.debugMode) + { + pangoSaveUnlockables(self); + } + + changeStateNameEntry(self); + } + + drawGameOver(&(self->radiostars), &(self->gameData)); + pa_updateLedsGameOver(&(self->gameData)); +} + +void changeStateGameOver(pango_t* self) +{ + self->gameData.frameCount = 0; + pa_resetGameDataLeds(&(self->gameData)); + soundPlayBgm(&(self->soundManager.bgmGameOver), BZR_STEREO); + self->update = &updateGameOver; +} + +void drawGameOver(font_t* font, paGameData_t* gameData) +{ + drawPangoHud(font, gameData); + drawText(font, c555, str_game_over, (TFT_WIDTH - textWidth(font, str_game_over)) / 2, 128); +} + +void changeStateTitleScreen(pango_t* self) +{ + self->gameData.frameCount = 0; + self->gameData.gameState = PA_ST_TITLE_SCREEN; + self->update = &updateTitleScreen; +} + +void changeStateLevelClear(pango_t* self) +{ + self->gameData.frameCount = 0; + self->gameData.checkpoint = 0; + self->gameData.levelDeaths = 0; + self->gameData.initialHp = self->entityManager.playerEntity->hp; + self->gameData.extraLifeCollected = false; + pa_resetGameDataLeds(&(self->gameData)); + self->update = &updateLevelClear; +} + +void updateLevelClear(pango_t* self, int64_t elapsedUs) +{ + // Clear the display + fillDisplayArea(0, 0, TFT_WIDTH, TFT_HEIGHT, self->gameData.bgColor); + + self->gameData.frameCount++; + + if (self->gameData.frameCount > 60) + { + if (self->gameData.countdown > 0) + { + self->gameData.countdown--; + + if (self->gameData.countdown % 2) + { + soundPlayBgm(&(self->soundManager.sndTally), BZR_STEREO); + } + + uint16_t comboPoints = 50 * self->gameData.combo; + + self->gameData.score += comboPoints; + self->gameData.comboScore = comboPoints; + + if (self->gameData.combo > 1) + { + self->gameData.combo--; + } + } + else if (self->gameData.frameCount % 60 == 0) + { + // Hey look, it's a frame rule! + + uint16_t levelIndex = getLevelIndex(self->gameData.world, self->gameData.level); + + if (levelIndex >= NUM_LEVELS - 1) + { + // Game Cleared! + + if (!self->gameData.debugMode) + { + // Determine achievements + self->unlockables.gameCleared = true; + + if (!self->gameData.continuesUsed) + { + self->unlockables.oneCreditCleared = true; + + if (self->gameData.inGameTimer < FAST_TIME) + { + self->unlockables.fastTime = true; + } + } + + if (self->gameData.score >= BIG_SCORE) + { + self->unlockables.bigScore = true; + } + + if (self->gameData.score >= BIGGER_SCORE) + { + self->unlockables.biggerScore = true; + } + } + + changeStateGameClear(self); + } + else + { + // Advance to the next level + self->gameData.level++; + if (self->gameData.level > 4) + { + self->gameData.world++; + self->gameData.level = 1; + } + + // Unlock the next level + levelIndex++; + if (levelIndex > self->unlockables.maxLevelIndexUnlocked) + { + self->unlockables.maxLevelIndexUnlocked = levelIndex; + } + + pa_setDifficultyLevel(&(pango->gameData), getLevelIndex(self->gameData.world, self->gameData.level)); + pa_loadMapFromFile(&(pango->tilemap), "preset.bin"); + pa_generateMaze(&(pango->tilemap)); + pa_placeEnemySpawns(&(pango->tilemap)); + + self->entityManager.activeEnemies = 0; + + changeStateReadyScreen(self); + } + + if (!self->gameData.debugMode) + { + pangoSaveUnlockables(self); + } + + return; + } + } + + pa_updateEntities(&(self->entityManager)); + pa_drawTileMap(&(self->tilemap)); + pa_drawEntities(&(self->entityManager)); + drawPangoHud(&(self->radiostars), &(self->gameData)); + drawLevelClear(&(self->radiostars), &(self->gameData)); + pa_updateLedsLevelClear(&(self->gameData)); +} + +void drawLevelClear(font_t* font, paGameData_t* gameData) +{ + drawPangoHud(font, gameData); + drawText(font, c000, str_well_done, (TFT_WIDTH - textWidth(font, str_well_done) + 1) >> 1, 129); + drawText(font, c553, str_well_done, (TFT_WIDTH - textWidth(font, str_well_done)) >> 1, 128); +} + +void changeStateGameClear(pango_t* self) +{ + self->gameData.frameCount = 0; + self->update = &updateGameClear; + pa_resetGameDataLeds(&(self->gameData)); + soundPlayBgm(&(self->soundManager.bgmSmooth), BZR_STEREO); +} + +void updateGameClear(pango_t* self, int64_t elapsedUs) +{ + // Clear the display + fillDisplayArea(0, 0, TFT_WIDTH, TFT_HEIGHT, c000); + + self->gameData.frameCount++; + + if (self->gameData.frameCount > 450) + { + if (self->gameData.lives > 0) + { + if (self->gameData.frameCount % 60 == 0) + { + self->gameData.lives--; + self->gameData.score += 200000; + soundPlaySfx(&(self->soundManager.snd1up), BZR_STEREO); + } + } + else if (self->gameData.frameCount % 960 == 0) + { + changeStateGameOver(self); + } + } + + drawPangoHud(&(self->radiostars), &(self->gameData)); + drawGameClear(&(self->radiostars), &(self->gameData)); + pa_updateLedsGameClear(&(self->gameData)); +} + +void drawGameClear(font_t* font, paGameData_t* gameData) +{ + drawPangoHud(font, gameData); + + char timeStr[32]; + snprintf(timeStr, sizeof(timeStr) - 1, "in %06" PRIu32 " seconds!", gameData->inGameTimer); + + drawText(font, yellowColors[(gameData->frameCount >> 3) % 4], str_congrats, + (TFT_WIDTH - textWidth(font, str_congrats)) / 2, 48); + + if (gameData->frameCount > 120) + { + drawText(font, c555, "You've completed your", 8, 80); + drawText(font, c555, "trip across Swadge Land", 8, 96); + } + + if (gameData->frameCount > 180) + { + drawText(font, (gameData->inGameTimer < FAST_TIME) ? cyanColors[(gameData->frameCount >> 3) % 4] : c555, + timeStr, (TFT_WIDTH - textWidth(font, timeStr)) / 2, 112); + } + + if (gameData->frameCount > 300) + { + drawText(font, c555, "The Swadge staff", 8, 144); + drawText(font, c555, "thanks you for playing!", 8, 160); + } + + if (gameData->frameCount > 420) + { + drawText(font, (gameData->lives > 0) ? highScoreNewEntryColors[(gameData->frameCount >> 3) % 4] : c555, + "Bonus 200000pts per life!", (TFT_WIDTH - textWidth(font, "Bonus 100000pts per life!")) / 2, 192); + } + + /* + drawText(font, c555, "Thanks for playing.", 24, 48); + drawText(font, c555, "Many more battle scenes", 8, 96); + drawText(font, c555, "will soon be available!", 8, 112); + drawText(font, c555, "Bonus 100000pts per life!", 8, 160); + */ +} + +void pangoInitializeHighScores(pango_t* self) +{ + self->highScores.scores[0] = 100000; + self->highScores.scores[1] = 80000; + self->highScores.scores[2] = 40000; + self->highScores.scores[3] = 20000; + self->highScores.scores[4] = 10000; + + for (uint8_t i = 0; i < NUM_PLATFORMER_HIGH_SCORES; i++) + { + self->highScores.initials[i][0] = 'J' + i; + self->highScores.initials[i][1] = 'P' - i; + self->highScores.initials[i][2] = 'V' + i; + } +} + +void loadPangoHighScores(pango_t* self) +{ + size_t size = sizeof(pangoHighScores_t); + // Try reading the value + if (false == readNvsBlob(KEY_SCORES, &(self->highScores), &(size))) + { + // Value didn't exist, so write the default + pangoInitializeHighScores(self); + } +} + +void pangoSaveHighScores(pango_t* self) +{ + size_t size = sizeof(pangoHighScores_t); + writeNvsBlob(KEY_SCORES, &(self->highScores), size); +} + +void pangoInitializeUnlockables(pango_t* self) +{ + self->unlockables.maxLevelIndexUnlocked = 0; + self->unlockables.gameCleared = false; + self->unlockables.oneCreditCleared = false; + self->unlockables.bigScore = false; + self->unlockables.fastTime = false; + self->unlockables.biggerScore = false; +} + +void loadPangoUnlockables(pango_t* self) +{ + size_t size = sizeof(pangoUnlockables_t); + // Try reading the value + if (false == readNvsBlob(KEY_UNLOCKS, &(self->unlockables), &(size))) + { + // Value didn't exist, so write the default + pangoInitializeHighScores(self); + } +} + +void pangoSaveUnlockables(pango_t* self) +{ + size_t size = sizeof(pangoUnlockables_t); + writeNvsBlob(KEY_UNLOCKS, &(self->unlockables), size); +} + +void drawPangoHighScores(font_t* font, pangoHighScores_t* highScores, paGameData_t* gameData) +{ + drawText(font, c555, "RANK SCORE NAME", 48, 96); + for (uint8_t i = 0; i < NUM_PLATFORMER_HIGH_SCORES; i++) + { + char rowStr[32]; + snprintf(rowStr, sizeof(rowStr) - 1, "%d %06" PRIu32 " %c%c%c", i + 1, highScores->scores[i], + highScores->initials[i][0], highScores->initials[i][1], highScores->initials[i][2]); + drawText(font, (gameData->rank == i) ? highScoreNewEntryColors[(gameData->frameCount >> 3) % 4] : c555, rowStr, + 60, 128 + i * 16); + } +} + +uint8_t getHighScoreRank(pangoHighScores_t* highScores, uint32_t newScore) +{ + uint8_t i; + for (i = 0; i < NUM_PLATFORMER_HIGH_SCORES; i++) + { + if (highScores->scores[i] < newScore) + { + break; + } + } + + return i; +} + +void insertScoreIntoHighScores(pangoHighScores_t* highScores, uint32_t newScore, char newInitials[], uint8_t rank) +{ + if (rank >= NUM_PLATFORMER_HIGH_SCORES) + { + return; + } + + for (uint8_t i = NUM_PLATFORMER_HIGH_SCORES - 1; i > rank; i--) + { + highScores->scores[i] = highScores->scores[i - 1]; + highScores->initials[i][0] = highScores->initials[i - 1][0]; + highScores->initials[i][1] = highScores->initials[i - 1][1]; + highScores->initials[i][2] = highScores->initials[i - 1][2]; + } + + highScores->scores[rank] = newScore; + highScores->initials[rank][0] = newInitials[0]; + highScores->initials[rank][1] = newInitials[1]; + highScores->initials[rank][2] = newInitials[2]; +} + +void changeStateNameEntry(pango_t* self) +{ + self->gameData.frameCount = 0; + uint8_t rank = getHighScoreRank(&(self->highScores), self->gameData.score); + self->gameData.rank = rank; + self->menuState = 0; + + pa_resetGameDataLeds(&(self->gameData)); + + if (rank >= NUM_PLATFORMER_HIGH_SCORES || self->gameData.debugMode) + { + self->menuSelection = 0; + self->gameData.rank = NUM_PLATFORMER_HIGH_SCORES; + pangoChangeStateShowHighScores(self); + return; + } + + soundPlayBgm(&(self->soundManager.bgmNameEntry), BZR_STEREO); + self->menuSelection = self->gameData.initials[0]; + self->update = &updateNameEntry; +} + +void updateNameEntry(pango_t* self, int64_t elapsedUs) +{ + fillDisplayArea(0, 0, TFT_WIDTH, TFT_HEIGHT, c000); + + self->gameData.frameCount++; + + if (self->gameData.btnState & PB_LEFT && !(self->gameData.prevBtnState & PB_LEFT)) + { + self->menuSelection--; + + if (self->menuSelection < 32) + { + self->menuSelection = 90; + } + + self->gameData.initials[self->menuState] = self->menuSelection; + soundPlaySfx(&(self->soundManager.sndMenuSelect), BZR_STEREO); + } + else if (self->gameData.btnState & PB_RIGHT && !(self->gameData.prevBtnState & PB_RIGHT)) + { + self->menuSelection++; + + if (self->menuSelection > 90) + { + self->menuSelection = 32; + } + + self->gameData.initials[self->menuState] = self->menuSelection; + soundPlaySfx(&(self->soundManager.sndMenuSelect), BZR_STEREO); + } + else if (self->gameData.btnState & PB_B && !(self->gameData.prevBtnState & PB_B)) + { + if (self->menuState > 0) + { + self->menuState--; + self->menuSelection = self->gameData.initials[self->menuState]; + soundPlaySfx(&(self->soundManager.sndMenuSelect), BZR_STEREO); + } + else + { + soundPlaySfx(&(self->soundManager.sndMenuDeny), BZR_STEREO); + } + } + else if (self->gameData.btnState & PB_A && !(self->gameData.prevBtnState & PB_A)) + { + self->menuState++; + + if (self->menuState > 2) + { + insertScoreIntoHighScores(&(self->highScores), self->gameData.score, self->gameData.initials, + self->gameData.rank); + pangoSaveHighScores(self); + pangoChangeStateShowHighScores(self); + soundPlaySfx(&(self->soundManager.sndPowerUp), BZR_STEREO); + } + else + { + self->menuSelection = self->gameData.initials[self->menuState]; + soundPlaySfx(&(self->soundManager.sndMenuSelect), BZR_STEREO); + } + } + + drawNameEntry(&(self->radiostars), &(self->gameData), self->menuState); + pa_updateLedsShowHighScores(&(self->gameData)); +} + +void drawNameEntry(font_t* font, paGameData_t* gameData, uint8_t currentInitial) +{ + drawText(font, greenColors[(pango->gameData.frameCount >> 3) % 4], str_initials, + (TFT_WIDTH - textWidth(font, str_initials)) / 2, 64); + + char rowStr[32]; + snprintf(rowStr, sizeof(rowStr) - 1, "%d %06" PRIu32, gameData->rank + 1, gameData->score); + drawText(font, c555, rowStr, 64, 128); + + for (uint8_t i = 0; i < 3; i++) + { + snprintf(rowStr, sizeof(rowStr) - 1, "%c", gameData->initials[i]); + drawText(font, (currentInitial == i) ? highScoreNewEntryColors[(gameData->frameCount >> 3) % 4] : c555, rowStr, + 192 + 16 * i, 128); + } +} + +void pangoChangeStateShowHighScores(pango_t* self) +{ + self->gameData.frameCount = 0; + self->update = &updateShowHighScores; +} + +void updateShowHighScores(pango_t* self, int64_t elapsedUs) +{ + fillDisplayArea(0, 0, TFT_WIDTH, TFT_HEIGHT, c000); + + self->gameData.frameCount++; + + if ((self->gameData.frameCount > 300) + || (((self->gameData.btnState & PB_START) && !(self->gameData.prevBtnState & PB_START)) + || ((self->gameData.btnState & PB_A) && !(self->gameData.prevBtnState & PB_A)))) + { + self->menuState = 0; + self->menuSelection = 0; + soundStop(true); + changeStateTitleScreen(self); + } + + drawShowHighScores(&(self->radiostars), self->menuState); + drawPangoHighScores(&(self->radiostars), &(self->highScores), &(self->gameData)); + + pa_updateLedsShowHighScores(&(self->gameData)); +} + +void drawShowHighScores(font_t* font, uint8_t menuState) +{ + if (pango->easterEgg) + { + drawText(font, highScoreNewEntryColors[(pango->gameData.frameCount >> 3) % 4], str_hbd, + (TFT_WIDTH - textWidth(font, str_hbd)) / 2, 32); + } + else if (menuState == 3) + { + drawText(font, redColors[(pango->gameData.frameCount >> 3) % 4], str_registrated, + (TFT_WIDTH - textWidth(font, str_registrated)) / 2, 32); + } + else + { + drawText(font, c555, str_do_your_best, (TFT_WIDTH - textWidth(font, str_do_your_best)) / 2, 32); + } +} + +void changeStatePause(pango_t* self) +{ + soundStop(true); + soundPlaySfx(&(self->soundManager.sndPause), BZR_STEREO); + self->update = &updatePause; +} + +void updatePause(pango_t* self, int64_t elapsedUs) +{ + if (((self->gameData.btnState & PB_START) && !(self->gameData.prevBtnState & PB_START))) + { + soundPlaySfx(&(self->soundManager.sndPause), BZR_STEREO); + self->gameData.changeBgm = self->gameData.currentBgm; + self->gameData.currentBgm = PA_BGM_NULL; + self->update = &updateGame; + } + + pa_drawTileMap(&(self->tilemap)); + pa_drawEntities(&(self->entityManager)); + drawPangoHud(&(self->radiostars), &(self->gameData)); + drawPause(&(self->radiostars)); +} + +void drawPause(font_t* font) +{ + drawText(font, c000, str_pause, (TFT_WIDTH - textWidth(font, str_pause) + 2) >> 1, 129); + drawText(font, c553, str_pause, (TFT_WIDTH - textWidth(font, str_pause)) >> 1, 128); +} + +uint16_t getLevelIndex(uint8_t world, uint8_t level) +{ + return (world - 1) * 4 + (level - 1); +} \ No newline at end of file diff --git a/main/modes/games/pango/pango.h b/main/modes/games/pango/pango.h new file mode 100644 index 000000000..74d86ca46 --- /dev/null +++ b/main/modes/games/pango/pango.h @@ -0,0 +1,47 @@ +#ifndef _MODE_PLATFORMER_H_ +#define _MODE_PLATFORMER_H_ +//============================================================================== +// Includes +//============================================================================== + +#include "pango_typedef.h" +#include "swadge2024.h" + +/*============================================================================== + * Constants + *============================================================================*/ + +#define NUM_PLATFORMER_HIGH_SCORES 5 + +extern const char pangoName[]; + +//============================================================================== +// Structs +//============================================================================== + +typedef struct +{ + uint32_t scores[NUM_PLATFORMER_HIGH_SCORES]; + char initials[NUM_PLATFORMER_HIGH_SCORES][3]; +} pangoHighScores_t; + +typedef struct +{ + uint8_t maxLevelIndexUnlocked; + bool gameCleared; + bool oneCreditCleared; + bool bigScore; + bool fastTime; + bool biggerScore; +} pangoUnlockables_t; + +//============================================================================== +// Prototypes +//============================================================================== + +void updateGame(pango_t* pango, int64_t elapsedUs); +void updateTitleScreen(pango_t* pango, int64_t elapsedUs); + +extern swadgeMode_t pangoMode; + +#endif diff --git a/main/modes/games/pango/pango_typedef.h b/main/modes/games/pango/pango_typedef.h new file mode 100644 index 000000000..151211633 --- /dev/null +++ b/main/modes/games/pango/pango_typedef.h @@ -0,0 +1,71 @@ +#ifndef PANGO_COMMON_TYPEDEF_INCLUDED +#define PANGO_COMMON_TYPEDEF_INCLUDED + +typedef struct pango_t pango_t; +typedef struct paEntityManager_t paEntityManager_t; +typedef struct paTilemap_t paTilemap_t; +typedef struct paEntity_t paEntity_t; + +typedef enum +{ + PA_ST_NULL, + PA_ST_TITLE_SCREEN, + PA_ST_READY_SCREEN, + PA_ST_GAME, + PA_ST_DEAD, + PA_ST_LEVEL_CLEAR, + PA_ST_WORLD_CLEAR, + PA_ST_GAME_CLEAR, + PA_ST_GAME_OVER, + PA_ST_HIGH_SCORE_ENTRY, + PA_ST_HIGH_SCORE_TABLE, + PA_ST_PAUSE +} pa_gameStateEnum_t; + +typedef enum +{ + PA_BGM_NO_CHANGE, + PA_BGM_MAIN, + PA_BGM_ATHLETIC, + PA_BGM_UNDERGROUND, + PA_BGM_FORTRESS, + PA_BGM_NULL +} pa_bgmEnum_t; + +typedef enum +{ + PA_SP_PLAYER_SOUTH, + PA_SP_PLAYER_WALK_SOUTH, + PA_SP_PLAYER_NORTH, + PA_SP_PLAYER_WALK_NORTH, + PA_SP_PLAYER_SIDE, + PA_SP_PLAYER_WALK_SIDE_1, + PA_SP_PLAYER_WALK_SIDE_2, + PA_SP_PLAYER_PUSH_SOUTH_1, + PA_SP_PLAYER_PUSH_SOUTH_2, + PA_SP_PLAYER_PUSH_NORTH_1, + PA_SP_PLAYER_PUSH_NORTH_2, + PA_SP_PLAYER_PUSH_SIDE_1, + PA_SP_PLAYER_PUSH_SIDE_2, + PA_SP_PLAYER_HURT, + PA_SP_PLAYER_WIN, + PA_SP_PLAYER_ICON, + PA_SP_BLOCK, + PA_SP_BONUS_BLOCK, + PA_SP_ENEMY_SOUTH, + PA_SP_ENEMY_NORTH, + PA_SP_ENEMY_SIDE_1, + PA_SP_ENEMY_SIDE_2, + PA_SP_ENEMY_DRILL_SOUTH, + PA_SP_ENEMY_DRILL_NORTH, + PA_SP_ENEMY_DRILL_SIDE_1, + PA_SP_ENEMY_DRILL_SIDE_2, + PA_SP_ENEMY_STUN, + PA_SP_BREAK_BLOCK, + PA_SP_BREAK_BLOCK_1, + PA_SP_BREAK_BLOCK_2, + PA_SP_BREAK_BLOCK_3, + PA_SP_BLOCK_FRAGMENT +} pa_spriteDef_t; + +#endif \ No newline at end of file diff --git a/main/modes/games/pinball/mode_pinball.h b/main/modes/games/pinball/mode_pinball.h deleted file mode 100644 index 20546c649..000000000 --- a/main/modes/games/pinball/mode_pinball.h +++ /dev/null @@ -1,111 +0,0 @@ -#pragma once - -//============================================================================== -// Includes -//============================================================================== - -#include -#include "swadge2024.h" - -//============================================================================== -// Defines -//============================================================================== - -#define PIN_US_PER_FRAME 16667 -#define NUM_ZONES 32 - -#define MAX_NUM_BALLS 512 -#define MAX_NUM_WALLS 1024 -#define MAX_NUM_BUMPERS 10 -#define MAX_NUM_TOUCHES 16 -#define MAX_NUM_FLIPPERS 6 - -#define NUM_FRAME_TIMES 60 - -//============================================================================== -// Enums -//============================================================================== - -typedef enum -{ - PIN_NO_SHAPE, - PIN_CIRCLE, - PIN_LINE, - PIN_RECT, - PIN_FLIPPER, -} pbShapeType_t; - -//============================================================================== -// Structs -//============================================================================== - -typedef struct -{ - circleFl_t c; - vecFl_t vel; // Velocity is in pixels per frame (@ 60fps, so pixels per 16.7ms) - vecFl_t accel; // Acceleration is pixels per frame squared - vecFl_t lastPos; // The previous postion, used to compare actual positional change to velocity - bool bounce; // true if the ball bounced this frame, false otherwise - uint32_t zoneMask; - paletteColor_t color; - bool filled; -} pbCircle_t; - -typedef struct -{ - lineFl_t l; - float length; - uint32_t zoneMask; - paletteColor_t color; -} pbLine_t; - -typedef struct -{ - rectangleFl_t r; - uint32_t zoneMask; - paletteColor_t color; -} pbRect_t; - -typedef struct -{ - pbCircle_t cPivot; ///< The circle that the flipper pivots on - pbCircle_t cTip; ///< The circle at the tip of the flipper - pbLine_t sideL; ///< The left side of the flipper when pointing upward - pbLine_t sideR; ///< The right side of the flipper when pointing upward - int32_t length; ///< The length of the flipper, from pivot center to tip center - float angle; ///< The current angle of the flipper - bool facingRight; ///< True if the flipper is facing right, false if left - bool buttonHeld; ///< True if the button is being held down, false if it is released - uint32_t zoneMask; ///< The zones this flipper is in -} pbFlipper_t; - -typedef struct -{ - const void* obj; - pbShapeType_t type; -} pbTouchRef_t; - -typedef struct -{ - pbCircle_t* balls; - uint32_t numBalls; - pbTouchRef_t** ballsTouching; - pbLine_t* walls; - uint32_t numWalls; - pbCircle_t* bumpers; - uint32_t numBumpers; - pbFlipper_t* flippers; - uint32_t numFlippers; - int32_t frameTimer; - pbRect_t zones[NUM_ZONES]; - font_t ibm_vga8; - - uint32_t frameTimes[NUM_FRAME_TIMES]; - uint32_t frameTimesIdx; -} pinball_t; - -//============================================================================== -// Extern variables -//============================================================================== - -extern swadgeMode_t pinballMode; diff --git a/main/modes/games/pinball/pinball_draw.c b/main/modes/games/pinball/pinball_draw.c deleted file mode 100644 index 0cdd62265..000000000 --- a/main/modes/games/pinball/pinball_draw.c +++ /dev/null @@ -1,134 +0,0 @@ -//============================================================================== -// Includes -//============================================================================== - -#include "pinball_draw.h" - -//============================================================================== -// Function Declarations -//============================================================================== - -static void drawPinCircle(pbCircle_t* c); -static void drawPinLine(pbLine_t* l); -// static void drawPinRect(pbRect_t* r); -static void drawPinFlipper(pbFlipper_t* f); - -//============================================================================== -// Functions -//============================================================================== - -/** - * @brief Draw a section of the background - * - * @param p Pinball state - * @param x The x coordinate of the background to draw - * @param y The y coordinate of the background to draw - * @param w The width of the background to draw - * @param h The height of the background to draw - */ -void pinballDrawBackground(pinball_t* p, int16_t x, int16_t y, int16_t w, int16_t h) -{ - // Fill with black - fillDisplayArea(x, y, x + w, y + h, c000); -} - -/** - * @brief Draw the foreground - * - * @param p Pinball state - */ -void pinballDrawForeground(pinball_t* p) -{ - // Draw walls - for (uint32_t wIdx = 0; wIdx < p->numWalls; wIdx++) - { - drawPinLine(&p->walls[wIdx]); - } - - // Draw bumpers - for (uint32_t uIdx = 0; uIdx < p->numBumpers; uIdx++) - { - drawPinCircle(&p->bumpers[uIdx]); - } - - // Draw balls - for (uint32_t bIdx = 0; bIdx < p->numBalls; bIdx++) - { - drawPinCircle(&p->balls[bIdx]); - } - - // Draw flippers - for (uint32_t fIdx = 0; fIdx < p->numFlippers; fIdx++) - { - drawPinFlipper(&p->flippers[fIdx]); - } - - // Debug draw zones - // for (int32_t i = 0; i < NUM_ZONES; i++) - // { - // drawPinRect(&p->zones[i]); - // } - - // Calculate and draw FPS - int32_t startIdx = (p->frameTimesIdx + 1) % NUM_FRAME_TIMES; - uint32_t tElapsed = p->frameTimes[p->frameTimesIdx] - p->frameTimes[startIdx]; - if (0 != tElapsed) - { - uint32_t fps = (1000000 * NUM_FRAME_TIMES) / tElapsed; - - char tmp[16]; - snprintf(tmp, sizeof(tmp) - 1, "%" PRIu32, fps); - drawText(&p->ibm_vga8, c555, tmp, 35, 2); - } -} - -/** - * @brief Draw a pinball circle - * - * @param c The circle to draw - */ -void drawPinCircle(pbCircle_t* circ) -{ - if (circ->filled) - { - drawCircleFilled((circ->c.pos.x), (circ->c.pos.y), (circ->c.radius), circ->color); - } - else - { - drawCircle((circ->c.pos.x), (circ->c.pos.y), (circ->c.radius), circ->color); - } -} - -/** - * @brief Draw a pinball line - * - * @param l The line to draw - */ -void drawPinLine(pbLine_t* line) -{ - drawLineFast((line->l.p1.x), (line->l.p1.y), (line->l.p2.x), (line->l.p2.y), line->color); -} - -/** - * @brief Draw a pinball rectangle - * - * @param r The rectangle to draw - */ -// void drawPinRect(pbRect_t* rect) -// { -// drawRect(rect->r.pos.x, rect->r.pos.y, rect->r.pos.x + rect->r.width, rect->r.pos.y + rect->r.height, -// rect->color); -// } - -/** - * @brief Draw a pinball flipper - * - * @param f The flipper to draw - */ -void drawPinFlipper(pbFlipper_t* f) -{ - drawPinCircle(&f->cPivot); - drawPinCircle(&f->cTip); - drawPinLine(&f->sideL); - drawPinLine(&f->sideR); -} diff --git a/main/modes/games/pinball/pinball_draw.h b/main/modes/games/pinball/pinball_draw.h deleted file mode 100644 index 2e10f138e..000000000 --- a/main/modes/games/pinball/pinball_draw.h +++ /dev/null @@ -1,6 +0,0 @@ -#pragma once - -#include "mode_pinball.h" - -void pinballDrawBackground(pinball_t* p, int16_t x, int16_t y, int16_t w, int16_t h); -void pinballDrawForeground(pinball_t* p); diff --git a/main/modes/games/pinball/pinball_physics.c b/main/modes/games/pinball/pinball_physics.c deleted file mode 100644 index 54568988f..000000000 --- a/main/modes/games/pinball/pinball_physics.c +++ /dev/null @@ -1,771 +0,0 @@ -//============================================================================== -// Includes -//============================================================================== - -#include -#include "pinball_physics.h" -#include "pinball_zones.h" - -//============================================================================== -// Function Declarations -//============================================================================== - -bool checkBallPbCircleCollision(pbCircle_t* ball, pbCircle_t* circle, pbTouchRef_t* touchRef); -bool checkBallPbLineCollision(pbCircle_t* ball, pbLine_t* line, pbTouchRef_t* touchRef); - -void checkBallBallCollisions(pinball_t* p); -void checkBallStaticCollision(pinball_t* p); -void sweepCheckFlippers(pinball_t* p); - -void moveBalls(pinball_t* p); - -void checkBallsNotTouching(pinball_t* p); -void setBallTouching(pbTouchRef_t* ballTouching, const void* obj, pbShapeType_t type); -pbShapeType_t ballIsTouching(pbTouchRef_t* ballTouching, const void* obj); - -void checkBallsAtRest(pinball_t* p); - -void moveBallBackFromLine(pbCircle_t* ball, pbLine_t* line, vecFl_t* collisionVec); -void moveBallBackFromCircle(pbCircle_t* ball, pbCircle_t* fixed); - -//============================================================================== -// Functions -//============================================================================== - -/** - * @brief TODO - * - * @param p - */ -void updatePinballPhysicsFrame(pinball_t* p) -{ - // Move balls along new vectors - moveBalls(p); - - // Move flippers rotationally - sweepCheckFlippers(p); - - // If there are multiple balls - if (1 < p->numBalls) - { - // Check for ball-ball collisions - checkBallBallCollisions(p); - } - - // Check for collisions between balls and static objects - checkBallStaticCollision(p); - - // Check if balls are actually at rest - checkBallsAtRest(p); - - // Clear references to balls touching things after moving - checkBallsNotTouching(p); -} - -/** - * @brief TODO - * - * @param p - */ -void checkBallBallCollisions(pinball_t* p) -{ - // For each ball, check collisions with other balls - for (uint32_t bIdx = 0; bIdx < p->numBalls; bIdx++) - { - pbCircle_t* ball = &p->balls[bIdx]; - for (uint32_t obIdx = bIdx + 1; obIdx < p->numBalls; obIdx++) - { - pbCircle_t* otherBall = &p->balls[obIdx]; - vecFl_t centerToCenter; - // Check for a new collision - if ((ball->zoneMask & otherBall->zoneMask) // In the same zone - && circleCircleFlIntersection(ball->c, otherBall->c, NULL, ¢erToCenter)) // and intersecting - { - // Move balls backwards equally from the midpoint to not clip - float halfDistM = (ball->c.radius + otherBall->c.radius - EPSILON) / 2.0f; - vecFl_t midwayPoint = divVecFl2d(addVecFl2d(ball->c.pos, otherBall->c.pos), 2.0f); - vecFl_t vecFromMid = mulVecFl2d(normVecFl2d(centerToCenter), halfDistM); - - // Move both balls - ball->c.pos = addVecFl2d(midwayPoint, vecFromMid); - otherBall->c.pos = subVecFl2d(midwayPoint, vecFromMid); - - // If the balls aren't touching yet, adjust velocities (bounce) - if (PIN_NO_SHAPE == ballIsTouching(p->ballsTouching[bIdx], otherBall)) - { - // Math for the first ball - vecFl_t v1 = ball->vel; - vecFl_t x1 = ball->c.pos; - vecFl_t v2 = otherBall->vel; - vecFl_t x2 = otherBall->c.pos; - vecFl_t x1_x2 = subVecFl2d(x1, x2); - vecFl_t v1_v2 = subVecFl2d(v1, v2); - float xSqMag = sqMagVecFl2d(x1_x2); - vecFl_t ballNewVel = ball->vel; - if (xSqMag > 0) - { - ballNewVel = subVecFl2d(v1, mulVecFl2d(x1_x2, (dotVecFl2d(v1_v2, x1_x2) / xSqMag))); - } - - // Flip everything for the other ball - v1 = otherBall->vel; - x1 = otherBall->c.pos; - v2 = ball->vel; - x2 = ball->c.pos; - x1_x2 = subVecFl2d(x1, x2); - v1_v2 = subVecFl2d(v1, v2); - xSqMag = sqMagVecFl2d(x1_x2); - if (xSqMag > 0) - { - otherBall->vel - = subVecFl2d(v1, mulVecFl2d(x1_x2, (dotVecFl2d(v1_v2, x1_x2) / sqMagVecFl2d(x1_x2)))); - } - - // Set the new velocity for the first ball after finding the second's - ball->vel = ballNewVel; - - // The balls are touching each other - setBallTouching(p->ballsTouching[bIdx], otherBall, PIN_CIRCLE); - setBallTouching(p->ballsTouching[obIdx], ball, PIN_CIRCLE); - - // Mark both balls as bounced - ball->bounce = true; - otherBall->bounce = true; - } - } - } - } -} - -/** - * @brief TODO - * - * @param ball - * @param circle - * @param touchRef - * @return true - * @return false - */ -bool checkBallPbCircleCollision(pbCircle_t* ball, pbCircle_t* circle, pbTouchRef_t* touchRef) -{ - bool bounced = false; - vecFl_t collisionVec; - - // Check for a collision - if ((ball->zoneMask & circle->zoneMask) // In the same zone - && circleCircleFlIntersection(ball->c, circle->c, NULL, &collisionVec)) // and intersecting - { - // Find the normalized vector along the collision normal - vecFl_t reflVec = normVecFl2d(collisionVec); - - // If the ball isn't already touching the circle - if (PIN_NO_SHAPE == ballIsTouching(touchRef, circle)) - { - // Bounced on a circle - ball->bounce = true; - bounced = true; - // Reflect the velocity vector along the normal between the two radii - // See http://www.sunshine2k.de/articles/coding/vectorreflection/vectorreflection.html - ball->vel = subVecFl2d(ball->vel, mulVecFl2d(reflVec, (2 * dotVecFl2d(ball->vel, reflVec)))); - // Lose some speed on the bounce - ball->vel = mulVecFl2d(ball->vel, WALL_BOUNCINESS); - // printf("%d,%.4f,%.4f\n", __LINE__, ball->vel.x, ball->vel.y); - // Mark this circle as being touched to not double-bounce - setBallTouching(touchRef, circle, PIN_CIRCLE); - } - - // Move ball back to not clip into the circle - moveBallBackFromCircle(ball, circle); - // ball->c.pos = addVecFl2d(circle->c.pos, mulVecFl2d(reflVec, ball->c.radius + circle->c.radius - EPSILON)); - } - return bounced; -} - -/** - * @brief TODO - * - * @param ball - * @param line - * @param touchRef - * @return true - * @return false - */ -bool checkBallPbLineCollision(pbCircle_t* ball, pbLine_t* line, pbTouchRef_t* touchRef) -{ - bool bounced = false; - vecFl_t collisionVec; - vecFl_t cpOnLine; - // Check for a collision - if ((ball->zoneMask & line->zoneMask) // In the same zone - && circleLineFlIntersection(ball->c, line->l, true, &cpOnLine, &collisionVec)) // and intersecting - { - /* TODO this reflection can have bad results when colliding with the tip of a line. - * The center-center vector can get weird if the ball moves fast and clips into the tip. - * The solution is probably to binary-search-move the ball as far as it'll go without clipping - */ - - // Find the normalized vector along the collision normal - vecFl_t reflVec = normVecFl2d(collisionVec); - - // If the ball isn't already touching the line - if (PIN_NO_SHAPE == ballIsTouching(touchRef, line)) - { - // The dot product with the collision normal is how much Y velocity component there is. - // If this value is small the ball should slide down the line (i.e. don't lose velocity on the bounce) - float velDotColNorm = dotVecFl2d(ball->vel, reflVec); - - // Bounce it by reflecting across the collision normal - ball->vel = subVecFl2d(ball->vel, mulVecFl2d(reflVec, (2 * velDotColNorm))); - - // Check if the ball should slide (i.e. not lose velocity) or bounce (i.e. lose velocity) - // 0 means the ball's velocity is parallel to the wall (slide) - // -mag(vel) means the ball's velocity is perpendicular to the wall (bounce) - if (velDotColNorm < -0.2f) - { - // Lose some speed on the bounce. - ball->vel = mulVecFl2d(ball->vel, WALL_BOUNCINESS); - } - - // Mark this line as being touched to not double-bounce - setBallTouching(touchRef, line, PIN_LINE); - - // Bounced off a line - ball->bounce = true; - bounced = true; - } - - // Move ball back to not clip into the bumper - // TODO accommodate line end collisions (circle) - moveBallBackFromLine(ball, line, &reflVec); - // ball->c.pos = addVecFl2d(cpOnLine, mulVecFl2d(reflVec, ball->c.radius - EPSILON)); - } - return bounced; -} - -/** - * @brief TODO - * - * @param p - */ -void checkBallStaticCollision(pinball_t* p) -{ - // For each ball, check collisions with static objects - for (uint32_t bIdx = 0; bIdx < p->numBalls; bIdx++) - { - // Reference and integer representation - pbCircle_t* ball = &p->balls[bIdx]; - - // Iterate over all bumpers - for (uint32_t uIdx = 0; uIdx < p->numBumpers; uIdx++) - { - checkBallPbCircleCollision(ball, &p->bumpers[uIdx], p->ballsTouching[bIdx]); - } - - // Iterate over all walls - for (uint32_t wIdx = 0; wIdx < p->numWalls; wIdx++) - { - checkBallPbLineCollision(ball, &p->walls[wIdx], p->ballsTouching[bIdx]); - } - } -} - -/** - * @brief TODO - * - * @param p - */ -void sweepCheckFlippers(pinball_t* p) -{ - // For each flipper - for (uint32_t fIdx = 0; fIdx < p->numFlippers; fIdx++) - { - pbFlipper_t* flipper = &p->flippers[fIdx]; - - // Check if the flipper is moving up or down - float angularVel = 0; - if (flipper->buttonHeld) - { - angularVel = FLIPPER_UP_DEGREES_PER_FRAME; - } - else - { - angularVel = -FLIPPER_DOWN_DEGREES_PER_FRAME; - } - - // Find the bounds for the flipper depending on the direction it's facing - float lBound = 0; - float uBound = 0; - if (flipper->facingRight) - { - lBound = M_PI_2 - FLIPPER_UP_ANGLE; - uBound = M_PI_2 + FLIPPER_DOWN_ANGLE; - // Flip velocity if facing right - angularVel *= -1; - } - else - { - lBound = (M_PI + M_PI_2) - FLIPPER_DOWN_ANGLE; - uBound = (M_PI + M_PI_2) + FLIPPER_UP_ANGLE; - } - - // Flipper starts here - float sweepStart = flipper->angle; - // Flipper ends here, bounded - float sweepEnd = flipper->angle + angularVel; - sweepEnd = CLAMP((sweepEnd), lBound, uBound); - - // Find sweep steps if in motion - float sweepStep = 0.0f; - int numSteps = 0; - if (sweepStart == sweepEnd) - { - // Flipper not in motion - angularVel = 0; - sweepStep = 0; - numSteps = 1; - } - else - { - // Flipper in motion - // TODO large sweep steps kill framerate.... - numSteps = 8; - sweepStep = (sweepEnd - sweepStart) / (float)numSteps; - } - - // Move the flipper a little, then check for collisions - for (int32_t step = 0; step < numSteps; step++) - { - // Sweep the flipper a little - flipper->angle += sweepStep; - updateFlipperPos(flipper); - - // Normal collision checks - // For each ball, check collisions with flippers objects - for (uint32_t bIdx = 0; bIdx < p->numBalls; bIdx++) - { - // Reference and integer representation - pbCircle_t* ball = &p->balls[bIdx]; - pbTouchRef_t* touchRef = p->ballsTouching[bIdx]; - - if (ball->zoneMask & flipper->zoneMask) - { - // Check if the ball is touching any part of the flipper - bool touching = false; - vecFl_t colPoint, colVec; - if (circleLineFlIntersection(ball->c, flipper->sideL.l, false, &colPoint, &colVec)) - { - // Move ball back to not clip into the flipper - colVec = normVecFl2d(colVec); - moveBallBackFromLine(ball, &flipper->sideL, &colVec); - // ball->c.pos = addVecFl2d(colPoint, mulVecFl2d(normVecFl2d(colVec), ball->c.radius - - // EPSILON)); - touching = true; - } - if (circleLineFlIntersection(ball->c, flipper->sideR.l, false, &colPoint, &colVec)) - { - // Move ball back to not clip into the flipper - colVec = normVecFl2d(colVec); - moveBallBackFromLine(ball, &flipper->sideR, &colVec); - // ball->c.pos = addVecFl2d(colPoint, mulVecFl2d(normVecFl2d(colVec), ball->c.radius - - // EPSILON)); - touching = true; - } - if (circleCircleFlIntersection(ball->c, flipper->cPivot.c, &colPoint, &colVec)) - { - // Move ball back to not clip into the flipper - moveBallBackFromCircle(ball, &flipper->cPivot); - // ball->c.pos = addVecFl2d(colPoint, mulVecFl2d(normVecFl2d(colVec), ball->c.radius - - // EPSILON)); - touching = true; - } - if (circleCircleFlIntersection(ball->c, flipper->cTip.c, &colPoint, &colVec)) - { - // Move ball back to not clip into the flipper - moveBallBackFromCircle(ball, &flipper->cTip); - // ball->c.pos = addVecFl2d(colPoint, mulVecFl2d(normVecFl2d(colVec), ball->c.radius - - // EPSILON)); - touching = true; - } - - // If the ball is touching the flipper for the first time - if (touching && (PIN_NO_SHAPE == ballIsTouching(touchRef, flipper))) - { - // Mark them as in contact - setBallTouching(touchRef, flipper, PIN_FLIPPER); - - // Bounce the ball - vecFl_t reflVec = normVecFl2d(colVec); - ball->vel = subVecFl2d(ball->vel, mulVecFl2d(reflVec, (2 * dotVecFl2d(ball->vel, reflVec)))); - - // If the flipper is in motion - if (0 != angularVel) - { - // Get the distance between the pivot and the ball - float dist = magVecFl2d(subVecFl2d(flipper->cPivot.c.pos, ball->c.pos)); - // Convert angular velocity of the flipper to linear velocity at that point - float impulseMag = (ABS(angularVel) * dist); - - // Impart an impulse on the ball along the collision normal - ball->vel = addVecFl2d(ball->vel, mulVecFl2d(reflVec, impulseMag)); - } - } - } - } - } - - // Make sure the final angle is correct - flipper->angle = sweepEnd; - } -} - -/** - * @brief TODO - * - * @param p - */ -void moveBalls(pinball_t* p) -{ - // For each ball, check collisions with static objects - for (uint32_t bIdx = 0; bIdx < p->numBalls; bIdx++) - { - pbCircle_t* ball = &p->balls[bIdx]; - - // Acceleration changes velocity - // TODO adjust gravity vector when on top of a line - ball->vel = addVecFl2d(ball->vel, ball->accel); - // printf("%d,%.4f,%.4f\n", __LINE__, ball->vel.x, ball->vel.y); - - // Save the last position to check if the ball is at rest - ball->lastPos = ball->c.pos; - - // Move the ball - ball->c.pos.x += (ball->vel.x); - ball->c.pos.y += (ball->vel.y); - - // Update zone mask - // TODO update this after nudging ball too? - ball->zoneMask = pinZoneCircle(p, *ball); - } -} - -/** - * @brief TODO - * - * @param p - */ -void checkBallsNotTouching(pinball_t* p) -{ - // For each ball - for (uint32_t bIdx = 0; bIdx < p->numBalls; bIdx++) - { - pbCircle_t* ball = &p->balls[bIdx]; - // For each thing it could be touching - for (uint32_t tIdx = 0; tIdx < MAX_NUM_TOUCHES; tIdx++) - { - pbTouchRef_t* tr = &p->ballsTouching[bIdx][tIdx]; - // If it's touching a thing - if (NULL != tr->obj) - { - bool setNotTouching = false; - switch (tr->type) - { - case PIN_CIRCLE: - { - const pbCircle_t* other = (const pbCircle_t*)tr->obj; - if ((0 == (ball->zoneMask & other->zoneMask)) // Not in the same zone - || !circleCircleFlIntersection(ball->c, other->c, NULL, NULL)) // or not touching - { - setNotTouching = true; - } - break; - } - case PIN_LINE: - { - const pbLine_t* other = (const pbLine_t*)tr->obj; - if ((0 == (ball->zoneMask & other->zoneMask)) // Not in the same zone - || !circleLineFlIntersection(ball->c, other->l, true, NULL, NULL)) // or not touching - { - setNotTouching = true; - } - break; - } - case PIN_RECT: - { - const pbRect_t* other = (const pbRect_t*)tr->obj; - if ((0 == (ball->zoneMask & other->zoneMask)) // Not in the same zone - || !circleRectFlIntersection(ball->c, other->r, NULL)) // or not touching - { - setNotTouching = true; - } - break; - } - case PIN_FLIPPER: - { - const pbFlipper_t* flipper = (const pbFlipper_t*)tr->obj; - if ((0 == (ball->zoneMask & flipper->zoneMask)) // Not in the same zone - || !(circleCircleFlIntersection(ball->c, flipper->cPivot.c, NULL, NULL) // or not touching - || circleCircleFlIntersection(ball->c, flipper->cTip.c, NULL, NULL) - || circleLineFlIntersection(ball->c, flipper->sideL.l, false, NULL, NULL) - || circleLineFlIntersection(ball->c, flipper->sideR.l, false, NULL, NULL))) - { - setNotTouching = true; - } - break; - } - default: - case PIN_NO_SHAPE: - { - // Not touching anything... - break; - } - } - - // If the object is no longer touching - if (setNotTouching) - { - // Clear the reference - tr->obj = NULL; - tr->type = PIN_NO_SHAPE; - } - } - } - } -} - -/** - * @brief TODO - * - * @param ballTouching - * @param obj - * @param type - */ -void setBallTouching(pbTouchRef_t* ballTouching, const void* obj, pbShapeType_t type) -{ - for (uint32_t i = 0; i < MAX_NUM_TOUCHES; i++) - { - if (NULL == ballTouching->obj) - { - ballTouching->obj = obj; - ballTouching->type = type; - return; - } - } -} - -/** - * @brief TODO - * - * @param ballTouching - * @param obj - * @return pbShapeType_t - */ -pbShapeType_t ballIsTouching(pbTouchRef_t* ballTouching, const void* obj) -{ - for (uint32_t i = 0; i < MAX_NUM_TOUCHES; i++) - { - if (ballTouching->obj == obj) - { - return ballTouching->type; - } - } - return PIN_NO_SHAPE; -} - -/** - * @brief TODO - * - * @param p - */ -void checkBallsAtRest(pinball_t* p) -{ - // For each ball - for (uint32_t bIdx = 0; bIdx < p->numBalls; bIdx++) - { - pbCircle_t* ball = &p->balls[bIdx]; - - // If the ball didn't bounce this frame (which can adjust position to not clip) - if (false == ball->bounce) - { - // And the ball is traveling downward - if (ball->vel.y > 0) - { - // See how far the ball actually traveled - float velM = sqMagVecFl2d(ball->vel); - - // If the ball is moving slowly - if (velM < 1.0f) - { - // And it didn't move as much as it should have - float posDeltaM = sqMagVecFl2d(subVecFl2d(ball->c.pos, ball->lastPos)); - if ((velM - posDeltaM) > 0.01f) - { - // Stop the ball altogether to not accumulate velocity - ball->vel.x = 0; - ball->vel.y = 0; - } - } - } - } - else - { - // Clear the bounce flag - ball->bounce = false; - } - } -} - -/** - * @brief Update the position of a flipper's tip circle and line walls depending on the flipper's angle. The pivot - * circle never changes. - * - * @param f The flipper to update - */ -void updateFlipperPos(pbFlipper_t* f) -{ - // Make sure the angle is between 0 and 360 - while (f->angle < 0) - { - f->angle += (2 * M_PI); - } - while (f->angle >= (2 * M_PI)) - { - f->angle -= (2 * M_PI); - } - - // This is the set of points to rotate - vecFl_t points[] = { - { - // Center of the tip of the flipper - .x = 0, - .y = -f->length, - }, - { - // Bottom point of the right side - .x = f->cPivot.c.radius, - .y = 0, - }, - { - // Top point of the right side - .x = f->cTip.c.radius, - .y = -f->length, - }, - { - // Bottom point of the left side - .x = -f->cPivot.c.radius, - .y = 0, - }, - { - // Top point of the left side - .x = -f->cTip.c.radius, - .y = -f->length, - }, - }; - - // This is where to write the rotated points - vecFl_t* dests[] = { - &f->cTip.c.pos, &f->sideR.l.p1, &f->sideR.l.p2, &f->sideL.l.p1, &f->sideL.l.p2, - }; - - // Get the trig values for all rotations, just once - float sinA = sinf(f->angle); - float cosA = cosf(f->angle); - - // For each point - for (int32_t idx = 0; idx < ARRAY_SIZE(points); idx++) - { - // Rotate the point - float oldX = points[idx].x; - float oldY = points[idx].y; - float newX = (oldX * cosA) - (oldY * sinA); - float newY = (oldX * sinA) + (oldY * cosA); - - // Translate relative to the pivot point - dests[idx]->x = f->cPivot.c.pos.x + newX; - dests[idx]->y = f->cPivot.c.pos.y + newY; - } -} - -/** - * @brief TODO - * - * see - * https://github.com/AEFeinstein/Super-2024-Swadge-FW/blob/4d7d41d9ab0e3a7670a967a0a4cd72364a8c39ac/main/modes/pinball/pinball_physics.c - * - * @param ball - * @param line - * @param collisionVec - */ -void moveBallBackFromLine(pbCircle_t* ball, pbLine_t* line, vecFl_t* collisionNorm) -{ - // Do a bunch of work to adjust the ball's position to not clip into this line. - // First create a copy of the line - lineFl_t barrierLine = line->l; - - // Then find the normal vector to the barrier, pointed towards the ball - vecFl_t barrierOffset = mulVecFl2d(*collisionNorm, ball->c.radius); - - // Translate the along the normal vector, the distance of the radius - // This creates a line parallel to the wall where the ball's center could be - barrierLine.p1 = addVecFl2d(barrierLine.p1, barrierOffset); - barrierLine.p2 = addVecFl2d(barrierLine.p2, barrierOffset); - - // Create a line for the ball's motion - lineFl_t ballLine = { - .p1 = ball->c.pos, - .p2 = addVecFl2d(ball->c.pos, ball->vel), - }; - - // Find the intersection between where the ball's center could be and the ball's trajectory. - // Set the ball's position to that point - ball->c.pos = infLineIntersectionPoint(barrierLine, ballLine); -} - -/** - * @brief TODO - * - * @param ball - * @param fixed - */ -void moveBallBackFromCircle(pbCircle_t* ball, pbCircle_t* fixed) -{ - // Create a barrier circle around the fixed that the ball's center can't pass through - circleFl_t barrier = fixed->c; - barrier.radius += ball->c.radius; - - // Create a line for the ball's motion - lineFl_t ballLine = { - .p1 = ball->c.pos, - .p2 = addVecFl2d(ball->c.pos, ball->vel), - }; - - vecFl_t intersection_1; - vecFl_t intersection_2; - switch (circleLineFlIntersectionPoints(barrier, ballLine, &intersection_1, &intersection_2)) - { - default: - case 0: - { - // No intersection? - break; - } - case 1: - { - ball->c.pos = intersection_1; - break; - } - case 2: - { - // Two intersection points, use the one closer to ball->c.pos - float diff1 = sqMagVecFl2d(subVecFl2d(ball->c.pos, intersection_1)); - float diff2 = sqMagVecFl2d(subVecFl2d(ball->c.pos, intersection_2)); - if (diff1 < diff2) - { - ball->c.pos = intersection_1; - } - else - { - ball->c.pos = intersection_2; - } - } - } -} \ No newline at end of file diff --git a/main/modes/games/pinball/pinball_physics.h b/main/modes/games/pinball/pinball_physics.h deleted file mode 100644 index ac33bb964..000000000 --- a/main/modes/games/pinball/pinball_physics.h +++ /dev/null @@ -1,16 +0,0 @@ -#pragma once - -#include "mode_pinball.h" - -#define PINBALL_GRAVITY (1 / 60.0f) ///< Gravitational constant - -#define WALL_BOUNCINESS 0.5f - -#define FLIPPER_UP_DEGREES_PER_FRAME 0.296705972839036f ///< Number of degrees (17) to move a flipper up per 60fps frame -#define FLIPPER_DOWN_DEGREES_PER_FRAME \ - 0.174532925199433f ///< Number of degrees (10) to move a flipper down per 60fps frame -#define FLIPPER_UP_ANGLE 0.349065850398866f ///< Angle of a flipper (20) when actuated -#define FLIPPER_DOWN_ANGLE 0.523598775598299f ///< Angle of a flipper (30) when idle - -void updateFlipperPos(pbFlipper_t* f); -void updatePinballPhysicsFrame(pinball_t* p); diff --git a/main/modes/games/pinball/pinball_test.c b/main/modes/games/pinball/pinball_test.c deleted file mode 100644 index 3af6cb4d4..000000000 --- a/main/modes/games/pinball/pinball_test.c +++ /dev/null @@ -1,316 +0,0 @@ -//============================================================================== -// Includes -//============================================================================== - -#include -#include "pinball_test.h" -#include "pinball_zones.h" -#include "pinball_physics.h" - -//============================================================================== -// Functions -//============================================================================== - -/** - * @brief TODO - * - * @param p - * @param x - * @param y - */ -void pbCreateBall(pinball_t* p, float x, float y) -{ - pbCircle_t* ball = &p->balls[p->numBalls++]; -#define BALL_RAD 5 - ball->c.radius = (BALL_RAD); - ball->c.pos.x = x; - ball->c.pos.y = y; - ball->lastPos.x = x; - ball->lastPos.y = y; - // #define MAX_VEL 128 - ball->vel.x = 0; - ball->vel.y = 0; - ball->accel.x = 0; - ball->accel.y = PINBALL_GRAVITY; - ball->color = c500; - ball->filled = true; -} - -/** - * @brief Create balls with random positions and velocities - * - * @param p The pinball state - * @param numBalls The number of balls to create - */ -void createRandomBalls(pinball_t* p, int32_t numBalls) -{ - // Don't overflow - if (numBalls > MAX_NUM_BALLS) - { - numBalls = MAX_NUM_BALLS; - } - p->numBalls = 0; - - // Make some balls - for (int32_t i = 0; i < numBalls; i++) - { - pbCircle_t* ball = &p->balls[p->numBalls++]; -#define BALL_RAD 5 - ball->c.radius = (BALL_RAD); - ball->c.pos.x = ((BALL_RAD + 1) + (esp_random() % (TFT_WIDTH - 2 * (BALL_RAD + 1)))); - ball->c.pos.y = ((BALL_RAD + 1) + (esp_random() % (TFT_HEIGHT - 2 * (BALL_RAD + 1)))); - ball->lastPos.x = ball->c.pos.x; - ball->lastPos.y = ball->c.pos.x; - ball->vel.x = 0; - ball->vel.y = 5 / 60.0f; - ball->accel.x = 0; - ball->accel.y = PINBALL_GRAVITY; - ball->color = esp_random() % cTransparent; - ball->filled = true; - } -} - -/** - * @brief TODO - * - * @param p - * @param numBumpers - */ -void createRandomBumpers(pinball_t* p, int32_t numBumpers) -{ - int fixedBumpersPlaced = 0; - vecFl_t fixedBumpers[] = { - { - .x = 140, - .y = 120, - }, - { - .x = 100, - .y = 80, - }, - { - .x = 180, - .y = 80, - }, - }; - numBumpers += ARRAY_SIZE(fixedBumpers); - - // Don't overflow - if (numBumpers > MAX_NUM_BUMPERS) - { - numBumpers = MAX_NUM_BUMPERS; - } - p->numBumpers = 0; - - // Make some balls - while (numBumpers > p->numBumpers) - { - pbCircle_t bumper = {0}; -#define BUMPER_RAD 10 - bumper.c.radius = (BUMPER_RAD); - if (fixedBumpersPlaced < ARRAY_SIZE(fixedBumpers)) - { - bumper.c.pos = fixedBumpers[fixedBumpersPlaced]; - fixedBumpersPlaced++; - } - else - { - bumper.c.pos.x = ((BUMPER_RAD + 1) + (esp_random() % (TFT_WIDTH - 2 * (BUMPER_RAD + 1)))); - bumper.c.pos.y = ((BUMPER_RAD + 1) + (esp_random() % (TFT_HEIGHT - 2 * (BUMPER_RAD + 1)))); - } - bumper.color = c050; - bumper.filled = false; - bumper.zoneMask = pinZoneCircle(p, bumper); - - bool intersection = false; - for (int32_t ol = 0; ol < p->numWalls; ol++) - { - if (circleLineFlIntersection(bumper.c, p->walls[ol].l, true, NULL, NULL)) - { - intersection = true; - break; - } - } - - for (int32_t ob = 0; ob < p->numBumpers; ob++) - { - if (circleCircleFlIntersection(bumper.c, p->bumpers[ob].c, NULL, NULL)) - { - intersection = true; - break; - } - } - - if (!intersection) - { - memcpy(&p->bumpers[p->numBumpers], &bumper, sizeof(pbCircle_t)); - p->numBumpers++; - } - } -} - -/** - * @brief Create random static walls - * - * @param p The pinball state - * @param numWalls The number of walls to create - */ -void createRandomWalls(pinball_t* p, int32_t numWalls) -{ - // Always Create a boundary - lineFl_t corners[] = { - { - .p1 = {.x = (0), .y = (0)}, - .p2 = {.x = (TFT_WIDTH - 1), .y = (0)}, - }, - { - .p1 = {.x = (TFT_WIDTH - 1), .y = (0)}, - .p2 = {.x = (TFT_WIDTH - 1), .y = (TFT_HEIGHT - 1)}, - }, - { - .p1 = {.x = (TFT_WIDTH - 1), .y = (TFT_HEIGHT - 1)}, - .p2 = {.x = (0), .y = (TFT_HEIGHT - 1)}, - }, - { - .p1 = {.x = (0), .y = (TFT_HEIGHT - 1)}, - .p2 = {.x = (0), .y = (0)}, - }, - // { - // .p1 = {.x = 0, .y = 90}, - // .p2 = {.x = 50, .y = 110}, - // }, - // { - // .p1 = {.x = 140, .y = 70}, - // .p2 = {.x = 210, .y = 80}, - // }, - - { - .p1 = {.x = 0, .y = 120}, - .p2 = {.x = 94, .y = 188}, - }, - { - .p1 = {.x = 279, .y = 120}, - .p2 = {.x = 186, .y = 188}, - }, - }; - - // Don't overflow - if (numWalls > MAX_NUM_WALLS - ARRAY_SIZE(corners)) - { - numWalls = MAX_NUM_WALLS - ARRAY_SIZE(corners); - } - p->numWalls = 0; - - for (int32_t i = 0; i < ARRAY_SIZE(corners); i++) - { - pbLine_t* pbl = &p->walls[p->numWalls++]; - pbl->l.p1.x = corners[i].p1.x; - pbl->l.p1.y = corners[i].p1.y; - pbl->l.p2.x = corners[i].p2.x; - pbl->l.p2.y = corners[i].p2.y; - vecFl_t delta = { - .x = pbl->l.p2.x - pbl->l.p1.x, - .y = pbl->l.p2.y - pbl->l.p1.y, - }; - pbl->length = magVecFl2d(delta); - pbl->color = c555; - pbl->zoneMask = pinZoneLine(p, *pbl); - } - - // Make a bunch of random lines - while (numWalls > p->numWalls) - { - pbLine_t pbl = {0}; // = &p->walls[p->numWalls++]; - -#define L_LEN 12 - - pbl.l.p1.x = (L_LEN + (esp_random() % (TFT_WIDTH - (L_LEN * 2)))); - pbl.l.p1.y = (L_LEN + (esp_random() % (TFT_HEIGHT - (L_LEN * 2)))); - pbl.l.p2.x = pbl.l.p1.x + ((esp_random() % (L_LEN * 2)) - L_LEN); - pbl.l.p2.y = pbl.l.p1.y + ((esp_random() % (L_LEN * 2)) - L_LEN); - vecFl_t delta = { - .x = pbl.l.p2.x - pbl.l.p1.x, - .y = pbl.l.p2.y - pbl.l.p1.y, - }; - pbl.length = magVecFl2d(delta); - pbl.color = c005; // esp_random() % cTransparent; - - if (pbl.l.p1.x == pbl.l.p2.x && pbl.l.p1.y == pbl.l.p2.y) - { - if (esp_random() % 2) - { - pbl.l.p2.x = (pbl.l.p2.x) + ((1)); - } - else - { - pbl.l.p2.y = (pbl.l.p2.y) + ((1)); - } - } - - pbl.zoneMask = pinZoneLine(p, pbl); - - bool intersection = false; - for (int32_t ol = 0; ol < p->numWalls; ol++) - { - if (lineLineFlIntersection(pbl.l, p->walls[ol].l)) - { - intersection = true; - } - } - - for (int32_t ob = 0; ob < p->numBumpers; ob++) - { - if (circleLineFlIntersection(p->bumpers[ob].c, pbl.l, true, NULL, NULL)) - { - intersection = true; - } - } - - if (!intersection) - { - memcpy(&p->walls[p->numWalls], &pbl, sizeof(pbLine_t)); - p->numWalls++; - } - } -} - -/** - * @brief Create a Flipper - * - * @param p The pinball state - * @param pivot_x - * @param pivot_y - * @param facingRight - */ -void createFlipper(pinball_t* p, int32_t pivot_x, int32_t pivot_y, bool facingRight) -{ - pbFlipper_t* f = &p->flippers[p->numFlippers]; - - f->cPivot.color = c505; - f->cTip.color = c505; - f->sideL.color = c505; - f->sideR.color = c505; - - f->cPivot.c.pos.x = pivot_x; - f->cPivot.c.pos.y = pivot_y; - f->cPivot.c.radius = 10; - f->length = 40; - f->cTip.c.radius = 5; - f->facingRight = facingRight; - - f->zoneMask = pinZoneFlipper(p, f); - - // Update angle and position after setting zone - if (f->facingRight) - { - f->angle = M_PI_2 + FLIPPER_DOWN_ANGLE; - } - else - { - f->angle = M_PI + M_PI_2 - FLIPPER_DOWN_ANGLE; - } - updateFlipperPos(f); - - // Update flipper count - p->numFlippers++; -} diff --git a/main/modes/games/pinball/pinball_test.h b/main/modes/games/pinball/pinball_test.h deleted file mode 100644 index 75903a64f..000000000 --- a/main/modes/games/pinball/pinball_test.h +++ /dev/null @@ -1,9 +0,0 @@ -#pragma once - -#include "mode_pinball.h" - -void pbCreateBall(pinball_t* p, float x, float y); -void createRandomBalls(pinball_t* p, int32_t numBalls); -void createRandomWalls(pinball_t* p, int32_t numWalls); -void createRandomBumpers(pinball_t* p, int32_t numBumpers); -void createFlipper(pinball_t* p, int32_t pivot_x, int32_t pivot_y, bool facingRight); diff --git a/main/modes/games/pinball/pinball_zones.c b/main/modes/games/pinball/pinball_zones.c deleted file mode 100644 index a6ea07ba7..000000000 --- a/main/modes/games/pinball/pinball_zones.c +++ /dev/null @@ -1,178 +0,0 @@ -//============================================================================== -// Includes -//============================================================================== - -#include -#include "pinball_zones.h" -#include "pinball_physics.h" - -//============================================================================== -// Functions -//============================================================================== - -/** - * @brief Split a table up into zones. Each object is assigned to one or more zones for a very quick first-pass - * collision check. - * - * @param p The pinball state - */ -void createTableZones(pinball_t* p) -{ - // Split the space into zones. Start with one big rectangle - int32_t splitOffset = (NUM_ZONES >> 1); - p->zones[0].r.pos.x = 0; - p->zones[0].r.pos.y = 0; - p->zones[0].r.width = (TFT_WIDTH); - p->zones[0].r.height = (TFT_HEIGHT); - p->zones[0].color = c505; - - // While more zoning needs to happen - while (splitOffset) - { - // Iterate over current zones, back to front - for (int32_t i = NUM_ZONES - 1; i >= 0; i--) - { - // If this is a real zone - if (0 < p->zones[i].r.height) - { - // Split it either vertically or horizontally, depending on which is larger - if (p->zones[i].r.height > p->zones[i].r.width) - { - // Split vertically - int32_t newHeight_1 = p->zones[i].r.height / 2; - int32_t newHeight_2 = p->zones[i].r.height - newHeight_1; - - // Shrink the original zone - p->zones[i].r.height = newHeight_1; - - // Create the new zone - p->zones[i + splitOffset].r.height = newHeight_2; - p->zones[i + splitOffset].r.pos.y = p->zones[i].r.pos.y + p->zones[i].r.height; - - p->zones[i + splitOffset].r.width = p->zones[i].r.width; - p->zones[i + splitOffset].r.pos.x = p->zones[i].r.pos.x; - } - else - { - // Split horizontally - int32_t newWidth_1 = p->zones[i].r.width / 2; - int32_t newWidth_2 = p->zones[i].r.width - newWidth_1; - - // Shrink the original zone - p->zones[i].r.width = newWidth_1; - - // Create the new zone - p->zones[i + splitOffset].r.width = newWidth_2; - p->zones[i + splitOffset].r.pos.x = p->zones[i].r.pos.x + p->zones[i].r.width; - - p->zones[i + splitOffset].r.height = p->zones[i].r.height; - p->zones[i + splitOffset].r.pos.y = p->zones[i].r.pos.y; - } - - // Give it a random color, just because - p->zones[i + splitOffset].color = esp_random() % cTransparent; - } - } - - // Half the split offset - splitOffset /= 2; - } -} - -/** - * @brief Determine which table zones a rectangle is in - * - * @param p The pinball state - * @param r The rectangle to zone - * @return A bitmask of the zones the rectangle is in - */ -uint32_t pinZoneRect(pinball_t* p, pbRect_t rect) -{ - uint32_t zoneMask = 0; - for (int16_t z = 0; z < NUM_ZONES; z++) - { - if (rectRectFlIntersection(p->zones[z].r, rect.r, NULL)) - { - zoneMask |= (1 << z); - } - } - return zoneMask; -} - -/** - * @brief Determine which table zones a line is in - * - * @param p The pinball state - * @param l The line to zone - * @return A bitmask of the zones the line is in - */ -uint32_t pinZoneLine(pinball_t* p, pbLine_t line) -{ - uint32_t zoneMask = 0; - for (int16_t z = 0; z < NUM_ZONES; z++) - { - if (rectLineFlIntersection(p->zones[z].r, line.l, NULL)) - { - zoneMask |= (1 << z); - } - } - return zoneMask; -} - -/** - * @brief Determine which table zones a circle is in - * - * @param p The pinball state - * @param r The circle to zone - * @return A bitmask of the zones the circle is in - */ -uint32_t pinZoneCircle(pinball_t* p, pbCircle_t circ) -{ - uint32_t zoneMask = 0; - for (int16_t z = 0; z < NUM_ZONES; z++) - { - if (circleRectFlIntersection(circ.c, p->zones[z].r, NULL)) - { - zoneMask |= (1 << z); - } - } - return zoneMask; -} - -/** - * @brief Determine which table zones a flipper is in. Note, this function will modify the flipper's angle - * - * @param p The pinball state - * @param f The flipper to zone - * @return A bitmask of the zones the circle is in - */ -uint32_t pinZoneFlipper(pinball_t* p, pbFlipper_t* f) -{ - pbRect_t boundingBox = {0}; - if (f->facingRight) - { - // Record the X position - boundingBox.r.pos.x = (f->cPivot.c.pos.x - f->cPivot.c.radius); - } - else - { - // Record the X position - boundingBox.r.pos.x = (f->cPivot.c.pos.x - f->length - f->cTip.c.radius); - } - - // Width is the same when facing left and right - boundingBox.r.width = (f->length + f->cPivot.c.radius + f->cTip.c.radius); - - // Height is the same too. Move the flipper up and record the Y start - f->angle = M_PI_2 - FLIPPER_UP_ANGLE; - updateFlipperPos(f); - boundingBox.r.pos.y = (f->cTip.c.pos.y - f->cTip.c.radius); - - // Move the flipper down and record the Y end - f->angle = M_PI_2 + FLIPPER_DOWN_ANGLE; - updateFlipperPos(f); - boundingBox.r.height = (f->cTip.c.pos.y + f->cTip.c.radius) - boundingBox.r.pos.y; - - // Return the zones of the bounding box - return pinZoneRect(p, boundingBox); -} \ No newline at end of file diff --git a/main/modes/games/pinball/pinball_zones.h b/main/modes/games/pinball/pinball_zones.h deleted file mode 100644 index c080c60ae..000000000 --- a/main/modes/games/pinball/pinball_zones.h +++ /dev/null @@ -1,9 +0,0 @@ -#pragma once - -#include "mode_pinball.h" - -void createTableZones(pinball_t* p); -uint32_t pinZoneRect(pinball_t* p, pbRect_t r); -uint32_t pinZoneLine(pinball_t* p, pbLine_t l); -uint32_t pinZoneCircle(pinball_t* p, pbCircle_t c); -uint32_t pinZoneFlipper(pinball_t* p, pbFlipper_t* f); diff --git a/main/modes/games/soko/soko.c b/main/modes/games/soko/soko.c new file mode 100644 index 000000000..0e54f57fa --- /dev/null +++ b/main/modes/games/soko/soko.c @@ -0,0 +1,364 @@ +#include + +#include "soko.h" +#include "soko_game.h" +#include "soko_gamerules.h" +#include "soko_save.h" + +static void sokoMainLoop(int64_t elapsedUs); +static void sokoEnterMode(void); +static void sokoExitMode(void); +static void sokoMenuCb(const char* label, bool selected, uint32_t settingVal); +static void sokoBackgroundDrawCallback(int16_t x, int16_t y, int16_t w, int16_t h, int16_t up, int16_t upNum); +static void sokoExtractLevelNamesAndIndices(soko_abs_t* self); + +// strings +static const char sokoModeName[] = "Sokobanabokabon"; +static const char sokoResumeGameLabel[] = "returnitytoit"; +static const char sokoNewGameLabel[] = "startsyfreshy"; + +// create the mode +swadgeMode_t sokoMode = { + .modeName = sokoModeName, + .wifiMode = NO_WIFI, + .overrideUsb = false, + .usesAccelerometer = false, + .usesThermometer = false, + .overrideSelectBtn = false, + .fnEnterMode = sokoEnterMode, + .fnExitMode = sokoExitMode, + .fnMainLoop = sokoMainLoop, + .fnAudioCallback = NULL, + .fnBackgroundDrawCallback = sokoBackgroundDrawCallback, + .fnEspNowRecvCb = NULL, + .fnEspNowSendCb = NULL, + .fnAdvancedUSB = NULL, +}; + +// soko_t* soko=NULL; +soko_abs_t* soko = NULL; + +static void sokoEnterMode(void) +{ + soko = calloc(1, sizeof(soko_abs_t)); + // Load a font + loadFont("ibm_vga8.font", &soko->ibm, false); + + // load sprite assets + // set pointer + soko->currentTheme = &soko->sokoDefaultTheme; + soko->sokoDefaultTheme.wallColor = c111; + soko->sokoDefaultTheme.floorColor = c444; + soko->sokoDefaultTheme.altFloorColor = c444; + soko->background = SKBG_FORREST; + // load or set themes... + // Default Theme + loadWsg("sk_pixel_front.wsg", &soko->sokoDefaultTheme.playerDownWSG, false); + loadWsg("sk_pixel_back.wsg", &soko->sokoDefaultTheme.playerUpWSG, false); + loadWsg("sk_pixel_left.wsg", &soko->sokoDefaultTheme.playerLeftWSG, false); + loadWsg("sk_pixel_right.wsg", &soko->sokoDefaultTheme.playerRightWSG, false); + loadWsg("sk_crate_2.wsg", &soko->sokoDefaultTheme.crateWSG, false); + loadWsg("sk_crate_ongoal.wsg", &soko->sokoDefaultTheme.crateOnGoalWSG, false); + loadWsg("sk_sticky_crate.wsg", &soko->sokoDefaultTheme.stickyCrateWSG, false); + loadWsg("sk_portal_complete.wsg", &soko->sokoDefaultTheme.portal_completeWSG, false); + loadWsg("sk_portal_incomplete.wsg", &soko->sokoDefaultTheme.portal_incompleteWSG, false); + loadWsg("sk_goal.wsg", &soko->sokoDefaultTheme.goalWSG, false); + + // we check against 0,0 as an invalid start location, and use file location instead. + soko->overworld_playerX = 0; + soko->overworld_playerY = 0; + + // Overworld Theme + soko->overworldTheme.playerDownWSG = soko->sokoDefaultTheme.playerDownWSG; + soko->overworldTheme.playerUpWSG = soko->sokoDefaultTheme.playerUpWSG; + soko->overworldTheme.playerLeftWSG = soko->sokoDefaultTheme.playerLeftWSG; + soko->overworldTheme.playerRightWSG = soko->sokoDefaultTheme.playerRightWSG; + soko->overworldTheme.crateWSG = soko->sokoDefaultTheme.crateWSG; + soko->overworldTheme.goalWSG = soko->sokoDefaultTheme.goalWSG; + soko->overworldTheme.crateOnGoalWSG = soko->sokoDefaultTheme.crateOnGoalWSG; + soko->overworldTheme.stickyCrateWSG = soko->sokoDefaultTheme.stickyCrateWSG; + soko->overworldTheme.portal_completeWSG = soko->sokoDefaultTheme.portal_completeWSG; + soko->overworldTheme.portal_incompleteWSG = soko->sokoDefaultTheme.portal_incompleteWSG; + soko->overworldTheme.wallColor = c111; + soko->overworldTheme.floorColor = c444; + + // Euler Theme + soko->eulerTheme.playerDownWSG = soko->sokoDefaultTheme.playerDownWSG; + soko->eulerTheme.playerUpWSG = soko->sokoDefaultTheme.playerUpWSG; + soko->eulerTheme.playerLeftWSG = soko->sokoDefaultTheme.playerLeftWSG; + soko->eulerTheme.playerRightWSG = soko->sokoDefaultTheme.playerRightWSG; + soko->eulerTheme.goalWSG = soko->sokoDefaultTheme.goalWSG; + + loadWsg("sk_e_crate.wsg", &soko->eulerTheme.crateWSG, false); + loadWsg("sk_sticky_trail_crate.wsg", &soko->eulerTheme.crateOnGoalWSG, false); + soko->eulerTheme.stickyCrateWSG = soko->sokoDefaultTheme.stickyCrateWSG; + soko->eulerTheme.portal_completeWSG = soko->sokoDefaultTheme.portal_completeWSG; + soko->eulerTheme.portal_incompleteWSG = soko->sokoDefaultTheme.portal_incompleteWSG; + soko->eulerTheme.wallColor = c000; + soko->eulerTheme.floorColor = c555; + soko->eulerTheme.altFloorColor = c433; // painted tiles color. + + // Initialize the menu + soko->menu = initMenu(sokoModeName, sokoMenuCb); + soko->menuManiaRenderer = initMenuManiaRenderer(&soko->ibm, NULL, NULL); + + addSingleItemToMenu(soko->menu, sokoResumeGameLabel); + addSingleItemToMenu(soko->menu, sokoNewGameLabel); + + // Set the mode to menu mode + soko->screen = SOKO_MENU; + soko->state = SKS_INIT; + + // load up the level list. + soko->levelFileText = loadTxt("SK_LEVEL_LIST.txt", true); + sokoExtractLevelNamesAndIndices(soko); + + // load level solved state. + sokoLoadLevelSolvedState(soko); +} + +static void sokoExitMode(void) +{ + // Deinitialize the menu + deinitMenu(soko->menu); + deinitMenuManiaRenderer(soko->menuManiaRenderer); + + // Free the font + freeFont(&soko->ibm); + + // free the level name file + freeTxt(soko->levelFileText); + + // free sprites + // default + freeWsg(&soko->sokoDefaultTheme.playerUpWSG); + freeWsg(&soko->sokoDefaultTheme.playerDownWSG); + freeWsg(&soko->sokoDefaultTheme.playerLeftWSG); + freeWsg(&soko->sokoDefaultTheme.playerRightWSG); + freeWsg(&soko->sokoDefaultTheme.crateWSG); + freeWsg(&soko->sokoDefaultTheme.crateOnGoalWSG); + freeWsg(&soko->sokoDefaultTheme.stickyCrateWSG); + freeWsg(&soko->sokoDefaultTheme.portal_completeWSG); + freeWsg(&soko->sokoDefaultTheme.portal_incompleteWSG); + freeWsg(&soko->sokoDefaultTheme.goalWSG); + // euler + freeWsg(&soko->eulerTheme.crateWSG); + freeWsg(&soko->eulerTheme.crateOnGoalWSG); + free(soko->levelBinaryData); // TODO is this the best place to free? + // Free everything else + free(soko); +} + +static void sokoMenuCb(const char* label, bool selected, uint32_t settingVal) +{ + if (selected) + { + // placeholder. + if (label == sokoResumeGameLabel) + { + int32_t data; + readNvs32("sk_data", &data); + // bitshift, etc, as needed. + uint16_t lastSaved = (uint16_t)data; + sokoLoadGameplay(soko, lastSaved, false); + sokoInitGameBin(soko); + soko->screen = SOKO_LEVELPLAY; + } + else if (label == sokoNewGameLabel) + { + // load level. + // we probably shouldn't have a new game option; just an overworld option. + sokoLoadGameplay(soko, 0, true); + sokoInitGameBin(soko); + soko->screen = SOKO_LEVELPLAY; + } + } +} + +static void sokoMainLoop(int64_t elapsedUs) +{ + // Pick what runs and draws depending on the screen being displayed + switch (soko->screen) + { + case SOKO_MENU: + { + // Process button events + buttonEvt_t evt = {0}; + while (checkButtonQueueWrapper(&evt)) + { + // Pass button events to the menu + soko->menu = menuButton(soko->menu, evt); + } + + // Draw the menu + drawMenuMania(soko->menu, soko->menuManiaRenderer, elapsedUs); + break; + } + case SOKO_LEVELPLAY: + { + // pass along to other gameplay, in other file + // Always process button events, regardless of control scheme, so the main menu button can be captured + buttonEvt_t evt = {0}; + while (checkButtonQueueWrapper(&evt)) + { + // Save the button state + soko->input.btnState = evt.state; + } + + // process input functions in input. + // Input will turn state into function calls into the game code, and handle complexities. + sokoPreProcessInput(&soko->input, elapsedUs); + // background had been drawn, input has been processed and functions called. Now do followup logic and draw + // level. gameplay loop + soko->gameLoopFunc(soko, elapsedUs); + break; + } + case SOKO_LOADNEWLEVEL: + { + sokoLoadGameplay(soko, soko->loadNewLevelIndex, soko->loadNewLevelFlag); + sokoInitNewLevel(soko, soko->currentLevel.gameMode); + printf("Go to gameplay\n"); + soko->loadNewLevelFlag = false; // reset flag. + soko->screen = SOKO_LEVELPLAY; + } + } +} + +// void freeEntity(soko_abs_t* self, sokoEntity_t* entity) // Free internal entity structures +// { +// if (entity->propFlag) +// { +// if (entity->properties->targetCount) +// { +// free(entity->properties->targetX); +// free(entity->properties->targetY); +// } +// free(entity->properties); +// entity->propFlag = false; +// } +// self->currentLevel.entityCount -= 1; +// } + +// placeholder. +static void sokoBackgroundDrawCallback(int16_t x, int16_t y, int16_t w, int16_t h, int16_t up, int16_t upNum) +{ + // Use TURBO drawing mode to draw individual pixels fast + SETUP_FOR_TURBO(); + uint16_t shiftReg = 0xACE1u; + uint16_t bit = 0; + switch (soko->background) + { + case SKBG_GRID: + { + for (int16_t yp = y; yp < y + h; yp++) + { + for (int16_t xp = x; xp < x + w; xp++) + { + if ((0 == xp % 20) || (0 == yp % 20)) + { + TURBO_SET_PIXEL(xp, yp, c002); + } + else + { + TURBO_SET_PIXEL(xp, yp, c001); + } + } + } + break; + } + case SKBG_BLACK: + { + for (int16_t yp = y; yp < y + h; yp++) + { + for (int16_t xp = x; xp < x + w; xp++) + { + TURBO_SET_PIXEL(xp, yp, c000); + } + } + break; + } + case SKBG_FORREST: + { + for (int16_t yp = y; yp < y + h; yp += 8) + { + for (int16_t xp = x; xp < x + w; xp += 8) + { + // not random enough but im going to leave it as is. + // LFSR + bit = ((shiftReg >> 0) ^ (shiftReg >> 2) ^ (shiftReg >> 3) ^ (shiftReg >> 5)) & 1u; + shiftReg = (shiftReg >> 1) | (bit << 15); + shiftReg = shiftReg + yp + xp * 3 + 1; + + for (int16_t ypp = yp; ypp < yp + 8; ypp++) + { + for (int16_t xpp = xp; xpp < xp + 8; xpp++) + { + if ((shiftReg & 3) == 0) + { + TURBO_SET_PIXEL(xpp, ypp, c020); + } + else + { + TURBO_SET_PIXEL(xpp, ypp, c121); + } + } + } + } + } + break; + } + default: + { + break; + } + } +} +// todo: move to soko_save +static void sokoExtractLevelNamesAndIndices(soko_abs_t* self) +{ + printf("Loading Level List...!\n"); + // printf("%s\n", self->levelFileText); + // printf("%d\n", (int)strlen(self->levelFileText)); + // char* a = strstr(self->levelFileText,":"); + // char* b = strstr(a,".bin:"); + // printf("%d",(int)((int)b-(int)a)); + // char* stringPtrs[30]; + // memset(stringPtrs,0,30*sizeof(char*)); + char** stringPtrs = soko->levelNames; + memset(stringPtrs, 0, sizeof(soko->levelNames)); + memset(soko->levelIndices, 0, sizeof(soko->levelIndices)); + int intInd = 0; + int ind = 0; + char* storageStr = strtok(self->levelFileText, ":"); + while (storageStr != NULL) + { + // strtol(storageStr, NULL, 10) && + if (!(strstr(storageStr, ".bin"))) // Make sure you're not accidentally reading a number from a filename + { + soko->levelIndices[intInd] = (int)strtol(storageStr, NULL, 10); + // printf("NumberThing: %s :: %d\n",storageStr,(int)strtol(storageStr,NULL,10)); + intInd++; + } + else + { + if (!strpbrk(storageStr, "\n\t\r ") && (strstr(storageStr, ".bin"))) + { + // int tokLen = strlen(storageStr); + // char* tempPtr = calloc((tokLen + 1), sizeof(char)); // Length plus null teminator + // strcpy(tempPtr,storageStr); + // stringPtrs[ind] = tempPtr; + stringPtrs[ind] = storageStr; + // printf("%s\n",storageStr); + ind++; + } + } + // printf("This guy!\n"); + storageStr = strtok(NULL, ":"); + } + printf("Strings: %d, Ints: %d\n", ind, intInd); + printf("Levels and indices:\n"); + for (int i = ind - 1; i > -1; i--) + { + printf("Index: %d : %d : %s\n", i, soko->levelIndices[i], stringPtrs[i]); + } +} diff --git a/main/modes/games/soko/soko.h b/main/modes/games/soko/soko.h new file mode 100644 index 000000000..e073be957 --- /dev/null +++ b/main/modes/games/soko/soko.h @@ -0,0 +1,279 @@ +#ifndef _SOKO_MODE_H_ +#define _SOKO_MODE_H_ + +#include "swadge2024.h" +#include "soko_input.h" +#include "soko_consts.h" + +extern swadgeMode_t sokoMode; + +typedef enum +{ + SOKO_OVERWORLD = 0, + SOKO_CLASSIC = 1, + SOKO_EULER = 2, + SOKO_LASERBOUNCE = 3 +} soko_var_t; + +typedef enum +{ + SOKO_MENU, + SOKO_LEVELPLAY, + SOKO_LOADNEWLEVEL +} sokoScreen_t; + +typedef enum +{ + SKBG_GRID = 0, + SKBG_BLACK = 1, + SKBG_FORREST = 2, +} sokoBackground_t; + +typedef enum +{ + SKB_EMPTY = 0, + SKB_WALL = 1, + SKB_FLOOR = 2, + SKB_GOAL = 3, + SKB_NO_WALK = 4, + SKB_OBJSTART = 201, // Object and Signal Bytes are over 200 + SKB_COMPRESS = 202, + SKB_PLAYER = 203, + SKB_CRATE = 204, + SKB_WARPINTERNAL = 205, + SKB_WARPINTERNALEXIT = 206, + SKB_WARPEXTERNAL = 207, + SKB_BUTTON = 208, + SKB_LASEREMITTER = 209, + SKB_LASERRECEIVEROMNI = 210, + SKB_LASERRECEIVER = 211, + SKB_LASER90ROTATE = 212, + SKB_GHOSTBLOCK = 213, + SKB_OBJEND = 230 +} soko_bin_t; // Binary file byte value decode list +typedef struct soko_portal_s +{ + uint8_t x; + uint8_t y; + uint8_t index; + bool levelCompleted; // use this to show completed levels later +} soko_portal_t; + +typedef struct soko_goal_s +{ + uint8_t x; + uint8_t y; +} soko_goal_t; + +typedef enum +{ + SKS_INIT, ///< meta enum used for edge cases + SKS_GAMEPLAY, + SKS_VICTORY, +} sokoGameState_t; + +/* +typedef enum +{ + SOKO_OVERWORLD = 0, + SOKO_CLASSIC = 1, + SOKO_EULER = 2 +} soko_var_t; +*/ + +typedef enum +{ + SKE_NONE = 0, + SKE_PLAYER = 1, + SKE_CRATE = 2, + SKE_LASER_90 = 3, + SKE_STICKY_CRATE = 4, + SKE_STICKY_TRAIL_CRATE = 5, + SKE_WARP = 11, + SKE_BUTTON = 6, + SKE_LASER_EMIT_UP = 7, + SKE_LASER_RECEIVE_OMNI = 8, + SKE_LASER_RECEIVE = 9, + SKE_GHOST = 10 +} sokoEntityType_t; + +typedef enum +{ + SKT_EMPTY = 0, + SKT_FLOOR = 1, + SKT_WALL = 2, + SKT_GOAL = 3, + SKT_NO_WALK = 4, + SKT_PORTAL = 5, + SKT_LASER_EMIT = 6, // To Be Removed + SKT_LASER_RECEIVE = 7, // To Be Removed + SKT_FLOOR_WALKED = 8 +} sokoTile_t; + +typedef struct +{ + bool sticky; // For Crates, this determines if crates stick to players. For Buttons, this determines if the button + // stays down. + bool trail; // Crates leave Euler trails + bool players; // For Crates, allow player push. For Button, allow player press. + bool crates; // For Buttons, allow crate push. For Portals, allow crate transport. + bool inverted; // For Buttons, invert default state of affected blocks. For ghost blocks, inverts default + // tangibility. Button and Ghostblock with both cancel. + uint8_t* targetX; + uint8_t* targetY; + uint8_t targetCount; + uint8_t hp; +} sokoEntityProperties_t; // this is a separate type so that it can be allocated as several different types with a void + // pointer and some aggressive casting. + +typedef struct +{ + sokoEntityType_t type; + uint16_t x; + uint16_t y; + sokoDirection_t facing; + sokoEntityProperties_t properties; + bool propFlag; +} sokoEntity_t; + +typedef struct sokoVec_s +{ + int16_t x; + int16_t y; +} sokoVec_t; +typedef struct +{ + uint16_t moveID; + bool isEntity; + sokoEntity_t* entity; + sokoTile_t tile; + uint16_t x; + uint16_t y; + sokoDirection_t facing; +} sokoUndoMove_t; + +typedef struct sokoCollision_s +{ + uint16_t x; + uint16_t y; + uint16_t entityFlag; + uint16_t entityIndex; + +} sokoCollision_t; + +typedef struct +{ + wsg_t playerWSG; + wsg_t playerUpWSG; + wsg_t playerRightWSG; + wsg_t playerLeftWSG; + wsg_t playerDownWSG; + wsg_t goalWSG; + wsg_t crateWSG; + wsg_t crateOnGoalWSG; + wsg_t stickyCrateWSG; + wsg_t portal_incompleteWSG; + wsg_t portal_completeWSG; + paletteColor_t wallColor; + paletteColor_t floorColor; + paletteColor_t altFloorColor; +} sokoTheme_t; + +typedef struct +{ + uint16_t levelScale; + uint8_t width; + uint8_t height; + uint8_t entityCount; + uint16_t playerIndex; // we could have multiple players... + sokoTile_t tiles[SOKO_MAX_LEVELSIZE][SOKO_MAX_LEVELSIZE]; + sokoEntity_t entities[SOKO_MAX_ENTITY_COUNT]; // todo: pointer and runtime array size + soko_var_t gameMode; +} sokoLevel_t; + +typedef struct soko_abs_s soko_abs_t; + +typedef struct soko_abs_s +{ + // meta + menu_t* menu; ///< The menu structure + menuManiaRenderer_t* menuManiaRenderer; ///< Renderer for the menu + font_t ibm; ///< The font used in the menu and game + sokoScreen_t screen; ///< The screen being displayed + + char* levelFileText; + char* levelNames[SOKO_LEVEL_COUNT]; + uint16_t levelIndices[SOKO_LEVEL_COUNT]; + bool levelSolved[SOKO_LEVEL_COUNT]; + + // game settings + uint16_t maxPush; ///< Maximum number of crates the player can push. Use 0 for no limit. + sokoGameState_t state; + + // theme settings + sokoTheme_t* currentTheme; ///< Points to one of the other themes. + sokoTheme_t overworldTheme; + sokoTheme_t eulerTheme; + sokoTheme_t sokoDefaultTheme; + sokoBackground_t background; + + // level + char* levels[SOKO_LEVEL_COUNT]; ///< List of wsg filenames. not comitted to storing level data like this, but idk if + ///< I need level names like picross. + // wsg_t levelWSG; ///< Current level + uint8_t* levelBinaryData; + + soko_portal_t portals[SOKO_MAX_PORTALS]; + uint8_t portalCount; + + soko_goal_t goals[SOKO_MAX_GOALS]; + uint8_t goalCount; + + // input + sokoGameplayInput_t input; + + // current level + uint16_t currentLevelIndex; + sokoLevel_t currentLevel; + + // undo ring buffer + sokoUndoMove_t history[SOKO_UNDO_BUFFER_SIZE]; // if >255, change index to uint16. + uint8_t historyBufferTail; + uint8_t historyCurrent; + bool historyNewMove; + + // todo: rename to 'isVictory' + bool allCratesOnGoal; + uint16_t moveCount; + uint16_t undoCount; + + // camera features + bool camEnabled; + uint16_t camX; + uint16_t camY; + uint16_t camPadExtentX; + uint16_t camPadExtentY; + uint16_t camWidth; + uint16_t camHeight; + + // game loop functions //Functions are moved into game struct so engine can support different game rules + void (*gameLoopFunc)(soko_abs_t* self, int64_t elapsedUs); + void (*sokoTryPlayerMovementFunc)(soko_abs_t* self); + bool (*sokoTryMoveEntityInDirectionFunc)(soko_abs_t* self, sokoEntity_t* entity, int dx, int dy, uint16_t push); + void (*drawTilesFunc)(soko_abs_t* self, sokoLevel_t* level); + bool (*isVictoryConditionFunc)(soko_abs_t* self); + sokoTile_t (*sokoGetTileFunc)(soko_abs_t* self, int x, int y); + + // Player Convenience Pointer + sokoEntity_t* soko_player; + // overworld enter/exit data + uint16_t overworld_playerX; + uint16_t overworld_playerY; + + bool loadNewLevelFlag; + uint8_t loadNewLevelIndex; + soko_var_t loadNewLevelVariant; + +} soko_abs_t; + +#endif diff --git a/main/modes/games/soko/soko_consts.h b/main/modes/games/soko/soko_consts.h new file mode 100644 index 000000000..d3da0a065 --- /dev/null +++ b/main/modes/games/soko/soko_consts.h @@ -0,0 +1,13 @@ +#ifndef SOKO_CONSTS_H +#define SOKO_CONSTS_H + +#define SOKO_LEVEL_COUNT 30 +#define SOKO_MAX_LEVELSIZE 30 +#define SOKO_MAX_ENTITY_COUNT 15 +#define SOKO_MAX_PORTALS 25 +#define SOKO_MAX_GOALS 20 +#define SOKO_MAX_REDIRECTS 15 // Should be equal to MAX_ENTITY until I find an edge case +#define SOKO_VICTORY_TIMER_US 1000000 +#define SOKO_UNDO_BUFFER_SIZE 255 + +#endif // SOKO_CONSTS_H \ No newline at end of file diff --git a/main/modes/games/soko/soko_game.c b/main/modes/games/soko/soko_game.c new file mode 100644 index 000000000..a93022eb2 --- /dev/null +++ b/main/modes/games/soko/soko_game.c @@ -0,0 +1,313 @@ +#include "soko_game.h" +#include "soko.h" +#include "soko_gamerules.h" + +/* +void sokoTryPlayerMovement(void); +sokoTile_t sokoGetTile(int, int); +bool sokoTryMoveEntityInDirection(sokoEntity_t*, int, int,uint16_t); +bool allCratesOnGoal(void); + +*/ +// sokoDirection_t sokoDirectionFromDelta(int, int); + +// soko_t* s; +// sokoEntity_t* player; + +soko_abs_t* soko_s; + +void sokoInitGameBin(soko_abs_t* soko) +{ + printf("init sokoban game from bin file"); + + soko_s = soko; + soko_s->soko_player = &soko_s->currentLevel.entities[soko_s->currentLevel.playerIndex]; + + soko->camX = soko_s->soko_player->x; + soko->camY = soko_s->soko_player->y; + + sokoInitInput(&soko_s->input); + + soko->state = SKS_GAMEPLAY; + + sokoConfigGamemode(soko, soko->currentLevel.gameMode); +} + +void sokoInitGame(soko_abs_t* soko) +{ + printf("init sokobon game.\n"); + + // Configure conveninence pointers. + soko_s = soko; + soko_s->soko_player = &soko_s->currentLevel.entities[soko_s->currentLevel.playerIndex]; + + // reset camera + soko->camX = soko_s->soko_player->x; + soko->camY = soko_s->soko_player->y; + + sokoInitInput(&soko_s->input); + + soko->state = SKS_GAMEPLAY; + + sokoConfigGamemode(soko, SOKO_OVERWORLD); + + // sokoConfigGamemode(soko,SOKO_EULER); +} + +void sokoInitNewLevel(soko_abs_t* soko, soko_var_t variant) +{ + printf("Init New Level.\n"); + + soko_s = soko; + soko_s->soko_player = &soko_s->currentLevel.entities[soko_s->currentLevel.playerIndex]; + sokoInitInput(&soko_s->input); + + // set gameplay settings from default settings, if we want powerups or whatever that adjusts them, or have a state + // machine. + soko_s->maxPush = 0; // set to 1 for "traditional" sokoban. + + soko->state = SKS_GAMEPLAY; + + sokoConfigGamemode(soko, variant); +} + +/* +void gameLoop(int64_t elapsedUs) +{ + if(s->state == SKS_GAMEPLAY) + { + //logic + sokoTryPlayerMovement(); + + //victory status. stored separate from gamestate because of future gameplay ideas/remixes. + s->allCratesOnGoal = allCratesOnGoal(); + if(s->allCratesOnGoal){ + s->state = SKS_VICTORY; + } + //draw level + drawTiles(&s->currentLevel); + + }else if(s->state == SKS_VICTORY) + { + //check for input for exit/next level. + drawTiles(&s->currentLevel); + } + + + //DEBUG PLACEHOLDER: + // Render the time to a string + char str[16] = {0}; + int16_t tWidth; + if(!s->allCratesOnGoal) + { + snprintf(str, sizeof(str) - 1, "sokoban"); + // Measure the width of the time string + tWidth = textWidth(&s->ibm, str); + // Draw the time string to the display, centered at (TFT_WIDTH / 2) + drawText(&s->ibm, c555, str, ((TFT_WIDTH - tWidth) / 2), 0); + }else + { + snprintf(str, sizeof(str) - 1, "sokasuccess"); + // Measure the width of the time string + tWidth = textWidth(&s->ibm, str); + // Draw the time string to the display, centered at (TFT_WIDTH / 2) + drawText(&s->ibm, c555, str, ((TFT_WIDTH - tWidth) / 2), 0); + } + +} + + +//Gameplay Logic +void sokoTryPlayerMovement() +{ + + if(s->input.playerInputDeltaX == 0 && s->input.playerInputDeltaY == 0) + { + return; + } + + sokoTryMoveEntityInDirection(player,s->input.playerInputDeltaX,s->input.playerInputDeltaY,0); +} + + +bool sokoTryMoveEntityInDirection(sokoEntity_t* entity, int dx, int dy, uint16_t push) +{ + //prevent infitnite loop where you push yourself nowhere. + if(dx == 0 && dy == 0 ) + { + return false; + } + + //maxiumum number of crates we can push. Traditional sokoban has a limit of one. I prefer infinite for challenges. + if(s->maxPush != 0 && push>s->maxPush) + { + return false; + } + + int px = entity->x+dx; + int py = entity->y+dy; + sokoTile_t nextTile = sokoGetTile(px,py); + + if(nextTile == SKT_FLOOR || nextTile == SKT_GOAL || nextTile == SKT_EMPTY) + { + //Is there an entity at this position? + for (size_t i = 0; i < s->currentLevel.entityCount; i++) + { + //is pushable. + if(s->currentLevel.entities[i].type == SKE_CRATE) + { + if(s->currentLevel.entities[i].x == px && s->currentLevel.entities[i].y == py) + { + if(sokoTryMoveEntityInDirection(&s->currentLevel.entities[i],dx,dy,push+1)) + { + entity->x += dx; + entity->y += dy; + entity->facing = sokoDirectionFromDelta(dx,dy); + return true; + }else{ + //can't push? can't move. + return false; + } + + } + } + + } + + //No wall in front of us and nothing to push, we can move. + entity->x += dx; + entity->y += dy; + entity->facing = sokoDirectionFromDelta(dx,dy); + return true; + } + + return false; +} + +//draw the tiles (and entities, for now) of the level. +void drawTiles(sokoLevel_t* level) +{ + SETUP_FOR_TURBO(); + uint16_t scale = level->levelScale; + uint16_t ox = (TFT_WIDTH/2)-((level->width)*scale/2); + uint16_t oy = (TFT_HEIGHT/2)-((level->height)*scale/2); + + for (size_t x = 0; x < level->width; x++) + { + for (size_t y = 0; y < level->height; y++) + { + paletteColor_t color = cTransparent; + switch (level->tiles[x][y]) + { + case SKT_FLOOR: + color = c444; + break; + case SKT_WALL: + color = c111; + break; + case SKT_GOAL: + color = c141; + break; + case SKT_EMPTY: + color = cTransparent; + default: + break; + } + + //Draw a square. + //none of this matters it's all getting replaced with drawwsg later. + if(color != cTransparent){ + for (size_t xd = ox+x*scale; xd < ox+x*scale+scale; xd++) + { + for (size_t yd = oy+y*scale; yd < oy+y*scale+scale; yd++) + { + TURBO_SET_PIXEL(xd, yd, color); + } + } + } + //draw outline around the square. + //drawRect(ox+x*s,oy+y*s,ox+x*s+s,oy+y*s+s,color); + } + } + + for (size_t i = 0; i < level->entityCount; i++) + { + switch (level->entities[i].type) + { + case SKE_PLAYER: + switch(level->entities[i].facing){ + case SKD_UP: + drawWsg(&s->playerUpWSG,ox+level->entities[i].x*scale,oy+level->entities[i].y*scale,false,false,0); + break; + case SKD_RIGHT: + drawWsg(&s->playerRightWSG,ox+level->entities[i].x*scale,oy+level->entities[i].y*scale,false,false,0); + break; + case SKD_LEFT: + drawWsg(&s->playerLeftWSG,ox+level->entities[i].x*scale,oy+level->entities[i].y*scale,false,false,0); + break; + case SKD_DOWN: + default: + drawWsg(&s->playerDownWSG,ox+level->entities[i].x*scale,oy+level->entities[i].y*scale,false,false,0); + break; + } + + break; + case SKE_CRATE: + drawWsg(&s->crateWSG,ox+level->entities[i].x*scale,oy+level->entities[i].y*scale,false,false,0); + case SKE_NONE: + default: + break; + } + } + +} + +bool allCratesOnGoal() +{ + for (size_t i = 0; i < s->currentLevel.entityCount; i++) + { + if(s->currentLevel.entities[i].type == SKE_CRATE) + { + if(s->currentLevel.tiles[s->currentLevel.entities[i].x][s->currentLevel.entities[i].y] != SKT_GOAL) + { + return false; + } + } + } + + return true; +} + + +sokoDirection_t sokoDirectionFromDelta(int dx,int dy) +{ + if(dx > 0 && dy == 0) + { + return SKD_RIGHT; + }else if(dx < 0 && dy == 0) + { + return SKD_LEFT; + }else if(dx == 0 && dy < 0) + { + return SKD_UP; + }else if(dx == 0 && dy > 0) + { + return SKD_DOWN; + } + + return SKD_NONE; +} +sokoTile_t sokoGetTile(int x, int y) +{ + if(x<0 || x >= s->currentLevel.width) + { + return SKT_WALL; + } + if(y<0 || y >= s->currentLevel.height) + { + return SKT_WALL; + } + + return s->currentLevel.tiles[x][y]; +} +*/ \ No newline at end of file diff --git a/main/modes/games/soko/soko_game.h b/main/modes/games/soko/soko_game.h new file mode 100644 index 000000000..41c60aaf0 --- /dev/null +++ b/main/modes/games/soko/soko_game.h @@ -0,0 +1,12 @@ +#ifndef SOKO_GAME_H +#define SOKO_GAME_H + +#include "soko.h" + +void sokoInitGame(soko_abs_t*); +void sokoInitGameBin(soko_abs_t*); +void sokoInitNewLevel(soko_abs_t* soko, soko_var_t variant); +void gameLoop(int64_t); +void drawTiles(sokoLevel_t*); + +#endif // SOKO_GAME_H \ No newline at end of file diff --git a/main/modes/games/soko/soko_gamerules.c b/main/modes/games/soko/soko_gamerules.c new file mode 100644 index 000000000..625a1591d --- /dev/null +++ b/main/modes/games/soko/soko_gamerules.c @@ -0,0 +1,1240 @@ +#include "soko_game.h" +#include "soko.h" +#include "soko_gamerules.h" +#include "soko_save.h" +#include "shapes.h" +#include "soko_undo.h" + +// clang-format off +// True if the entity CANNOT go on the tile +bool sokoEntityTileCollision[6][9] = { + // Empty, //floor//wall//goal//noWalk//portal //l-emit //l-receive //walked + {true, false, true, false, true,false, false, false, false}, // SKE_NONE + {true, false, true, false, true,false, false, false, true}, // PLAYER + {true, false, true, false, true,false, false, false, false}, // CRATE + {true, false, true, false, true,false, false, false, false}, // LASER + {true, false, true, false, true,false, false, false, false}, // STICKY CRATE + {true, false, true, false, true,false, false, false, true} // STICKY_TRAIL_CRATE +}; +// clang-format on + +uint64_t victoryDanceTimer; + +void sokoConfigGamemode( + soko_abs_t* soko, + soko_var_t variant) // This should be called when you reload a level to make sure game rules are correct +{ + soko->currentTheme = &soko->sokoDefaultTheme; + soko->background = SKBG_GRID; + + if (variant == SOKO_CLASSIC) // standard gamemode. Check 'variant' variable + { + printf("Config Soko to Classic\n"); + soko->maxPush = 1; // set to 1 for "traditional" sokoban. + soko->gameLoopFunc = absSokoGameLoop; + soko->sokoTryPlayerMovementFunc = absSokoTryPlayerMovement; + soko->sokoTryMoveEntityInDirectionFunc = absSokoTryMoveEntityInDirection; + soko->drawTilesFunc = absSokoDrawTiles; + soko->isVictoryConditionFunc = absSokoAllCratesOnGoal; + soko->sokoGetTileFunc = absSokoGetTile; + } + else if (variant == SOKO_EULER) // standard gamemode. Check 'variant' variable + { + printf("Config Soko to Euler\n"); + soko->maxPush = 0; // set to 0 for infinite push. + soko->gameLoopFunc = absSokoGameLoop; + soko->sokoTryPlayerMovementFunc = eulerSokoTryPlayerMovement; + soko->sokoTryMoveEntityInDirectionFunc = absSokoTryMoveEntityInDirection; + soko->drawTilesFunc = absSokoDrawTiles; + soko->isVictoryConditionFunc = eulerNoUnwalkedFloors; + soko->currentTheme = &soko->eulerTheme; + soko->background = SKBG_BLACK; + + // Initialze spaces below the player and sticky. + for (size_t i = 0; i < soko->currentLevel.entityCount; i++) + { + if (soko->currentLevel.entities[i].type == SKE_PLAYER + || soko->currentLevel.entities[i].type == SKE_STICKY_TRAIL_CRATE) + { + soko->currentLevel.tiles[soko->currentLevel.entities[i].x][soko->currentLevel.entities[i].y] + = SKT_FLOOR_WALKED; + } + } + } + else if (variant == SOKO_OVERWORLD) + { + printf("Config Soko to Overworld\n"); + soko->maxPush = 0; // set to 0 for infinite push. + soko->gameLoopFunc = overworldSokoGameLoop; + soko->sokoTryPlayerMovementFunc = absSokoTryPlayerMovement; + soko->sokoTryMoveEntityInDirectionFunc = absSokoTryMoveEntityInDirection; + soko->drawTilesFunc = absSokoDrawTiles; + soko->isVictoryConditionFunc = overworldPortalEntered; + soko->sokoGetTileFunc = absSokoGetTile; + soko->currentTheme = &soko->overworldTheme; + // set position to previous overworld positon when re-entering the overworld + // but like... not an infinite loop? + soko->soko_player->x = soko->overworld_playerX; + soko->soko_player->y = soko->overworld_playerY; + soko->background = SKBG_FORREST; + + for (size_t i = 0; i < soko->portalCount; i++) + { + if (soko->portals[i].index < SOKO_LEVEL_COUNT) + { + soko->portals[i].levelCompleted = soko->levelSolved[soko->portals[i].index]; + } + } + } + else if (variant == SOKO_LASERBOUNCE) + { + printf("Config Soko to Laser Bounce\n"); + soko->maxPush = 0; // set to 0 for infinite push. + soko->gameLoopFunc = laserBounceSokoGameLoop; + soko->sokoTryPlayerMovementFunc = absSokoTryPlayerMovement; + soko->sokoTryMoveEntityInDirectionFunc = absSokoTryMoveEntityInDirection; + soko->drawTilesFunc = absSokoDrawTiles; + soko->isVictoryConditionFunc = absSokoAllCratesOnGoal; + soko->sokoGetTileFunc = absSokoGetTile; + } + else + { + printf("invalid gamemode."); + } + + // add conditional for alternative variants + sokoInitHistory(soko); +} + +void laserBounceSokoGameLoop(soko_abs_t* self, int64_t elapsedUs) +{ + if (self->state == SKS_GAMEPLAY) + { + // logic + self->sokoTryPlayerMovementFunc(self); + + // victory status. stored separate from gamestate because of future gameplay ideas/remixes. + // todo: rename to isVictory or such. + self->allCratesOnGoal = self->isVictoryConditionFunc(self); + if (self->allCratesOnGoal) + { + self->state = SKS_VICTORY; + victoryDanceTimer = 0; + } + // draw level + self->drawTilesFunc(self, &self->currentLevel); + drawLaserFromEntity(self, self->soko_player); + } + else if (self->state == SKS_VICTORY) + { + // check for input for exit/next level. + self->drawTilesFunc(self, &self->currentLevel); + victoryDanceTimer += elapsedUs; + if (victoryDanceTimer > SOKO_VICTORY_TIMER_US) + { + sokoSolveCurrentLevel(self); + self->loadNewLevelIndex = 0; + self->loadNewLevelFlag = true; + self->screen = SOKO_LOADNEWLEVEL; + } + } + + // DEBUG PLACEHOLDER: + // Render the time to a string + char str[16] = {0}; + int16_t tWidth; + if (!self->allCratesOnGoal) + { + // snprintf(buffer, buflen - 1, "%s%s", item->label, item->options[item->currentOpt]); + snprintf(str, sizeof(str) - 1, "%s", self->levelNames[self->currentLevelIndex]); + // Measure the width of the time string + tWidth = textWidth(&self->ibm, str); + // Draw the time string to the display, centered at (TFT_WIDTH / 2) + drawText(&self->ibm, c555, str, ((TFT_WIDTH - tWidth) / 2), 0); + } + else + { + snprintf(str, sizeof(str) - 1, "sokasuccess"); + // Measure the width of the time string + tWidth = textWidth(&self->ibm, str); + // Draw the time string to the display, centered at (TFT_WIDTH / 2) + drawText(&self->ibm, c555, str, ((TFT_WIDTH - tWidth) / 2), 0); + } + sharedGameLoop(self); +} + +void absSokoGameLoop(soko_abs_t* soko, int64_t elapsedUs) +{ + if (soko->state == SKS_GAMEPLAY) + { + // logic + soko->sokoTryPlayerMovementFunc(soko); + + // undo check + if (soko->input.undo) + { + sokoUndo(soko); + } + + // victory status. stored separate from gamestate because of future gameplay ideas/remixes. + // todo: rename to isVictory or such. + soko->allCratesOnGoal = soko->isVictoryConditionFunc(soko); + if (soko->allCratesOnGoal) + { + soko->state = SKS_VICTORY; + victoryDanceTimer = 0; + } + // draw level + soko->drawTilesFunc(soko, &soko->currentLevel); + } + else if (soko->state == SKS_VICTORY) + { + // check for input for exit/next level. + soko->drawTilesFunc(soko, &soko->currentLevel); + victoryDanceTimer += elapsedUs; + if (victoryDanceTimer > SOKO_VICTORY_TIMER_US) + { + sokoSolveCurrentLevel(soko); + soko->loadNewLevelIndex = 0; + soko->loadNewLevelFlag = true; + soko->screen = SOKO_LOADNEWLEVEL; + } + } + + // DEBUG PLACEHOLDER: + char str[16] = {0}; + int16_t tWidth; + if (!soko->allCratesOnGoal) + { + snprintf(str, sizeof(str) - 1, "%s", soko->levelNames[soko->currentLevelIndex]); + // Measure the width of the time string + tWidth = textWidth(&soko->ibm, str); + // Draw the time string to the display, centered at (TFT_WIDTH / 2) + drawText(&soko->ibm, c555, str, ((TFT_WIDTH - tWidth) / 2), 0); + } + else + { + snprintf(str, sizeof(str) - 1, "sokasuccess"); + // Measure the width of the time string + tWidth = textWidth(&soko->ibm, str); + // Draw the time string to the display, centered at (TFT_WIDTH / 2) + drawText(&soko->ibm, c555, str, ((TFT_WIDTH - tWidth) / 2), 0); + } + sharedGameLoop(soko); +} + +void sharedGameLoop(soko_abs_t* self) +{ + if (self->input.restartLevel) + { + restartCurrentLevel(self); + } + else if (self->input.exitToOverworld) + { + exitToOverworld(self); + } +} + +// Gameplay Logic +void absSokoTryPlayerMovement(soko_abs_t* soko) +{ + if (soko->input.playerInputDeltaX == 0 && soko->input.playerInputDeltaY == 0) + { + return; + } + + bool b = soko->sokoTryMoveEntityInDirectionFunc(soko, soko->soko_player, soko->input.playerInputDeltaX, + soko->input.playerInputDeltaY, 0); + sokoHistoryTurnOver(soko); + if (b) + { + soko->moveCount++; + } +} + +bool absSokoTryMoveEntityInDirection(soko_abs_t* self, sokoEntity_t* entity, int dx, int dy, uint16_t push) +{ + // prevent infitnite loop where you push yourself nowhere. + if (dx == 0 && dy == 0) + { + return false; + } + + // maxiumum number of crates we can push. Traditional sokoban has a limit of one. Euler is infinity. + if (self->maxPush != 0 && push > self->maxPush) + { + return false; + } + + int px = entity->x + dx; + int py = entity->y + dy; + sokoTile_t nextTile = self->sokoGetTileFunc(self, px, py); + + // when this is false, we CAN move. True for Collision. + if (!sokoEntityTileCollision[entity->type][nextTile]) + { + // Is there an entity at this position? + for (size_t i = 0; i < self->currentLevel.entityCount; i++) + { + // is pushable. + if (self->currentLevel.entities[i].type == SKE_CRATE + || self->currentLevel.entities[i].type == SKE_STICKY_CRATE) + { + if (self->currentLevel.entities[i].x == px && self->currentLevel.entities[i].y == py) + { + if (self->sokoTryMoveEntityInDirectionFunc(self, &self->currentLevel.entities[i], dx, dy, push + 1)) + { + sokoAddEntityMoveToHistory(self, entity, entity->x, entity->y, entity->facing); + entity->x += dx; + entity->y += dy; + entity->facing = sokoDirectionFromDelta(dx, dy); + return true; // if entities overlap, we should not break here? + } + else + { + // can't push? can't move. + return false; + } + } + } + else if (self->currentLevel.entities[i].type == SKE_STICKY_TRAIL_CRATE) + { + // previous + // for euler. todo: make EulerTryMoveEntityInDirection instead of an if statement. + if (self->currentLevel.entities[i].x == px && self->currentLevel.entities[i].y == py) + { + if (self->sokoTryMoveEntityInDirectionFunc(self, &self->currentLevel.entities[i], dx, dy, push + 1)) + { + sokoAddEntityMoveToHistory(self, entity, entity->x, entity->y, entity->facing); + entity->x += dx; + entity->y += dy; + entity->facing = sokoDirectionFromDelta(dx, dy); + return true; + } + else + { + // can't push? can't move. + return false; + } + } + } + } + + // todo: this is a hack, we should have separate absSokoTryMoveEntityInDirection functions. + if (self->currentLevel.gameMode == SOKO_EULER && entity->propFlag && entity->properties.trail) + { + if (self->currentLevel.tiles[entity->x + dx][entity->y + dy] == SKT_FLOOR) + { + sokoAddTileMoveToHistory(self, entity->x + dx, entity->y + dy, SKT_FLOOR); + self->currentLevel.tiles[entity->x + dx][entity->y + dy] = SKT_FLOOR_WALKED; + } + + if (self->currentLevel.tiles[entity->x][entity->y] == SKT_FLOOR) + { + sokoAddTileMoveToHistory(self, entity->x, entity->y, SKT_FLOOR); + self->currentLevel.tiles[entity->x][entity->y] = SKT_FLOOR_WALKED; + } + } + // No wall in front of us and nothing to push, we can move. + // we assume the player never gets pushed for undo here, so if it's the player moving, thats a new move. + sokoAddEntityMoveToHistory(self, entity, entity->x, entity->y, entity->facing); + entity->x += dx; + entity->y += dy; + entity->facing = sokoDirectionFromDelta(dx, dy); + return true; + } + // all other floor types invalid. Be careful when we add tile types in different rule sets. + + return false; +} + +// draw the tiles (and entities, for now) of the level. +void absSokoDrawTiles(soko_abs_t* self, sokoLevel_t* level) +{ + uint16_t scale = level->levelScale; + // These are in level space (not pixels) and must be within bounds of currentLevel.tiles. + int16_t screenMinX, screenMaxX, screenMinY, screenMaxY; + // offsets. + uint16_t ox, oy; + + // Recalculate Camera Position + // todo: extract to a function if we end up with different draw functions. Part of future pointer refactor. + if (self->camEnabled) + { + // calculate camera position. Shift if needed. Cam position was initiated to player position. + if (self->soko_player->x > self->camX + self->camPadExtentX) + { + self->camX = self->soko_player->x - self->camPadExtentX; + } + else if (self->soko_player->x < self->camX - self->camPadExtentX) + { + self->camX = self->soko_player->x + self->camPadExtentX; + } + else if (self->soko_player->y > self->camY + self->camPadExtentY) + { + self->camY = self->soko_player->y - self->camPadExtentY; + } + else if (self->soko_player->y < self->camY - self->camPadExtentY) + { + self->camY = self->soko_player->y + self->camPadExtentY; + } + + // calculate offsets + ox = -self->camX * scale + (TFT_WIDTH / 2); + oy = -self->camY * scale + (TFT_HEIGHT / 2); + + // calculate out of bounds draws. todo: make tenery operators. + screenMinX = self->camX - self->camWidth / 2 - 1; + if (screenMinX < 0) + { + screenMinX = 0; + } + screenMaxX = self->camX + self->camWidth / 2 + 1; + if (screenMaxX > level->width) + { + screenMaxX = level->width; + } + screenMinY = self->camY - self->camHeight / 2 - 1; + if (screenMinY < 0) + { + screenMinY = 0; + } + screenMaxY = self->camY + self->camHeight / 2 + 1; + if (screenMaxY > level->height) + { + screenMaxY = level->height; + } + } + else + { // no camera + // calculate offsets to center the level. + ox = (TFT_WIDTH / 2) - ((level->width) * scale / 2); + oy = (TFT_HEIGHT / 2) - ((level->height) * scale / 2); + + // bounds are just the level. + screenMinX = 0; + screenMaxX = level->width; + screenMinY = 0; + screenMaxY = level->height; + } + + SETUP_FOR_TURBO(); + + // Tile Drawing (bg layer) + for (size_t x = screenMinX; x < screenMaxX; x++) + { + for (size_t y = screenMinY; y < screenMaxY; y++) + { + paletteColor_t color = cTransparent; + switch (level->tiles[x][y]) + { + case SKT_FLOOR: + { + color = self->currentTheme->floorColor; + break; + } + case SKT_WALL: + { + color = self->currentTheme->wallColor; + break; + } + case SKT_GOAL: + { + color = self->currentTheme->floorColor; + break; + } + case SKT_FLOOR_WALKED: + { + color = self->currentTheme->altFloorColor; + break; + } + case SKT_EMPTY: + { + color = cTransparent; + break; + } + case SKT_PORTAL: + { // todo: draw completed or not completed. + color = c441; + // color = self->currentTheme->floorColor; + break; + } + default: + { + break; + } + } + + // Draw a square. + // none of this matters it's all getting replaced with drawwsg later. + if (color != cTransparent) + { + for (size_t xd = ox + x * scale; xd < ox + x * scale + scale; xd++) + { + for (size_t yd = oy + y * scale; yd < oy + y * scale + scale; yd++) + { + TURBO_SET_PIXEL(xd, yd, color); + } + } + } + + if (level->tiles[x][y] == SKT_GOAL) + { + drawWsg(&self->currentTheme->goalWSG, ox + x * scale, oy + y * scale, false, false, 0); + } + + // DEBUG_DRAW_COUNT++; + // draw outline around the square. + // drawRect(ox+x*s,oy+y*s,ox+x*s+s,oy+y*s+s,color); + } + } + + // draw portal in overworld before entities. + // hypothetically, we can get rid of the overworld check, and there just won't be other portals? but there could be? + // sprint("a\n"); + if (self->currentLevel.gameMode == SOKO_OVERWORLD) + { + for (int i = 0; i < self->portalCount; i++) + { + if (self->portals[i].x >= screenMinX && self->portals[i].x <= screenMaxX && self->portals[i].y >= screenMinY + && self->portals[i].y <= screenMaxY) + { + if (self->portals[i].levelCompleted) + { + drawWsg(&self->currentTheme->portal_completeWSG, ox + self->portals[i].x * scale, + oy + self->portals[i].y * scale, false, false, 0); + } + else + { + drawWsg(&self->currentTheme->portal_incompleteWSG, ox + self->portals[i].x * scale, + oy + self->portals[i].y * scale, false, false, 0); + } + } + } + } + + // draw entities + for (size_t i = 0; i < level->entityCount; i++) + { + // don't bother drawing off screen + if (level->entities[i].x >= screenMinX && level->entities[i].x <= screenMaxX + && level->entities[i].y >= screenMinY && level->entities[i].y <= screenMaxY) + { + switch (level->entities[i].type) + { + case SKE_PLAYER: + { + switch (level->entities[i].facing) + { + case SKD_UP: + { + drawWsg(&self->currentTheme->playerUpWSG, ox + level->entities[i].x * scale, + oy + level->entities[i].y * scale, false, false, 0); + break; + } + case SKD_RIGHT: + { + drawWsg(&self->currentTheme->playerRightWSG, ox + level->entities[i].x * scale, + oy + level->entities[i].y * scale, false, false, 0); + break; + } + case SKD_LEFT: + { + drawWsg(&self->currentTheme->playerLeftWSG, ox + level->entities[i].x * scale, + oy + level->entities[i].y * scale, false, false, 0); + break; + } + case SKD_DOWN: + default: + { + drawWsg(&self->currentTheme->playerDownWSG, ox + level->entities[i].x * scale, + oy + level->entities[i].y * scale, false, false, 0); + break; + } + } + + break; + } + case SKE_CRATE: + { + if (self->currentLevel.tiles[self->currentLevel.entities[i].x][self->currentLevel.entities[i].y] + == SKT_GOAL) + { + drawWsg(&self->currentTheme->crateOnGoalWSG, ox + level->entities[i].x * scale, + oy + level->entities[i].y * scale, false, false, 0); + } + else + { + drawWsg(&self->currentTheme->crateWSG, ox + level->entities[i].x * scale, + oy + level->entities[i].y * scale, false, false, 0); + } + break; + } + case SKE_STICKY_CRATE: + { + drawWsg(&self->currentTheme->stickyCrateWSG, ox + level->entities[i].x * scale, + oy + level->entities[i].y * scale, false, false, 0); + break; + } + case SKE_STICKY_TRAIL_CRATE: + { + drawWsg(&self->currentTheme->crateOnGoalWSG, ox + level->entities[i].x * scale, + oy + level->entities[i].y * scale, false, false, 0); + break; + } + case SKE_NONE: + default: + { + break; + } + } + } + } +} + +bool absSokoAllCratesOnGoal(soko_abs_t* soko) +{ + for (size_t i = 0; i < soko->currentLevel.entityCount; i++) + { + if (soko->currentLevel.entities[i].type == SKE_CRATE) + { + if (soko->currentLevel.tiles[soko->currentLevel.entities[i].x][soko->currentLevel.entities[i].y] + != SKT_GOAL) + { + return false; + } + } + } + return true; +} + +sokoTile_t absSokoGetTile(soko_abs_t* self, int x, int y) +{ + if (x < 0 || x >= self->currentLevel.width) + { + return SKT_WALL; + } + if (y < 0 || y >= self->currentLevel.height) + { + return SKT_WALL; + } + + return self->currentLevel.tiles[x][y]; +} + +sokoDirection_t sokoDirectionFromDelta(int dx, int dy) +{ + if (dx > 0 && dy == 0) + { + return SKD_RIGHT; + } + else if (dx < 0 && dy == 0) + { + return SKD_LEFT; + } + else if (dx == 0 && dy < 0) + { + return SKD_UP; + } + else if (dx == 0 && dy > 0) + { + return SKD_DOWN; + } + + return SKD_NONE; +} + +sokoVec_t sokoGridToPix(soko_abs_t* self, sokoVec_t grid) // Convert grid position to screen pixel position +{ + sokoVec_t retVec; + uint16_t scale + = self->currentLevel + .levelScale; //@todo These should be in constants, but too lazy to change all references at the moment. + uint16_t ox = (TFT_WIDTH / 2) - ((self->currentLevel.width) * scale / 2); + uint16_t oy = (TFT_HEIGHT / 2) - ((self->currentLevel.height) * scale / 2); + retVec.x = ox + scale * grid.x + scale / 2; + retVec.y = oy + scale * grid.y + scale / 2; + return retVec; +} + +void drawLaserFromEntity(soko_abs_t* self, sokoEntity_t* emitter) +{ + sokoCollision_t impactSpot = sokoBeamImpact(self, self->soko_player); + // printf("Player Pos: x:%d,y:%d Facing:%d Impact Result: x:%d,y:%d, Flag:%d + // Index:%d\n",self->soko_player->x,self->soko_player->y,self->soko_player->facing,impactSpot.x,impactSpot.y,impactSpot.entityFlag,impactSpot.entityIndex); + sokoVec_t playerGrid, impactGrid; + playerGrid.x = emitter->x; + playerGrid.y = emitter->y; + impactGrid.x = impactSpot.x; + impactGrid.y = impactSpot.y; + sokoVec_t playerPix = sokoGridToPix(self, playerGrid); + sokoVec_t impactPix = sokoGridToPix(self, impactGrid); + drawLine(playerPix.x, playerPix.y, impactPix.x, impactPix.y, c500, 0); +} + +// void sokoDoBeam(soko_abs_t* self) +// { +// // bool receiverImpact; +// // for (int entInd = 0; entInd < self->currentLevel.entityCount; entInd++) +// // { +// // if (self->currentLevel.entities[entInd].type == SKE_LASER_EMIT_UP) +// // { +// // self->currentLevel.entities[entInd].properties->targetCount = 0; +// // receiverImpact = sokoBeamImpactRecursive( +// // self, self->currentLevel.entities[entInd].x, self->currentLevel.entities[entInd].y, +// // self->currentLevel.entities[entInd].type, &self->currentLevel.entities[entInd]); +// // } +// // } +// } + +bool sokoLaserTileCollision(sokoTile_t testTile) +{ + switch (testTile) + { + case SKT_EMPTY: + { + return false; + } + case SKT_FLOOR: + { + return false; + } + case SKT_WALL: + { + return true; + } + case SKT_GOAL: + { + return false; + } + case SKT_PORTAL: + { + return false; + } + case SKT_FLOOR_WALKED: + { + return false; + } + case SKT_NO_WALK: + { + return false; + } + default: + { + return false; + } + } +} + +bool sokoLaserEntityCollision(sokoEntityType_t testEntity) +{ + switch (testEntity) // Anything that doesn't unconditionally pass should return true + { + case SKE_NONE: + { + return false; + } + case SKE_PLAYER: + { + return false; + } + case SKE_CRATE: + { + return true; + } + case SKE_LASER_90: + { + return true; + } + case SKE_STICKY_CRATE: + { + return true; + } + case SKE_WARP: + { + return false; + } + case SKE_BUTTON: + { + return false; + } + case SKE_LASER_EMIT_UP: + { + return true; + } + case SKE_LASER_RECEIVE_OMNI: + { + return true; + } + case SKE_LASER_RECEIVE: + { + return true; + } + case SKE_GHOST: + { + return true; + } + default: + { + return false; + } + } +} + +sokoDirection_t sokoRedirectDir(sokoDirection_t emitterDir, bool inverted) +{ + switch (emitterDir) + { + case SKD_UP: + { + return inverted ? SKD_LEFT : SKD_RIGHT; + } + case SKD_DOWN: + { + return inverted ? SKD_RIGHT : SKD_LEFT; + } + case SKD_RIGHT: + { + return inverted ? SKD_DOWN : SKD_UP; + } + case SKD_LEFT: + { + return inverted ? SKD_UP : SKD_DOWN; + } + default: + { + return SKD_NONE; + } + } +} + +int sokoBeamImpactRecursive(soko_abs_t* self, int emitter_x, int emitter_y, sokoDirection_t emitterDir, + sokoEntity_t* rootEmitter) +{ + sokoDirection_t dir = emitterDir; + sokoVec_t projVec = {0, 0}; + sokoVec_t emitVec = {emitter_x, emitter_y}; + switch (dir) + { + case SKD_DOWN: + { + projVec.y = 1; + break; + } + case SKD_UP: + { + projVec.y = -1; + break; + } + case SKD_LEFT: + { + projVec.x = -1; + break; + } + case SKD_RIGHT: + { + projVec.x = 1; + break; + } + default: + { + projVec.y = -1; + break; + // return base entity position + } + } + + // Iterate over tiles in ray to edge of level + sokoVec_t testPos = sokoAddCoord(emitVec, projVec); + int entityCount = self->currentLevel.entityCount; + // todo: make first pass pack a statically allocated array with only the entities in the path of the laser. + + int16_t possibleSquares = 0; + if (dir == SKD_RIGHT) // move these checks into the switch statement + { + possibleSquares = self->currentLevel.width - emitVec.x; // Up to and including far wall + } + if (dir == SKD_LEFT) + { + possibleSquares = emitVec.x + 1; + } + if (dir == SKD_UP) + { + possibleSquares = emitVec.y + 1; + } + if (dir == SKD_DOWN) + { + possibleSquares = self->currentLevel.height - emitVec.y; + } + + int tileCollFlag, entCollFlag, entCollInd; + tileCollFlag = entCollFlag = entCollInd = 0; + + bool retVal; + // printf("emitVec(%d,%d)",emitVec.x,emitVec.y); + // printf("projVec:(%d,%d) possibleSquares:%d ",projVec.x,projVec.y,possibleSquares); + + for (int n = 0; n < possibleSquares; n++) + { + sokoTile_t posTile = absSokoGetTile(self, testPos.x, testPos.y); + // printf("|n:%d,posTile:(%d,%d):%d|",n,testPos.x,testPos.y,posTile); + if (sokoLaserTileCollision(posTile)) + { + tileCollFlag = 1; + break; + } + for (int m = 0; m < entityCount; m++) // iterate over tiles/entities to check for laser collision. First pass + // finds everything in the path of the + { + sokoEntity_t candidateEntity = self->currentLevel.entities[m]; + // printf("|m:%d;CE:(%d,%d)%d",m,candidateEntity.x,candidateEntity.y,candidateEntity.type); + if (candidateEntity.x == testPos.x && candidateEntity.y == testPos.y) + { + // printf(";POSMATCH;Coll:%d",entityCollision[candidateEntity.type]); + if (sokoLaserEntityCollision(candidateEntity.type)) + { + entCollFlag = 1; + entCollInd = m; + // printf("|"); + break; + } + } + // printf("|"); + } + sokoEntityProperties_t* entProps = &rootEmitter->properties; + if (tileCollFlag) + { + entProps->targetX[entProps->targetCount] = testPos.x; // Pack target properties with every impacted + // position. + entProps->targetY[entProps->targetCount] = testPos.y; + entProps->targetCount++; + } + if (entCollFlag) + { + sokoEntityType_t entType = self->currentLevel.entities[entCollInd].type; + + entProps->targetX[entProps->targetCount] = testPos.x; // Pack target properties with every impacted entity. + entProps->targetY[entProps->targetCount] + = testPos.y; // If there's a redirect, it will be added after this one. + entProps->targetCount++; + if (entType == SKE_LASER_90) + { + sokoDirection_t redirectDir + = sokoRedirectDir(emitterDir, self->currentLevel.entities[entCollInd].facing); // SKD_UP or SKD_DOWN + sokoBeamImpactRecursive(self, testPos.x, testPos.y, redirectDir, rootEmitter); + } + + break; + } + testPos = sokoAddCoord(testPos, projVec); + } + retVal = self->currentLevel.entities[entCollInd].properties.targetCount; + // printf("\n"); + // retVal.x = testPos.x; + // retVal.y = testPos.y; + // retVal.entityIndex = entCollInd; + // retVal.entityFlag = entCollFlag; + // printf("impactPoint:(%d,%d)\n",testPos.x,testPos.y); + return retVal; +} + +sokoCollision_t sokoBeamImpact(soko_abs_t* self, sokoEntity_t* emitter) +{ + sokoDirection_t dir = emitter->facing; + sokoVec_t projVec = {0, 0}; + sokoVec_t emitVec = {emitter->x, emitter->y}; + switch (dir) + { + case SKD_DOWN: + { + projVec.y = 1; + break; + } + case SKD_UP: + { + projVec.y = -1; + break; + } + case SKD_LEFT: + { + projVec.x = -1; + break; + } + case SKD_RIGHT: + { + projVec.x = 1; + break; + } + default: + { // return base entity position + } + } + + // Iterate over tiles in ray to edge of level + sokoVec_t testPos = sokoAddCoord(emitVec, projVec); + int entityCount = self->currentLevel.entityCount; + // todo: make first pass pack a statically allocated array with only the entities in the path of the laser. + + uint8_t tileCollision[] + = {0, 0, 1, 0, 0, 1, 1}; // There should be a pointer internal to the game state so this can vary with game mode + uint8_t entityCollision[] = {0, 0, 1, 1}; + + int16_t possibleSquares = 0; + if (dir == SKD_RIGHT) // move these checks into the switch statement + { + possibleSquares = self->currentLevel.width - emitVec.x; // Up to and including far wall + } + if (dir == SKD_LEFT) + { + possibleSquares = emitVec.x + 1; + } + if (dir == SKD_UP) + { + possibleSquares = emitVec.y + 1; + } + if (dir == SKD_DOWN) + { + possibleSquares = self->currentLevel.height - emitVec.y; + } + + // int tileCollFlag = 0; + int entCollFlag = 0; + int entCollInd = 0; + + sokoCollision_t retVal; + // printf("emitVec(%d,%d)",emitVec.x,emitVec.y); + // printf("projVec:(%d,%d) possibleSquares:%d ",projVec.x,projVec.y,possibleSquares); + + for (int n = 0; n < possibleSquares; n++) + { + sokoTile_t posTile = absSokoGetTile(self, testPos.x, testPos.y); + // printf("|n:%d,posTile:(%d,%d):%d|",n,testPos.x,testPos.y,posTile); + if (tileCollision[posTile]) + { + // tileCollFlag = 1; + break; + } + for (int m = 0; m < entityCount; m++) // iterate over tiles/entities to check for laser collision. First pass + // finds everything in the path of the + { + sokoEntity_t candidateEntity = self->currentLevel.entities[m]; + // printf("|m:%d;CE:(%d,%d)%d",m,candidateEntity.x,candidateEntity.y,candidateEntity.type); + if (candidateEntity.x == testPos.x && candidateEntity.y == testPos.y) + { + // printf(";POSMATCH;Coll:%d",entityCollision[candidateEntity.type]); + if (entityCollision[candidateEntity.type]) + { + entCollFlag = 1; + entCollInd = m; + // printf("|"); + break; + } + } + // printf("|"); + } + + if (entCollFlag) + { + break; + } + testPos = sokoAddCoord(testPos, projVec); + } + // printf("\n"); + retVal.x = testPos.x; + retVal.y = testPos.y; + retVal.entityIndex = entCollInd; + retVal.entityFlag = entCollFlag; + // printf("impactPoint:(%d,%d)\n",testPos.x,testPos.y); + return retVal; +} + +sokoVec_t sokoAddCoord(sokoVec_t op1, sokoVec_t op2) +{ + sokoVec_t retVal; + retVal.x = op1.x + op2.x; + retVal.y = op1.y + op2.y; + return retVal; +} + +// Euler Game Modes +void eulerSokoTryPlayerMovement(soko_abs_t* self) +{ + if (self->input.playerInputDeltaX == 0 && self->input.playerInputDeltaY == 0) + { + return; + } + + uint16_t x = self->soko_player->x; + uint16_t y = self->soko_player->y; + bool moved = self->sokoTryMoveEntityInDirectionFunc(self, self->soko_player, self->input.playerInputDeltaX, + self->input.playerInputDeltaY, 0); + + if (moved) + { + // Paint Floor + + // previous + if (self->currentLevel.tiles[x][y] == SKT_FLOOR) + { + sokoAddTileMoveToHistory(self, x, y, SKT_FLOOR); + self->currentLevel.tiles[x][y] = SKT_FLOOR_WALKED; + } + if (self->currentLevel.tiles[self->soko_player->x][self->soko_player->y] == SKT_FLOOR) + { + sokoAddTileMoveToHistory(self, self->soko_player->x, self->soko_player->y, SKT_FLOOR); + self->currentLevel.tiles[self->soko_player->x][self->soko_player->y] = SKT_FLOOR_WALKED; + } + + // Try Sticky Blocks + // Loop through all entities is probably not really slower than sampling? We usually have <5 entities. + for (size_t i = 0; i < self->currentLevel.entityCount; i++) + { + if (self->currentLevel.entities[i].type == SKE_STICKY_CRATE) + { + if (self->currentLevel.entities[i].x == x && self->currentLevel.entities[i].y == y + 1) + { + absSokoTryMoveEntityInDirection(self, &self->currentLevel.entities[i], + self->input.playerInputDeltaX, self->input.playerInputDeltaY, 0); + } + else if (self->currentLevel.entities[i].x == x && self->currentLevel.entities[i].y == y - 1) + { + absSokoTryMoveEntityInDirection(self, &self->currentLevel.entities[i], + self->input.playerInputDeltaX, self->input.playerInputDeltaY, 0); + } + else if (self->currentLevel.entities[i].y == y && self->currentLevel.entities[i].x == x + 1) + { + absSokoTryMoveEntityInDirection(self, &self->currentLevel.entities[i], + self->input.playerInputDeltaX, self->input.playerInputDeltaY, 0); + } + else if (self->currentLevel.entities[i].y == y && self->currentLevel.entities[i].x == x - 1) + { + absSokoTryMoveEntityInDirection(self, &self->currentLevel.entities[i], + self->input.playerInputDeltaX, self->input.playerInputDeltaY, 0); + } + } + } + sokoHistoryTurnOver(self); + } +} + +bool eulerNoUnwalkedFloors(soko_abs_t* self) +{ + for (size_t x = 0; x < self->currentLevel.width; x++) + { + for (size_t y = 0; y < self->currentLevel.height; y++) + { + if (self->currentLevel.tiles[x][y] == SKT_FLOOR) + { + return false; + } + } + } + + return true; +} + +void overworldSokoGameLoop(soko_abs_t* self, int64_t elapsedUs) +{ + if (self->state == SKS_GAMEPLAY) + { + // logic + + // by saving this before we move, we lag by one position. The final movement onto a portal doesn't get saved, as + // this loopin't entered again then we return to the position we were at before the last loop. + self->overworld_playerX = self->soko_player->x; + self->overworld_playerY = self->soko_player->y; + + self->sokoTryPlayerMovementFunc(self); + + // victory status. stored separate from gamestate because of future gameplay ideas/remixes. + // todo: rename 'allCrates' to isVictory or such. + self->allCratesOnGoal = self->isVictoryConditionFunc(self); + if (self->allCratesOnGoal) + { + self->state = SKS_VICTORY; + + printf("Player at %d,%d\n", self->soko_player->x, self->soko_player->y); + victoryDanceTimer = 0; + } + // draw level + self->drawTilesFunc(self, &self->currentLevel); + } + else if (self->state == SKS_VICTORY) + { + self->drawTilesFunc(self, &self->currentLevel); + + // check for input for exit/next level. + uint8_t targetWorldIndex = 0; + for (int i = 0; i < self->portalCount; i++) + { + if (self->soko_player->x == self->portals[i].x && self->soko_player->y == self->portals[i].y) + { + targetWorldIndex = self->portals[i].index; + break; + } + } + + self->loadNewLevelIndex = targetWorldIndex; + self->loadNewLevelFlag = false; // load saved data. + self->screen = SOKO_LOADNEWLEVEL; + } + + // DEBUG PLACEHOLDER: + // Render the time to a string + char str[16] = {0}; + int16_t tWidth; + if (!self->allCratesOnGoal) + { + snprintf(str, sizeof(str) - 1, "sokoban"); + // Measure the width of the time string + tWidth = textWidth(&self->ibm, str); + // Draw the time string to the display, centered at (TFT_WIDTH / 2) + drawText(&self->ibm, c555, str, ((TFT_WIDTH - tWidth) / 2), 0); + } + else + { + snprintf(str, sizeof(str) - 1, "sokasuccess"); + // Measure the width of the time string + tWidth = textWidth(&self->ibm, str); + // Draw the time string to the display, centered at (TFT_WIDTH / 2) + drawText(&self->ibm, c555, str, ((TFT_WIDTH - tWidth) / 2), 0); + } +} + +bool overworldPortalEntered(soko_abs_t* self) +{ + for (uint8_t i = 0; i < self->portalCount; i++) + { + if (self->soko_player->x == self->portals[i].x && self->soko_player->y == self->portals[i].y) + { + return true; + } + } + return false; +} + +void restartCurrentLevel(soko_abs_t* self) +{ + // assumed this is set already? + // self->loadNewLevelIndex = self->loadNewLevelIndex; + + // todo: what can we do about screen flash when restarting? + self->loadNewLevelFlag = true; + self->screen = SOKO_LOADNEWLEVEL; +} + +void exitToOverworld(soko_abs_t* soko) +{ + printf("Exit to Overworld\n"); + // save. todo: skip if victory. + if (soko->currentLevel.gameMode == SOKO_EULER) + { + // sokoSaveEulerTiles(soko); + } + // sokoSaveCurrentLevelEntities(soko); + + soko->loadNewLevelIndex = 0; + soko->loadNewLevelFlag = true; + // self->state = SKS_GAMEPLAY; + soko->screen = SOKO_LOADNEWLEVEL; +} diff --git a/main/modes/games/soko/soko_gamerules.h b/main/modes/games/soko/soko_gamerules.h new file mode 100644 index 000000000..4588d201a --- /dev/null +++ b/main/modes/games/soko/soko_gamerules.h @@ -0,0 +1,49 @@ +#ifndef SOKO_GAMERULES_H +#define SOKO_GAMERULES_H + +/// @brief call [entity][tile] to get a bool that is true if that entity can NOT walk (or get pushed onto) that tile. +// bool sokoEntityTileCollision[4][8]; + +sokoTile_t sokoGetTile(int, int); +void sokoConfigGamemode(soko_abs_t* gamestate, soko_var_t variant); + +// utility/shared functions. +void sharedGameLoop(soko_abs_t* self); +sokoDirection_t sokoDirectionFromDelta(int, int); + +// entity pushing. +void sokoTryPlayerMovement(void); +bool sokoTryMoveEntityInDirection(sokoEntity_t*, int, int, uint16_t); + +// classic and default +void absSokoGameLoop(soko_abs_t* self, int64_t elapsedUs); +void absSokoTryPlayerMovement(soko_abs_t* self); +bool absSokoTryMoveEntityInDirection(soko_abs_t* self, sokoEntity_t* entity, int dx, int dy, uint16_t push); +void absSokoDrawTiles(soko_abs_t* self, sokoLevel_t* level); +bool absSokoAllCratesOnGoal(soko_abs_t* self); +sokoTile_t absSokoGetTile(soko_abs_t* self, int x, int y); +bool allCratesOnGoal(void); + +// euler +void eulerSokoTryPlayerMovement(soko_abs_t* self); +bool eulerNoUnwalkedFloors(soko_abs_t* self); + +// lasers +sokoCollision_t sokoBeamImpact(soko_abs_t* self, sokoEntity_t* emitter); +int sokoBeamImpactRecursive(soko_abs_t* self, int emitter_x, int emitter_y, sokoDirection_t emitterDir, + sokoEntity_t* rootEmitter); +sokoDirection_t sokoRedirectDir(sokoDirection_t emitterDir, bool inverted); +bool sokoLaserEntityCollision(sokoEntityType_t testEntity); +bool sokoLaserTileCollision(sokoTile_t testTile); +void laserBounceSokoGameLoop(soko_abs_t* self, int64_t elapsedUs); +sokoVec_t sokoGridToPix(soko_abs_t* self, sokoVec_t grid); +void drawLaserFromEntity(soko_abs_t* self, sokoEntity_t* emitter); +sokoVec_t sokoAddCoord(sokoVec_t op1, sokoVec_t op2); + +// overworld +void overworldSokoGameLoop(soko_abs_t* self, int64_t elapsedUs); +bool overworldPortalEntered(soko_abs_t* self); +void restartCurrentLevel(soko_abs_t* self); +void exitToOverworld(soko_abs_t* self); + +#endif // SOKO_GAMERULES_H \ No newline at end of file diff --git a/main/modes/games/soko/soko_input.c b/main/modes/games/soko/soko_input.c new file mode 100644 index 000000000..ab8fa4127 --- /dev/null +++ b/main/modes/games/soko/soko_input.c @@ -0,0 +1,179 @@ +#include "soko_input.h" + +/** + * @brief Initialize Input. Does this for every puzzle start, to reset button state. + * Also where config like dastime is set. + * + * @param input + */ +void sokoInitInput(sokoGameplayInput_t* input) +{ + input->dasTime = 100000; + input->firstDASTime = 500000; + input->DASActive = false; + input->prevHoldingDir = SKD_NONE; + input->prevBtnState = 0; + input->playerInputDeltaX = 0; + input->playerInputDeltaY = 0; + input->restartLevel = false; + input->exitToOverworld = false; + input->undo = false; +} +/** + * @brief Input preprocessing turns btnstate into game-logic usable data. + * Input variables only set on press, as appropriate. + * Handles DAS, settings, etc. + * Called once a frame before game loop. + * + * @param input + */ +void sokoPreProcessInput(sokoGameplayInput_t* input, int64_t elapsedUs) +{ + uint16_t btn = input->btnState; + // reset output data. + input->playerInputDeltaY = 0; + input->playerInputDeltaX = 0; + + // Non directional buttons + if ((btn & PB_B) && !(input->prevBtnState & PB_B)) + { + input->restartLevel = true; + } + else + { + input->restartLevel = false; + } + + if ((btn & PB_A) && !(input->prevBtnState & PB_A)) + { + input->undo = true; + } + else + { + input->undo = false; + } + + if ((btn & PB_START) && !(input->prevBtnState & PB_START)) + { + input->exitToOverworld = true; + } + else + { + input->exitToOverworld = false; + } + + // update holding direction + if ((btn & PB_UP) && !(btn & 0b1110)) + { + input->holdingDir = SKD_UP; + } + else if ((btn & PB_DOWN) && !(btn & 0b1101)) + { + input->holdingDir = SKD_DOWN; + } + else if ((btn & PB_LEFT) && !(btn & 0b1011)) + { + input->holdingDir = SKD_LEFT; + } + else if ((btn & PB_RIGHT) && !(btn & 0b0111)) + { + input->holdingDir = SKD_RIGHT; + } + else + { + input->holdingDir = SKD_NONE; + input->DASActive = false; + input->timeHeldDirection = 0; // reset when buttons change or multiple buttons. + } + + // going from one button to another without letting go could cheese DAS. + if (input->holdingDir != input->prevHoldingDir) + { + input->DASActive = false; + input->timeHeldDirection = 0; + } + + // increment DAS time. + if (input->holdingDir != SKD_NONE) + { + input->timeHeldDirection += elapsedUs; + } + + // two cases when DAS gets triggered: initial and every one after the initial. + bool triggerDAS = false; + if (input->DASActive == false && input->timeHeldDirection > input->firstDASTime) + { + triggerDAS = true; + input->DASActive = true; + } + + if (input->DASActive == true && input->timeHeldDirection > input->dasTime) + { + triggerDAS = true; + } + + if (triggerDAS) + { + // reset timer + input->timeHeldDirection = 0; + + // trigger movement + // todo: in sokogame i had to write delta to direction. This is basically directionenum to delta, which could be + // extracted too. + switch (input->holdingDir) + { + case SKD_RIGHT: + { + input->playerInputDeltaX = 1; + break; + } + case SKD_LEFT: + { + input->playerInputDeltaX = -1; + break; + } + case SKD_UP: + { + input->playerInputDeltaY = -1; + break; + } + case SKD_DOWN: + { + input->playerInputDeltaY = 1; + break; + } + case SKD_NONE: + default: + { + break; + } + } + } + else + { // if !trigger DAS + + // holdingDir is ONLY holding one button. So we use normal buttonstate for taps so we can tap button two before + // releasing button one. + if (input->btnState & PB_UP && !(input->prevBtnState & PB_UP)) + { + input->playerInputDeltaY = -1; + } + else if (input->btnState & PB_DOWN && !(input->prevBtnState & PB_DOWN)) + { + input->playerInputDeltaY = 1; + } + else if (input->btnState & PB_LEFT && !(input->prevBtnState & PB_LEFT)) + { + input->playerInputDeltaX = -1; + } + else if (input->btnState & PB_RIGHT && !(input->prevBtnState & PB_RIGHT)) + { + input->playerInputDeltaX = 1; + } + + } // end !triggerDAS + + // do this last + input->prevBtnState = btn; + input->prevHoldingDir = input->holdingDir; +} diff --git a/main/modes/games/soko/soko_input.h b/main/modes/games/soko/soko_input.h new file mode 100644 index 000000000..0ed23478c --- /dev/null +++ b/main/modes/games/soko/soko_input.h @@ -0,0 +1,40 @@ +#include "swadge2024.h" + +// there is a way to set clever ints here such that we can super quickly convert to dx and dy with bit ops. I'll think +// it through eventually. +typedef enum +{ + SKD_UP, + SKD_DOWN, + SKD_RIGHT, + SKD_LEFT, + SKD_NONE +} sokoDirection_t; + +typedef struct +{ + // input input data. + uint16_t btnState; ///< The button state. Provided to input For PreProcess. + + // input meta data. Used by PreProcess. + uint16_t prevBtnState; ///< The button state from the previous frame. + sokoDirection_t holdingDir; ///< What direction we are holding down. + sokoDirection_t prevHoldingDir; ///< What direction we are holding down. + uint64_t timeHeldDirection; ///< The amount of time we have been holding a single button down. Used for DAS. + bool DASActive; ///< If DAS has begun. User may be holding before first DAS, this is false. After first, it becomes + ///< true. + uint64_t dasTime; ///< How many microseconds before DAS starts + uint64_t firstDASTime; ///< how many microseconds after DAS has started before the next DAS + + // input output data. ie: usable Gameplay data. + // todo: use Direction in input + int playerInputDeltaX; + int playerInputDeltaY; + bool undo; + bool restartLevel; + bool exitToOverworld; + +} sokoGameplayInput_t; + +void sokoInitInput(sokoGameplayInput_t*); +void sokoPreProcessInput(sokoGameplayInput_t*, int64_t); diff --git a/main/modes/games/soko/soko_save.c b/main/modes/games/soko/soko_save.c new file mode 100644 index 000000000..0df4d9c10 --- /dev/null +++ b/main/modes/games/soko/soko_save.c @@ -0,0 +1,680 @@ +#include "soko.h" +#include "soko_save.h" + +static void sokoLoadCurrentLevelEntities(soko_abs_t* soko); +static void sokoSetLevelSolvedState(soko_abs_t* soko, uint16_t levelIndex, bool solved); +static void sokoLoadBinTiles(soko_abs_t* soko, int byteCount); +static int sokoFindIndex(soko_abs_t* self, int targetIndex); +void sokoSaveEulerTiles(soko_abs_t* soko); +void sokoLoadEulerTiles(soko_abs_t* soko); +void sokoSaveCurrentLevelEntities(soko_abs_t* soko); + +/// @brief Called on 'resume' from the menu. +/// @param soko +void sokoLoadGameplay(soko_abs_t* soko, uint16_t levelIndex, bool loadNew) +{ + // save previous level if needed. + sokoSaveGameplay(soko); + + // load current level + int32_t data = 0; + readNvs32("sk_data", &data); + // bitshift, etc, as needed. + uint16_t lastSaved = (uint16_t)data; + + sokoLoadBinLevel(soko, levelIndex); + if (levelIndex == lastSaved && !loadNew) + { + printf("Load Saved Data for level %i\n", lastSaved); + // current level entity positions + sokoLoadCurrentLevelEntities(soko); + + if (soko->currentLevel.gameMode == SOKO_EULER) + { + sokoLoadEulerTiles(soko); + } + } +} + +void sokoSaveGameplay(soko_abs_t* soko) +{ + printf("Save Gameplay\n"); + + // save current level + if (soko->currentLevelIndex == 0) + { + // overworld gets saved separately. + return; + } + int current = soko->currentLevelIndex; + // current level entity positions + uint32_t data = current; + // what other data gets encoded? we can also save the sk_tiles count. + writeNvs32("sk_data", data); + + sokoSaveCurrentLevelEntities(soko); + + if (soko->currentLevel.gameMode == SOKO_EULER) + { + sokoSaveEulerTiles(soko); + } +} + +void sokoLoadLevelSolvedState(soko_abs_t* soko) +{ + // todo: automatically split for >32, >64 levels using 2 loops. + + int32_t lvs = 0; + readNvs32("sklv1", &lvs); + // i<32... + for (size_t i = 0; i < SOKO_LEVEL_COUNT; i++) + { + soko->levelSolved[i] = (1 & lvs >> i) == 1; + } + // now the next 32 bytes! + // readNvs32("sklv2",&lvs); + // for (size_t i = 32; i < SOKO_LEVEL_COUNT || i < 64; i++) + // { + // soko->levelSolved[i] = (1 & lvs>>i) == 1; + // } + + // etc. Probably won't bother cleaning it into nested loop until over 32*4 levels... + // so .. never? +} + +void sokoSetLevelSolvedState(soko_abs_t* soko, uint16_t levelIndex, bool solved) +{ + printf("save level solved status %d\n", levelIndex); + // todo: changes a single levels bool in the sokoSolved array, + soko->levelSolved[levelIndex] = true; + + int section = levelIndex / 32; + int index = levelIndex; + int32_t lvs = 0; + + if (section == 0) + { + readNvs32("sklv1", &lvs); + } + else if (section == 1) + { + readNvs32("sklv2", &lvs); + index -= 32; + } // else, 64, + + // write the bit. + if (solved) + { + // set bit + lvs = lvs | (1 << index); + } + else + { + // clear bit + lvs = lvs & ~(1 << index); + } + + // write the bit out to data. + if (section == 0) + { + writeNvs32("sklv1", lvs); + } + else if (section == 1) + { + writeNvs32("sklv2", lvs); + } +} + +void sokoSolveCurrentLevel(soko_abs_t* soko) +{ + if (soko->currentLevelIndex == 0) + { + // overworld level. + return; + } + else + { + sokoSetLevelSolvedState(soko, soko->currentLevelIndex, true); + } +} + +// Saving Progress +// soko->overworldX +// soko->overworldY +// current level? or just stick on overworld? + +// current level progress (all entitity positions/data, entities array. non-entities comes from file.) +// euler encoding? (do like picross level?) + +void sokoSaveCurrentLevelEntities(soko_abs_t* soko) +{ + // todo: the overworld will have >max entities... and they never need to be serialized... + // so maybe just make a separate array for portals that is entities of size maxLevelCount... + // and then treat it completely separately in the game loops. + + // sort of feels like we should do something similar to the blob packing of the levels. + // Then write a function that's like "get entity from bytes" where we pass it an array-slice of bytes, and get back + // some entity object. except, we have to include x,y data here... so it would be different... + + // instead, we can have our own binary encoding. Some entities never move, and can be loaded from the disk. + // after they are loaded, we save "Index, X, Y, Extra" binary sets, and replace the values for the entities at the + // index position. I think it will work such that, for a level, entities will always have the same index position in + // the entities array... this is ONLY true if we never actually 'destroy' or 'CREATE' entities, but just flip some + // 'dead' flag. + + // if each entity is 4 bytes, then we can save (adjust) all entities as a single blob, always, since it's a + // pre-allocated array. + char* entities = calloc(soko->currentLevel.entityCount * 4, sizeof(char)); + + for (int i = 0; i < soko->currentLevel.entityCount; i++) + { + entities[i * 4] = i; + // todo: facing... + // sokoentityproperties? will these ever change at runtime? there is an "hp" that was made for laserbounce... + // do we need the propflag? + entities[i * 4 + 1] = soko->currentLevel.entities[i].x; + entities[i * 4 + 2] = soko->currentLevel.entities[i].y; + entities[i * 4 + 3] = soko->currentLevel.entities[i].facing; + } + size_t size = sizeof(char) * (soko->currentLevel.entityCount) * 4; + writeNvsBlob("sk_ents", entities, size); + free(entities); +} +// todo: there is no clean place to return to the main menu right now, so gotta write that function/flow so this can get +// called. + +/// @brief After loading the level into currentLevel, this updates the entity array with saved +/// @param soko +void sokoLoadCurrentLevelEntities(soko_abs_t* soko) +{ + printf("loading current level entities.\n"); + + char* entities = calloc(soko->currentLevel.entityCount * 4, sizeof(char)); + size_t size = sizeof(char) * (soko->currentLevel.entityCount * 4); + readNvsBlob("sk_ents", entities, &size); + + for (int i = 0; i < soko->currentLevel.entityCount; i++) + { + // todo: wait, if all entities are the same length, we don't actually need to save the index... + soko->currentLevel.entities[i].x = entities[i * 4 + 1]; + soko->currentLevel.entities[i].y = entities[i * 4 + 2]; + soko->currentLevel.entities[i].facing = entities[i * 4 + 3]; + } + free(entities); +} + +void sokoSaveEulerTiles(soko_abs_t* soko) +{ + printf("encoding euler tiles.\n"); + + sokoTile_t prevTile = SKT_FLOOR; + int w = soko->currentLevel.width; + uint16_t i = 0; + char* blops = (char*)calloc(255, sizeof(char)); + for (uint16_t y = 0; y < soko->currentLevel.height; y++) + { + for (uint16_t x = 0; x < w; x++) + { + sokoTile_t t = soko->currentLevel.tiles[x][y]; + if (t == SKT_FLOOR || t == SKT_FLOOR_WALKED) + { + if (t == prevTile) + { + blops[i] = blops[i] + 1; + } + else + { + prevTile = t; + i++; + blops[i] = blops[i] + 1; + if (i > 255) + { + printf("ERROR This level is too big to save for euler???\n"); + break; + } + } + } + } + } + i++; + writeNvsBlob("sk_e_t_c", &i, sizeof(uint16_t)); + writeNvsBlob("sk_e_ts", blops, sizeof(char) * i); + + free(blops); +} + +void sokoLoadEulerTiles(soko_abs_t* soko) +{ + printf("Load Euler Tiles\n"); + sokoTile_t runningTile = SKT_FLOOR; + uint16_t w = soko->currentLevel.width; + uint16_t total = 0; + // i don't think i need to calloc before reading the blob? + + size_t size = sizeof(uint16_t); + readNvsBlob("sk_e_t_c", &total, &size); + + char* blops = calloc(total, sizeof(char)); + size = sizeof(char) * total; + readNvsBlob("sk_e_ts", blops, &size); + + uint16_t bi = 0; + if (blops[0] == 0) + { + // pre-flip, basically... + runningTile = SKT_FLOOR_WALKED; + bi = 1; // doesn't mess up our count, because 0 counts for 0 tiles. + } + for (size_t y = 0; y < soko->currentLevel.height; y++) + { + for (size_t x = 0; x < w; x++) + { + sokoTile_t t = soko->currentLevel.tiles[x][y]; + if (t == SKT_FLOOR || t == SKT_FLOOR_WALKED) + { + soko->currentLevel.tiles[x][y] = runningTile; + blops[bi] = blops[bi] - 1; + + if (blops[bi] == 0) + { + bi++; + // flop + if (runningTile == SKT_FLOOR) + { + runningTile = SKT_FLOOR_WALKED; + } + else if (runningTile == SKT_FLOOR_WALKED) + { + runningTile = SKT_FLOOR; + } + } + } + } + } + free(blops); +} + +// Level loading +void sokoLoadBinLevel(soko_abs_t* soko, uint16_t levelIndex) +{ + printf("load bin level %d, %s\n", levelIndex, soko->levelNames[levelIndex]); + soko->state = SKS_INIT; + size_t fileSize; + if (soko->levelBinaryData) + { + free(soko->levelBinaryData); + } + soko->levelBinaryData + = cnfsReadFile(soko->levelNames[levelIndex], &fileSize, true); // Heap CAPS malloc/calloc allocation for SPI RAM + + // The pointer returned by spiffsReadFile can be freed with free() with no additional steps. + soko->currentLevel.width = soko->levelBinaryData[0]; // first two bytes of a level's data always describe the + // bounding width and height of the tilemap. + soko->currentLevel.height = soko->levelBinaryData[1]; // Max Theoretical Level Bounding Box Size is 255x255, though + // you'll likely run into issues with entities first. + soko->currentLevel.gameMode = (soko_var_t)soko->levelBinaryData[2]; + // for(int i = 0; i < fileSize; i++) + //{ + // printf("%d, ",soko->levelBinaryData[i]); + // } + // printf("\n"); + soko->currentLevelIndex = levelIndex; + soko->currentLevel.levelScale = 16; + soko->camWidth = TFT_WIDTH / (soko->currentLevel.levelScale); + soko->camHeight = TFT_HEIGHT / (soko->currentLevel.levelScale); + soko->camEnabled = soko->camWidth < soko->currentLevel.width || soko->camHeight < soko->currentLevel.height; + soko->camPadExtentX = soko->camWidth * 0.6 * 0.5; + soko->camPadExtentY = soko->camHeight * 0.6 * 0.5; + + // incremented by loadBinTiles. + soko->currentLevel.entityCount = 0; + soko->portalCount = 0; + + sokoLoadBinTiles(soko, (int)fileSize); + + if (levelIndex == 0) + { + if (soko->overworld_playerX == 0 && soko->overworld_playerY == 0) + { + printf("resetting player position from loaded entity\n"); + soko->overworld_playerX = soko->soko_player->x; + soko->overworld_playerY = soko->soko_player->y; + } + } + + printf("Loaded level w: %i, h %i, entities: %i\n", soko->currentLevel.width, soko->currentLevel.height, + soko->currentLevel.entityCount); +} + +// todo: rename self to soko +void sokoLoadBinTiles(soko_abs_t* self, int byteCount) +{ + const int HEADER_BYTE_OFFSET = 3; // width,height,mode + // int totalTiles = self->currentLevel.width * self->currentLevel.height; + int tileIndex = 0; + int prevTileType = 0; + self->currentLevel.entityCount = 0; + self->goalCount = 0; + + for (int i = HEADER_BYTE_OFFSET; i < byteCount; i++) + { + // Objects in level data should be of the form + // SKB_OBJSTART, SKB_[Object Type], [Data Bytes] , SKB_OBJEND + if (self->levelBinaryData[i] == SKB_OBJSTART) + { + int objX = (tileIndex - 1) % (self->currentLevel.width); // Look at the previous + int objY = (tileIndex - 1) / (self->currentLevel.width); + uint8_t flagByte, direction; + bool players, crates, sticky, trail, inverted; + int hp; //, targetX, targetY; + // printf("reading object byte after start: %i,%i:%i\n",objX,objY,self->levelBinaryData[i+1]); + + switch (self->levelBinaryData[i + 1]) // On creating entities, index should be advanced to the SKB_OBJEND + // byte so the post-increment moves to the next tile. + { + case SKB_COMPRESS: + { + i += 2; + // we should not have dound this, we are inside of an object! + break; // Not yet implemented + } + case SKB_PLAYER: + { // moved gamemode to bit 3 of level data in header. + // self->currentLevel.gameMode = self->levelBinaryData[i + 2]; + self->currentLevel.entities[self->currentLevel.entityCount].type = SKE_PLAYER; + self->currentLevel.entities[self->currentLevel.entityCount].x = objX; + self->currentLevel.entities[self->currentLevel.entityCount].y = objY; + self->soko_player = &self->currentLevel.entities[self->currentLevel.playerIndex]; + self->currentLevel.playerIndex = self->currentLevel.entityCount; + self->currentLevel.entityCount += 1; + i += 2; // start, player, end. + break; + } + case SKB_CRATE: + { + flagByte = self->levelBinaryData[i + 2]; + sticky = !!(flagByte & (0x1 << 0)); + trail = !!(flagByte & (0x1 << 1)); + if (sticky && trail) + { + self->currentLevel.entities[self->currentLevel.entityCount].type = SKE_STICKY_TRAIL_CRATE; + } + else if (sticky) + { + self->currentLevel.entities[self->currentLevel.entityCount].type = SKE_STICKY_CRATE; + } + else + { + self->currentLevel.entities[self->currentLevel.entityCount].type = SKE_CRATE; + } + self->currentLevel.entities[self->currentLevel.entityCount].x = objX; + self->currentLevel.entities[self->currentLevel.entityCount].y = objY; + self->currentLevel.entities[self->currentLevel.entityCount].propFlag = true; + self->currentLevel.entities[self->currentLevel.entityCount].properties.sticky = sticky; + self->currentLevel.entities[self->currentLevel.entityCount].properties.trail = trail; + self->currentLevel.entityCount += 1; + i += 3; + break; + } + case SKB_WARPINTERNAL: //[type][flags][hp][destx][desty] + { + flagByte = self->levelBinaryData[i + 2]; + crates = !!(flagByte & (0x1 << 0)); + hp = self->levelBinaryData[i + 3]; + // targetX = self->levelBinaryData[i + 4]; + // targetY = self->levelBinaryData[i + 5]; + + self->currentLevel.entities[self->currentLevel.entityCount].type = SKE_WARP; + self->currentLevel.entities[self->currentLevel.entityCount].x = objX; + self->currentLevel.entities[self->currentLevel.entityCount].y = objY; + self->currentLevel.entities[self->currentLevel.entityCount].propFlag = true; + self->currentLevel.entities[self->currentLevel.entityCount].properties.crates = crates; + self->currentLevel.entities[self->currentLevel.entityCount].properties.hp = hp; + self->currentLevel.entities[self->currentLevel.entityCount].properties.targetX + = malloc(sizeof(uint8_t)); + self->currentLevel.entities[self->currentLevel.entityCount].properties.targetY + = malloc(sizeof(uint8_t)); + self->currentLevel.entities[self->currentLevel.entityCount].properties.targetCount = 1; + self->currentLevel.entityCount += 1; + i += 6; + break; + } + case SKB_WARPINTERNALEXIT: + { + flagByte = self->levelBinaryData[i + 2]; + + i += 2; // No data or properties in this object. + break; // Can be used later on for verifying valid warps from save files. + } + case SKB_WARPEXTERNAL: //[typep][flags][index] + { // todo implement extraction of index value and which values should be used for auto-indexed portals + self->currentLevel.tiles[objX][objY] = SKT_PORTAL; + flagByte = self->levelBinaryData[i + 2]; // destination + self->portals[self->portalCount].index + = sokoFindIndex(self, flagByte); // For basic test, 1 indexed with levels, but multi-room + // overworld needs more sophistication to keep indices correct. + self->portals[self->portalCount].x = objX; + self->portals[self->portalCount].y = objY; + self->portalCount += 1; + i += 3; + break; + } + case SKB_BUTTON: //[type][flag][numTargets][targetx][targety]... + { + flagByte = self->levelBinaryData[i + 2]; + crates = !!(flagByte & (0x1 << 0)); + players = !!(flagByte & (0x1 << 1)); + inverted = !!(flagByte & (0x1 << 2)); + sticky = !!(flagByte & (0x1 << 3)); + hp = self->levelBinaryData[i + 3]; + + self->currentLevel.entities[self->currentLevel.entityCount].type = SKE_BUTTON; + self->currentLevel.entities[self->currentLevel.entityCount].x = objX; + self->currentLevel.entities[self->currentLevel.entityCount].y = objY; + self->currentLevel.entities[self->currentLevel.entityCount].propFlag = true; + self->currentLevel.entities[self->currentLevel.entityCount].properties.targetX + = malloc(sizeof(uint8_t) * hp); + self->currentLevel.entities[self->currentLevel.entityCount].properties.targetY + = malloc(sizeof(uint8_t) * hp); + self->currentLevel.entities[self->currentLevel.entityCount].properties.targetCount = true; + self->currentLevel.entities[self->currentLevel.entityCount].properties.crates = crates; + self->currentLevel.entities[self->currentLevel.entityCount].properties.players = players; + self->currentLevel.entities[self->currentLevel.entityCount].properties.inverted = inverted; + self->currentLevel.entities[self->currentLevel.entityCount].properties.sticky = sticky; + for (int j = 0; j < hp; j++) + { + self->currentLevel.entities[self->currentLevel.entityCount].properties.targetX[j] + = self->levelBinaryData[3 + 2 * j + 1]; + self->currentLevel.entities[self->currentLevel.entityCount].properties.targetY[j] + = self->levelBinaryData[3 + 2 * (j + 1)]; + } + self->currentLevel.entities[self->currentLevel.entityCount].properties.targetCount = hp; + self->currentLevel.entities[self->currentLevel.entityCount].properties.players = players; + self->currentLevel.entities[self->currentLevel.entityCount].properties.crates = crates; + self->currentLevel.entities[self->currentLevel.entityCount].properties.inverted = inverted; + self->currentLevel.entities[self->currentLevel.entityCount].properties.sticky = sticky; + self->currentLevel.entityCount += 1; + i += (4 + 2 * hp); + break; + } + case SKB_LASEREMITTER: //[type][flag] + { + flagByte = self->levelBinaryData[i + 2]; + direction = (flagByte & (0x3 << 6)) >> 6; // flagbyte stores direction in 0bDD0000P0 Where D is + // direction bits and P is player push + players = !!(flagByte & (0x1 < 1)); + + self->currentLevel.entities[self->currentLevel.entityCount].type = SKE_LASER_EMIT_UP; + self->currentLevel.entities[self->currentLevel.entityCount].x = objX; + self->currentLevel.entities[self->currentLevel.entityCount].y = objY; + self->currentLevel.entities[self->currentLevel.entityCount].facing = direction; + self->currentLevel.entities[self->currentLevel.entityCount].propFlag = true; + self->currentLevel.entities[self->currentLevel.entityCount].properties.players = players; + self->currentLevel.entities[self->currentLevel.entityCount].properties.targetX + = calloc(SOKO_MAX_ENTITY_COUNT, sizeof(uint8_t)); + self->currentLevel.entities[self->currentLevel.entityCount].properties.targetX + = calloc(SOKO_MAX_ENTITY_COUNT, sizeof(uint8_t)); + self->currentLevel.entities[self->currentLevel.entityCount].properties.targetCount = 0; + self->currentLevel.entityCount += 1; + i += 3; + break; + } + case SKB_LASERRECEIVEROMNI: + { + self->currentLevel.entities[self->currentLevel.entityCount].type = SKE_LASER_RECEIVE_OMNI; + self->currentLevel.entities[self->currentLevel.entityCount].x = objX; + self->currentLevel.entities[self->currentLevel.entityCount].y = objY; + self->currentLevel.entityCount += 1; + i += 2; + break; + } + case SKB_LASERRECEIVER: + { + flagByte = self->levelBinaryData[i + 2]; + direction = (flagByte & (0x3 << 6)) >> 6; // flagbyte stores direction in 0bDD0000P0 Where D is + // direction bits and P is player push + players = !!(flagByte & (0x1 < 1)); + + self->currentLevel.entities[self->currentLevel.entityCount].type = SKE_LASER_RECEIVE; + self->currentLevel.entities[self->currentLevel.entityCount].x = objX; + self->currentLevel.entities[self->currentLevel.entityCount].y = objY; + self->currentLevel.entities[self->currentLevel.entityCount].facing = direction; + self->currentLevel.entityCount += 1; + i += 3; + break; + } + case SKB_LASER90ROTATE: + { + flagByte = self->levelBinaryData[i + 2]; + direction = !!(flagByte & (0x1 < 0)); + players = !!(flagByte & (0x1 < 1)); + + self->currentLevel.entities[self->currentLevel.entityCount].type = SKE_LASER_90; + self->currentLevel.entities[self->currentLevel.entityCount].x = objX; + self->currentLevel.entities[self->currentLevel.entityCount].y = objY; + self->currentLevel.entities[self->currentLevel.entityCount].facing = direction; + self->currentLevel.entities[self->currentLevel.entityCount].propFlag = true; + self->currentLevel.entities[self->currentLevel.entityCount].properties.players = players; + self->currentLevel.entityCount += 1; + i += 3; + break; + } + case SKB_GHOSTBLOCK: + { + flagByte = self->levelBinaryData[i + 2]; + inverted = !!(flagByte & (0x1 < 2)); + players = !!(flagByte & (0x1 < 1)); + + self->currentLevel.entities[self->currentLevel.entityCount].type = SKE_GHOST; + self->currentLevel.entities[self->currentLevel.entityCount].x = objX; + self->currentLevel.entities[self->currentLevel.entityCount].y = objY; + self->currentLevel.entities[self->currentLevel.entityCount].propFlag = true; + self->currentLevel.entities[self->currentLevel.entityCount].properties.players = players; + self->currentLevel.entityCount += 1; + i += 3; + break; + } + case SKB_OBJEND: + { + i += 1; + break; + } + default: // Make the best of an undefined object type and try to skip it by finding its end byte + { + bool objEndFound = false; + int undefinedObjectLength = 0; + while (!objEndFound) + { + undefinedObjectLength += 1; + if (self->levelBinaryData[i + undefinedObjectLength] == SKB_OBJEND) + { + objEndFound = true; + } + } + i += undefinedObjectLength; // Move to the completion byte of an undefined object type and hope it + // doesn't have two end bytes. + break; + } + } + } + else + { + int tileX = (tileIndex) % (self->currentLevel.width); + int tileY = (tileIndex) / (self->currentLevel.width); + // self->currentLevel.tiles[tileX][tileY] = self->levelBinaryData[i]; + int tileType = 0; + switch (self->levelBinaryData[i]) // This is a bit easier to read than two arrays + { + case SKB_EMPTY: + { + tileType = SKT_EMPTY; + break; + } + case SKB_WALL: + { + tileType = SKT_WALL; + break; + } + case SKB_FLOOR: + { + tileType = SKT_FLOOR; + break; + } + case SKB_NO_WALK: + { + tileType = SKT_FLOOR; //@todo Add No-Walk floors that can only accept crates or pass lasers + break; + } + case SKB_GOAL: + { + tileType = SKT_GOAL; + self->goals[self->goalCount].x = tileX; + self->goals[self->goalCount].y = tileY; + self->goalCount++; + break; + } + case SKB_COMPRESS: + { + tileType = prevTileType; + // decrement the next one + if (self->levelBinaryData[i + 1] > 1) + { + self->levelBinaryData[i + 1] -= 1; + i -= 1; // unloop the loop! deloop! Cursed loops! + } + else + { + i += 1; + } + + break; + } + default: + { + tileType = SKT_EMPTY; + break; + } + } + self->currentLevel.tiles[tileX][tileY] = tileType; + prevTileType = tileType; + // printf("BinData@%d: %d Tile: %d at (%d,%d) + // index:%d\n",i,self->levelBinaryData[i],tileType,tileX,tileY,tileIndex); + tileIndex++; + } + } +} + +static int sokoFindIndex(soko_abs_t* self, int targetIndex) +{ + // Filenames are formatted like '1:sk_level.bin:' + int retVal = -1; + for (int i = 0; i < SOKO_LEVEL_COUNT; i++) + { + if (self->levelIndices[i] == targetIndex) + { + retVal = i; + break; + } + } + return retVal; +} diff --git a/main/modes/games/soko/soko_save.h b/main/modes/games/soko/soko_save.h new file mode 100644 index 000000000..f24d0d72b --- /dev/null +++ b/main/modes/games/soko/soko_save.h @@ -0,0 +1,5 @@ +void sokoLoadGameplay(soko_abs_t* soko, uint16_t levelIndex, bool loadNew); +void sokoSaveGameplay(soko_abs_t* soko); +void sokoLoadLevelSolvedState(soko_abs_t* soko); +void sokoSolveCurrentLevel(soko_abs_t* soko); +void sokoLoadBinLevel(soko_abs_t* soko, uint16_t levelIndex); diff --git a/main/modes/games/soko/soko_undo.c b/main/modes/games/soko/soko_undo.c new file mode 100644 index 000000000..2bb262874 --- /dev/null +++ b/main/modes/games/soko/soko_undo.c @@ -0,0 +1,124 @@ +#include "soko_undo.h" + +void sokoInitHistory(soko_abs_t* soko) +{ + soko->historyCurrent = 0; + soko->historyBufferTail = 0; + soko->history[0].moveID = 0; + soko->historyNewMove = true; +} + +void sokoHistoryTurnOver(soko_abs_t* soko) +{ + soko->historyNewMove = true; +} + +void sokoAddTileMoveToHistory(soko_abs_t* soko, uint16_t tileX, uint16_t tileY, sokoTile_t oldTileType) +{ + uint16_t moveID = soko->history[soko->historyCurrent].moveID; + if (soko->historyNewMove) + { + moveID += 1; + soko->historyNewMove = false; + } + // i think this should basically always be false for tiles. + + soko->historyCurrent++; + if (soko->historyCurrent >= SOKO_UNDO_BUFFER_SIZE) + { + soko->historyCurrent = 0; + } + if (soko->historyCurrent == soko->historyBufferTail) + { + soko->historyBufferTail++; + if (soko->historyBufferTail >= SOKO_UNDO_BUFFER_SIZE) + { + soko->historyBufferTail = 0; + } + } + sokoUndoMove_t* move = &soko->history[soko->historyCurrent]; + + move->moveID = moveID; + move->isEntity = false; + move->tile = oldTileType; + move->x = tileX; + move->y = tileY; +} + +void sokoAddEntityMoveToHistory(soko_abs_t* soko, sokoEntity_t* entity, uint16_t oldX, uint16_t oldY, + sokoDirection_t oldFacing) +{ + uint16_t moveID = soko->history[soko->historyCurrent].moveID; + // should basically only be true for the player... + if (soko->historyNewMove) + { + moveID += 1; + soko->historyNewMove = false; + // printf("first invalid move (oldest) %i\n",soko->historyOldestValidMoveID); + } + + soko->historyCurrent++; + if (soko->historyCurrent >= SOKO_UNDO_BUFFER_SIZE) + { + soko->historyCurrent = 0; + } + if (soko->historyCurrent == soko->historyBufferTail) + { + soko->historyBufferTail++; + if (soko->historyBufferTail >= SOKO_UNDO_BUFFER_SIZE) + { + soko->historyBufferTail = 0; + } + } + + sokoUndoMove_t* move = &soko->history[soko->historyCurrent]; + move->moveID = moveID; + move->isEntity = true; + move->entity = entity; + move->x = oldX; + move->y = oldY; + move->facing = oldFacing; +} + +void sokoUndo(soko_abs_t* soko) +{ + // HistoryCurrent points to the last added move. + uint16_t undoMoveId = soko->history[soko->historyCurrent].moveID; + + // nope! can't undo! out of history. + if (undoMoveId == soko->history[soko->historyBufferTail].moveID) + { + return; + } + + while (soko->history[soko->historyCurrent].moveID == undoMoveId) + { + // history can partially overwrite the oldest move in the buffer. + // we can fix that by uh... storing the last move we overwrote in a 'invalidUndo' and stopping undoes of it? + sokoUndoMove_t* m = &soko->history[soko->historyCurrent]; + // undo this move. + if (m->isEntity) + { + // undo the entity + m->entity->x = m->x; + m->entity->y = m->y; + m->entity->facing = m->facing; + // todo: facing + } + else + { + // undo the tile + soko->currentLevel.tiles[m->x][m->y] = m->tile; + } + // ring buffer + if (soko->historyCurrent > 0) + { + soko->historyCurrent--; + } + else + { + soko->historyCurrent = SOKO_UNDO_BUFFER_SIZE - 1; + } + } + soko->undoCount++; +} diff --git a/main/modes/games/soko/soko_undo.h b/main/modes/games/soko/soko_undo.h new file mode 100644 index 000000000..0fc70572c --- /dev/null +++ b/main/modes/games/soko/soko_undo.h @@ -0,0 +1,17 @@ +#ifndef SOKO_UNDO_H + #define SOKO_UNDO_H + + #include "swadge2024.h" + #include "soko.h" + +// if isEntity is true, then x and y are the position to return the entity at entityindex to, rest is ignored. +// if isentity is false, then set the tile at position x,y to tile. rest is ignored. + +#endif + +void sokoHistoryTurnOver(soko_abs_t* soko); +void sokoAddTileMoveToHistory(soko_abs_t* soko, uint16_t tileX, uint16_t tileY, sokoTile_t oldTileType); +void sokoAddEntityMoveToHistory(soko_abs_t* soko, sokoEntity_t* entity, uint16_t oldX, uint16_t oldY, + sokoDirection_t oldFacing); +void sokoUndo(soko_abs_t* soko); +void sokoInitHistory(soko_abs_t* soko); \ No newline at end of file diff --git a/main/modes/music/jukebox/jukebox.c b/main/modes/music/jukebox/jukebox.c index 4288dbe0c..6f8957037 100644 --- a/main/modes/music/jukebox/jukebox.c +++ b/main/modes/music/jukebox/jukebox.c @@ -32,7 +32,6 @@ #include "mainMenu.h" #include "modeTimer.h" #include "mode_credits.h" -#include "mode_pinball.h" #include "touchTest.h" #include "tunernome.h" #include "midiPlayer.h" diff --git a/main/modes/music/usbsynth/mode_synth.c b/main/modes/music/usbsynth/mode_synth.c index da238c3d0..5bdb34af9 100644 --- a/main/modes/music/usbsynth/mode_synth.c +++ b/main/modes/music/usbsynth/mode_synth.c @@ -18,6 +18,8 @@ #include "cnfs_image.h" #include "ctype.h" +#include + #include "midiPlayer.h" #include "midiFileParser.h" #include "midiUsb.h" @@ -283,6 +285,7 @@ typedef struct lfsrState_t shuffleState; int32_t shufflePos; int32_t headroom; + bool gmMode; wsg_t instrumentImages[16]; wsg_t percussionImage; @@ -1038,6 +1041,7 @@ static const char* menuItemIgnore = "Enabled: "; static const char* menuItemBank = "Bank Select: "; static const char* menuItemInstrument = "Instrument: "; static const char* menuItemResetAll = "Reset All Channels"; +static const char* menuItemGm = "General MIDI: "; static const char* menuItemReset = "Reset"; static const char* menuItemControls = "Controllers"; @@ -1057,6 +1061,7 @@ static const char* const nvsKeyIgnoreChan = "synth_ignorech"; static const char* const nvsKeyChanPerc = "synth_chpercus"; static const char* const nvsKeySynthConf = "synth_confblob"; static const char* const nvsKeySynthControlConf = "synth_ctrlconf"; +static const char* const nvsKeyGmEnabled = "synth_gmmode"; static const char* const menuItemModeOptions[] = { "Streaming", @@ -1190,6 +1195,11 @@ static const int32_t menuItemChannelsValues[] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, }; +static const int32_t menuItemGmValues[] = { + 0, + 1, +}; + static settingParam_t menuItemModeBounds = { .def = 0, .min = 0, @@ -1288,6 +1298,13 @@ static settingParam_t menuItemChannelsBounds = { .key = NULL, }; +static settingParam_t menuItemGmBounds = { + .def = 0, + .min = 0, + .max = 1, + .key = nvsKeyGmEnabled, +}; + static const synthConfig_t defaultSynthConfig = { .ignoreChannelMask = 0, .percChannelMask = 0x0200, // Channel 10 set only @@ -1306,6 +1323,24 @@ static const synthConfig_t defaultSynthConfig = { .controlCounts = 0, }; +static const synthConfig_t nonGmSynthConfig = { + .ignoreChannelMask = 0, + .percChannelMask = 0x0600, // Channel 10 and 11 set + .programs = { + 0, 1, 2, 3, + 4, 5, 6, 7, + 8, 0, 0, 0, + 0, 0, 0, 0, + }, + .banks = { + 1, 1, 1, 1, + 1, 1, 1, 1, + 1, 0, 1, 0, + 0, 0, 0, 0, + }, + .controlCounts = 0, +}; + const char synthModeName[] = "MIDI Player"; static const char intermissionMsg[] = "SWADGAOKE!"; @@ -1351,7 +1386,7 @@ static synthData_t* sd; static void synthEnterMode(void) { - sd = calloc(1, sizeof(synthData_t)); + sd = heap_caps_calloc(1, sizeof(synthData_t), MALLOC_CAP_SPIRAM); loadFont("ibm_vga8.font", &sd->font, true); loadFont("sonic.font", &sd->betterFont, true); makeOutlineFont(&sd->betterFont, &sd->betterOutline, true); @@ -1428,6 +1463,12 @@ static void synthEnterMode(void) sd->headroom = nvsRead; sd->midiPlayer.headroom = sd->headroom; + if (!readNvs32(nvsKeyGmEnabled, &nvsRead)) + { + nvsRead = 0; + } + sd->gmMode = nvsRead ? true : false; + bool useDefaultConfig = true; size_t configBlobLen; if (readNvsBlob(nvsKeySynthConf, NULL, &configBlobLen)) @@ -1473,7 +1514,8 @@ static void synthEnterMode(void) { for (int blobIdx = 0; blobIdx < sd->synthConfig.controlCounts; blobIdx++) { - synthControlConfig_t* copy = (synthControlConfig_t*)malloc(sizeof(synthControlConfig_t)); + synthControlConfig_t* copy + = (synthControlConfig_t*)heap_caps_malloc(sizeof(synthControlConfig_t), MALLOC_CAP_SPIRAM); if (copy) { memcpy(copy, &configs[blobIdx], sizeof(synthControlConfig_t)); @@ -1490,7 +1532,7 @@ static void synthEnterMode(void) size_t savedNameLen; if (readNvsBlob(nvsKeyLastSong, NULL, &savedNameLen)) { - sd->filenameBuf = malloc(savedNameLen < 128 ? 128 : savedNameLen + 1); + sd->filenameBuf = heap_caps_malloc(savedNameLen < 128 ? 128 : savedNameLen + 1, MALLOC_CAP_SPIRAM); if (readNvsBlob(nvsKeyLastSong, sd->filenameBuf, &savedNameLen)) { @@ -1986,6 +2028,15 @@ static void synthApplyConfig(void) { sd->midiPlayer.headroom = sd->headroom; + if (sd->gmMode) + { + midiGmOn(&sd->midiPlayer); + } + else + { + midiGmOff(&sd->midiPlayer); + } + for (int i = 0; i < 16; i++) { uint16_t channelBit = (1 << i); @@ -2044,7 +2095,7 @@ static void synthSaveControl(uint8_t channel, uint8_t control, uint8_t value) } // Not found, add one - synthControlConfig_t* conf = calloc(1, sizeof(synthControlConfig_t)); + synthControlConfig_t* conf = heap_caps_calloc(1, sizeof(synthControlConfig_t), MALLOC_CAP_SPIRAM); if (conf) { conf->control = control; @@ -2351,6 +2402,11 @@ static void addChannelsMenu(menu_t* menu, const synthConfig_t* config) addSingleItemToMenu(menu, menuItemResetAll); wheelMenuSetItemInfo(sd->wheelMenu, menuItemResetAll, &sd->resetImage, rotTopMenu++, NO_SCROLL); + + addSettingsOptionsItemToMenu(menu, menuItemGm, menuItemOffOnOptions, menuItemGmValues, ARRAY_SIZE(menuItemGmValues), + &menuItemGmBounds, sd->gmMode); + wheelMenuSetItemInfo(sd->wheelMenu, menuItemGm, NULL, rotTopMenu++, SCROLL_HORIZ_R); + wheelMenuSetItemTextIcon(sd->wheelMenu, menuItemGm, "GM"); } static void synthSetupMenu(bool forceReset) @@ -2521,7 +2577,7 @@ static void preloadLyrics(karaokeInfo_t* karInfo, const midiFile_t* midiFile) // TODO we could save a couple bytes if we parsed the file an additional time to check how many events // there are in total... - midiTextInfo_t* info = (midiTextInfo_t*)malloc(sizeof(midiTextInfo_t)); + midiTextInfo_t* info = (midiTextInfo_t*)heap_caps_malloc(sizeof(midiTextInfo_t), MALLOC_CAP_SPIRAM); if (info) { @@ -3956,7 +4012,7 @@ static void midiTextCallback(metaEventType_t type, const char* text, uint32_t le return; } - midiTextInfo_t* info = (midiTextInfo_t*)malloc(sizeof(midiTextInfo_t)); + midiTextInfo_t* info = (midiTextInfo_t*)heap_caps_malloc(sizeof(midiTextInfo_t), MALLOC_CAP_SPIRAM); if (info) { @@ -4124,7 +4180,7 @@ static void synthMenuCb(const char* label, bool selected, uint32_t value) sd->screen = SS_FILE_SELECT; if (!sd->filenameBuf) { - sd->filenameBuf = calloc(1, 128); + sd->filenameBuf = heap_caps_calloc(1, 128, MALLOC_CAP_SPIRAM); } if (!sd->filenameBuf) @@ -4233,6 +4289,24 @@ static void synthMenuCb(const char* label, bool selected, uint32_t value) sd->menuSelectedChannel = value; sd->updateMenu = true; } + else if (label == menuItemGm) + { + if (value != sd->gmMode) + { + // Also reset all channels + memcpy(&sd->synthConfig, (value ? &defaultSynthConfig : &nonGmSynthConfig), sizeof(synthConfig_t)); + synthControlConfig_t* control; + while (NULL != (control = pop(&sd->controllerSettings))) + { + free(control); + } + + sd->gmMode = value; + writeNvs32(nvsKeyGmEnabled, value); + synthApplyConfig(); + sd->updateMenu = true; + } + } else { // Mapped menu items diff --git a/main/modes/system/mainMenu/mainMenu.c b/main/modes/system/mainMenu/mainMenu.c index 83ae4eb0a..177f96546 100644 --- a/main/modes/system/mainMenu/mainMenu.c +++ b/main/modes/system/mainMenu/mainMenu.c @@ -16,13 +16,15 @@ #include "mainMenu.h" #include "modeTimer.h" #include "mode_credits.h" -#include "mode_pinball.h" #include "mode_bigbug.h" #include "mode_synth.h" #include "ultimateTTT.h" +#include "pango.h" +#include "soko.h" #include "touchTest.h" #include "tunernome.h" #include "keebTest.h" +#include "mode_2048.h" #include "settingsManager.h" @@ -151,8 +153,10 @@ static void mainMenuEnterMode(void) // Add single items mainMenu->menu = startSubMenu(mainMenu->menu, "Games"); addSingleItemToMenu(mainMenu->menu, tttMode.modeName); - addSingleItemToMenu(mainMenu->menu, pinballMode.modeName); + addSingleItemToMenu(mainMenu->menu, pangoMode.modeName); + addSingleItemToMenu(mainMenu->menu, t48Mode.modeName); addSingleItemToMenu(mainMenu->menu, bigbugMode.modeName); + addSingleItemToMenu(mainMenu->menu, sokoMode.modeName); mainMenu->menu = endSubMenu(mainMenu->menu); mainMenu->menu = startSubMenu(mainMenu->menu, "Music"); @@ -358,14 +362,18 @@ static void mainMenuCb(const char* label, bool selected, uint32_t settingVal) { switchToSwadgeMode(&bigbugMode); } - else if (label == pinballMode.modeName) + else if (label == sokoMode.modeName) { - switchToSwadgeMode(&pinballMode); + switchToSwadgeMode(&sokoMode); } else if (label == tttMode.modeName) { switchToSwadgeMode(&tttMode); } + else if (label == pangoMode.modeName) + { + switchToSwadgeMode(&pangoMode); + } else if (label == timerMode.modeName) { switchToSwadgeMode(&timerMode); @@ -382,6 +390,10 @@ static void mainMenuCb(const char* label, bool selected, uint32_t settingVal) { switchToSwadgeMode(&tunernomeMode); } + else if (label == t48Mode.modeName) + { + switchToSwadgeMode(&t48Mode); + } else if (label == factoryResetName) { if (!mainMenu->resetConfirmShown) diff --git a/main/swadge2024.c b/main/swadge2024.c index 84b88d8a0..3a3603bad 100644 --- a/main/swadge2024.c +++ b/main/swadge2024.c @@ -27,6 +27,9 @@ * If you just want to run the Swadge emulator without setting up a development environment, see the \ref emulator * for an installation guide and usage instructions. * + * If you want to learn about creating MIDI song files for the Swadge, see the \ref MIDI guide. See also the + * \ref emulator which you can use to listen to MIDI files. + * * If you're just starting Swadge development, you're already at the right place to start! Here's a good sequence of * pages to read from here. * diff --git a/main/utils/fl_math/vectorFl2d.c b/main/utils/fl_math/vectorFl2d.c index 10aba55c0..bc5a0b938 100644 --- a/main/utils/fl_math/vectorFl2d.c +++ b/main/utils/fl_math/vectorFl2d.c @@ -134,4 +134,19 @@ vecFl_t normVecFl2d(vecFl_t in) .y = in.y / len, }; return norm; -} \ No newline at end of file +} + +/** + * @brief Return a vector perpendicular to the input + * + * @param in The input vector + * @return The perpendicular vector + */ +vecFl_t perpendicularVecFl2d(vecFl_t in) +{ + vecFl_t perp = { + .x = -in.y, + .y = in.x, + }; + return perp; +} diff --git a/main/utils/fl_math/vectorFl2d.h b/main/utils/fl_math/vectorFl2d.h index b4cc03248..fe75eced2 100644 --- a/main/utils/fl_math/vectorFl2d.h +++ b/main/utils/fl_math/vectorFl2d.h @@ -49,5 +49,6 @@ vecFl_t rotateVecFl2d(vecFl_t vector, float radians); float magVecFl2d(vecFl_t vector); float sqMagVecFl2d(vecFl_t vector); vecFl_t normVecFl2d(vecFl_t in); +vecFl_t perpendicularVecFl2d(vecFl_t in); #endif \ No newline at end of file diff --git a/main/utils/macros.h b/main/utils/macros.h index 34793c526..23026f2a5 100644 --- a/main/utils/macros.h +++ b/main/utils/macros.h @@ -47,9 +47,9 @@ */ #ifdef __APPLE__ /* Force a compilation error if condition is true, but also produce a - result (of value 0 and type size_t), so the expression can be used - e.g. in a structure initializer (or where-ever else comma expressions - aren't permitted). */ + result (of value 0 and type size_t), so the expression can be used + e.g. in a structure initializer (or where-ever else comma expressions + aren't permitted). */ #define BUILD_BUG_ON_ZERO(e) (sizeof(struct { int : -!!(e); })) /// Helper macro to determine the number of elements in an array. Should not be used directly @@ -79,4 +79,25 @@ */ #define POS_MODULO_ADD(a, b, d) ((a + (b % d) + d) % d) -#endif \ No newline at end of file +/** + * @brief Run timer_code every period, using tracking it with timer + * + * @param timer The accumulator variable, must persist between calls + * @param period The period at which timer_code should be run + * @param elapsed The time elapsed since this was last called + * @param timer_code The code to execute every period + */ +#define RUN_TIMER_EVERY(timer, period, elapsed, timer_code) \ + do \ + { \ + timer += elapsed; \ + while (timer > period) \ + { \ + timer -= period; \ + { \ + timer_code \ + } \ + } \ + } while (0) + +#endif diff --git a/makefile b/makefile index 0ba928d2f..96f04b0be 100644 --- a/makefile +++ b/makefile @@ -66,6 +66,7 @@ SRC_FILES = $(CNFS_FILE) SRC_DIRS = $(shell $(FIND) $(SRC_DIRS_RECURSIVE) -type d) $(SRC_DIRS_FLAT) # This is all the source files combined and deduplicated SOURCES = $(sort $(shell $(FIND) $(SRC_DIRS) -maxdepth 1 -iname "*.[c]") $(SRC_FILES)) +SOURCES := $(filter-out main/utils/cnfs.c, $(SOURCES)) # The emulator doesn't build components, but there is a target for formatting them ALL_FILES = $(shell $(FIND) components $(SRC_DIRS_RECURSIVE) -iname "*.[c|h]") @@ -321,7 +322,7 @@ assets: ./tools/assets_preprocessor/assets_preprocessor -i ./assets/ -o ./assets_image/ # To build the main file, you have to compile the objects -$(EXECUTABLE): $(OBJECTS) +$(EXECUTABLE): $(CNFS_FILE) $(OBJECTS) $(CC) $(OBJECTS) $(LIBRARY_FLAGS) -o $@ # This compiles each c file into an o file @@ -331,10 +332,16 @@ $(EXECUTABLE): $(OBJECTS) # To create the c file with assets, run these tools $(CNFS_FILE): +# Sokoban .tmx to bin preprocessor + python ./tools/soko/soko_tmx_preprocessor.py ./assets/soko/ ./assets_image/ + $(MAKE) -C ./tools/assets_preprocessor/ ./tools/assets_preprocessor/assets_preprocessor -i ./assets/ -o ./assets_image/ $(MAKE) -C ./tools/cnfs/ ./tools/cnfs/cnfs_gen assets_image/ main/utils/cnfs_image.c main/utils/cnfs_image.h + + + bundle: SwadgeEmulator.app diff --git a/tools/pango_editor/mockup.bin b/tools/pango_editor/mockup.bin new file mode 100644 index 0000000000000000000000000000000000000000..b5519a2b6c80ef11660094c55880c696256c3797 GIT binary patch literal 274 zcmZvX+YW#r3`6S`#*2yn|FN#riHkNNETcOe=P}J$3n~EfL-Afw&Ac+I-`;sSZV%tl r + + + + +2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,4,0, +9,0,0,0,10,0,0,0,0,0,0,0,10,0,0,0,5,0, +9,10,10,0,10,0,10,10,10,10,10,0,10,0,10,10,5,0, +9,0,10,0,0,0,0,0,0,0,10,0,10,0,0,0,5,0, +9,0,10,10,10,0,11,10,10,0,10,0,10,0,10,10,5,0, +9,0,0,0,0,0,0,0,10,0,0,0,0,0,10,0,5,0, +9,10,10,10,10,10,10,0,10,0,10,10,10,10,10,0,5,0, +9,0,10,0,0,0,10,0,0,0,0,0,11,0,0,0,5,0, +9,0,10,0,10,0,10,10,10,10,10,0,10,0,10,10,5,0, +9,0,10,0,10,0,10,0,0,0,0,0,0,0,10,0,5,0, +9,0,11,10,10,0,10,0,10,0,10,10,10,10,10,0,5,0, +9,0,10,0,0,0,10,0,10,0,10,0,10,0,0,0,5,0, +9,0,10,0,10,10,10,0,10,0,10,0,10,0,10,10,5,0, +9,0,0,0,0,0,0,0,10,0,0,0,0,0,0,0,5,0, +7,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,6,0 + + + diff --git a/tools/pango_editor/pa-tileset.png b/tools/pango_editor/pa-tileset.png new file mode 100644 index 0000000000000000000000000000000000000000..a836e3057b6d9131ddcdd9fb2956c414f9365557 GIT binary patch literal 1209 zcmds#*-OEgh-%>V~h0QdPyTLq9!GoOEuI((I&LeMM_Muh)S}F zOv#}1LF}Qx2qY1TnnEIjx&)CA78WgrLPFE0`7;{OA2a8i@A)pby*#`*5T6_`B7w5f zk_x`#{4I@(<$w9*n4f%Nj#g|glEIfLpG6jQly9vH@u)sov`r3&YS}MJ^lQrn4GGt9p_%>XCB^GP@gop`qtv2Uz2Hvw$z2Yrq@QF zd~J%O`}Uo=NLxZ&ATE$`^X`MQ zeZ4ofjWt(PjjbpMC8oO8lPYy|s7+cLf_q1YGWHZ7ShPRySYq?l;;E-KiQQ={uNUt4 z-4JZN6xrSq_i9PUpWd_$;}i2Er(91YYh?ae=CbaBUHS3B&z literal 0 HcmV?d00001 diff --git a/tools/pango_editor/pa-tileset.tsx b/tools/pango_editor/pa-tileset.tsx new file mode 100644 index 000000000..75765c532 --- /dev/null +++ b/tools/pango_editor/pa-tileset.tsx @@ -0,0 +1,4 @@ + + + + diff --git a/tools/pango_editor/pango-editor.js b/tools/pango_editor/pango-editor.js new file mode 100644 index 000000000..ffe438efd --- /dev/null +++ b/tools/pango_editor/pango-editor.js @@ -0,0 +1,111 @@ +tiled.registerMapFormat("Pango", { + name: "Pango map format", + extension: "bin", + read: (fileName) => { + var file = new BinaryFile(fileName, BinaryFile.ReadOnly); + var filePath = FileInfo.path(fileName); + var buffer = file.read(file.size); + var view = new Uint8Array(buffer); + const tileDataOffset = 2; + const tileSizeInPixels = 16; + + var map = new TileMap(); + + //The first two bytes contain the width and height of the tilemap in tiles + var tileDataLength = view[0] * view[1]; + map.setSize(view[0], view[1]); + map.setTileSize(tileSizeInPixels, tileSizeInPixels); + + var tileset = tiled.open(filePath + '/pa-tileset.tsx'); + + var layer = new TileLayer(); + + map.addTileset(tileset); + + layer.width = map.width; + layer.height = map.height; + layer.name = 'Main'; + + var layerEdit = layer.edit(); + var importTileX = 0; + var importTileY = 0; + + + //Import tile data + for(let i = 0; i < tileDataLength; i++){ + let tileId = view[i + tileDataOffset]; + layerEdit.setTile(importTileX, importTileY, tileset.tile(tileId)); + + importTileX++; + if(importTileX >= map.width){ + importTileY++; + importTileX=0; + } + } + + layerEdit.apply(); + + map.addLayer(layer); + file.close(); + return map; + }, + write: (map, fileName) => { + for (let i = 0; i < map.layerCount; ++i) { + const layer = map.layerAt(i); + + if (!layer.isTileLayer) { + continue; + } + + let file = new BinaryFile(fileName, BinaryFile.WriteOnly); + let buffer = new ArrayBuffer(2 + layer.width * layer.height + 2); //Buffer sized to lenth byte + width byte + length * width of level bytes + 2 total target tile bytes + let view = new Uint8Array(buffer); + + //The first two bytes contain the width and height of the tilemap in tiles + view[0]=layer.width; + view[1]=layer.height; + let writePosition = 2; + let totalTargetBlockTiles = 0; + + for (let y = 0; y < layer.height; ++y) { + const row = []; + + for (let x = 0; x < layer.width; ++x) { + const tile = layer.tileAt(x, y); + if(!tile){ + //file.write(0); + view[writePosition] = 0; + writePosition++; + continue; + } + + const tileId = tile.id; + + //Handle "target block tiles" + //These are the blocks that the player must break to complete the level. + //if(tileId >= 16 && tileId <= 127) { + // totalTargetBlockTiles++; + //} + + //Handling every tile + view[writePosition]=tileId; + + writePosition++; + } + } + + //The last 2 bytes hold the total number of "target block tiles" + //Forced into a (hopefully) unsigned 16 bit integer, little endian + //There's probably a better way to do this... + //let totalTargetBlockTilesLowerByte = totalTargetBlockTiles & 255; + //let totalTargetBlockTilesUpperByte = (totalTargetBlockTiles >> 8) & 255; + //view[writePosition] = totalTargetBlockTilesLowerByte; + //writePosition++; + //view[writePosition] = totalTargetBlockTilesUpperByte; + //writePosition++; + + file.write(buffer); + file.commit(); + } + }, +}); \ No newline at end of file diff --git a/tools/pango_editor/pango-tiles.tsx b/tools/pango_editor/pango-tiles.tsx new file mode 100644 index 000000000..ed777d450 --- /dev/null +++ b/tools/pango_editor/pango-tiles.tsx @@ -0,0 +1,4 @@ + + + + diff --git a/tools/pango_editor/pango.tiled-project b/tools/pango_editor/pango.tiled-project new file mode 100644 index 000000000..d58954aef --- /dev/null +++ b/tools/pango_editor/pango.tiled-project @@ -0,0 +1,11 @@ +{ + "automappingRulesFile": "", + "commands": [ + ], + "extensionsPath": "extensions", + "folders": [ + "." + ], + "propertyTypes": [ + ] +} diff --git a/tools/pango_editor/pango.tiled-session b/tools/pango_editor/pango.tiled-session new file mode 100644 index 000000000..eca5100ff --- /dev/null +++ b/tools/pango_editor/pango.tiled-session @@ -0,0 +1,65 @@ +{ + "Map/SizeTest": { + "height": 4300, + "width": 2 + }, + "activeFile": "mockup.tmx", + "expandedProjectPaths": [ + ], + "file.lastUsedOpenFilter": "Pango map format (*.bin)", + "fileStates": { + "mockup.bin": { + "scale": 2.3914930555555554, + "selectedLayer": 0, + "viewCenter": { + "x": 144.47041742286754, + "y": 120.63593466424685 + } + }, + "mockup.tmx": { + "scale": 2.39, + "selectedLayer": 0, + "viewCenter": { + "x": 144.14225941422592, + "y": 120.2928870292887 + } + }, + "pa-tileset.tsx": { + "scaleInDock": 1, + "scaleInEditor": 1 + }, + "preset.tmx": { + "scale": 2.39, + "selectedLayer": 0, + "viewCenter": { + "x": 144.14225941422592, + "y": 120.2928870292887 + } + } + }, + "last.exportedFilePath": "/Users/jvega/Development/Swadge/esp32/2025/Swadge-IDF-5.0/tools/pango_editor", + "last.imagePath": "/Users/jvega/Development/Swadge/esp32/2025/Swadge-IDF-5.0/tools/pango_editor", + "map.height": 15, + "map.lastUsedExportFilter": "Pango map format (*.bin)", + "map.lastUsedFormat": "tmx", + "map.tileHeight": 16, + "map.tileWidth": 16, + "map.width": 18, + "openFiles": [ + "pa-tileset.tsx", + "mockup.tmx", + "preset.tmx" + ], + "project": "pango.tiled-project", + "recentFiles": [ + "pa-tileset.tsx", + "preset.tmx", + "mockup.tmx", + "mockup.bin" + ], + "tileset.lastUsedFormat": "tsx", + "tileset.tileSize": { + "height": 16, + "width": 16 + } +} diff --git a/tools/pango_editor/preset.tmx b/tools/pango_editor/preset.tmx new file mode 100644 index 000000000..9933afcd7 --- /dev/null +++ b/tools/pango_editor/preset.tmx @@ -0,0 +1,23 @@ + + + + + +2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,4,0, +9,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,5,0, +9,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,5,0, +9,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,5,0, +9,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,5,0, +9,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,5,0, +9,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,5,0, +9,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,5,0, +9,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,5,0, +9,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,5,0, +9,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,5,0, +9,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,5,0, +9,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,5,0, +9,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,5,0, +7,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,6,0 + + + diff --git a/tools/sandbox_test/usbhid_test/sandbox.c b/tools/sandbox_test/usbhid_test/sandbox.c index cdc6bb66d..b7f177c00 100644 --- a/tools/sandbox_test/usbhid_test/sandbox.c +++ b/tools/sandbox_test/usbhid_test/sandbox.c @@ -124,7 +124,7 @@ int16_t sandboxAdvancedUSB(uint8_t* buffer, uint16_t length, uint8_t isGet) buffer[1] = 0x55; buffer[2] = 0xaa; buffer[3] = 0x55; - return 64; + return 254; } else { diff --git a/tools/sandbox_test/usbhid_test/test/Makefile b/tools/sandbox_test/usbhid_test/test/Makefile index ac4a4fb25..448ff94cf 100644 --- a/tools/sandbox_test/usbhid_test/test/Makefile +++ b/tools/sandbox_test/usbhid_test/test/Makefile @@ -1,7 +1,17 @@ all : hidtest +ifeq ($(shell uname), Linux) +CFLAGS:=-g -O0 +LDFLAGS:=-ludev +CC:=gcc +else +CFLAGS:=-Os -s +CC:=gcc +LDFLAGS:=C:/windows/system32/setupapi.dll +endif + hidtest : hidtest.c - gcc -o $@ $^ -ludev + $(CC) -o $@ $^ $(CFLAGS) $(LDFLAGS) clean : rm -rf hidtest diff --git a/tools/sandbox_test/usbhid_test/test/hidtest.c b/tools/sandbox_test/usbhid_test/test/hidtest.c index 37e23b9a4..0b29082cf 100644 --- a/tools/sandbox_test/usbhid_test/test/hidtest.c +++ b/tools/sandbox_test/usbhid_test/test/hidtest.c @@ -1,6 +1,5 @@ #include #include -#include #include #include @@ -12,9 +11,12 @@ #define PID 0x4269 #ifdef WIN32 -const int reg_packet_length = 65; +const int reg_packet_length = 255; +const int reg_back_rest = 256; #else -const int reg_packet_length = 64; +#include +const int reg_packet_length = 254; +const int reg_back_rest = 254; #endif hid_device * hd; @@ -28,7 +30,7 @@ int main( int argc, char ** argv ) if( !hd ) { fprintf( stderr, "Could not open USB\n" ); return -94; } // Disable tick. - uint8_t rdata[65] = { 0 }; + uint8_t rdata[256] = { 0 }; rdata[0] = 173; r = hid_get_feature_report( hd, rdata, reg_packet_length ); printf( "Got data: %d bytes\n", r ); @@ -43,13 +45,14 @@ int main( int argc, char ** argv ) for( i = 0; i < 1024; i++ ) { r = hid_get_feature_report( hd, rdata, reg_packet_length ); - if( r != reg_packet_length ) + rdata[0] = 173; + if( r != reg_back_rest ) { fprintf( stderr, "Error reading message (%d)\n", r ); } } double dEnd = OGGetAbsoluteTime(); - printf( "Reads: %5.0f/sec / %3.2f kB/s\n", 1024.0/(dEnd - dStart), (63)/(dEnd - dStart)); + printf( "Reads: %5.0f/sec / %3.2f kB/s\n", 1024.0/(dEnd - dStart), (reg_packet_length)/(dEnd - dStart)); dStart = OGGetAbsoluteTime(); for( i = 0; i < 1024; i++ ) @@ -59,11 +62,11 @@ int main( int argc, char ** argv ) r = hid_send_feature_report( hd, rdata, reg_packet_length ); if( r != reg_packet_length ) { - fprintf( stderr, "Error reading message (%d)\n", r ); + fprintf( stderr, "Error writing message (%d)\n", r ); } } dEnd = OGGetAbsoluteTime(); - printf( "Writes: %5.0f/sec / %3.2f kB/s\n", 1024.0/(dEnd - dStart), (63)/(dEnd - dStart) ); + printf( "Writes: %5.0f/sec / %3.2f kB/s\n", 1024.0/(dEnd - dStart), (reg_packet_length)/(dEnd - dStart) ); rdata[0] = 173; rdata[1] = 0x00; @@ -78,7 +81,7 @@ int main( int argc, char ** argv ) int f; for( f = 0; f < 10; f++ ) { - for( y = 0; y < 240; y++ ) + for( y = 0; y < 240; y+=10 ) { for( x = 0; x < 280; x += 56 ) { diff --git a/tools/sandbox_test/usbhid_test/test/winbuild.bat b/tools/sandbox_test/usbhid_test/test/winbuild.bat new file mode 100644 index 000000000..0011c1a9c --- /dev/null +++ b/tools/sandbox_test/usbhid_test/test/winbuild.bat @@ -0,0 +1 @@ +tcc hidtest.c C:/windows/system32/setupapi.dll \ No newline at end of file diff --git a/tools/soko/plugin/sokoban_tiled_importer.js b/tools/soko/plugin/sokoban_tiled_importer.js new file mode 100644 index 000000000..c9aece315 --- /dev/null +++ b/tools/soko/plugin/sokoban_tiled_importer.js @@ -0,0 +1,21 @@ +//import "classic" sokoban text levels via tiled. +//http://www.sokobano.de/wiki/index.php?title=Level_format +//these levels are plaintext, and use the following scheme: +// # for walls +// @ for the player. + for player on goal +// . for the goal +// $ for a box, * for box on goal +// space for floor. + +//while our game treats empty and wall the same, proper sokoban levels must be enclosed by a wall + +// local d = Dialog("Paste Text as level") +// :label{id="lab1",label="",text="Import tiles."} +// :text{id="text1"} +// :label{id="lab3", label="",text="Max supported tilemap size: 255x255"} +// :separator{} +// :button{id="ok",text="&OK",focus=true} +// :button{text="&Cancel" } +// :show() + +tiled.register \ No newline at end of file diff --git a/tools/soko/plugin/sokobon_binary_conversion_script.lua b/tools/soko/plugin/sokobon_binary_conversion_script.lua new file mode 100644 index 000000000..b6e69191a --- /dev/null +++ b/tools/soko/plugin/sokobon_binary_conversion_script.lua @@ -0,0 +1,91 @@ +-- Script to export tilemap data as a binary file. +-- Original script by Zeltrix (https://pastebin.com/mQGiKAgR) +-- Export to binary by JVeg199X +-- Note: This script only works with tilemaps of 255x255 tiles or less + +-- DO NOT USE. WIP for original binary +-- Check .asp file and .config file + +if TilesetMode == nil then return app.alert "Use Aseprite 1.3" end +local spr = app.activeSprite + +if not spr then return end + +-- TODO Add Multi-File Selection for multiple levels and config files + +local d = Dialog("Export Tilemap as .bin File") +d:label{id="lab1",label="",text="Export Tilemap as .bin File for your own GameEngine"} + :file{id = "path", label="Export Path", filename="",open=false,filetypes={"bin"}, save=true, focus=true} + :label{id="lab3", label="",text="Max supported tilemap size: 255x255"} + :separator{} + :label{id="lab2", label="",text="In the last row of the tilemap-layer there has to be at least one Tile \"colored\" to fully export the whole Tilemap"} + :button{id="ok",text="&OK",focus=true} + :button{text="&Cancel" } + :show() + + + +--Initialize warp data array +local warps = {} +for i=0, 15 do + warps[i] = {} + warps[i][0] = 0; + warps[i][1] = 0; +end + +local data = d.data +if not data.ok then return end + local lay = app.activeLayer + if(#data.path<=0)then app.alert("No path selected") end + if not lay.isTilemap then return app.alert("Layer is not tilemap") end + pc = app.pixelColor + mapFile = io.open(data.path,"w") + + for _,c in ipairs(lay.cels) do + local img = c.image + + --The first two bytes contain the width and height of the tilemap in tiles + mapFile:write(string.char(img.width)) + mapFile:write(string.char(img.height)) + + --The next section of bytes is the tilemap itself + for p in img:pixels() do + if(p ~= nil) then + local tileId = p() + + --if(tileId == 130) then + -- local d2 = Dialog(tileId) + -- d2:show() + --end + + if(tileId > 0 and tileId < 17) then + --warp tiles + + tileBelowCurrentTile = img:getPixel(p.x, p.y+1) + if(tileBelowCurrentTile == 34 or tileBelowCurrentTile == 64 or tileBelowCurrentTile == 158) then + --if tile below warp tile is brick block or container or checkpoint, write it like normal + mapFile:write(string.char(tileId)) + else + --otherwise store it in warps array and don't write it into the file just yet + warps[tileId-1][0] = p.x + warps[tileId-1][1] = p.y + mapFile:write(string.char(0)) + end + + else + --every other tile + mapFile:write(string.char(tileId)) + end + + end + end + + --The last 32 bytes are warp x and y locations + for i=0, 15 do + mapFile:write(string.char(warps[i][0])) + mapFile:write(string.char(warps[i][1])) + end + end + + mapFile:close() + \ No newline at end of file diff --git a/tools/soko/soko_tmx_preprocessor.py b/tools/soko/soko_tmx_preprocessor.py new file mode 100644 index 000000000..19e45e293 --- /dev/null +++ b/tools/soko/soko_tmx_preprocessor.py @@ -0,0 +1,68 @@ +import sys +import os +from tmx_to_binary import convertTMX +count = 0 +total = 0 +raw_total = 0 +comp_total = 0 +def main(): + print("Starting soko tmx conversion") + + inputdir = sys.argv[1] + # check if output is real directory and create it if it does not exist. + outputdir = sys.argv[2] + if not os.path.exists(outputdir): + os.makedirs(outputdir) + + if not os.path.exists(outputdir): + print("oh no! input directory for soko tmx preprocessor doesn't exist!") + return + + + # todo: automatically check and move SK_LEVEL_LIST.txt, it doesn't update automatically. + + # todo: ensure output ends in a trailing slash. + convertDir(inputdir,outputdir) + print("Completed soko tmx converstion. "+str(count)+ "/"+str(total)+" tmx files converted. "+str(comp_total)+"/"+str(raw_total)+" - "+str(raw_total-comp_total)+" (of converted) bytes saved with compression.") + + +def convertDir(dir,output): + global count,total, raw_total, comp_total + # todo: check file modification dates. + # lol no + for file in os.scandir(dir): + if os.path.isfile(file): + name, ext = os.path.splitext(file) + if ext == '.tmx': + lastMod = os.path.getmtime(file) + fname = getNameFromPath(file) + out_file = output+fname+".bin" + if(os.path.isfile(out_file)): + lastOutMod = os.path.getmtime(out_file) + if(lastMod < lastOutMod): + #print("skipping "+fname) + total+=1 + continue + convertAndSave(file.path,output) + count+=1 + total+=1 + elif os.path.isdir(file): + convertDir(file,output) + +def convertAndSave(filepath,output): + global raw_total, comp_total + rawbytes, r,c = convertTMX(filepath) + raw_total += r + comp_total += c + fname = getNameFromPath(filepath) + outfile_file = output+fname+".bin" + with open(outfile_file,"wb") as binary_file: + binary_file.write(rawbytes) + +def getNameFromPath(p): + base = os.path.basename(p) + fp = base.split(".") + fname = fp[len(fp)-2] + return fname + +main() \ No newline at end of file diff --git a/tools/soko/templateTiledProject/README.md b/tools/soko/templateTiledProject/README.md new file mode 100644 index 000000000..291705d94 --- /dev/null +++ b/tools/soko/templateTiledProject/README.md @@ -0,0 +1,62 @@ +Open the project 'templateProject.tiled-project' using the most recent version of Tiled tilemap editor. + +## IF YOUR OBJECTS DO NOT SNAP TO THE CENTER OF THE GRID TILES, GO TO EDIT>PREFERENCE>FINE GRID DIVISIONS AND SET IT TO 2. + +### All tiles should go in the 'tiles' tilemap layer. Use the 'tilesheet' tileset to place walls, floors, and goals. +### All entities should go in the 'entities' object layer. Use the 'objLayers' tileset to place objects with baked-in data. + +### Level List File +The game uses an overworld for level selection. In order to designate the level to be loaded, an index number should be provided. Please prefix your level binary 'sk_' and end it with '.bin'. The former prevents filename collisions and the latter is mandatory to be properly copied into system memory. The 'SK_LEVEL_LIST.txt' file should be edited to include the desired index and name of your level. The level list file is formatted as such: +``` +1:sk_overworld.bin: +7:sk_test1.bin: +8:sk_test2.bin: +9:sk_test3.bin: +2:sk_warehouse.bin: +``` +## Entities: + +### Player: + Be sure to set the 'gamemode' property. + Valid values are: + SOKO_OVERWORLD, + SOKO_CLASSIC, + SOKO_EULER, + SOKO_LASERBOUNCE + +### Crate: + The 'sticky' property indicates whether the crate will stick to a player's sprite. + The 'trail' property indicates whether a crate will leave its own trail in a SOKO_EULER puzzle. + +### Button: + The 'playerPress' property indicates whether a player can depress the button. + The 'cratePress' property indicates whether a crate can depress the button. + The 'invertAction' property inverts the button's effects on all of its target blocks. For instance, all non-inverted Ghost Blocks targeted by the Button will start intangible. + The 'stayDownOnPress' property indicates whether the button will remain depressed after its first press after resets once players or crates are removed. + To target a ghostblock, find the Object ID of the target in Tiled and populate the target#id property with that ID (start at target1id and count up). + Be sure to set the 'numTargets' property to the number of targeted blocks. + +### Ghost Block: + Target a Ghost Block with a Button. + The 'playerMove' property indicates whether a player can move the ghost block like a crate while in its tangible state. + The 'inverted' property indicates whether a Ghost block will start intangible (unless the button targeting it is intangible). + +### Internal Warp and Internal Warp Exit: + The 'hp' property indicates how many times a Warp can be entered. + The 'allow_crates' property indicates whether an Internal Warp may pass crates to their destination. Note that the destination will be blocked by a Crate on its destination. + To target another internal warp, find the Object ID of the target in Tiled and populate the 'target_id' field with that ID. + Warps may only target Internal Warp and Internal Warp Exit blocks. Internal Warp Exits have no function in gameplay and serve only as destination markers for Internal Warps. To make a 2-Way Portal, have two Internal Warps target one another's IDs. + +### External Warps: + External warps are used in the overworld for level selection. When the player steps on an External Warp, the level pointed to by the associated index (See Level List File) will be loaded. When the player completes the loaded puzzle, they will automatically reload the overworld level they came from. + The 'manuallyIndexed' property, when true, indicates that the game should check the 'target_id' value to find the appropriate level index. When false, this property indicates that the game may use this warp to point to a level which is not already attached to another external warp. Automatically indexed external warps will be assigned the lowest unused level index from the Level List File. + +### Laser Emitter/Receiver: + Be sure to set the 'emitDirection' property. + Valid values are: + UP, + DOWN, + RIGHT, + LEFT + The 'playerMove' property indicates where the Laser Emitter can be pushed by players. + diff --git a/tools/soko/templateTiledProject/entitySprites/button16.png b/tools/soko/templateTiledProject/entitySprites/button16.png new file mode 100644 index 0000000000000000000000000000000000000000..2065777b120913be172e63fcada53749955c02a2 GIT binary patch literal 192 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|sytmBLo9l) zPB_hbK!L}ZH)(N+npN;j&NMH6jRg)zmrq|c&BEbPy3m8{ncozdY>Z@jm&Y9On0(|5 zOBciEXA>QwZ*G}z|GVVhzm^J96+cO=EAkX||0y};DaX9V1zoF^Dz<&zwlR)jmg+OX p2JXb{PW3%(94W3z?->4H;oZ8~ylV$DLkrLq44$rjF6*2UngE!lM*#o; literal 0 HcmV?d00001 diff --git a/tools/soko/templateTiledProject/entitySprites/crate16.png b/tools/soko/templateTiledProject/entitySprites/crate16.png new file mode 100644 index 0000000000000000000000000000000000000000..aac13c44399de403c4b930f79c2ece24b2fc5228 GIT binary patch literal 181 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|@;zM~Lo9li zPP)k3V8G$x|Jd#BO6!+CMw9C@1-Rri<9!cTTTY5Tw>N&qj8ZnXhPRa_LK8d~7?j-o zw!ho=Ls(?Z<)S8rXDn>n(w{ZSTZju3=qNngxPYPBQScu}$>E3_>Kca{R(_hD5)ipv dSaroY-r34Z%I?cFV}VvPc)I$ztaD0e0swuxJyZYy literal 0 HcmV?d00001 diff --git a/tools/soko/templateTiledProject/entitySprites/ghostblock16.png b/tools/soko/templateTiledProject/entitySprites/ghostblock16.png new file mode 100644 index 0000000000000000000000000000000000000000..cef0a40c90b1a009cfe909092a7458f3e31553e8 GIT binary patch literal 176 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|vOHZJLo9li zUOvftK!Jz#qN11kyUsmdKI;Vst0FB2z(f|Me literal 0 HcmV?d00001 diff --git a/tools/soko/templateTiledProject/entitySprites/laser90Right16.png b/tools/soko/templateTiledProject/entitySprites/laser90Right16.png new file mode 100644 index 0000000000000000000000000000000000000000..f620ba50097d4f24adc476a96eabb3c1a65a9214 GIT binary patch literal 252 zcmVPx#wn;=mR5*>DlR*;0AP4~0On$_l;7#}^-b4@4Bu#Z{%S8{YuquTXqMlNXl?ZjB zS^?Mr)IFs(PIgV8!t635CED9O!hsHhm(E$yXYJ~c^-=E;P z^Z;(qw%YaxhIWZD0A&IrSHAQEMhf35_ZTXy!efOAJkxwrm{q<%=~UsDwm%M--S#Yk zOKIFVdQ&MBb@076?rIsgCw literal 0 HcmV?d00001 diff --git a/tools/soko/templateTiledProject/entitySprites/laserReceiveOmni16.png b/tools/soko/templateTiledProject/entitySprites/laserReceiveOmni16.png new file mode 100644 index 0000000000000000000000000000000000000000..a12ad9b3279804c5678b55af43631665e6cba50f GIT binary patch literal 242 zcmVPx#tVu*cR5*>Llid-6FbsnQ*NwE3m?_!GGo=qCt>gT(*NO)Y(5E;y0X7m)1eBd; zFR4&z8*p^~)SJ!jJK00|x?bGrVVkUAu<+b3bo!61hLx;(yv}HfA*{#@o(A}kR{jbG zfI~nrrBLb(D%SvM=mkMAUwA}_^=fY;d=~Qeue=qN3Z&mp<2T3o2_wi#Y91PhoB#j-07*qoM6N<$f)>VOqW}N^ literal 0 HcmV?d00001 diff --git a/tools/soko/templateTiledProject/entitySprites/laserReceiveUp16.png b/tools/soko/templateTiledProject/entitySprites/laserReceiveUp16.png new file mode 100644 index 0000000000000000000000000000000000000000..1a7283a84755c130ebce1e73c762dcac520f6cff GIT binary patch literal 317 zcmV-D0mA-?P)Px#_en%SR5*=wliRVxAPhqfJGUeCWY-jWa!uS1UJlDTlT1i#NfxFEHAYAQKn)6j zK?A7tHGqj0p_N;N+Faz~o^4HtsJ_2VvO!$t%L1`yB}GhS@8asdh(DSA7C{_qu;*!= zKzv?*?Sbn3zJkFNGl@4qlAc4OBo{;XN{SBf)rLJpl7ANGD5kzup=91Dv^oIHzPu1Z z;kyNeuOyA@*3P=Zw}ObCEXDF7)}!k`;i`Kv(XJlMp0^_Swb{Fc!G0R{3uJotfj$to z;?su=GTYvBP#s+c=mwGp?e8q8?yr+K`dF;luLyMjDV8KttmB;%Zy^Q$6K~PLBx}xqW`)Kz7T=Z!-(^Uy3ocCPo4}^d ztZUP_(^t69a97#$P_N5cKc2kL;h$^%Xlmz1#^kLNAJ2GwoU1&cu}iZ3U{>*@rk%NA zKiq2!Z!mmepPYAndJe;bcbp8ij|`$x-sc^t?ic@Gd!a+(?HyZT3)%AXk^4&Y-vymB z?OgO~x~RmizApdd`;rWg&98QTQS;-y(bBQ~j{YpB#83H_(Px03VDNPHb6Mw<&;$S= C7iHxD literal 0 HcmV?d00001 diff --git a/tools/soko/templateTiledProject/entitySprites/warpexternal16.png b/tools/soko/templateTiledProject/entitySprites/warpexternal16.png new file mode 100644 index 0000000000000000000000000000000000000000..d622ba0f6523e5c263150d6aa23c32c49b7383d6 GIT binary patch literal 220 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|rg^$JhFJ72 z4Zh8L*np$ugtV-2rN()ev_i5=tK6|g}(eG*b51Cm%ShOF% SQ@$JMLIzJ)KbLh*2~7YET2;aT literal 0 HcmV?d00001 diff --git a/tools/soko/templateTiledProject/entitySprites/warpinternal16.png b/tools/soko/templateTiledProject/entitySprites/warpinternal16.png new file mode 100644 index 0000000000000000000000000000000000000000..3ac464870a64f295f4cec4c81b3ce3ac2b1405fb GIT binary patch literal 241 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|R(rZQhFJ7Y zoqSUCumX?EQ=4)V+dvW1NYk6_98$U#`c@}9e%|^}$eHKaRk9?V+(_VSC oJEf5G!-Tzi^Ai6nhkj(&TJCc*WWDfQptBh~UHx3vIVCg!0QzWJYybcN literal 0 HcmV?d00001 diff --git a/tools/soko/templateTiledProject/entitySprites/warpinternalexit16.png b/tools/soko/templateTiledProject/entitySprites/warpinternalexit16.png new file mode 100644 index 0000000000000000000000000000000000000000..b785f3666eabe4c6450a47d1bc047eb8577bd61c GIT binary patch literal 257 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|c6+)whFJ7Y zoqSUCumX?E@yeGORY8HTJWVG$Z@9p8j(gt?vCopaF-Igcx-a}XJ>Bur_t)|t_3B*N z%L7{78&;^Q^Q}l)?GNRt)6tPir(vIzXS^>t5pRr-NDIiD>LZ7pFuuu<6hhb{MQ z$1G^bjVpISC literal 0 HcmV?d00001 diff --git a/tools/soko/templateTiledProject/extensions/export-to-soko.js b/tools/soko/templateTiledProject/extensions/export-to-soko.js new file mode 100644 index 000000000..78a111d0f --- /dev/null +++ b/tools/soko/templateTiledProject/extensions/export-to-soko.js @@ -0,0 +1,391 @@ +var customMapFormat = { + name: "Swadge Sokobon Level Format", + extension: "bin", + write: + + function(p_map,p_fileName) { + + //Special Characters + var sokoSigs = + { + stackInPlace: 201, + compress: 202, + player: 203, + crate: 204, + warpinternal: 205, + warpinternalexit: 206, + warpexternal: 207, + button: 208, + laserEmitUp: 209, + laserReceiveOmni: 210, + laserReceiveUp: 211, + laser90Right: 212, + ghostblock: 213, + stackObjEnd: 230 + } + + var m = { + width: p_map.width, + height: p_map.height, + layers: [] + }; + + var sokoTileLayer, sokoObjectLayer; + var objArr = []; + //tiled.time("Export completed in"); + for (var i = 0; i < p_map.layerCount; ++i) + { + var layer = p_map.layerAt(i); + if(layer.isTileLayer) + { + sokoTileLayer = layer; + tiled.log("Layer " + i + " is Tile Layer"); + } + if(layer.isObjectLayer) + { + sokoObjectLayer = layer; + tiled.log("Layer " + i + " is Object Layer"); + } + } + sokoObjectLayer.objects.forEach( function(arrItem, ind) + { + tiled.log(ind); + + tiled.log(arrItem.tile.className); + var xval = Math.round(arrItem.x / arrItem.width); + var yval = Math.round(arrItem.y / arrItem.height - 1); + var posit = xval + yval * sokoTileLayer.width; + + tiled.log("(" + xval + "," + yval + ") Pos: " + posit + "(Width: " + sokoTileLayer.width + ")"); + var props = arrItem.resolvedProperties(); + tiled.log(JSON.stringify(props)) + tiled.log("-------------"); + var objItem = + { + obj: arrItem, + pos: posit, + x: xval, + y: yval, + index: ind + }; + objArr.push(objItem); + } + + + + ) + objArr.sort((a,b)=>(a.pos > b.pos) ? -1 : 1); //Sort by index in descending order so we can just split and insert stacked objects + objArr.forEach( function(arrItem) + { + tiled.log("Index:" + arrItem.index + ";Pos:" + arrItem.pos + ":(" + arrItem.x + ","+arrItem.y + ")"); + } + ) + for (var i = 0; i < p_map.layerCount; ++i) + { + var layer = p_map.layerAt(i); + + if(!layer.isTileLayer) + { + continue; + } + + data = []; + if(layer.isTileLayer) { + data.push(layer.width); + data.push(layer.height); + var rows = []; + for (y = 0; y < layer.height; ++y) { + var row = []; + for (x = 0; x < layer.width; ++x) + { + row.push(layer.cellAt(x,y).tileId); + data.push(layer.cellAt(x,y).tileId+1); + } + rows.push(row); + } + + //PackInObjects + if(1) + { + objArr.forEach(function(objItem, ind, objArr) + { + headerOffset = 2; + var objClassName = objItem.obj.tile.className; + tiled.log(sokoSigs[objClassName]); + var propertyVals = [111]; + propertyVals = propExtract(objItem, objArr); + insertionData = [sokoSigs.stackInPlace, sokoSigs[objClassName]].concat(propertyVals).concat([sokoSigs.stackObjEnd]); + tiled.log("DataBefore: " + data.slice(0,objItem.pos+headerOffset+1)); + tiled.log("InsertionData: " + insertionData); + tiled.log("DataAfter: " + data.slice(objItem.pos+headerOffset+1)); + data = data.slice(0,objItem.pos+headerOffset+1).concat(insertionData).concat(data.slice(objItem.pos+headerOffset+1)); + } + + ) + } + m.layers.push(rows); + tiled.log(m.layers); + //var file = new TextFile(fileName, TextFile.WriteOnly); + tiled.log("Export to " + p_fileName); + let view = Uint8Array.from(data); + let fileHand = new BinaryFile(p_fileName, BinaryFile.WriteOnly); + let buffer = view.buffer.slice(view.byteOffset, view.byteLength + view.byteOffset); + //let buffer = view.buffer; + tiled.log(view); + fileHand.write(buffer); + fileHand.commit(); + tiled.log(buffer); + } + } + + + } + + +} + +function findObjCoordById(objArr,id) +{ + var loopArgs = + { + id: id, + retVal: { + x: 0, + y: 0, + valid: false, + index: 0 + } + } + objArr.forEach( function(objEntry, ind, arr){ + //tiled.log("Target ID: " + this.id + " Entry ID: " + objEntry.obj.id + " Pos:(" + objEntry.x + "," + objEntry.y + ")"); + + if(this.id == objEntry.obj.id) + { + //tiled.log("MATCH!"); + this.retVal = { + x: objEntry.x, + y: objEntry.y, + valid: true, + index: ind + }; + } + + } , loopArgs + ) + return loopArgs.retVal; +} + +function propExtract(objItem, objArr) +{ + + soko_direction = + { + UP: 0, + DOWN: 1, + RIGHT: 2, + LEFT: 3 + }; + soko_player_gamemodes = + { + SOKO_OVERWORLD: 0, + SOKO_CLASSIC: 1, + SOKO_EULER: 2, + SOKO_LASERBOUNCE: 3 + }; + soko_crate_properties = + { + sticky: 0b1, + trail: 0b10 + }; + soko_warpinternal_properties = + { + allow_crates: 0b1 + }; + soko_warpexternal_properties = + { + manualIndex: 0b1 + }; + soko_laser90Right_properties = + { + emitDirection: 0b1, + playerMove: 0b10 + }; + soko_laserEmitUp_properties = + { + playerMove: 0b10 + }; + soko_button_properties = + { + cratePress: 0b1, + playerPress: 0b10, + invertAction: 0b100, + stayDownOnPress: 0b1000 + }; + soko_ghostblock_properties = + { + inverted: 0b100, + playerMove: 0b10 + }; + + var properties = objItem.obj.resolvedProperties(); + + retVal = []; + + switch(objItem.obj.tile.className) + { + case "player": + retVal.push(soko_player_gamemodes[properties.gamemode]); + break; + case "crate": + var variant = 0b0; + if(properties.sticky) + { + variant = variant | soko_crate_properties.sticky; + } + if(properties.trail) + { + variant = variant | soko_crate_properties.trail; + } + retVal.push(variant); + break; + case "laser90Right": + var variant = 0b0; + if(properties.emitDirection) + { + variant = variant | soko_laser90Right_properties.emitDirection; + //tiled.log("laser90Right:emitDirection:" + properties.emitDirection); + } + if(properties.playerMove) + { + variant = variant | soko_laser90Right_properties.playerMove; + //tiled.log("laser90Right:emitDirection:" + properties.playerMove); + } + retVal.push(variant); + break; + case "laserEmitUp": + var variant = 0b0; + if(properties.playerMove) + { + variant = variant | soko_laserEmitUp_properties.playerMove; + } + //tiled.log("" + properties.emitDirection + " " + soko_direction[properties.emitDirection]); + variant = variant | ((soko_direction[properties.emitDirection] & 0b11) << 6); + retVal.push(variant); + break; + case "laserReceiveUp": + var variant = 0b0; + tiled.log("" + properties.emitDirection + " " + soko_direction[properties.emitDirection]); + variant = variant | ((soko_direction[properties.emitDirection] & 0b11) << 6); + retVal.push(variant); + break + case "warpinternal": + var variant = 0b0; + if(properties.allow_crates) + { + variant = variant | soko_warpinternal_properties.allow_crates; + //tiled.log("laser90Right:emitDirection:" + properties.emitDirection); + } + retVal.push(variant); + retVal.push(properties.hp); + var targetCoord = findObjCoordById(objArr,properties.target_id); + //if(targetCoord.valid) + //{ + //tiled.log("className === warpinternalexit || className === warpinternal: " + ((objArr[targetCoord.index].obj.tile.className === "warpinternalexit") || (objArr[targetCoord.index].obj.tile.className === "warpinternal"))); + //} + if(!targetCoord.valid) + { + tiled.log("No Valid Warp Exit at target_id"); + } + if(targetCoord.valid && ((objArr[targetCoord.index].obj.tile.className === "warpinternalexit") || (objArr[targetCoord.index].obj.tile.className === "warpinternal"))){ + tiled.log("Warp Valid ID: " + properties.target_id + " at Coord(" + targetCoord.x + "," + targetCoord.y + ")"); + //retVal.push(properties[idString]); + retVal.push(targetCoord.x); + retVal.push(targetCoord.y); + } + break; + case "warpexternal": + var variant = 0b0; + var target_id = properties.target_id; + if(properties.manualIndex) + { + variant = variant | soko_warpexternal_properties.manualIndex; + //tiled.log("laser90Right:emitDirection:" + properties.emitDirection); + } + retVal.push(variant); + retVal.push(target_id); + break; + case "button": + var variant = 0b0; + if(properties.cratePress) + { + variant = variant | soko_button_properties.cratePress; + } + if(properties.invertAction) + { + variant = variant | soko_button_properties.invertAction; + } + if(properties.playerPress) + { + variant = variant | soko_button_properties.playerPress; + } + if(properties.stayDownOnPress) + { + variant = variant | soko_button_properties.stayDownOnPress; + } + var numTarg = (properties.numTargets & 0b111); + variant = variant | (numTarg << 5); //store the number of targets in the upper 5 bits (up to 7 targets per button) + retVal.push(variant); + for(var i = 0; i < numTarg; ++i) + { + idString = "target" + (i+1) + "id"; + var targetCoord = findObjCoordById(objArr,properties[idString]); + if(targetCoord.valid){ + tiled.log("Valid ID:" + properties[idString] + " at Coord(" + targetCoord.x + "," + targetCoord.y + ")"); + //retVal.push(properties[idString]); + retVal.push(targetCoord.x); + retVal.push(targetCoord.y); + } + else + { + numTarg -= 1; //discard invalid target ID, reduce target count by 1 + variant = variant & 0b11111; + variant = variant | (numTarg << 5); + retVal[0] = variant; + } + } + break; + case "ghostblock": + var variant = 0b0; + if(properties.inverted) + { + variant = variant | soko_ghostblock_properties.inverted; + } + if(properties.playerMove) + { + variant = variant | soko_ghostblock_properties.playerMove; + } + + } + return retVal; +} + +tiled.log("Registering Soko Map Export"); +//tiled.log(tiled.activeAsset.layers[0].cellAt(3,1)); +//map = tiled.activeAsset; +//dat = []; +//dat.push(map.width); +//dat.push(map.height); +//for (var y = 0; y < map.height; ++y) +/* +{ + for(var x = 0; x + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tools/soko/templateTiledProject/sk_binOverworld.bin b/tools/soko/templateTiledProject/sk_binOverworld.bin new file mode 100644 index 0000000000000000000000000000000000000000..9aab206e2aa6ebc100fa5b8095345f6b9b083c66 GIT binary patch literal 107 qcmd<&WP}4I2w-GHFixIlV0gyFgoQ#@1yq0XG)Ns2nkZNVW + + + + + + + + + + + + + + +12,12,12,12,12,12,12,12,12, +12,13,13,13,13,13,13,13,12, +12,13,13,13,13,13,13,13,12, +12,13,13,13,13,13,13,13,12, +12,13,13,13,13,13,13,13,12, +12,13,13,13,13,13,13,13,12, +12,13,13,13,13,13,13,13,12, +12,13,13,13,13,13,13,13,12, +12,12,12,12,12,12,12,12,12 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tools/soko/templateTiledProject/sk_laserTest.bin b/tools/soko/templateTiledProject/sk_laserTest.bin new file mode 100644 index 0000000000000000000000000000000000000000..b0208cc4b6e9d4248b446d8deb1df5a054aa43fc GIT binary patch literal 123 zcmd<&WP}5zlb0QyF@XUiBZOuGGfrM%0E(Zy!U!ZG0%t%1K%vvj&wv6D8JH@N6te!4 P7aJgouP}jSVD + + + + + + + + + + + + + + +12,12,12,12,12,12,12,12,12, +12,13,13,13,13,13,13,13,12, +12,13,13,13,13,13,13,13,12, +12,13,13,13,13,13,13,13,12, +12,13,13,13,13,13,13,13,12, +12,13,13,13,13,13,13,13,12, +12,13,13,13,13,13,13,13,12, +12,13,13,13,13,13,13,13,12, +12,12,12,12,12,12,12,12,12 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tools/soko/templateTiledProject/soko_entities.tsx b/tools/soko/templateTiledProject/soko_entities.tsx new file mode 100644 index 000000000..9d0caa3ac --- /dev/null +++ b/tools/soko/templateTiledProject/soko_entities.tsx @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tools/soko/templateTiledProject/templateMap.bin b/tools/soko/templateTiledProject/templateMap.bin new file mode 100644 index 0000000000000000000000000000000000000000..cc7f267ef3b78593c935178de507ec1f7694f6d6 GIT binary patch literal 99 zcmXv`+X;X$5WA#OT*WOM&Gil+RMZtRhj)eMBOwnmR}9ckvr4g${;cXA5J11gL}j$8a?MjXQc literal 0 HcmV?d00001 diff --git a/tools/soko/templateTiledProject/templateMap.tmx b/tools/soko/templateTiledProject/templateMap.tmx new file mode 100644 index 000000000..be9e1a588 --- /dev/null +++ b/tools/soko/templateTiledProject/templateMap.tmx @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + +12,12,12,12,12,12,12,12, +12,13,13,14,13,13,13,12, +12,13,13,13,13,13,13,12, +12,13,13,13,13,13,15,12, +12,13,13,13,13,13,13,12, +12,12,12,12,12,12,12,12 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tools/soko/templateTiledProject/templateProject.tiled-project b/tools/soko/templateTiledProject/templateProject.tiled-project new file mode 100644 index 000000000..d0eb59206 --- /dev/null +++ b/tools/soko/templateTiledProject/templateProject.tiled-project @@ -0,0 +1,14 @@ +{ + "automappingRulesFile": "", + "commands": [ + ], + "compatibilityVersion": 1100, + "extensionsPath": "extensions", + "folders": [ + "." + ], + "properties": [ + ], + "propertyTypes": [ + ] +} diff --git a/tools/soko/templateTiledProject/templateProject.tiled-session b/tools/soko/templateTiledProject/templateProject.tiled-session new file mode 100644 index 000000000..ea98e1286 --- /dev/null +++ b/tools/soko/templateTiledProject/templateProject.tiled-session @@ -0,0 +1,42 @@ +{ + "activeFile": "templateMap.tmx", + "expandedProjectPaths": [ + "." + ], + "file.lastUsedOpenFilter": "All Files (*)", + "fileStates": { + "": { + "scaleInEditor": 1 + }, + "objLayers.tsx": { + "dynamicWrapping": true, + "scaleInDock": 1, + "scaleInEditor": 1 + }, + "templateMap.tmx": { + "scale": 4.492708333333334, + "selectedLayer": 1, + "viewCenter": { + "x": 58.094134013447714, + "y": 48.18919545559935 + } + }, + "templateMap.tmx#tilesheet": { + "dynamicWrapping": false, + "scaleInDock": 1 + } + }, + "last.exportedFilePath": "C:/Users/grana/repos/Swadge-IDF-5.0/tools/soko/templateTiledProject", + "last.imagePath": "C:/Users/grana/repos/Swadge-IDF-5.0/tools/soko/templateTiledProject/tileSprites", + "map.lastUsedExportFilter": "All Files (*)", + "openFiles": [ + "templateMap.tmx", + "objLayers.tsx" + ], + "project": "templateProject.tiled-project", + "property.type": "bool", + "recentFiles": [ + "objLayers.tsx", + "templateMap.tmx" + ] +} diff --git a/tools/soko/templateTiledProject/tileSprites/tilesheet.png b/tools/soko/templateTiledProject/tileSprites/tilesheet.png new file mode 100644 index 0000000000000000000000000000000000000000..90c89a827e95d5ec8d788723291ac1339242a266 GIT binary patch literal 792 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7uRSjKx9jP7LeL$-HD>V7lb#;uumf z=k3kJywwH*ZWqsomOZiUy*}mUrGx^CJ@%rQad(!{M z@A#KrW_js`y!BL-mDjBPAE@QOz4`ya%{SK6q!!9*cQ3oSepT4A^`g~vfBB0mST^)B z9PR&g?O$D%r{>q&QM0dp7M-lF>hCq_Sxd+H^Y30?uBn_`^)2ju-Ln6JzvW6x>RGle zn^L5?>2>Kk-!p5T2AQZkiEsZ`vvkUp^DifI2-cMQ&%XXYVET)Tr&S7ePTTu;_rv;< zB^^hed(UED_2W+W?g!PHEAx$| z+JBf{yYeihaY=sHfx>nDDiV_wpQjcafB5g_zoVwR6XXAWj7m-Wyz!_xKa0tvBeS+^ zaM<5{r(__MWEga3P7s41`@7p2r?wnlxnKq#`(_E&2WOp`e!M-{x8CN##A(V0ZhAg= zr@77WLY!Au?(h3I`$G?;_%TdB>)5}%Az7%$b>Vg4aK=eR*}j*uK8goBY?=99S?F0q zhQFgm>@S7y{q?^4Jh-m@-YCJU^M85hfwjIYSrY}c*Q|W2$@Kf%B>DMP{}!CSW&FR+ zQX?}?G{=_Z)n1uDTAKd0d$Jc*zg8sD`PU@nTG`tFZY=SaazzcozV$bjRqk7U zYKBSrS|%mVCC3-mT}*3VIsJXKT-~NAn{UmVCwoj&`5v3x+n?Xlzdzr@d&YA0lu1e- zCOymAe~0V%zld+Ev)0Q0H#1nfm+zjk(8BX`!zUg1T + + + + + + + diff --git a/tools/soko/templateTiledProject/warehouse.bin b/tools/soko/templateTiledProject/warehouse.bin new file mode 100644 index 0000000000000000000000000000000000000000..216b974867b2a214e09281942a452bb49adee519 GIT binary patch literal 106 zcmX|&%MAb^2t(m<(fy|~ItOz|F|mh2e`A`tAO`9NYkRVeWo!8op{zs)_2UFuwGqvq JRHSkh9xm>s6i@&F literal 0 HcmV?d00001 diff --git a/tools/soko/templateTiledProject/warehouse.tmx b/tools/soko/templateTiledProject/warehouse.tmx new file mode 100644 index 000000000..328d85892 --- /dev/null +++ b/tools/soko/templateTiledProject/warehouse.tmx @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + +0,0,12,12,12,12,12,0, +12,12,12,13,13,13,12,0, +12,14,13,13,13,13,12,0, +12,12,12,13,13,14,12,0, +12,14,12,12,13,13,12,0, +12,13,12,13,14,13,12,12, +12,13,13,14,13,13,14,12, +12,13,13,13,14,13,13,12, +12,12,12,12,12,12,12,12 + + + + + + + + + + + + + diff --git a/tools/soko/tmx_to_binary.py b/tools/soko/tmx_to_binary.py new file mode 100644 index 000000000..6141fb703 --- /dev/null +++ b/tools/soko/tmx_to_binary.py @@ -0,0 +1,222 @@ +import math +from itertools import groupby +from xml.dom.minidom import parse,parseString +import os + +SIG_BYTE_SIMPLE_SEQUENCE = 200 #This byte will capture long strings of bytes in compact form. Example [12][12][12]...[12] => [200][#][12] where # is number of 12 tiles + +OBJECT_START_BYTE = 201 +SKB_EMPTY = 0 +SKB_WALL = 1 +SKB_FLOOR = 2 +SKB_GOAL = 3 +SKB_NO_WALK = 4 +SKB_OBJSTART = 201 +SKB_COMPRESS = 202 +SKB_PLAYER = 203 +SKB_CRATE = 204 +SKB_WARPINTERNAL = 205 +SKB_WARPINTERNALEXIT = 206 +SKB_WARPEXTERNAL = 207 +SKB_BUTTON = 208 +SKB_LASEREMITTER = 209 +SKB_LASERRECEIVEROMNI = 210 +SKB_LASERRECEIVER = 211 +SKB_LASER90ROTATE = 212 +SKB_GHOSTBLOCK = 213 +SKB_OBJEND = 230 + +classToID = { + "wall": SKB_WALL, + "wal": SKB_WALL, + "block": SKB_WALL, + "floor": SKB_FLOOR, + "ground": SKB_FLOOR, + "goal":SKB_GOAL, + "floornowalk":SKB_NO_WALK, + "nowalk":SKB_NO_WALK, + "nowalkfloor":SKB_NO_WALK, + "empty":SKB_EMPTY, + "nothing":SKB_EMPTY, + "player":SKB_PLAYER, + "crate":SKB_CRATE, + "warpexternal":SKB_WARPEXTERNAL, + "portal":SKB_WARPEXTERNAL, + "button":SKB_BUTTON, +} + +def insert_position(position, sourceList, insertionList): + return sourceList[:position] + insertionList + sourceList[position:] + +# root = tk.Tk() +# root.withdraw() + +def convertTMX(file_path): + print("convert "+file_path) + document = parse(file_path) + entities = {} + mapHeaderWidth = int(document.getElementsByTagName("map")[0].attributes.getNamedItem("width").nodeValue) + mapHeaderHeight = int(document.getElementsByTagName("map")[0].attributes.getNamedItem("height").nodeValue) + mode = "SOKO_UNDEFINED" + + + allProps = document.getElementsByTagName("property") + for prop in allProps: + if(prop.getAttribute("name") == "gamemode"): + mode=prop.getAttribute("value") + break + if(mode == "SOKO_UNDEFINED"): + print("Preprocessor Warning. "+file_path+" has no properly set gamemode. setting gamemode to SOKO_CLASSIC") + mode = "SOKO_CLASSIC" + modeInt = getModeInt(mode) + + #get firstgid of tilesheet + tileLookups = {} #offset from the tilesheet + tilesets = document.getElementsByTagName("tileset") + for tileset in tilesets: + x =(int((tileset.getAttribute("firstgid")))) + source = tileset.getAttribute("source") + if(source != ""): + current_dir = os.path.dirname(file_path) + tpath = os.path.normpath(current_dir+"/"+source) + if(os.path.splitext(tpath)[1] == ".tsx"): + doc = parse(tpath) + tileLookups[x] = loadTilesetLookup(doc) + else: + tileLookups[x] = loadTilesetLookup(tileset) + + # populate entities dictionary + + # loop through entities and add values. + objectlayers = document.getElementsByTagName("objectgroup") + entityContainer = objectlayers[0] + if(entityContainer.getAttribute("name") != "entities"): #todo: get length of container. number children, i guess? + print("Warning, object layer not called 'entities' or there is more than one object layer. there should be just one, called entities.") + + for entity in entityContainer.getElementsByTagName("object"): + ebytes = getEntityBytesFromEntity(entity,tileLookups) + x = int(float(entity.getAttribute("x"))/16) + y = int(float(entity.getAttribute("y"))/16) + #print(str(x)+","+str(y)+" = "+str(ebytes)) + entities[str(x)+","+str(y)] = ebytes + + + + dataText = document.getElementsByTagName("data")[0].firstChild.nodeValue + scrub = "".join(dataText.split()) #Remove all residual whitespace in data block + scrub = [(int(i)) for i in scrub.split(",")] #Convert all tileIDs to int. + + # fisrt, our HEADER data: width, height, modeint + output = [mapHeaderWidth,mapHeaderHeight,modeInt] + for i in range(len(scrub)): + x = (i-1)%mapHeaderWidth + y = ((i-1)//mapHeaderWidth)+1 #todo: figure out why this is +1 + keypos = str(x)+","+str(y) + #print("playing with "+key) + if(keypos in entities): + #append each byte of the entity data. + for b in entities[keypos]: + output.append(b) + output.append(int(getTile(scrub[i],tileLookups))) + + # output now is a list of tiles. + + output2 = compress(output) + #output2 = output + rawsize = len(output) + compsize = len(output2) + rawBytesc = bytearray(output2) + rawBytesImmut = bytes(rawBytesc) + return rawBytesImmut, rawsize, compsize + # outfile_path = "".join([file_path.split(".")[0],".bin"]) + # with open(outfile_path,"wb") as binary_file: + # binary_file.write(rawBytesImmut) + +def compress(bytes): + res = [] + for k,i in groupby(bytes): + run = list(i) + if(len(run)>3): + res.extend([k,SKB_COMPRESS,len(run)-1]) + else: + res.extend(run) + return res + +# These need to match the enum int casts in soko.h +def getModeInt(mode): + mode = mode.upper() + if(mode == "SOKO_OVERWORLD" or mode == "OVERWORLD"): + return 0 + elif mode == "SOKO_CLASSIC" or mode == "CLASSIC": + return 1 + elif mode == "SOKO_EULER" or mode == "EULER": + return 2 + elif mode == "SOKO_LASER" or mode == "LASER" or mode == "SOKO_LASERBOUNCE" or mode == "LASERBOUNCE": + return 3 + + +def getEntityBytesFromEntity(entity,lookups): + #todo: look up data in the tsx. which we have loaded? I think? + #SKB_OBJSTART, SKB_[Object Type], [Data Bytes] , SKB_OBJEND + gid = int(entity.getAttribute("gid")) + tid = getTile(gid,lookups) + + otype = 0 + if(tid == SKB_PLAYER): + return [SKB_OBJSTART,SKB_PLAYER,SKB_OBJEND] + elif(tid == SKB_WARPEXTERNAL): + # index of destination or x,y? + id = int(getEntityPropValue(entity,"target_id",None)) + return [SKB_OBJSTART,SKB_WARPEXTERNAL,id,SKB_OBJEND] + elif(tid == SKB_CRATE): + # bit 0 is sticky ob01 + # bit 1 is trail ob10 + sticky = 0 + trail = 0 + if getEntityPropValue(entity,"sticky","false") == "true": + sticky = 1 + if getEntityPropValue(entity,"trail","false") == "true": + trail = 2 + flag = trail+sticky + return [SKB_OBJSTART,SKB_CRATE,flag,SKB_OBJEND] + # etc + print("could not get entity..."+str(gid)); + return [] + return [SKB_OBJSTART,SKB_CRATE,SKB_OBJEND] + +def getTile(i,lookups): + if(i == 0): + # empty from tiled + return 0 # whatever our empty is. + for k,v in lookups.items(): + ix = i-k + if(k > i): + continue + if ix in v: + s = v[ix] + if s in classToID: + x = classToID[s] + return x + else: + print("what's the byte for "+str(s)) + print("uh oh"+str(i)+"-"+str(k)+"-"+str(lookups)) + return i + +def loadTilesetLookup(doc): + # turn root object into dictonary of id's->classnames. + tiles = doc.getElementsByTagName("tile") + lookup = {} + for tile in tiles: + lookup[int(tile.getAttribute("id"))] = tile.getAttribute("type") + + return lookup + +def getEntityPropValue(entity, property, default=0): + props = entity.getElementsByTagName("property") + for prop in props: + if prop.getAttribute("name") == property: + return prop.getAttribute("value") + + return default + + \ No newline at end of file diff --git a/tools/svg-to-pinball/.gitignore b/tools/svg-to-pinball/.gitignore new file mode 100755 index 000000000..dfe5670df --- /dev/null +++ b/tools/svg-to-pinball/.gitignore @@ -0,0 +1,4 @@ +*.xml +*.xsd +*.svg +*.bin \ No newline at end of file diff --git a/tools/svg-to-pinball/.vscode/launch.json b/tools/svg-to-pinball/.vscode/launch.json new file mode 100755 index 000000000..2f59b07d4 --- /dev/null +++ b/tools/svg-to-pinball/.vscode/launch.json @@ -0,0 +1,15 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Python Debugger: svg-to-pinball.py", + "type": "debugpy", + "request": "launch", + "program": "${workspaceFolder}/svg-to-pinball.py", + "console": "integratedTerminal" + } + ] +} \ No newline at end of file diff --git a/tools/svg-to-pinball/.vscode/settings.json b/tools/svg-to-pinball/.vscode/settings.json new file mode 100755 index 000000000..1c83c6cb0 --- /dev/null +++ b/tools/svg-to-pinball/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "cSpell.words": [ + "Bezier", + "Subpaths", + "svgelements" + ] +} \ No newline at end of file diff --git a/tools/svg-to-pinball/README.md b/tools/svg-to-pinball/README.md new file mode 100755 index 000000000..2798c4065 --- /dev/null +++ b/tools/svg-to-pinball/README.md @@ -0,0 +1,55 @@ +If objects aren't where they're supposed to be, svgelements may not be applying transforms correctly. Use this Inkscape plugin to apply transforms to objects before saving the SVG: https://github.com/Klowner/inkscape-applytransforms + +## File Format + +1. Number of Groups +1. Number of lines + * Line objects +1. Number of circles + * circles objects +1. Number of rectangles + * rectangles objects +1. Number of flippers + * flippers objects + +### Line +**Byte**|**Size**|**Value** +:-----:|:-----:|:-----: +0|2|ID +2|1|Group ID +3|2|p1.x +5|2|p1.y +7|2|p2.x +9|2|p2.y +11|1|Type +12|1|Push Velocity +13|1|Is Solid + +### Circle +**Byte**|**Size**|**Value** +:-----:|:-----:|:-----: +0|2|ID +2|1|Group ID +3|2|x +5|2|y +7|1|radius +8|1|Push Velocity + +### Rectangle +**Byte**|**Size**|**Value** +:-----:|:-----:|:-----: +0|2|ID +2|1|Group ID +3|2|x +5|2|y +7|2|width +9|2|height + +### Flipper +**Byte**|**Size**|**Value** +:-----:|:-----:|:-----: +0|2|x +2|2|y +4|1|Radius +5|1|Length +6|1|Facing Right \ No newline at end of file diff --git a/tools/svg-to-pinball/gentable.sh b/tools/svg-to-pinball/gentable.sh new file mode 100755 index 000000000..38168d84c --- /dev/null +++ b/tools/svg-to-pinball/gentable.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +find ../../ -iname cnfs_image* -delete +find ../../ -iname table.bin -delete +find ../../ -iname pinball.raw -delete +make -C ../cnfs/ clean +python svg-to-pinball.py +cp table.bin ../../assets/pinball.raw diff --git a/tools/svg-to-pinball/pinball.svg b/tools/svg-to-pinball/pinball.svg new file mode 100755 index 000000000..88b9b5383 --- /dev/null +++ b/tools/svg-to-pinball/pinball.svg @@ -0,0 +1,778 @@ + + + + diff --git a/tools/svg-to-pinball/requirements.txt b/tools/svg-to-pinball/requirements.txt new file mode 100755 index 000000000..56a41c393 --- /dev/null +++ b/tools/svg-to-pinball/requirements.txt @@ -0,0 +1 @@ +svgelements==1.9.6 diff --git a/tools/svg-to-pinball/svg-to-pinball.py b/tools/svg-to-pinball/svg-to-pinball.py new file mode 100755 index 000000000..5f48dd7a3 --- /dev/null +++ b/tools/svg-to-pinball/svg-to-pinball.py @@ -0,0 +1,487 @@ +from svgelements import SVG +from svgelements import Group +from svgelements import Path +from svgelements import Point +from svgelements import Circle +from svgelements import Rect +from math import sqrt, pow +from enum import Enum + +groups = [] + + +def getIntGroupId(gId: str) -> int: + try: + if gId.startswith("group_"): + gInt = int(gId.split("_")[1]) + if gInt not in groups: + groups.append(gInt) + return gInt + return 0 + except: + return 0 + + +def getIntId(id: str) -> int: + try: + return int(id) + except: + return 0 + + +class LineType(Enum): + JS_WALL = 0 + JS_SLINGSHOT = 1 + JS_DROP_TARGET = 2 + JS_STANDUP_TARGET = 3 + JS_SPINNER = 4 + JS_SCOOP = 5 + JS_BALL_LOST = 6 + JS_LAUNCH_DOOR = 7 + + +class CircleType(Enum): + JS_BUMPER = 0 + JS_ROLLOVER = 1 + + +class PointType(Enum): + JS_BALL_SPAWN = 0 + JS_ITEM_SPAWN = 1 + + +class xyPoint: + def __init__(self, p: Point = None, x: int = 0, y: int = 0) -> None: + if p is not None: + self.x: int = int(p.x) + self.y: int = int(p.y) + else: + self.x: int = int(x) + self.y: int = int(y) + + def toBytes(self) -> bytearray: + return bytearray( + [(self.x >> 8) & 0xFF, self.x & 0xFF, (self.y >> 8) & 0xFF, self.y & 0xFF] + ) + + def __eq__(self, other: object) -> bool: + return self.x == other.x and self.y == other.y + + +class pbPoint: + def __init__(self, pos: xyPoint, type: LineType, gId: str, id: str) -> None: + self.pos = pos + self.type: int = type.value + self.gId: int = getIntGroupId(gId) + self.id: int = getIntId(id) + + def toBytes(self) -> bytearray: + b = bytearray([(self.id >> 8), self.id & 0xFF, self.gId]) + b.extend(self.pos.toBytes()) + b.append(self.type) + return b + + +class pbLine: + def __init__( + self, p1: xyPoint, p2: xyPoint, type: LineType, gId: str, id: str + ) -> None: + self.p1 = p1 + self.p2 = p2 + self.type: int = type.value + self.gId: int = getIntGroupId(gId) + self.id: int = getIntId(id) + + match (type): + case LineType.JS_WALL: + self.isSolid = True + self.pushVel = 0 + pass + case LineType.JS_SLINGSHOT: + self.isSolid = True + self.pushVel = 80 + pass + case LineType.JS_DROP_TARGET: + self.isSolid = True + self.pushVel = 40 + pass + case LineType.JS_STANDUP_TARGET: + self.isSolid = True + self.pushVel = 0 + pass + case LineType.JS_SPINNER: + self.isSolid = False + self.pushVel = 0 + pass + case LineType.JS_SCOOP: + self.isSolid = True + self.pushVel = 0 + pass + case LineType.JS_BALL_LOST: + self.isSolid = True + self.pushVel = 0 + pass + case LineType.JS_LAUNCH_DOOR: + self.isSolid = False + self.pushVel = 0 + + def __str__(self) -> str: + return "{.p1 = {.x = %d, .y = %d}, .p2 = {.x = %d, .y = %d}}," % ( + self.p1.x, + self.p1.y, + self.p2.x, + self.p2.y, + ) + + def toBytes(self) -> bytearray: + b = bytearray([(self.id >> 8), self.id & 0xFF, self.gId]) + b.extend(self.p1.toBytes()) + b.extend(self.p2.toBytes()) + b.append(self.type) + b.append(self.pushVel) + b.append(self.isSolid) + # print(' '.join(['%02X' % x for x in b])) + return b + + +class pbCircle: + def __init__( + self, + pos: xyPoint, + radius: int, + type: CircleType, + pushVel: int, + gId: str, + id: str, + ) -> None: + self.position = pos + self.radius = int(radius) + self.type = type.value + self.gId: int = getIntGroupId(gId) + self.id: int = getIntId(id) + self.pushVel = int(pushVel) + + def __str__(self) -> str: + return "{.pos = {.x = %d, .y = %d}, .radius = %d}," % ( + self.position.x, + self.position.y, + self.radius, + ) + + def toBytes(self) -> bytearray: + b = bytearray([(self.id >> 8), self.id & 0xFF, self.gId]) + b.extend(self.position.toBytes()) + b.append(self.radius) + b.append(self.type) + b.append(self.pushVel) + # print(' '.join(['%02X' % x for x in b])) + return b + + +class pbRectangle: + def __init__(self, position: xyPoint, size: xyPoint, gId: str, id: str) -> None: + self.position = position + self.size = size + self.gId: int = getIntGroupId(gId) + self.id: int = getIntId(id) + + def __str__(self) -> str: + return "{.pos = {.x = %d, .y = %d}, .width = %d, .height = %d}," % ( + self.position.x, + self.position.y, + self.size.x, + self.size.y, + ) + + def toBytes(self) -> bytearray: + b = bytearray([(self.id >> 8), self.id & 0xFF, self.gId]) + b.extend(self.position.toBytes()) + b.extend(self.size.toBytes()) + # print(' '.join(['%02X' % x for x in b])) + return b + + +class pbTriangle: + def __init__(self, vertices: list[xyPoint], gId: str, id: str) -> None: + self.vertices = vertices + self.gId: int = getIntGroupId(gId) + self.id: int = getIntId(id) + + def toBytes(self) -> bytearray: + b = bytearray([(self.id >> 8), self.id & 0xFF, self.gId]) + for point in self.vertices: + b.extend(point.toBytes()) + return b + + +class pbFlipper: + def __init__( + self, pivot: xyPoint, radius: int, length: int, facingRight: bool + ) -> None: + self.pivot = pivot + self.radius = int(radius) + self.length = int(length) + self.facingRight = bool(facingRight) + + def __str__(self) -> str: + return ( + "{.cPivot = {.pos = {.x = %d, .y = %d}, .radius = %d}, .len = %d, .facingRight = %s}," + % ( + self.pivot.x, + self.pivot.y, + self.radius, + self.length, + "true" if self.facingRight else "false", + ) + ) + + def toBytes(self) -> bytearray: + b = bytearray() + b.extend(self.pivot.toBytes()) + b.append(self.radius) + b.append(self.length) + b.append(self.facingRight) + # print(' '.join(['%02X' % x for x in b])) + return b + + +def extractCircles(gs: list, type: CircleType, gId: str) -> list[pbCircle]: + """Recursively extract all circles from this list of SVG things + + Args: + gs (list): A list that contains Group and Circle + + Returns: + list[str]: A list of C strings for the circles + """ + circles = [] + for g in gs: + if isinstance(g, Circle): + circles.append( + pbCircle( + xyPoint(x=g.cx, y=g.cy), (g.rx + g.ry) / 2, type, 120, gId, g.id + ) + ) + elif isinstance(g, Group): + circles.extend(extractCircles(g, type, g.id)) + else: + print("Found " + str(type(g)) + " when extracting Circles") + return circles + + +def extractPoints(gs: list, type: PointType, gId: str) -> list[pbPoint]: + """Recursively extract all points from this list of SVG things + + Args: + gs (list): A list that contains Group and Point + + Returns: + list[str]: A list of C strings for the points + """ + points = [] + for g in gs: + if isinstance(g, Circle): + points.append(pbPoint(xyPoint(x=g.cx, y=g.cy), type, gId, g.id)) + elif isinstance(g, Group): + points.extend(extractPoints(g, type, g.id)) + else: + print("Found " + str(type(g)) + " when extracting Points") + return points + + +def extractRectangles(gs: list, gId: str) -> list[pbRectangle]: + """Recursively extract all circles from this list of SVG things + + Args: + gs (list): A list that contains Group and Circle + + Returns: + list[str]: A list of C strings for the circles + """ + rectangles = [] + for g in gs: + if isinstance(g, Rect): + rectangles.append( + pbRectangle( + xyPoint(x=g.x, y=g.y), xyPoint(x=g.width, y=g.height), gId, g.id + ) + ) + elif isinstance(g, Group): + rectangles.extend(extractRectangles(g, g.id)) + else: + print("Found " + str(type(g)) + " when extracting Rects") + return rectangles + + +def extractPaths(gs: list, lineType: LineType, gId: str) -> list[pbLine]: + """Recursively extract all paths from this list of SVG things + + Args: + gs (list): A list that contains Group and Path + + Returns: + list[str]: A list of C strings for the path segments + """ + lines = [] + for g in gs: + if isinstance(g, Path): + lastPoint: Point = None + point: Point + for point in g.as_points(): + if lastPoint is not None and lastPoint != point: + lines.append( + pbLine( + xyPoint(p=lastPoint), xyPoint(p=point), lineType, gId, g.id + ) + ) + lastPoint = point + elif isinstance(g, Group): + lines.extend(extractPaths(g, lineType, g.id)) + else: + print("Found " + str(type(g)) + " when extracting Paths") + return lines + + +def extractTriangles(gs: list, gId: str) -> list[pbTriangle]: + """Recursively extract all triangles from this list of SVG things + + Args: + gs (list): A list that contains Group and Path + + Returns: + list[str]: A list of C strings for the path segments + """ + triangles = [] + vertices = [] + for g in gs: + if isinstance(g, Path): + point: Point + for point in g.as_points(): + pbp = xyPoint(p=point) + + if 3 == len(vertices) and pbp == vertices[0]: + # Save the triangle + triangles.append(pbTriangle(vertices, gId, g.id)) + # Start a new one + vertices = [] + elif len(vertices) == 0 or pbp != vertices[-1]: + vertices.append(pbp) + elif isinstance(g, Group): + triangles.extend(extractTriangles(g, g.id)) + else: + print("Found " + str(type(g)) + " when extracting Triangles") + return triangles + + +def extractFlippers(gs: list, gId: str) -> list[pbFlipper]: + """Recursively extract all flippers (groups of circles and paths) from this list of SVG things + + Args: + gs (list): A list that contains stuff + + Returns: + list[str]: A list of C strings for the path segments + """ + lines = [] + flipperParts: list[Circle] = [] + for g in gs: + if isinstance(g, Circle): + flipperParts.append(g) + elif isinstance(g, Path): + pass + elif isinstance(g, Group): + lines.extend(extractFlippers(g, g.id)) + else: + print("Found " + str(type(g)) + " when extracting Flippers") + + if 2 == len(flipperParts): + if "pivot" in flipperParts[0].id.lower(): + pivot = flipperParts[0] + tip = flipperParts[1] + else: + pivot = flipperParts[1] + tip = flipperParts[0] + + if pivot.cx < tip.cx: + facingRight = True + else: + facingRight = False + + flipperLen = sqrt(pow(pivot.cx - tip.cx, 2) + pow(pivot.cy - tip.cy, 2)) + + lines.append( + pbFlipper( + xyPoint(x=pivot.cx, y=pivot.cy), pivot.rx, flipperLen, facingRight + ) + ) + + return lines + + +def addLength(tableData: bytearray, array: int): + length = len(array) + b = [(length >> 8) & 0xFF, (length) & 0xFF] + tableData.extend(b) + # print(' '.join(['%02X' % x for x in b])) + + +def main(): + # Load the SVG + g: Group = SVG().parse("pinball.svg") + + lines: list[pbLine] = [] + lines.extend(extractPaths(g.objects["350_Walls"], LineType.JS_WALL, None)) + lines.extend(extractPaths(g.objects["300_Scoops"], LineType.JS_SCOOP, None)) + lines.extend(extractPaths(g.objects["250_Slingshots"], LineType.JS_SLINGSHOT, None)) + lines.extend(extractPaths(g.objects["200_Drop_Targets"], LineType.JS_DROP_TARGET, None)) + lines.extend( + extractPaths(g.objects["150_Standup_Targets"], LineType.JS_STANDUP_TARGET, None) + ) + lines.extend(extractPaths(g.objects["400_Ball_Lost"], LineType.JS_BALL_LOST, None)) + lines.extend(extractPaths(g.objects["650_Launch_Door"], LineType.JS_LAUNCH_DOOR, None)) + + circles: list[pbCircle] = [] + circles.extend(extractCircles(g.objects["450_Rollovers"], CircleType.JS_ROLLOVER, None)) + circles.extend(extractCircles(g.objects["500_Bumpers"], CircleType.JS_BUMPER, None)) + + points: list[pbPoint] = [] + points.extend(extractPoints(g.objects["050_Ball_Spawn"], PointType.JS_BALL_SPAWN, None)) + points.extend(extractPoints(g.objects["100_Item_Spawn"], PointType.JS_ITEM_SPAWN, None)) + + launchers = extractRectangles(g.objects["600_Launchers"], None) + flippers = extractFlippers(g.objects["550_Flippers"], None) + triangles = extractTriangles(g.objects["000_Indicators"], None) + + tableData: bytearray = bytearray() + tableData.append(max(groups)) + + addLength(tableData, lines) + for line in sorted(lines, key=lambda x: x.id): + tableData.extend(line.toBytes()) + + addLength(tableData, circles) + for circle in sorted(circles, key=lambda x: x.id): + tableData.extend(circle.toBytes()) + + addLength(tableData, launchers) + for launcher in sorted(launchers, key=lambda x: x.id): + tableData.extend(launcher.toBytes()) + + addLength(tableData, flippers) + for flipper in flippers: + tableData.extend(flipper.toBytes()) + + addLength(tableData, triangles) + for triangle in sorted(triangles, key=lambda x: x.id): + tableData.extend(triangle.toBytes()) + + addLength(tableData, points) + for point in sorted(points, key=lambda x: x.id): + tableData.extend(point.toBytes()) + + with open("table.bin", "wb") as outFile: + outFile.write(tableData) + + +if __name__ == "__main__": + main()