From 66951160a864c0d227481a8cd1510706da81197f Mon Sep 17 00:00:00 2001 From: gelakinetic Date: Sun, 22 Sep 2024 14:15:53 -0400 Subject: [PATCH] Sokobokobanaban first merge (#296) --- 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 docs/soko/readme.md | 43 + docs/soko/soko_levels.md | 96 ++ emulator/src/extensions/modes/ext_modes.c | 2 + main/CMakeLists.txt | 7 + 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/system/mainMenu/mainMenu.c | 6 + makefile | 6 + 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 +++ 103 files changed, 5924 insertions(+) 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 create mode 100644 docs/soko/readme.md create mode 100644 docs/soko/soko_levels.md 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/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 diff --git a/assets/soko/levels/SK_LEVEL_LIST.txt b/assets/soko/levels/SK_LEVEL_LIST.txt new file mode 100644 index 000000000..a82eb3fe3 --- /dev/null +++ b/assets/soko/levels/SK_LEVEL_LIST.txt @@ -0,0 +1,22 @@ +0:sk_e_overworld.bin: +1:sk_e_start.bin: +2:sk_e_tunnels.bin: +3:sk_e_groundwork.bin: +4:sk_e_tunnels.bin: +5:sk_e_camera.bin: +6:sk_e_doubleblock.bin: +7:sk_e_mouse.bin: +30:sk_e_threestep.bin: +31:sk_e_harmonica.bin: +32:sk_e_spine.bin: +33:sk_e_a-frame.bin: +34:sk_e_curlingiron.bin: +35:sk_e_copymachine.bin: +36:sk_e_spiral.bin: +37:sk_e_steeringwheel.bin: +38:sk_e_casette.bin: +101:sk_e_apollo.bin: +102:sk_e_waterwheel.bin: +103:sk_e_feint.bin: +104:sk_e_throughput.bin: +105:sk_e_alkatraz.bin: diff --git a/assets/soko/levels/classic/sck_c_threes.tmx b/assets/soko/levels/classic/sck_c_threes.tmx new file mode 100644 index 000000000..62cce165b --- /dev/null +++ b/assets/soko/levels/classic/sck_c_threes.tmx @@ -0,0 +1,26 @@ + + + + + + +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 :{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/src/extensions/modes/ext_modes.c b/emulator/src/extensions/modes/ext_modes.c index 880fc016f..e35c18e88 100644 --- a/emulator/src/extensions/modes/ext_modes.c +++ b/emulator/src/extensions/modes/ext_modes.c @@ -31,6 +31,7 @@ #include "mode_synth.h" #include "modeTimer.h" #include "pango.h" +#include "soko.h" #include "touchTest.h" #include "tunernome.h" #include "ultimateTTT.h" @@ -72,6 +73,7 @@ static swadgeMode_t* allSwadgeModes[] = { &mainMenuMode, &modeCredits, &pangoMode, + &sokoMode, &synthMode, &t48Mode, &timerMode, diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index eb5d6eb71..d891f43f5 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -41,6 +41,12 @@ idf_component_register(SRCS "asset_loaders/common/heatshrink_encoder.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" @@ -112,6 +118,7 @@ idf_component_register(SRCS "asset_loaders/common/heatshrink_encoder.c" "./modes/games/2048" "./modes/games/bigbug" "./modes/games/pango" + "./modes/games/soko" "./modes/games/ultimateTTT" "./modes/music" "./modes/music/colorchord" 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/system/mainMenu/mainMenu.c b/main/modes/system/mainMenu/mainMenu.c index 837315b38..177f96546 100644 --- a/main/modes/system/mainMenu/mainMenu.c +++ b/main/modes/system/mainMenu/mainMenu.c @@ -20,6 +20,7 @@ #include "mode_synth.h" #include "ultimateTTT.h" #include "pango.h" +#include "soko.h" #include "touchTest.h" #include "tunernome.h" #include "keebTest.h" @@ -155,6 +156,7 @@ static void mainMenuEnterMode(void) 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"); @@ -360,6 +362,10 @@ static void mainMenuCb(const char* label, bool selected, uint32_t settingVal) { switchToSwadgeMode(&bigbugMode); } + else if (label == sokoMode.modeName) + { + switchToSwadgeMode(&sokoMode); + } else if (label == tttMode.modeName) { switchToSwadgeMode(&tttMode); diff --git a/makefile b/makefile index 60555451d..96f04b0be 100644 --- a/makefile +++ b/makefile @@ -332,10 +332,16 @@ $(EXECUTABLE): $(CNFS_FILE) $(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/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